{"id":17088099,"url":"https://github.com/kciter/thing","last_synced_at":"2025-04-12T21:07:16.597Z","repository":{"id":156870903,"uuid":"622251428","full_name":"kciter/thing","owner":"kciter","description":"A rule-based entity management library written in Kotlin","archived":false,"fork":false,"pushed_at":"2023-07-06T14:54:06.000Z","size":163,"stargazers_count":65,"open_issues_count":0,"forks_count":4,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-12T21:07:10.682Z","etag":null,"topics":["dsl","entity","kotlin","model","object","spring","validation","validator"],"latest_commit_sha":null,"homepage":"","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/kciter.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-04-01T15:07:45.000Z","updated_at":"2025-01-03T15:40:48.000Z","dependencies_parsed_at":"2023-05-02T00:02:02.145Z","dependency_job_id":"77b2f9fd-50ea-4b03-9d0e-e33fadbcb342","html_url":"https://github.com/kciter/thing","commit_stats":{"total_commits":61,"total_committers":3,"mean_commits":"20.333333333333332","dds":0.09836065573770492,"last_synced_commit":"eb4d1bfe5038a619fff8740bbe574af6adce54f4"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kciter%2Fthing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kciter%2Fthing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kciter%2Fthing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kciter%2Fthing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kciter","download_url":"https://codeload.github.com/kciter/thing/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248631677,"owners_count":21136562,"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":["dsl","entity","kotlin","model","object","spring","validation","validator"],"created_at":"2024-10-14T13:36:14.245Z","updated_at":"2025-04-12T21:07:16.575Z","avatar_url":"https://github.com/kciter.png","language":"Kotlin","readme":"\u003ch1 align='center'\u003e\n  Thing :dizzy:\n\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\u003cstrong\u003eA rule-based entity management library written in Kotlin\u003c/strong\u003e\u003c/p\u003e\n\n\u003cp align='center'\u003e\n  \u003ca href=\"https://cobalt.run\"\u003e\n    \u003cimg src=\"https://cobalt-static.s3.ap-northeast-2.amazonaws.com/cobalt-badge.svg\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://central.sonatype.com/artifact/so.kciter/thing\"\u003e\n    \u003cimg src='https://img.shields.io/maven-central/v/so.kciter/thing' alt='Latest version'\u003e\n  \u003c/a\u003e\n  \u003ca href=\"\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg\" alt=\"PRs welcome\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n## :rocket: Getting started\n```kotlin\nimplementation(\"so.kciter:thing:{version}\")\n```\n\n## :eyes: At a glance\n```kotlin\ndata class Person(\n  val email: String,\n  val creditCard: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cPerson\u003e\n    get() = Rule {\n      Normalization {\n        Person::email { trim() }\n        Person::creditCard { trim() }\n      }\n\n      Validation {\n        Person::email { email() }\n      }\n\n      Redaction {\n        Person::creditCard { creditCard() }\n      }\n    }\n}\n\nval person = Person(\n  email = \" kciter@naver   \",\n  creditCard = \"1234-1234-1234-1234\"\n)\n\nprintln(person)\n// Person(email= kciter@naver   , creditCard=1234-1234-1234-1234)\nprintln(person.normalize())\n// Person(email=kciter@naver, creditCard=1234-1234-1234-1234)\nprintln(person.validate())\n// ValidationResult.Invalid(dataPath=.email, message=must be a valid email address)\nprintln(person.redact())\n// Person(email=kciter@naver, creditCard=[REDACTED])\n```\n\n## :sparkles: Usecase\n### Validation\nBad data can always be entered. You need to filter out bad data. In this case, you can use `Validation`.\n\nFor example, you can validate the email field.\n```kotlin\ndata class Person(\n  val email: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cPerson\u003e\n    get() = Rule {\n      Validation {\n        Person::email { email() }\n      }\n    }\n}\n```\nThen run the `validate` function, which returns the result of the validation.\n```kotlin\nval person = Person(\n  email = \"kciter@naver\"\n)\nprintln(person.validate())\n// ValidationResult.Invalid(dataPath=.email, message=must be a valid email address)\n```\n\nYou can also use different logic based on the validation results.\n\n```kotlin\nval result = person.validate()\nwhen (result) {\n  is ValidationResult.Valid -\u003e {\n    /* ... */\n  }\n  is ValidationResult.Invalid -\u003e {\n    /* ... */\n  }\n}\n```\n\n### Normalization\nData often comes to us in the wrong form. Sometimes it's unavoidable if it's very different, but sometimes a little tweaking will put it in the right shape. In these cases, you can use `Normalization`.\n\nFor example, you can `trim` login form data.\n```kotlin\ndata class Login(\n  val email: String,\n  val password: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cLogin\u003e\n    get() = Rule {\n      Normalization {\n        Login::email { trim() }\n        Login::password { trim() }\n      }\n    }\n}\n```\nThen run the `normalize` function, which changes the data to the correct form.\n```kotlin\nval loginData = Login(\n  email = \"  kciter@naver.com    \",\n  password = \"1q2w3e4r!\"\n)\nprintln(loginData.normalize()) // Login(email=kciter@naver.com, password=1q2w3e4r!)\n```\n\n### Redaction\nSometimes there's information you don't want to show. In such cases we can use the `Redaction`.\n\nFor example, card information can be sensitive, so write a condition in the `rule` to redact if the `creditCard` field contains card information.\n```kotlin\ndata class Foo(\n  val creditCard: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cFoo\u003e\n    get() = Rule {\n      Redaction {\n        Foo::creditCard { creditCard() }\n      }\n    }\n}\n```\n\nThen run the `redact` function, which changes the data to `[REDACTED]`.\n```kotlin\nval foo = Foo(\n  creditCard = \"1234-1234-1234-1234\"\n)\n\nfoo.redact()\nprintln(foo) // Foo(creditCard=[REDACTED])\n```\n\n### With Spring Boot\nIf you want to use Spring Boot and Thing together, you can use `thing-spring`.\n\n```kotlin\nimplementation(\"so.kciter:thing:{version}\")\nimplementation(\"so.kciter:thing-spring:{version}\")\n```\n\nYou can use the `@ThingHandler` annotation instead of the `@Validated` annotation in [Bean Validation(JSR-380)](https://beanvalidation.org/2.0-jsr380/spec/).\n\nIf the Controller contains the `@ThingHandler` annotation, `ThingPostProcessor` check to see if a `Thing` object exists when the function is executed. If a `Thing` object exists, it normalizes it before running the function and then performs validation. And when we return the result, if it's a `Thing` object, we return it after redacting.\n\n\u003cimg src=\"./images/process.png\" style=\"width: 100%\"\u003e\n\n```kotlin\n@ThingHandler\n@RestController\n@RequestMapping(\"/api\")\nclass ApiController {\n  @PostMapping\n  fun createPerson(@RequestBody person: Person): AnyResponse {\n    /* ... */\n  }\n}\n```\n\nInstead of adding an annotation to a class, you can also add it to a method. In this case, it will only work on specific methods, not the class as a whole.\n\n```kotlin\n@ThingHandler\n@PostMapping\nfun createPerson(@RequestBody person: Person): AnyResponse {\n  /* ... */\n}\n```\n\nUnfortunately, this library has not yet ignored a response with `rule` property. So, when responding to the `Thing` entity, you need to add `@JsonIgnore` annotation to `rule` property.\n\n```kotlin\ndata class Person(\n  val email: String\n): Thing\u003cPerson\u003e {\n  @JsonIgnore\n  override val rule: Rule\u003cPerson\u003e\n    get() = Rule {\n      Validation {\n        Person::email { email() }\n      }\n    }\n}\n```\n\nIf you want more detail, see [thing-spring-example](./thing-spring-example).\n\n#### 🆚 Bean Validation\n|                              | Thing          | Bean Validation     |\n|------------------------------|----------------|---------------------|\n| How to use                   | Kotlin DSL     | Annotation          |\n| Custom Validation            | Easy to extend | Difficult to extend |\n| Nested                       | Easy           | Confuse             |\n| Support Iterable, Array, Map | ✅              | ❌                   |\n| Validation                   | ✅              | ✅                   |\n| Normalization                | ✅              | ❌                   |\n| Redaction                    | ✅              | ❌                   |\n| Can use with Spring Boot     | ✅              | ✅                   |\n\nBean Validation is a great library. However, it is not suitable for all cases. For example, if you want to normalize or redact data, you can't do it with Bean Validation. In this case, you can use Thing.\n\n### Nested\nThing supports nested data. For example, if you have a `Group` object that contains a `person` field, you can use it as follows:\n\n```kotlin\ndata class Person(\n  val name: String,\n  val email: String\n)\n\ndata class Group(\n  val person: Person\n): Thing\u003cGroup\u003e {\n  override val rule: Rule\u003cGroup\u003e\n    get() = Rule {\n      Validation {\n        Group::person {\n          Person::name { notEmpty() }\n          Person::email { email() }\n        }\n      }\n    }\n}\n```\n\n### Iterable, Array, Map\nThing supports `Iterable`, `Array`, and `Map` types. For example, if you have a `Group` object that contains a `people` field, you can use it as follows:\n\n```kotlin\ndata class Person(\n  val name: String,\n  val email: String\n)\n\ndata class Group(\n  val people: List\u003cPerson\u003e\n): Thing\u003cGroup\u003e {\n  override val rule: Rule\u003cGroup\u003e\n    get() = Rule {\n      Validation {\n        Group::people {\n          onEach {\n            Person::name { notEmpty() }\n            Person::email { email() }\n          }\n        }\n      }\n    }\n}\n```\n\n### Add Custom Rule\nYou may want to add custom rules in addition to the ones provided by default. If so, you can do so as follows:\n\n```kotlin\ndata class Foo(\n  val data: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cFoo\u003e\n    get() = Rule {\n      Validation {\n        Foo::data {\n          addValidator(\"must be `bar`\") {\n            it == \"bar\"\n          }\n        }\n      }\n    }\n}\n```\n`Normalization` and `Redaction` also addition custom rules.\n\n```kotlin\ndata class Foo(\n  val data: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cFoo\u003e\n    get() = Rule {\n      Normalization {\n        Foo::data {\n          addNormalizer {\n            it.replace(\"bar\", \"foo\")\n          }\n        }\n      }\n\n      Redaction {\n        Foo::data {\n          addRedactor(\"[REDACTED]\") {\n            it.contains(\"bar\")\n          }\n        }\n      }\n    }\n}\n```\n\nIf you need a common rule that you use in multiple places, you can write it like this:\n\n```kotlin\nfun ValidationRuleBuilder\u003cString\u003e.isBar() {\n  addValidator(\"must be `bar`\") {\n    it == \"bar\"\n  }\n}\n\nfun NormalizationRuleBuilder\u003cString\u003e.replaceBarToFoo() {\n  addNormalizer {\n    it.replace(\"bar\", \"foo\")\n  }\n}\n\nfun RedactionRuleBuilder\u003cString\u003e.redactBar() {\n  addRedactor(\"[REDACTED]\") {\n    it.contains(\"bar\")\n  }\n}\n\ndata class Foo(\n  val data: String\n): Thing\u003cPerson\u003e {\n  override val rule: Rule\u003cFoo\u003e\n    get() = Rule {\n      Normalization {\n        Foo::data { replaceBarToFoo() }\n      }\n\n      Validation {\n        Foo::data { isBar() }\n      }\n\n      Redaction {\n        Foo::data { redactBar() }\n      }\n    }\n}\n```\n\n## :love_letter: Reference\n* [konform](https://github.com/konform-kt/konform)\n  * If you need multi-platform validator, recommend this.\n* [redact-pii](https://github.com/solvvy/redact-pii)\n\n## :page_facing_up: License\n\nThing is made available under the [MIT License](./LICENSE).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkciter%2Fthing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkciter%2Fthing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkciter%2Fthing/lists"}