├── .gitignore ├── LICENSE ├── README.adoc ├── TODO.adoc ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── typedmap-core ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── me │ │ └── broot │ │ └── typedmap │ │ └── core │ │ ├── api │ │ ├── ITypedKey.kt │ │ ├── MutableTypedMap.kt │ │ └── TypedMap.kt │ │ ├── impl │ │ ├── KeyForType.kt │ │ ├── SimpleTypedMap.kt │ │ └── TypedKey.kt │ │ └── util │ │ ├── AutoKey.kt │ │ ├── OptionalValue.kt │ │ └── extensions.kt │ └── test │ └── kotlin │ └── me │ └── broot │ └── typedmap │ └── core │ ├── AbstractMutableTypedMapTest.kt │ ├── SessionDataScenarioTest.kt │ ├── impl │ ├── KeyForTypeTest.kt │ ├── SimpleTypedMapTest.kt │ └── TypedKeyTest.kt │ ├── test_utils │ ├── test-utils-test.kt │ └── test-utils.kt │ └── util │ └── OptionalValueTest.kt └── typedmap-examples ├── build.gradle.kts └── src └── main └── kotlin └── me └── broot └── typedmap └── examples └── examples.kt /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | 4 | build 5 | out 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Typedmap 2 | 3 | [.normal] 4 | `typedmap` is an implementation of heterogeneous type-safe map pattern in Kotlin. It is a data structure similar to a regular map, but with two somewhat contradicting features: 5 | 6 | * Heterogeneous - it can store items of completely different types (so this is like `Map`). 7 | * Type-safe - we can access the data in a type-safe manner and without manual casting (unlike `Map`). 8 | 9 | To accomplish this, instead of parameterizing the map as usual, we need to parameterize the key. Keys are used both for identifying items in the map and to provide us with the information about the type of their associated values. 10 | 11 | As this is much easier to explain and understand by looking at examples, we will go straight to the code! 12 | 13 | == Table of Contents 14 | 15 | * <> 16 | ** <> 17 | ** <> 18 | ** <> 19 | * <> 20 | * <> 21 | * <> 22 | ** <> 23 | ** <> 24 | * <> 25 | 26 | == Examples 27 | 28 | === Get by Type 29 | 30 | Probably the most common example of a similar data structure is an API where we provide a `Class` to get an instance of it. In Java, it could look like this: 31 | 32 | [source,java] 33 | ---- 34 | public T get(Class cls) 35 | ---- 36 | 37 | Guava's https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ClassToInstanceMap.java[ClassToInstanceMap] is a good example of such a data structure. Additionally, there are e.g. https://javaee.github.io/javaee-spec/javadocs/javax/persistence/EntityManager.html#unwrap-java.lang.Class-[EntityManager.unwrap()] and https://javaee.github.io/javaee-spec/javadocs/javax/enterprise/inject/spi/BeanManager.html#getExtension-java.lang.Class-[BeanManager.getExtension()] methods that utilize similar API, however, their purpose is more specialized. 38 | 39 | `typedmap` supports this feature with a clean API: 40 | 41 | [source,kotlin] 42 | ---- 43 | // create a typed map 44 | val sess = simpleTypedMap() 45 | 46 | // add a User item 47 | sess += User("alice") 48 | 49 | // get an item of the User type 50 | val user = sess.get() 51 | 52 | println("User: $user") 53 | // User(username=alice) 54 | ---- 55 | 56 | Due to advanced type inferring in Kotlin, in many cases we don't need to specify a type when getting an item: 57 | 58 | [source,kotlin] 59 | ---- 60 | fun processUser(user: User) { ... } 61 | 62 | processUser(sess.get()) // Works as expected 63 | ---- 64 | 65 | [source,kotlin] 66 | ---- 67 | fun getUser(): User { 68 | return sess.get() // Works as expected 69 | } 70 | ---- 71 | 72 | `typedmap` fully supports parameterized types: 73 | 74 | [source,kotlin] 75 | ---- 76 | sess += listOf(1, 2, 3, 4, 5) 77 | sess += listOf("a", "b", "c", "d", "e") 78 | 79 | println("List: ${sess.get>()}") 80 | // [1, 2, 3, 4, 5] 81 | println("List: ${sess.get>()}") 82 | // [a, b, c, d, e] 83 | ---- 84 | 85 | NOTE: `SimpleTypedMap`, which we use here, does not support polymorphism. Both `get>()` and `get>()` would not find a requested item and throw an exception. Polymorphism could be supported by more advanced implementations of `TypedMap`. 86 | 87 | === Get by Key 88 | 89 | Looking for items by their type is very convenient, but in many cases this is not enough. For example, it is difficult to store multiple instances of the same class and access them individually. Moreover, if we need to store an item of a common type, e.g. `String`, then the code `get()` becomes enigmatic, because it is not clear what is the `String` we requested. In such cases, we can create keys to identify items in the map. 90 | 91 | Let's assume we develop a web application, and we store some data in the web session. In the previous example, we stored a user object in the session, but this time we just need to store a username. In addition, we would like to store a session ID and visits count. We have to create a key for each item and use these keys to identify values: 92 | 93 | [source,kotlin] 94 | ---- 95 | object Username : TypedKey() 96 | object SessionId : TypedKey() 97 | object VisitsCount : TypedKey() 98 | 99 | sess[Username] = "alice" 100 | sess[SessionId] = "0123456789abcdef" 101 | sess[VisitsCount] = 42 102 | 103 | println("Username: ${sess[Username]}") 104 | // "alice" 105 | println("SessionId: ${sess[SessionId]}") 106 | // "0123456789abcdef" 107 | println("VisitsCount: ${sess[VisitsCount]}") 108 | // 42 109 | ---- 110 | 111 | When creating a key, we need to provide a type of its associated value. This makes possible to provide a fully type-safe API: 112 | 113 | [source,kotlin] 114 | ---- 115 | val username = sess[Username] // type: String 116 | val visits = sess[VisitsCount] // type: Int 117 | 118 | sess[Username] = 50 // compile error 119 | ---- 120 | 121 | Similarly as in the previous section, we can use keys in conjunction with parameterized types: 122 | 123 | [source,kotlin] 124 | ---- 125 | object UserIds : TypedKey>() 126 | object Labels : TypedKey>() 127 | 128 | sess[UserIds] = listOf(1, 2, 3, 4, 5) 129 | sess[Labels] = listOf("a", "b", "c", "d", "e") 130 | sess[Labels] = listOf(1, 2, 3, 4, 5) // compile error 131 | 132 | println("UserIds: ${sess[UserIds]}") 133 | // [1, 2, 3, 4, 5] 134 | println("Labels: ${sess[Labels]}") 135 | // [a, b, c, d, e] 136 | ---- 137 | 138 | === Key With Data 139 | 140 | Declaring keys in the way described above is fine if we need to store a finite set of known items, so we can create a distinct key for each of them. In practice though, we very often need to create keys dynamically and store an arbitrary number of items in a map. This is supported by `typedmap` as well, and we still keep its type-safety feature. In fact, this case is implemented in `typedmap` in a very similar way to regular maps. 141 | 142 | Instead of creating the key as a singleton object, we need to define it as a class. `hashCode()` and `equals()` have to be properly implemented, so the easiest is to use a `data class`: 143 | 144 | [source,kotlin] 145 | ---- 146 | // value 147 | data class Order( 148 | val orderId: Int, 149 | val items: List 150 | ) 151 | 152 | // key 153 | data class OrderKey( 154 | val orderId: Int 155 | ) : TypedKey() 156 | 157 | sess[OrderKey(1)] = Order(1, listOf("item1", "item2")) 158 | sess[OrderKey(2)] = Order(2, listOf("item3", "item4")) 159 | 160 | println("OrderKey(1): ${sess[OrderKey(1)]}") 161 | // Order(orderId=1, items=[item1, item2]) 162 | println("OrderKey(2): ${sess[OrderKey(2)]}") 163 | // Order(orderId=2, items=[item3, item4]) 164 | ---- 165 | 166 | This example could be improved by using the `AutoKey` util. `AutoKey` is a very simple interface that we can implement to make map items responsible for creating their keys: 167 | 168 | [source,kotlin] 169 | ---- 170 | data class Order( 171 | val orderId: Int, 172 | val items: List 173 | ) : AutoKey { 174 | override val typedKey get() = OrderKey(orderId) 175 | } 176 | 177 | sess += Order(1, listOf("item1", "item2")) 178 | sess += Order(2, listOf("item3", "item4")) 179 | ---- 180 | 181 | NOTE: You could notice that we used `plusAssign()` operator (`+=`) earlier, and it had a different meaning. This is true, `sess += Order()` could be interpreted both as "set by autokey" (so the key is `OrderKey` object) or as "set by type" (key is similar to `Class`). By default, objects implementing `AutoKey` are stored by autokey, which is probably what we really need. To store autokey objects by their type, we need to use `setByType()` function explicitly. 182 | 183 | == Installation 184 | 185 | Add a following dependency to the gradle/maven file: 186 | 187 | .build.gradle 188 | [source,groovy] 189 | ---- 190 | dependencies { 191 | implementation "me.broot.typedmap:typedmap-core:${version}" 192 | } 193 | ---- 194 | 195 | .build.gradle.kts 196 | [source,kotlin] 197 | ---- 198 | dependencies { 199 | implementation("me.broot.typedmap:typedmap-core:${version}") 200 | } 201 | ---- 202 | 203 | .pom.xml 204 | [source,xml] 205 | ---- 206 | 207 | me.broot.typedmap 208 | typedmap-core 209 | ${version} 210 | 211 | ---- 212 | 213 | Replace `${version}` with e.g.: "1.0.0". The latest available version is: image:https://img.shields.io/maven-central/v/me.broot.typedmap/typedmap-core[Maven Central] 214 | 215 | Now, we can start using `typedmap`: 216 | 217 | [source,kotlin] 218 | val map = simpleTypedMap() 219 | 220 | == Building 221 | 222 | To build the project from sources, run the following command: 223 | 224 | .Linux / macOS 225 | [source,shell] 226 | $ ./gradlew build 227 | 228 | .Windows 229 | [source,dos] 230 | gradlew.bat build 231 | 232 | After a successful build, the resulting jar file will be placed in: 233 | 234 | * `typedmap-core/build/libs/typedmap-core.jar` 235 | 236 | == Use Cases 237 | 238 | Some people may ask: what do we need this for? Or even more specifically: how is the typed map better than just a regular class with known and fully typed properties? Well, in most cases it is not. However, there are cases where such a data structure could be very useful. 239 | 240 | Sometimes, we need to separate the code responsible for providing a data storage and the code storing its data there. In such a case, the first component knows nothing about the data it stores, so the data container can't be typed easily. Often, it is represented as `Map`, `Map` or just `Any/Object`. 241 | 242 | Examples: 243 | 244 | * Session data in web frameworks - framework provides the storage, web application uses it. 245 | * Request/response objects in web/network frameworks - they often contain untyped data storage, so middleware or application developer could attach additional data to request/response. 246 | * Applications with support for plugins - plugins often need to store their data somewhere and application provides a place for it. 247 | * Data storage shared between loosely coupled modules. 248 | + 249 | Let's assume we develop some kind of data processing software. Our data processing is very complex, so we divided the whole process into several smaller tasks and organized the code into clean architecture of multiple packages or even separate libraries. Modules produce results of their processing and may consume results of other modules, so we need a central cache for storing these results. 250 | + 251 | The problem is: central cache needs to know data structures of all available modules, so we partially lose benefits of our clean design. It is even worse if modules are provided as external libraries. 252 | * Objects designed to be externally extensible, i.e. by other means than subtyping. We can easily add new behavior to a class by extension or static functions, but we can't add any additional data fields to it. Similar example are classes allowing to attach hooks to affect their behavior. 253 | * Separation of concerns. Often, we divide the code of our application into a utility of generic usage and a code related to an application logic. In such a case we don't want to pollute utility classes with an application logic, but sometimes we still need to somehow reference application objects from utility classes. Usually, it can be solved with generics though. 254 | 255 | Above cases aren't very common, some of them are rather rare. Still, it happens from time to time. Generally speaking, whenever we design or use a class which owns a property like `Any` or `Map` with contract like: "Put there any data you need, it won't be modified, but just kept for you", the typed map structure could be potentially useful. Such properties are often named "extras", "extra data", "properties", etc. 256 | 257 | === Real-World Examples 258 | 259 | There are several existing examples in Java with similar requirements to described above and implemented using either untyped container and manual casting or with a class-to-instance map or function: 260 | 261 | * In Servlet API we could store and retrieve additional untyped data at request level (https://jakarta.ee/specifications/platform/9/apidocs/jakarta/servlet/servletrequest#getAttribute-java.lang.String-[ServletRequest.getAttribute()]), session level (https://jakarta.ee/specifications/platform/9/apidocs/jakarta/servlet/http/httpsession#getAttribute-java.lang.String-[HttpSession.getAttribute()]) and globally (https://jakarta.ee/specifications/platform/9/apidocs/jakarta/servlet/servletcontext#getAttribute-java.lang.String-[ServletContext.getAttribute()]). 262 | * Jax-RS Filters (middleware) could store/retrieve additional request data using: https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/container/containerrequestcontext#getProperty-java.lang.String-[ContainerRequestContext.getProperty()]. 263 | * In JPA we could store custom data in EntityManager in its properties: https://jakarta.ee/specifications/platform/9/apidocs/jakarta/persistence/entitymanager#getProperties--[EntityManager.getProperties()], although I'm not sure this functionality was intended for such purposes. 264 | * Spring Framework uses untyped map for session data: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/WebSession.html#getAttribute-java.lang.String-[WebSession.getAttribute()], https://docs.spring.io/spring-session/docs/current/api/org/springframework/session/Session.html#getAttribute-java.lang.String-[Session.getAttribute()]. It is a little smarter than previous examples, because it uses type inference, so we don't need to cast the value manually. It doesn't change much though - we still need to remember which value was stored with which key and specify the type manually. 265 | * In Spring Integration one of its main components, https://docs.spring.io/spring-integration/docs/current/reference/html/message.html#message[Message], is a generic data holder/wrapper and it could contain https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/MessageHeaders.html[headers] which is basically an untyped map. Additionally, there are wrappers for this untyped map to provide a strongly typed API, e.g.: https://github.com/spring-projects/spring-framework/blob/main/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java[SimpMessageHeaderAccessor]. 266 | * In Android, we very often pass data as untyped https://developer.android.com/reference/android/os/Bundle[Bundle] map, for example in https://developer.android.com/reference/android/content/Intent#getExtras()[Intent extras]. 267 | 268 | Additionally, there are examples of data structures very similar to `typedmap`. In fact, they exist in one of the most popular libraries for Kotlin: 269 | 270 | * https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/[CoroutineContext] - its `Key` interface and `get()` function. It allows to store any data within a coroutine. 271 | * https://ktor.io/docs/attributes.html[Attributes] of Ktor web framework by JetBrains. It is used as a storage for middleware. 272 | 273 | === Alternatives 274 | 275 | Typed maps aren't the only solution to a similar problem. There are other techniques, including: 276 | 277 | * Use untyped map (e.g. `Map`) as a central storage and provide strongly-typed accessors by clients/modules. Accessors could be: extension functions, static functions or even classes that wrap untyped map and provide an easy to use API. 278 | + 279 | This solution could be very convenient to use, especially with extension functions, however, writing accessors requires much more work than just creating a typed key. Furthermore, `typedmap` naturally guarantees that each key is unique. Accessors need to do the same or they would risk conflicts. 280 | * class-to-instance maps. 281 | + 282 | In many cases they are less convenient to use. For example, if we need to store multiple simple items (strings, integers), we need to create a wrapper class for each of them and then wrap/unwrap a value whenever storing/retrieving it. Also, it is not trivial to store collections of items as in <>. 283 | 284 | == References 285 | 286 | * https://www.informit.com/articles/article.aspx?p=2861454&seqNum=8[Effective Java by Joshua Bloch, Item 33: Consider typesafe heterogeneous containers] 287 | -------------------------------------------------------------------------------- /TODO.adoc: -------------------------------------------------------------------------------- 1 | = TODO 2 | 3 | * Measure the performance impact of `ITypedKey.keyType` and `ITypedKey.valueType`. 4 | ** We don't really use these properties for most cases - they could be useful for more specific needs. 5 | ** They require using reflection whenever we use `typedKey()` or initialize a key with data. 6 | ** It could be optimized with global cache if needed. 7 | * Advanced typed map implementations: 8 | ** Polymorphic. 9 | ** Map returning nulls for missing nullable items. 10 | ** Map that creates values on demand with value providers/factories. 11 | ** Factory for creating a typed map for specific needs, with required features. 12 | * Java interop. 13 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.palantir.gradle.gitversion.VersionDetails 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | kotlin("jvm") version "1.5.0" 6 | `maven-publish` 7 | signing 8 | 9 | id("org.jetbrains.dokka") version "1.4.32" apply false 10 | id("com.palantir.git-version") version "0.12.3" apply false 11 | } 12 | 13 | try { 14 | apply(plugin = "com.palantir.git-version") 15 | } catch (e: Exception) { 16 | project.logger.warn(e.message, e) 17 | } 18 | 19 | val projectVersion = createProjectVersion() 20 | 21 | allprojects { 22 | group = "me.broot.typedmap" 23 | version = projectVersion.name 24 | 25 | repositories { 26 | mavenCentral() 27 | } 28 | } 29 | 30 | subprojects { 31 | apply(plugin = "org.jetbrains.kotlin.jvm") 32 | apply(plugin = "org.jetbrains.dokka") 33 | 34 | tasks.test { 35 | useTestNG() 36 | } 37 | 38 | tasks.withType { 39 | kotlinOptions { 40 | jvmTarget = "11" 41 | freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn" 42 | } 43 | } 44 | 45 | val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) 46 | val dokkaJavadoc by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) 47 | 48 | val dokkaJar by tasks.registering(Jar::class) { 49 | dependsOn(dokkaHtml) 50 | archiveClassifier.set("dokka") 51 | from(dokkaHtml.outputDirectory) 52 | } 53 | 54 | val javadocJar by tasks.registering(Jar::class) { 55 | dependsOn(dokkaJavadoc) 56 | archiveClassifier.set("javadoc") 57 | from(dokkaJavadoc.outputDirectory) 58 | } 59 | 60 | dependencies { 61 | testImplementation(kotlin("test-testng")) 62 | } 63 | 64 | if (name in listOf("typedmap-core")) { 65 | apply(plugin = "maven-publish") 66 | apply(plugin = "signing") 67 | 68 | java { 69 | withSourcesJar() 70 | } 71 | 72 | kotlin { 73 | explicitApi() 74 | } 75 | 76 | publishing { 77 | publications { 78 | create("mavenCentral") { 79 | version = if (projectVersion.isRelease) projectVersion.name else "${projectVersion.name}-SNAPSHOT" 80 | 81 | from(components["java"]) 82 | artifact(javadocJar) 83 | artifact(dokkaJar) 84 | 85 | pom { 86 | name.set("typedmap") 87 | description.set("Type-safe heterogeneous map in Kotlin.") 88 | url.set("https://github.com/brutall/typedmap") 89 | 90 | licenses { 91 | license { 92 | name.set("The Apache License, Version 2.0") 93 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 94 | } 95 | } 96 | developers { 97 | developer { 98 | name.set("Ryszard Wiśniewski") 99 | email.set("brut.alll@gmail.com") 100 | } 101 | } 102 | scm { 103 | connection.set("scm:git:git://github.com/brutall/typedmap.git") 104 | developerConnection.set("scm:git:ssh://git@github.com/brutall/typedmap.git") 105 | url.set("https://github.com/brutall/typedmap") 106 | } 107 | } 108 | } 109 | } 110 | 111 | repositories { 112 | maven { 113 | name = "ossrh" 114 | 115 | if (projectVersion.isRelease) { 116 | setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 117 | } else { 118 | setUrl("https://s01.oss.sonatype.org/content/repositories/snapshots") 119 | } 120 | 121 | val ossrhUsername: String? by project 122 | val ossrhPassword: String? by project 123 | if (ossrhUsername != null && ossrhPassword != null) { 124 | credentials { 125 | username = ossrhUsername 126 | password = ossrhPassword 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | signing { 134 | useGpgCmd() 135 | sign(publishing.publications["mavenCentral"]) 136 | } 137 | } 138 | } 139 | 140 | fun Project.createProjectVersion() : ProjectVersion { 141 | if (!extra.has("versionDetails")) { 142 | return ProjectVersion("unknown", false, null) 143 | } 144 | 145 | val versionDetails: groovy.lang.Closure by extra 146 | val details = versionDetails(mapOf("prefix" to "release/")) 147 | 148 | val name = buildString { 149 | append(details.lastTag) 150 | 151 | if (details.commitDistance != 0) { 152 | details.branchName?.let { 153 | append('-') 154 | append(it.replace('/', '-')) 155 | } 156 | 157 | append('-') 158 | append(details.gitHash) 159 | } 160 | 161 | if (!details.isCleanTag) { 162 | append("-dirty") 163 | } 164 | } 165 | 166 | return ProjectVersion( 167 | name, 168 | details.commitDistance == 0 && details.isCleanTag, 169 | details 170 | ) 171 | } 172 | 173 | data class ProjectVersion( 174 | val name: String, 175 | val isRelease: Boolean, 176 | val details: VersionDetails? 177 | ) 178 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/broo2s/typedmap/bebd5d12219fc2365404d51b7aa1211a3e1a0f1d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "typedmap" 2 | 3 | include("typedmap-core") 4 | include("typedmap-examples") 5 | -------------------------------------------------------------------------------- /typedmap-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | dependencies { 6 | implementation(kotlin("reflect")) 7 | } 8 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/api/ITypedKey.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.api 2 | 3 | import kotlin.reflect.KType 4 | 5 | /** 6 | * Key used to access the data in [TypedMap]. 7 | * 8 | * It is parameterized with the type of a value associated with the key. For example, `ITypedKey` could be 9 | * used to store/retrieve a `String` instance in the `TypedMap`. 10 | * 11 | * In most cases this interface should not be implemented directly. Use 12 | * [typedKey()][me.broot.typedmap.core.impl.typedKey] or subtype [TypedKey][me.broot.typedmap.core.impl.TypedKey] 13 | * class in order to create an `ITypedKey`. 14 | * 15 | * @see TypedMap 16 | * @see me.broot.typedmap.core.impl.TypedKey 17 | * @see me.broot.typedmap.core.impl.typedKey 18 | */ 19 | public interface ITypedKey { 20 | // val keyType: KType 21 | 22 | /** 23 | * `KType` of the value associated with this key. 24 | * 25 | * It must always be the same type as `V`. For example, `valueType` of `ITypedKey` must be a `String` type. 26 | */ 27 | public val valueType: KType 28 | } 29 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/api/MutableTypedMap.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.api 2 | 3 | /** 4 | * [TypedMap] with read-write access. 5 | * 6 | * It extends `TypedMap` with methods used to modify the contents of the map. 7 | * 8 | * @see TypedMap 9 | * @see ITypedKey 10 | * @see me.broot.typedmap.core.impl.simpleTypedMap 11 | */ 12 | public interface MutableTypedMap : TypedMap { 13 | /** 14 | * Stores the value and associates it with the provided key. 15 | */ 16 | public operator fun set(key: ITypedKey, value: V) 17 | 18 | /** 19 | * Removes a value associated with the provided key and returns this value. 20 | * 21 | * It throws `NoSuchElementException` if an item does not exist. 22 | * 23 | * @throws NoSuchElementException 24 | */ 25 | public fun remove(key: ITypedKey): V 26 | 27 | /** 28 | * Removes a value associated with the provided key. Returns this value or null if it does not exist. 29 | */ 30 | public fun removeIfSet(key: ITypedKey): V? 31 | } 32 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/api/TypedMap.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.api 2 | 3 | import me.broot.typedmap.core.util.OptionalValue 4 | 5 | /** 6 | * Type-safe heterogeneous map with read-only access. 7 | * 8 | * Type-safe heterogeneous map (or typed map) allows to store items of various types and access them in a type-safe 9 | * manner. This is possible by parameterizing keys used with this map. 10 | * 11 | * This interface represents a read-only access to a typed map. Read-write access is provided by [MutableTypedMap]. 12 | * 13 | * The easiest way to create a `TypedMap` is to use [simpleTypedMap()][me.broot.typedmap.core.impl.simpleTypedMap]. 14 | * 15 | * @see MutableTypedMap 16 | * @see ITypedKey 17 | * @see me.broot.typedmap.core.impl.simpleTypedMap 18 | */ 19 | public interface TypedMap { 20 | /** 21 | * Returns a value associated with the provided key. 22 | * 23 | * It throws `NoSuchElementException` if an item does not exist. 24 | * 25 | * @throws NoSuchElementException 26 | */ 27 | public operator fun get(key: ITypedKey): V = getOptional(key).get() 28 | 29 | /** 30 | * Returns a value associated with the provided key or null if an item does not exist. 31 | */ 32 | public fun getOrNull(key: ITypedKey): V? = getOptional(key).getOrNull() 33 | 34 | /** 35 | * Checks whether the provided key exists in the map. 36 | */ 37 | public operator fun contains(key: ITypedKey): Boolean = getOptional(key).isPresent 38 | 39 | /** 40 | * Returns a value associated with the provided key with three possible states: non-null value, null or not exist. 41 | * 42 | * This is useful if a value is nullable and we need to distinguish whether it was set explicitly to null or 43 | * it was not set. 44 | * 45 | * @see OptionalValue 46 | */ 47 | public fun getOptional(key: ITypedKey): OptionalValue 48 | } 49 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/impl/KeyForType.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.impl 2 | 3 | import me.broot.typedmap.core.api.ITypedKey 4 | import kotlin.reflect.KType 5 | import kotlin.reflect.KTypeProjection 6 | import kotlin.reflect.full.createType 7 | import kotlin.reflect.typeOf 8 | 9 | /** 10 | * Creates an [ITypedKey] for the provided value type. 11 | * 12 | * It allows to easily create unnamed keys for storing and retrieving items by their type. For example, we can store 13 | * a `User` object and retrieve it later by just looking for the `User` type. We can do this by: 14 | * 15 | * ```kotlin 16 | * val key = typedKey() 17 | * map[key] = user1 18 | * ... 19 | * val user2 = map[key] 20 | * ``` 21 | * 22 | * To make it easier to use, extension functions with reified type parameters were provided for the most of typed map 23 | * operations. Above example could be simplified to: 24 | * 25 | * ```kotlin 26 | * map += user1 // Or: map.setByType(user1) 27 | * ... 28 | * val user2 = map.get() 29 | * ``` 30 | * 31 | * Accessing items by their type is simple and easy, but it is very limited, e.g. it allows to store only one item 32 | * per type. More advanced use cases could be implemented with [TypedKey]. 33 | * 34 | * @see TypedKey 35 | */ 36 | @ExperimentalStdlibApi 37 | public inline fun typedKey(): ITypedKey = KeyForType(typeOf()) 38 | 39 | @PublishedApi 40 | internal class KeyForType(override val valueType: KType) : ITypedKey { 41 | // override val keyType = KeyForType::class.createType(listOf(KTypeProjection.invariant(valueType))) 42 | 43 | override fun equals(other: Any?): Boolean { 44 | return this === other || (other is KeyForType<*> && valueType == other.valueType) 45 | } 46 | 47 | override fun hashCode(): Int { 48 | return valueType.hashCode() 49 | } 50 | 51 | override fun toString(): String { 52 | return "KeyForType($valueType)" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/impl/SimpleTypedMap.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.impl 2 | 3 | import me.broot.typedmap.core.api.ITypedKey 4 | import me.broot.typedmap.core.api.MutableTypedMap 5 | import me.broot.typedmap.core.util.OptionalValue 6 | import me.broot.typedmap.core.util.getOptional 7 | import me.broot.typedmap.core.util.getOrElse 8 | 9 | /** 10 | * Creates an empty [MutableTypedMap]. 11 | * 12 | * This is a basic implementation of `MutableTypedMap`. It does not provide any advanced features like e.g. polymorphic 13 | * value access. 14 | */ 15 | public fun simpleTypedMap(): MutableTypedMap = SimpleTypedMap() 16 | 17 | internal class SimpleTypedMap : MutableTypedMap { 18 | private val map = mutableMapOf, Any?>() 19 | 20 | override fun set(key: ITypedKey, value: V) { 21 | @Suppress("UNCHECKED_CAST") 22 | map[key as ITypedKey] = value 23 | } 24 | 25 | override fun remove(key: ITypedKey): V { 26 | @Suppress("UNCHECKED_CAST") 27 | if (!map.containsKey(key as ITypedKey)) { 28 | throw NoSuchElementException("Tried to remove a key which is missing: $key") 29 | } 30 | 31 | @Suppress("UNCHECKED_CAST") 32 | return map.remove(key) as V 33 | } 34 | 35 | override fun removeIfSet(key: ITypedKey): V? { 36 | @Suppress("UNCHECKED_CAST") 37 | return map.remove(key as ITypedKey) as V? 38 | } 39 | 40 | override fun get(key: ITypedKey): V { 41 | return getOptional(key).getOrElse { throw NoSuchElementException("Key is missing: $key") } 42 | } 43 | 44 | override fun getOrNull(key: ITypedKey): V? { 45 | @Suppress("UNCHECKED_CAST") 46 | return map[key as ITypedKey] as V? 47 | } 48 | 49 | override fun contains(key: ITypedKey): Boolean { 50 | @Suppress("UNCHECKED_CAST") 51 | return map.contains(key as ITypedKey) 52 | } 53 | 54 | override fun getOptional(key: ITypedKey): OptionalValue { 55 | @Suppress("UNCHECKED_CAST") 56 | return map.getOptional(key as ITypedKey) as OptionalValue 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/impl/TypedKey.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.impl 2 | 3 | import me.broot.typedmap.core.api.ITypedKey 4 | import kotlin.reflect.KType 5 | import kotlin.reflect.KTypeParameter 6 | import kotlin.reflect.full.* 7 | 8 | /** 9 | * Utility class for creating [ITypedKey] instances by subtyping it. 10 | * 11 | * It allows to easily create keys for [me.broot.typedmap.core.api.TypedMap]. To create a key, we need to subtype 12 | * `TypedKey` and provide the associated value type in its `V` type parameter. If we need to store one item per key, 13 | * we can use singleton keys: 14 | * 15 | * ```kotlin 16 | * object Username : TypedKey() 17 | * object VisitsCount : TypedKey() 18 | * 19 | * map[Username] = "alice" 20 | * val visits = map[VisitsCount] 21 | * ``` 22 | * 23 | * Sometimes, we need to store multiple items per a key type, for example store multiple users and identify them by 24 | * their ids. In that case we need to extend [ITypedKey] with additional properties and provide a proper `equals()` 25 | * and `hashCode()` implementation. The easiest is to use Kotlin's data class: 26 | * 27 | * ```kotlin 28 | * data class UserKey(val id: Int) : TypedKey() 29 | * 30 | * map[UserKey(1)] = user1 31 | * user2 = map[UserKey(2)] 32 | * ``` 33 | * 34 | * In fact, this is very similar to regular maps and keys, but still it provides us with its type-safety feature. 35 | * 36 | * It is possible to use multi-level subtyping and generics with `TypedKey`, but `V` has to be known at runtime, 37 | * it can't become removed by type erasure. For example: 38 | * 39 | * ```kotlin 40 | * // This is ok 41 | * abstract class AbstractStringKey : TypedKey() 42 | * object SomeStringKey : AbstractStringKey() 43 | * 44 | * // This is ok 45 | * open class ParamKey : TypedKey() 46 | * object AnotherStringKey : ParamKey() 47 | * 48 | * // It throws exception as `String` is erased at compile time. 49 | * val key = ParamKey() 50 | * ``` 51 | * 52 | * @see ITypedKey 53 | */ 54 | public abstract class TypedKey : ITypedKey { 55 | // final override val keyType: KType 56 | final override val valueType: KType 57 | 58 | init { 59 | val c = this::class 60 | // keyType = c.starProjectedType 61 | 62 | // Look for base `TypedKey` KType. 63 | val baseType = c.allSupertypes.single { it.classifier == TypedKey::class } 64 | 65 | // We assume it is impossible to use star projection or variance when extending `TypedKey`, so `type` property 66 | // should be always not null and projection should be invariant. 67 | valueType = checkNotNull(baseType.arguments[0].type) 68 | 69 | require(valueType.classifier !is KTypeParameter) { 70 | "V type parameter of TypedKey must be known at runtime, it can't become removed by type erasure. See documentation of TypedKey for more details." 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/util/AutoKey.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.util 2 | 3 | import me.broot.typedmap.core.api.ITypedKey 4 | import me.broot.typedmap.core.api.MutableTypedMap 5 | 6 | /** 7 | * Classes meant to be stored in [MutableTypedMap] could implement this interface to generate keys for themselves. 8 | */ 9 | public interface AutoKey { 10 | public val typedKey: ITypedKey 11 | } 12 | 13 | /** 14 | * Stores the value and identify it by its auto-key. 15 | * 16 | * @see setByAutoKey 17 | * @see MutableTypedMap.set 18 | * @see AutoKey 19 | */ 20 | @Suppress("NOTHING_TO_INLINE") 21 | public inline operator fun > MutableTypedMap.plusAssign(autoKey: V) : Unit = setByAutoKey(autoKey) 22 | 23 | /** 24 | * Stores the value and identify it by its auto-key. 25 | * 26 | * @see MutableTypedMap.set 27 | * @see AutoKey 28 | */ 29 | public fun > MutableTypedMap.setByAutoKey(autoKey: V) { 30 | this[autoKey.typedKey] = autoKey 31 | } 32 | 33 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/util/OptionalValue.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") 2 | @file:OptIn(ExperimentalContracts::class) 3 | 4 | package me.broot.typedmap.core.util 5 | 6 | import kotlin.contracts.ExperimentalContracts 7 | import kotlin.contracts.InvocationKind 8 | import kotlin.contracts.contract 9 | 10 | /** 11 | * Optional/Maybe type for Kotlin. 12 | * 13 | * Simple immutable value wrapper with one of three possible states: 14 | * 15 | * - non-null value, 16 | * - null value, 17 | * - absent/unset/undefined/unspecified/missing. 18 | * 19 | * This is useful when we need to store information whether some variable/value was specified or not and at the same 20 | * time this variable is nullable, so we can't use `null` as "not specified". One example is parsing of JSON if we 21 | * need to distinguish latter two cases: 22 | * 23 | * - `{"foo": "bar"}` 24 | * - `{"foo": null}` 25 | * - `{}` 26 | */ 27 | @JvmInline 28 | public value class OptionalValue @PublishedApi internal constructor( 29 | @PublishedApi 30 | internal val value: Any? 31 | ) { 32 | public companion object { 33 | public inline fun of(value: V): OptionalValue = OptionalValue(value) 34 | public inline fun absent(): OptionalValue = OptionalValue(Absent) 35 | public inline fun ofNullable(value: V?): OptionalValue = 36 | if (value == null) absent() else of(value) 37 | } 38 | 39 | public inline val isPresent: Boolean get() = value !== Absent 40 | public inline val isAbsent: Boolean get() = value === Absent 41 | 42 | public inline fun get(): V = getOrElse { throw NoSuchElementException() } 43 | public inline fun getOrNull(): V? = getOrElse { null } 44 | 45 | override fun toString(): String { 46 | return if (isPresent) { 47 | "OptionalValue.of($value)" 48 | } else { 49 | "OptionalValue.absent()" 50 | } 51 | } 52 | 53 | @PublishedApi 54 | internal object Absent 55 | } 56 | 57 | public inline fun OptionalValue.getOrElse(onAbsent: () -> V): V { 58 | contract { 59 | callsInPlace(onAbsent, InvocationKind.AT_MOST_ONCE) 60 | } 61 | return map({ return@map it }, onAbsent) 62 | } 63 | 64 | public inline fun OptionalValue.mapValue(transform: (V) -> R): OptionalValue { 65 | contract { 66 | callsInPlace(transform, InvocationKind.AT_MOST_ONCE) 67 | } 68 | return map({ OptionalValue.of(transform(it)) }, { OptionalValue.absent() }) 69 | } 70 | 71 | public inline fun OptionalValue.ifPresent(onPresent: (V) -> Unit) { 72 | contract { 73 | callsInPlace(onPresent, InvocationKind.AT_MOST_ONCE) 74 | } 75 | map(onPresent) {} 76 | } 77 | 78 | public inline fun OptionalValue<*>.ifAbsent(onAbsent: () -> Unit) { 79 | contract { 80 | callsInPlace(onAbsent, InvocationKind.AT_MOST_ONCE) 81 | } 82 | map({}, onAbsent) 83 | } 84 | 85 | public inline fun OptionalValue.map(onPresent: (V) -> R, onAbsent: () -> R): R { 86 | contract { 87 | callsInPlace(onPresent, InvocationKind.AT_MOST_ONCE) 88 | callsInPlace(onAbsent, InvocationKind.AT_MOST_ONCE) 89 | } 90 | return if (isPresent) onPresent(value as V) else onAbsent() 91 | } 92 | 93 | public fun Map.getOptional(key: K): OptionalValue { 94 | val value = this[key] 95 | return if (value != null || containsKey(key)) { 96 | OptionalValue.of(value as V) 97 | } else { 98 | OptionalValue.absent() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /typedmap-core/src/main/kotlin/me/broot/typedmap/core/util/extensions.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.util 2 | 3 | import me.broot.typedmap.core.api.MutableTypedMap 4 | import me.broot.typedmap.core.api.TypedMap 5 | import me.broot.typedmap.core.impl.typedKey 6 | 7 | /** 8 | * Returns a value of the provided type. 9 | * 10 | * @throws NoSuchElementException 11 | * @see TypedMap.get 12 | * @see typedKey 13 | */ 14 | @ExperimentalStdlibApi 15 | public inline fun TypedMap.get(): V = get(typedKey()) 16 | 17 | /** 18 | * Returns a value of the provided type or null if it does not exist. 19 | * 20 | * @see TypedMap.getOrNull 21 | * @see typedKey 22 | */ 23 | @ExperimentalStdlibApi 24 | public inline fun TypedMap.getOrNull(): V? = getOrNull(typedKey()) 25 | 26 | /** 27 | * Checks whether a value of the provided type exists in the map. 28 | * 29 | * @see TypedMap.contains 30 | * @see typedKey 31 | */ 32 | @ExperimentalStdlibApi 33 | public inline fun TypedMap.contains(): Boolean = contains(typedKey()) 34 | 35 | /** 36 | * Returns a value of the provided type with three possible states: non-null value, null or not exist. 37 | * 38 | * @see TypedMap.getOptional 39 | * @see typedKey 40 | * @see OptionalValue 41 | */ 42 | @ExperimentalStdlibApi 43 | public inline fun TypedMap.getOptional(): OptionalValue = getOptional(typedKey()) 44 | 45 | /** 46 | * Removes a value of the provided type and returns this value. 47 | * 48 | * @throws NoSuchElementException 49 | * @see MutableTypedMap.remove 50 | * @see typedKey 51 | */ 52 | @ExperimentalStdlibApi 53 | public inline fun MutableTypedMap.remove(): V = remove(typedKey()) 54 | 55 | /** 56 | * Removes a value of the provided type. Returns this value or null if it does not exist. 57 | * 58 | * @see MutableTypedMap.removeIfSet 59 | * @see typedKey 60 | */ 61 | @ExperimentalStdlibApi 62 | public inline fun MutableTypedMap.removeIfSet(): V? = removeIfSet(typedKey()) 63 | 64 | /** 65 | * Stores the value and identify it by its type. 66 | * 67 | * @see MutableTypedMap.set 68 | * @see typedKey 69 | */ 70 | @ExperimentalStdlibApi 71 | public inline fun MutableTypedMap.setByType(value: V) { 72 | set(typedKey(), value) 73 | } 74 | 75 | /** 76 | * Stores the value and identify it by its type. 77 | * 78 | * @see setByType 79 | * @see MutableTypedMap.set 80 | * @see typedKey 81 | */ 82 | @ExperimentalStdlibApi 83 | public inline operator fun MutableTypedMap.plusAssign(value: V) : Unit = setByType(value) 84 | 85 | /** 86 | * Returns read-only wrapper around `MutableTypedMap`. 87 | * 88 | * Returned map reacts to changes in the original map. 89 | */ 90 | public fun MutableTypedMap.toTypedMap(): TypedMap = object : TypedMap by this {} 91 | -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/AbstractMutableTypedMapTest.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core 2 | 3 | import me.broot.typedmap.core.api.MutableTypedMap 4 | import me.broot.typedmap.core.impl.TypedKey 5 | import kotlin.test.* 6 | 7 | @OptIn(ExperimentalStdlibApi::class) 8 | abstract class AbstractMutableTypedMapTest { 9 | protected abstract fun createInstance(): T 10 | 11 | private lateinit var _map: T 12 | protected val map: T get() = _map 13 | 14 | @BeforeTest 15 | fun beforeTest() { 16 | _map = createInstance() 17 | } 18 | 19 | @Test 20 | fun `get() for existing item should return it`() { 21 | map[Username] = "alice" 22 | assertEquals("alice", map[Username]) 23 | } 24 | 25 | @Test 26 | fun `get() for missing item should throw NoSuchElementException`() { 27 | map[Username] = "alice" 28 | assertFailsWith { 29 | map[SessionId] 30 | } 31 | } 32 | 33 | @Test 34 | fun `getOrNull() for existing item should return it`() { 35 | map[Username] = "alice" 36 | assertEquals("alice", map.getOrNull(Username)) 37 | } 38 | 39 | @Test 40 | fun `getOrNull() for missing item should return null`() { 41 | map[Username] = "alice" 42 | assertNull(map.getOrNull(SessionId)) 43 | } 44 | 45 | @Test 46 | fun `contains() for existing item should return true`() { 47 | map[Username] = "alice" 48 | assertTrue(Username in map) 49 | } 50 | 51 | @Test 52 | fun `contains() for missing item should return false`() { 53 | map[Username] = "alice" 54 | assertFalse(SessionId in map) 55 | } 56 | 57 | @Test 58 | fun `getOptional() for existing item should return present optional`() { 59 | map[Username] = "alice" 60 | val opt = map.getOptional(Username) 61 | assertTrue(opt.isPresent) 62 | assertEquals("alice", opt.get()) 63 | } 64 | 65 | @Test 66 | fun `getOptional() for missing item should return absent optional`() { 67 | map[Username] = "alice" 68 | assertTrue(map.getOptional(SessionId).isAbsent) 69 | } 70 | 71 | @Test 72 | fun `set() should replace existing item`() { 73 | map[Username] = "alice" 74 | map[Username] = "bob" 75 | assertEquals("bob", map[Username]) 76 | } 77 | 78 | @Test 79 | fun `remove() for existing item should remove it and return it`() { 80 | map[Username] = "alice" 81 | val username = map.remove(Username) 82 | assertEquals("alice", username) 83 | assertFalse(Username in map) 84 | } 85 | 86 | @Test 87 | fun `remove() for missing item should throw NoSuchElementException`() { 88 | map[Username] = "alice" 89 | assertFailsWith { 90 | map.remove(SessionId) 91 | } 92 | } 93 | 94 | @Test 95 | fun `removeIfSet() for existing item should remove it and return it`() { 96 | map[Username] = "alice" 97 | val username = map.removeIfSet(Username) 98 | assertEquals("alice", username) 99 | assertFalse(Username in map) 100 | } 101 | 102 | @Test 103 | fun `removeIfSet() for missing item should do nothing and return null`() { 104 | map[Username] = "alice" 105 | assertNull(map.removeIfSet(SessionId)) 106 | } 107 | 108 | private object Username : TypedKey() 109 | private object SessionId : TypedKey() 110 | } 111 | -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/SessionDataScenarioTest.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core 2 | 3 | import me.broot.typedmap.core.impl.TypedKey 4 | import me.broot.typedmap.core.impl.simpleTypedMap 5 | import me.broot.typedmap.core.util.* 6 | import kotlin.test.* 7 | 8 | @OptIn(ExperimentalStdlibApi::class) 9 | class SessionDataScenarioTest { 10 | @Test 11 | fun test() { 12 | val sess = simpleTypedMap() 13 | 14 | sess += User("alice") 15 | assertEquals("alice", sess.get().username) 16 | 17 | sess += User("bob") 18 | assertEquals("bob", sess.get().username) 19 | 20 | assertTrue(sess.contains()) 21 | sess.remove() 22 | assertFalse(sess.contains()) 23 | assertFailsWith { 24 | sess.get() 25 | } 26 | 27 | sess += listOf(1, 2, 3, 4, 5) 28 | sess += listOf("a", "b", "c", "d", "e") 29 | assertEquals(listOf(1, 2, 3, 4, 5), sess.get()) 30 | assertEquals(listOf("a", "b", "c", "d", "e"), sess.get()) 31 | 32 | sess[Username] = "alice" 33 | sess[SessionId] = "0123456789abcdef" 34 | sess[VisitsCount] = 42 35 | assertEquals("alice", sess[Username]) 36 | assertEquals("0123456789abcdef", sess[SessionId]) 37 | assertEquals(42, sess[VisitsCount]) 38 | 39 | sess[Username] = "bob" 40 | assertEquals("bob", sess[Username]) 41 | 42 | assertTrue(Username in sess) 43 | sess.remove(Username) 44 | assertFalse(Username in sess) 45 | assertFailsWith { 46 | sess[Username] 47 | } 48 | 49 | sess[UserIds] = listOf(1, 2, 3, 4, 5) 50 | sess[Labels] = listOf("a", "b", "c", "d", "e") 51 | assertEquals(listOf(1, 2, 3, 4, 5), sess[UserIds]) 52 | assertEquals(listOf("a", "b", "c", "d", "e"), sess[Labels]) 53 | 54 | val o1 = Order(1, listOf("item1", "item2")) 55 | val o2 = Order(2, listOf("item3", "item4")) 56 | val o3 = Order(3, listOf("item5", "item6")) 57 | sess[OrderKey(1)] = o1 58 | sess[OrderKey(2)] = o2 59 | sess += o3 60 | 61 | assertEquals(o1, sess[OrderKey(1)]) 62 | assertEquals(o2, sess[OrderKey(2)]) 63 | assertFails { sess[OrderKey(3)] } 64 | 65 | assertEquals(o3, sess.get()) 66 | sess += o2 67 | assertEquals(o2, sess.get()) 68 | 69 | val ao1 = AutoOrder(1, listOf("item1", "item2")) 70 | val ao2 = AutoOrder(2, listOf("item3", "item4")) 71 | val ao3 = AutoOrder(3, listOf("item5", "item6")) 72 | sess += ao1 73 | sess += ao2 74 | sess += ao3 75 | 76 | assertEquals(ao1, sess[AutoOrderKey(1)]) 77 | assertEquals(ao2, sess[AutoOrderKey(2)]) 78 | assertEquals(ao3, sess[AutoOrderKey(3)]) 79 | 80 | assertFails { sess.get() } 81 | sess.setByType(ao1) 82 | assertEquals(ao1, sess.get()) 83 | } 84 | 85 | private object Username : TypedKey() 86 | private object SessionId : TypedKey() 87 | private object VisitsCount : TypedKey() 88 | 89 | object UserIds : TypedKey>() 90 | object Labels : TypedKey>() 91 | 92 | data class User( 93 | val username: String 94 | ) 95 | 96 | data class Order( 97 | val orderId: Int, 98 | val items: List 99 | ) 100 | 101 | data class OrderKey( 102 | val orderId: Int 103 | ) : TypedKey() 104 | 105 | data class AutoOrder( 106 | val orderId: Int, 107 | val items: List 108 | ) : AutoKey { 109 | override val typedKey get() = AutoOrderKey(orderId) 110 | } 111 | 112 | data class AutoOrderKey( 113 | val orderId: Int 114 | ) : TypedKey() 115 | } -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/impl/KeyForTypeTest.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.impl 2 | 3 | import me.broot.typedmap.core.test_utils.assertType 4 | import kotlin.test.Test 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertTrue 7 | 8 | @OptIn(ExperimentalStdlibApi::class) 9 | class KeyForTypeTest { 10 | @Test 11 | fun `valueType should be correct`() { 12 | assertType(typedKey().valueType) 13 | assertType(typedKey().valueType) 14 | assertType>(typedKey>().valueType) 15 | } 16 | 17 | @Test 18 | fun `keys with the same type should be equal`() { 19 | assertTrue(typedKey() == typedKey()) 20 | assertTrue(typedKey() == typedKey()) 21 | assertTrue(typedKey>() == typedKey>()) 22 | } 23 | 24 | @Test 25 | fun `keys with different types should not be equal`() { 26 | assertFalse(typedKey() == typedKey()) 27 | assertFalse(typedKey() == typedKey()) 28 | assertFalse(typedKey>() == typedKey>()) 29 | assertFalse(typedKey>() == typedKey>()) 30 | } 31 | } -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/impl/SimpleTypedMapTest.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.impl 2 | 3 | import me.broot.typedmap.core.AbstractMutableTypedMapTest 4 | import me.broot.typedmap.core.api.MutableTypedMap 5 | import kotlin.test.Test 6 | 7 | @Test 8 | class SimpleTypedMapTest : AbstractMutableTypedMapTest() { 9 | override fun createInstance() = simpleTypedMap() 10 | } 11 | -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/impl/TypedKeyTest.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.impl 2 | 3 | import me.broot.typedmap.core.test_utils.assertType 4 | import kotlin.test.* 5 | 6 | class TypedKeyTest { 7 | @Test 8 | fun `valueType should be correct for simple types`() { 9 | assertType(StringKey.valueType) 10 | assertType(StringKey2.valueType) 11 | assertType(IntKey.valueType) 12 | assertType(NumberKey.valueType) 13 | } 14 | 15 | @Test 16 | fun `valueType should be correct for parameterized types`() { 17 | assertType>(StringListKey.valueType) 18 | assertType>(IntListKey.valueType) 19 | assertType>(NumberListKey.valueType) 20 | assertType>(IntCollectionKey.valueType) 21 | } 22 | 23 | @Test 24 | fun `valueType should be correct for nullable types`() { 25 | assertType(NullableStringKey.valueType) 26 | assertType>(NullableStringListKey.valueType) 27 | assertType?>(StringNullableListKey.valueType) 28 | } 29 | 30 | @Test 31 | fun `valueType should be correct for subtyped keys`() { 32 | assertType(StringKey3.valueType) 33 | assertType(StringKey4.valueType) 34 | } 35 | 36 | @Test 37 | fun `distinct keys with the same or similar valueType should not be equal`() { 38 | assertFalse(StringKey.equals(StringKey2)) 39 | assertFalse(StringKey3.equals(StringKey4)) 40 | assertFalse(StringListKey.equals(IntListKey)) 41 | assertFalse(StringKey.equals(NullableStringKey)) 42 | } 43 | 44 | @Test 45 | fun `creating TypedKey with erased V should fail`() { 46 | assertFails { 47 | ParamKey() 48 | } 49 | } 50 | 51 | private object StringKey : TypedKey() 52 | private object StringKey2 : TypedKey() 53 | private object IntKey : TypedKey() 54 | private object NumberKey : TypedKey() 55 | 56 | private object StringListKey : TypedKey>() 57 | private object IntListKey : TypedKey>() 58 | private object NumberListKey : TypedKey>() 59 | private object IntCollectionKey : TypedKey>() 60 | 61 | private object NullableStringKey : TypedKey() 62 | private object NullableStringListKey : TypedKey>() 63 | private object StringNullableListKey : TypedKey?>() 64 | 65 | private abstract class AbstractStringKey : TypedKey() 66 | private object StringKey3 : AbstractStringKey() 67 | private open class ParamKey : TypedKey() 68 | private object StringKey4 : ParamKey() 69 | } 70 | -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/test_utils/test-utils-test.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.test_utils 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class TestUtilsTest { 7 | @OptIn(ExperimentalStdlibApi::class) 8 | @Test 9 | fun `compileType() and assertType() tests`() { 10 | assertType(compileType { "foo" }) 11 | assertType(compileType { 5 }) 12 | 13 | val intAsNumber: Number = 5 14 | assertEquals(Int::class, intAsNumber::class) 15 | assertType(compileType { intAsNumber }) 16 | 17 | assertType(compileType { "5".toIntOrNull() }) 18 | assertType>(compileType { listOf(1, 2, 3) }) 19 | assertType(compileType {}) 20 | assertEquals(NOTHING_TYPE, compileType { throw Exception() }) 21 | } 22 | } 23 | 24 | private fun returnsNothing(): Nothing = throw Exception() 25 | private val NOTHING_TYPE = ::returnsNothing.returnType 26 | -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/test_utils/test-utils.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.test_utils 2 | 3 | import kotlin.reflect.KType 4 | import kotlin.reflect.jvm.ExperimentalReflectionOnLambdas 5 | import kotlin.reflect.jvm.reflect 6 | import kotlin.reflect.typeOf 7 | import kotlin.test.assertEquals 8 | 9 | @OptIn(ExperimentalReflectionOnLambdas::class) 10 | fun compileType(expr: () -> T): KType { 11 | return checkNotNull(expr.reflect()?.returnType) 12 | } 13 | 14 | @OptIn(ExperimentalStdlibApi::class) 15 | inline fun assertType(actual: KType) { 16 | assertEquals(typeOf(), actual) 17 | } 18 | -------------------------------------------------------------------------------- /typedmap-core/src/test/kotlin/me/broot/typedmap/core/util/OptionalValueTest.kt: -------------------------------------------------------------------------------- 1 | package me.broot.typedmap.core.util 2 | 3 | import me.broot.typedmap.core.test_utils.assertType 4 | import me.broot.typedmap.core.test_utils.compileType 5 | import kotlin.test.* 6 | 7 | class OptionalValueTest { 8 | @Test 9 | fun `of() should always return present optional`() { 10 | assertTrue { OptionalValue.of(5).isPresent } 11 | assertFalse { OptionalValue.of(5).isAbsent } 12 | assertTrue { OptionalValue.of(null).isPresent } 13 | assertFalse { OptionalValue.of(null).isAbsent } 14 | } 15 | 16 | @Test 17 | fun `ofNullable() should return present or absent optional`() { 18 | assertTrue { OptionalValue.ofNullable(5).isPresent } 19 | assertFalse { OptionalValue.ofNullable(5).isAbsent } 20 | assertFalse { OptionalValue.ofNullable(null).isPresent } 21 | assertTrue { OptionalValue.ofNullable(null).isAbsent } 22 | } 23 | 24 | @Test 25 | fun `absent() should always return absent optional`() { 26 | assertFalse { OptionalValue.absent().isPresent } 27 | assertTrue { OptionalValue.absent().isAbsent } 28 | } 29 | 30 | @Test 31 | fun `get() on present should return optional value`() { 32 | assertEquals(5, OptionalValue.of(5).get()) 33 | assertNull(OptionalValue.of(null).get()) 34 | assertEquals(5, OptionalValue.ofNullable(5).get()) 35 | } 36 | 37 | @Test 38 | @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION") 39 | fun `get() on absent should throw NoSuchElementException`() { 40 | assertFailsWith { 41 | OptionalValue.absent().get() 42 | } 43 | assertFailsWith { 44 | OptionalValue.ofNullable(null).get() 45 | } 46 | } 47 | 48 | @Test 49 | fun `getOrNull() on present should return optional value`() { 50 | assertEquals(5, OptionalValue.of(5).getOrNull()) 51 | assertNull(OptionalValue.of(null).getOrNull()) 52 | assertEquals(5, OptionalValue.ofNullable(5).getOrNull()) 53 | } 54 | 55 | @Test 56 | fun `getOrNull() on absent should return null`() { 57 | assertNull(OptionalValue.absent().getOrNull()) 58 | assertNull(OptionalValue.ofNullable(null).getOrNull()) 59 | } 60 | 61 | @Test 62 | fun `getOrElse() on present should return optional value`() { 63 | assertEquals(5, OptionalValue.of(5).getOrElse { 7 }) 64 | assertNull(OptionalValue.of(null).getOrElse { 7 }) 65 | assertEquals(5, OptionalValue.ofNullable(5).getOrElse { 7 }) 66 | } 67 | 68 | @Test 69 | fun `getOrElse() on absent should return else value`() { 70 | assertEquals(7, OptionalValue.absent().getOrElse { 7 }) 71 | assertEquals(7, OptionalValue.ofNullable(null).getOrElse { 7 }) 72 | } 73 | 74 | @Test 75 | fun `getOrElse() should pass exceptions to caller`() { 76 | assertFailsWith { 77 | OptionalValue.absent().getOrElse { throw IllegalStateException() } 78 | } 79 | } 80 | 81 | @Test 82 | fun `mapValue() on present should return new optional`() { 83 | assertEquals(10, OptionalValue.of(5).mapValue { it * 2 }.get()) 84 | } 85 | 86 | @Test 87 | fun `mapValue() on absent should return absent`() { 88 | assertTrue(OptionalValue.ofNullable(null).mapValue { it * 2 }.isAbsent) 89 | } 90 | 91 | @Test 92 | fun `mapValue() should pass exceptions to caller`() { 93 | assertFailsWith { 94 | OptionalValue.of(5).mapValue { throw IllegalStateException() } 95 | } 96 | } 97 | 98 | @Test 99 | fun `Map-getOptional() should return present for existing non-null values`() { 100 | val map = mapOf("foo" to 5, "bar" to null) 101 | val value = map.getOptional("foo") 102 | assertTrue(value.isPresent) 103 | assertEquals(5, value.get()) 104 | } 105 | 106 | @Test 107 | fun `Map-getOptional() should return present for existing null values`() { 108 | val map = mapOf("foo" to 5, "bar" to null) 109 | val value = map.getOptional("bar") 110 | assertTrue(value.isPresent) 111 | assertNull(value.get()) 112 | 113 | val map2 = map.toMutableMap() 114 | map2["baz"] = null 115 | assertTrue(map2.getOptional("baz").isPresent) 116 | } 117 | 118 | @Test 119 | fun `Map-getOptional() should return absent for missing values`() { 120 | val map = mapOf("foo" to 5, "bar" to null) 121 | assertTrue(map.getOptional("baz").isAbsent) 122 | 123 | val map2 = map.toMutableMap() 124 | assertFalse(map2.getOptional("foo").isAbsent) 125 | map2.remove("foo") 126 | assertTrue(map2.getOptional("foo").isAbsent) 127 | } 128 | 129 | @Test 130 | fun `getOrElse() API tests`() { 131 | val int = 5 132 | val number: Number = 5 133 | val string = "foo" 134 | val intNullable: Int? = 5 135 | 136 | assertType(compileType { OptionalValue.of(int).getOrElse { int } }) 137 | assertType(compileType { OptionalValue.of(int).getOrElse { number } }) 138 | assertType(compileType { OptionalValue.of(number).getOrElse { int } }) 139 | assertType(compileType { OptionalValue.of(int).getOrElse { string } }) 140 | assertType(compileType { OptionalValue.of(string).getOrElse { int } }) 141 | assertType(compileType { OptionalValue.of(int).getOrElse { intNullable } }) 142 | assertType(compileType { OptionalValue.of(intNullable).getOrElse { int } }) 143 | 144 | // Actually, they compile to Any. It may be impossible to fix. 145 | // assertType(compileType { OptionalValue.of(int).getOrElse { float } }) 146 | // assertType(compileType { OptionalValue.of(float).getOrElse { int } }) 147 | 148 | assertType(compileType { OptionalValue.of(int).getOrElse { throw Exception() } }) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /typedmap-examples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":typedmap-core")) 7 | 8 | implementation(kotlin("reflect")) 9 | } 10 | -------------------------------------------------------------------------------- /typedmap-examples/src/main/kotlin/me/broot/typedmap/examples/examples.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalStdlibApi::class) 2 | package me.broot.typedmap.examples 3 | 4 | import me.broot.typedmap.core.impl.TypedKey 5 | import me.broot.typedmap.core.impl.simpleTypedMap 6 | import me.broot.typedmap.core.util.* 7 | 8 | fun main() { 9 | val sess = simpleTypedMap() 10 | 11 | sess += User("alice") 12 | 13 | println("User: ${sess.get()}") 14 | 15 | fun processUser(user: User) { 16 | println("Processed user: $user") 17 | } 18 | processUser(sess.get()) 19 | 20 | fun getUser(): User { 21 | return sess.get() 22 | } 23 | 24 | sess += listOf(1, 2, 3, 4, 5) 25 | sess += listOf("a", "b", "c", "d", "e") 26 | 27 | println("List: ${sess.get>()}") 28 | println("List: ${sess.get>()}") 29 | 30 | sess[Username] = "alice" 31 | sess[SessionId] = "0123456789abcdef" 32 | sess[VisitsCount] = 42 33 | 34 | println("Username: ${sess[Username]}") 35 | println("SessionId: ${sess[SessionId]}") 36 | println("Visits: ${sess[VisitsCount]}") 37 | 38 | sess[UserIds] = listOf(1, 2, 3, 4, 5) 39 | sess[Labels] = listOf("a", "b", "c", "d", "e") 40 | 41 | println("UserIds: ${sess[UserIds]}") 42 | println("Labels: ${sess[Labels]}") 43 | 44 | sess[OrderKey(1)] = Order(1, listOf("item1", "item2")) 45 | sess[OrderKey(2)] = Order(2, listOf("item3", "item4")) 46 | 47 | println("OrderKey(1): ${sess[OrderKey(1)]}") 48 | println("OrderKey(2): ${sess[OrderKey(2)]}") 49 | 50 | sess += AutoOrder(1, listOf("item1", "item2")) 51 | sess += AutoOrder(2, listOf("item3", "item4")) 52 | 53 | println("AutoOrderKey(1): ${sess[AutoOrderKey(1)]}") 54 | println("AutoOrderKey(2): ${sess[AutoOrderKey(2)]}") 55 | } 56 | 57 | data class User( 58 | val username: String 59 | ) 60 | 61 | object Username : TypedKey() 62 | object SessionId : TypedKey() 63 | object VisitsCount : TypedKey() 64 | 65 | object UserIds : TypedKey>() 66 | object Labels : TypedKey>() 67 | 68 | data class Order( 69 | val orderId: Int, 70 | val items: List 71 | ) 72 | 73 | data class OrderKey( 74 | val orderId: Int 75 | ) : TypedKey() 76 | 77 | data class AutoOrder( 78 | val orderId: Int, 79 | val items: List 80 | ) : AutoKey { 81 | override val typedKey get() = AutoOrderKey(orderId) 82 | } 83 | 84 | data class AutoOrderKey( 85 | val orderId: Int 86 | ) : TypedKey() 87 | --------------------------------------------------------------------------------