{"id":15343823,"url":"https://github.com/rocketraman/kpropmap","last_synced_at":"2025-04-15T03:25:21.300Z","repository":{"id":57722455,"uuid":"143082851","full_name":"rocketraman/kpropmap","owner":"rocketraman","description":"Type-safe (ish) maps in Kotlin","archived":false,"fork":false,"pushed_at":"2020-05-08T04:17:04.000Z","size":151,"stargazers_count":15,"open_issues_count":3,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-23T05:12:11.752Z","etag":null,"topics":["kotlin"],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rocketraman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-08-01T00:16:32.000Z","updated_at":"2023-11-15T08:50:40.000Z","dependencies_parsed_at":"2022-08-29T23:02:04.109Z","dependency_job_id":null,"html_url":"https://github.com/rocketraman/kpropmap","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocketraman%2Fkpropmap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocketraman%2Fkpropmap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocketraman%2Fkpropmap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocketraman%2Fkpropmap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rocketraman","download_url":"https://codeload.github.com/rocketraman/kpropmap/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240930306,"owners_count":19880448,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kotlin"],"created_at":"2024-10-01T10:51:31.788Z","updated_at":"2025-02-26T20:30:43.102Z","avatar_url":"https://github.com/rocketraman.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kpropmap\n\n[![Bintray](https://img.shields.io/bintray/v/rocketraman/maven/kpropmap)](https://bintray.com/rocketraman/maven/kpropmap)\n[![Maven Central](https://img.shields.io/maven-central/v/com.github.rocketraman/kpropmap)](https://search.maven.org/artifact/com.github.rocketraman/kpropmap)\n\n## Introduction\n\nKpropmap is a small Kotlin library that combines benefits from untyped and unstructured data like `Map`s, with\nthe typed and structured data such as provided by data classes. The main class `PropertyMap` implements \n`Map\u003cString, Any?\u003e`, but provides some additional methods to make consuming `Map` content safer and easier by\nallowing it's JSON-compatible values to be accessed with types using Kotlin `KProperty` instances (see \n[KProperty](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-property/index.html)).\n\nProperties are stored and matched by name and not by class. The extra meta-data provided by the properties of the data \nclass associated with the `PropertyMap` can be used for type checking and automatic conversions / copies of the \nunderlying data class or classes.\n\nThe primary use case for this is representing part of data classes as a flexible `Map` that represents all or subset of\nan immutable data class, while at the same time keeping much of the type information, which can be useful to check and\naccess the data fluently. This is useful when the data comes from \"messy\" external systems such as REST interfaces.\n\nImmutable data classes are all-or-nothing. If, say, one needs to implement a REST service that can receive updates to a\nsubset of the fields of a model data class, delegation to the REST deserialization layer / Jackson will not work, as\nthere is not enough information received to rebuild the entire data class. Furthermore, there may be complex business\nlogic associated with processing the information from the client. This type of partial / arbitrary data cannot be easily\nrepresented in a mutable data class structure, nor would that approach be very\n[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), since many fields will need to be declared at least twice:\nonce in the immutable structure in primary use as a model, and then again for the value-object layer used to communicate\nwith other architectural components.\n\n`PropertyMap` can be a useful in-between structure.\n\n### Introspecting Untyped Data via Data Class Properties\n\nThe untyped data received from an external system can be introspected and validated against the expected types\ngiven a data class of set of `KProperty`s. This can make validation and parsing logic type-safe and fluent,\nand works well with Kotlin's type inference capabilities.\n\n## Caveats\n\nThis does make heavy use of reflection, so benchmarking in your environment may be necessary. In addition,\nthis library currently requires the full 2MB+ `kotlin-reflect` artifact.\n\n## Prerequisites\n\nThere are no dependencies except `kotlin-reflect`.\n\n## Getting Started\n\n### Creating a Property Map\n\nThe primary class in `kpropmap` is `PropertyMap`. A `PropertyMap` can be created by wrapping an existing\n`Map` or `vararg Pair` e.g.:\n\n```kotlin\npropMapOf(\"foo\" to \"bar\")\n```\n\nIf the keys are `KProperty` instances, the key is automatically converted to the property name. Otherwise, the\n`toString()` of the passed value is used as the key. So this would be a more type-safe way to create the same\n`PropertyMap` as above:\n\n```kotlin\npropMapOf(MyDataClass::foo to \"bar\")\n```\n\nOr one can also be created from an existing data class via reflection:\n\n```kotlin\ndata class Foo(val a: Int, val b: Int)\nval foo = Foo(...)\nval p = propMapOf(foo)\n```\n\n### Using a Property Map\n\n#### Property Maps are Maps\n\nSince it implements `Map\u003cString, Any?\u003e` the standard map interface with `String` keys is always available. However, we\nrecommend using `KProperty` keys for type-safety. For example, to access a property value corresponding to `myProperty`\nin data class `MyDataClass` -- the return value is automatically the correct type, though it is always nullable to\nconform to the `Map` interface, which returns `null` for keys not present, as well as for keys present but with a `null`\nvalue:\n\n```kotlin\n// p is of type MyDataClass::myProperty (but nullable)\nval p = propMap[MyDataClass::myProperty]\n```\n\nand standard overloaded operators like `contains` can be used e.g.\n\n```kotlin\nval contains = MyDataClass::myProperty in propMap\n```\n\nand other standard Map operations such as `remove` also have overloads that accept a `KProperty` and return a value of\nthe appropriate type (though always nullable, because the contract of `remove` is like `get`: it returns `null` if\neither the key was not in the map, or the value was `null`:\n\n```kotlin\nval removed = propMap.remove(MyDataClass::myProperty)\n```\n\nSince `PropertyMap`'s use Strings as keys, one cannot define a property map like this:\n\n```kotlin\ndata class Foo(val a: Int)\ndata class Bar(val a: Int)\n\npropMapOf(Foo::a to 1, Bar::a to 2)\n```\n\nThis will fail-fast by throwing an `AssertionError` because the two `KProperty` keys have the same `name`, and\nthus creating the `PropertyMap` would lose data. Generally this is not a big issue since `PropertyMap`s are most\nuseful when they contain and represent data from one data class.\n\n#### Property Maps Have Typed Accessors\n\nProperty maps accessed with `KProperty` are typed, and will throw useful exceptions when the value type does\nnot match the property type. For example:\n\n```kotlin\ndata class MyDataClass(val foo: Int)\n\n// somebody sends us some data\nval p = propMapOf(\"foo\" to \"bar\")\n\n// throws a TypeMismatchException, because f is inferred as an Int? based on the type of `foo`\nval f = p[MyDataClass::foo]\n```\n\n#### Property Maps Can Be Applied to Existing Data Classes\n\nA `PropertyMap` can be \"applied\" to an existing data class to support the partial update / patching case. For example:\n\n```kotlin\ndata class Foo(val a: Int, val b: Int)\n\nval f = Foo(1, 2)\nval p = propMapOf(\"b\" to 5)\n\nval f1 = p.applyProps(f) // f1 = Foo(1, 5)\n```\n\n#### Property Maps Have Useful Accessors and Extensions\n\n##### Required\n\nThe `required` extension can be used to obtain a property that *must* be in the map. It will throw an\n`InvalidInputDataException` if the value is not in the map, and ALSO if the value is in the map but `null`:\n\n```kotlin\nval p = propMap.required(MyDataClass::myProperty)\n// if MyDataClass::myProperty is type T, then p is T\n```\n\nThe `required` extension behaves like a normal `get` in other respects. For example, it will throw\n`TypeMismatchException` like `get` if the field is present, but is the wrong type.\n \nThe `requiredNullable` extension can be used to obtain a nullable property that *must* be in the map. It will\nthrow an `InvalidInputDataException` if the value is not in the map, BUT NOT if the value in the map is `null`,\nin which case it will return `null`:\n\n```kotlin\nval p = propMap.requiredNullable(MyDataClass::myProperty)\n// if MyDataClass::myProperty is type T, then p is T?\n```\n\nIf `MyDataClass::myProperty` is not nullable, `requiredNullable` will throw an `IllegalArgumentException`.\n\n##### @Updateable Field Checks\n\nThe `checkRequired` extension can be used with data classes that have `@Updateable` annotations on the class itself,\nor on individual fields. If `checkRequired` is called, it will throw an `InvalidInputDataException` if an `Updateable`\nproperty is specified, is non-nullable, but has a null value in the `PropertyMap`. If this call succeeds without\nerror, one can be comfortable in using `required` in subsequently accessing individual fields.\n\nSimilarly, `checkInvalid` can be used to validate that only fields marked `@Updateable` have been provided in the\n`PropertyMap`. Any other fields present will cause a `InvalidInputDataException`. This can be a useful check before\n[applying](#property-maps-can-be-applied-to-existing-data-classes) a `PropertyMap` to a data class.\n\n##### Has Changed\n\nThe `hasChanged` method allows one to determine whether the `PropertyMap` contains a value that is different than the\none in an existing data class (or any arbitrary value obtained via a lambda).\n\n##### With Change\n\nThe `withChanged` methods allow one to execute code, or return a changed value, only if the `PropertyMap` contains a\nvalue that is different than the one in an existing data class (or any arbitrary value obtained via a lambda).\n\n##### Extraneous Keys\n\nThe `PropertyMap` may contain extraneous keys that are not mappable to a particular data class. The `keysNotIn` and\n`keysNotInProps` methods allow for easy identification of such data. For example:\n\n```kotlin\ndata class MyDataClass(val foo: Int)\nval p = propMapOf(\n  \"whatever\" to 1,\n  \"foo\" to 2\n)\nval keys = p.keysNotInProps(MyDataClass::class.memberProperties)\n// keys is list(\"whatever\")\n```\n\n#### Property Maps Can Be Deserialized to Data Classes\n\nIf all the properties are available, `PropertyMap`'s can be deserialized to data classes. The `deserialize` extension\ncan do this given a target type e.g.:\n\n```kotlin\nval foo = propMap.deserialize\u003cFoo\u003e()\n```\n\nConsider Jackson (with the kotlin module) for this type of wholesale deserialization.\n\n#### Property Maps Can Be Nested\n\nProperty maps can be nested and can thus map to and from nested data classes e.g.:\n\n```kotlin\ndata class Foo(val a: Int, val b: Bar)\ndata class Bar(val a: Int)\nval l = propMapOf(\n  Foo::a to 1, \n  Foo::b to propMapOf(\n    Bar:a to 2\n  )\n)\n```\n\nPartially supplied nested properties can also be applied to existing data structures as\n[above](#property-maps-can-be-applied-to-existing-data-classes).\n\nIf all the properties are available in the `PropertyMap`, then the entire structure can be deserialized as\n[above](#property-maps-can-be-deserialized-to-data-classes).\n\n#### Some Useful Conversions are Available\n\nSome additional useful conventions / conversions automatically performed. More may be added to this list as use cases\narise.\n\n##### Pairs are 2-element Lists\n\nA `PropertyMap` containing a 2-element `List` as a value can be obtained as a `Pair` when using a `PropertyMap` e.g.:\n\n```kotlin\ndata class Foo(val l: Pair\u003cString, Int\u003e)\nval p = propMapOf(Foo::l to listOf(\"answer\", 42))\nval pair = p[Foo::l] // pair is Pair\u003cString, Int\u003e(\"answer\", 42)\n```\n\n##### Instant and OffsetDateTime are Supported\n\nConversions for `Instant` and `OffsetDateTime` are supported.\n\n#### Exceptions Provide Useful Information\n\nBecause `PropertyMap` is generally used to read externally produced data, exceptional conditions are not unusual. For\nexample, non-nullable fields may be `null`, field values may not be the correct type, and so on.\n\nWhen a subclass of `PropertyMapException` occurs, the `Exception` will contain useful information about why and where\nthe Exception occurred. This information is suitable for producing good descriptive error responses e.g. a `400 Bad\nRequest` with details about which fields were problematic.\n\n## Built With\n\n* [Kotlin](https://kotlinlang.org/)\n\n## Contributing\n\nBe nice. Submit issues. Submit pull requests.\n\n## Authors\n\n* **Raman Gupta** - *Initial work* - [LinkedIn](https://www.linkedin.com/in/rocketraman/)\n\n## Contributors\n\nTODO()\n\n## TODOs\n\n* Make it more modular e.g. allow users to register additional conversions, make existing conversions come from\nbuilt-in modules\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frocketraman%2Fkpropmap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frocketraman%2Fkpropmap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frocketraman%2Fkpropmap/lists"}