A rule-based entity management library written in Kotlin
implementation("so.kciter:thing:{version}")
data class Person(
val email: String,
val creditCard: String
): Thing<Person> {
override val rule: Rule<Person>
get() = Rule {
Normalization {
Person::email { trim() }
Person::creditCard { trim() }
}
Validation {
Person::email { email() }
}
Redaction {
Person::creditCard { creditCard() }
}
}
}
val person = Person(
email = " kciter@naver ",
creditCard = "1234-1234-1234-1234"
)
println(person)
// Person(email= kciter@naver , creditCard=1234-1234-1234-1234)
println(person.normalize())
// Person(email=kciter@naver, creditCard=1234-1234-1234-1234)
println(person.validate())
// ValidationResult.Invalid(dataPath=.email, message=must be a valid email address)
println(person.redact())
// Person(email=kciter@naver, creditCard=[REDACTED])
Bad data can always be entered. You need to filter out bad data. In this case, you can use Validation
.
For example, you can validate the email field.
data class Person(
val email: String
): Thing<Person> {
override val rule: Rule<Person>
get() = Rule {
Validation {
Person::email { email() }
}
}
}
Then run the validate
function, which returns the result of the validation.
val person = Person(
email = "kciter@naver"
)
println(person.validate())
// ValidationResult.Invalid(dataPath=.email, message=must be a valid email address)
You can also use different logic based on the validation results.
val result = person.validate()
when (result) {
is ValidationResult.Valid -> {
/* ... */
}
is ValidationResult.Invalid -> {
/* ... */
}
}
Data 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
.
For example, you can trim
login form data.
data class Login(
val email: String,
val password: String
): Thing<Person> {
override val rule: Rule<Login>
get() = Rule {
Normalization {
Login::email { trim() }
Login::password { trim() }
}
}
}
Then run the normalize
function, which changes the data to the correct form.
val loginData = Login(
email = " [email protected] ",
password = "1q2w3e4r!"
)
println(loginData.normalize()) // Login([email protected], password=1q2w3e4r!)
Sometimes there's information you don't want to show. In such cases we can use the Redaction
.
For example, card information can be sensitive, so write a condition in the rule
to redact if the creditCard
field contains card information.
data class Foo(
val creditCard: String
): Thing<Person> {
override val rule: Rule<Foo>
get() = Rule {
Redaction {
Foo::creditCard { creditCard() }
}
}
}
Then run the redact
function, which changes the data to [REDACTED]
.
val foo = Foo(
creditCard = "1234-1234-1234-1234"
)
foo.redact()
println(foo) // Foo(creditCard=[REDACTED])
If you want to use Spring Boot and Thing together, you can use thing-spring
.
implementation("so.kciter:thing:{version}")
implementation("so.kciter:thing-spring:{version}")
You can use the @ThingHandler
annotation instead of the @Validated
annotation in Bean Validation(JSR-380).
If 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.
@ThingHandler
@RestController
@RequestMapping("/api")
class ApiController {
@PostMapping
fun createPerson(@RequestBody person: Person): AnyResponse {
/* ... */
}
}
Instead 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.
@ThingHandler
@PostMapping
fun createPerson(@RequestBody person: Person): AnyResponse {
/* ... */
}
Unfortunately, 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.
data class Person(
val email: String
): Thing<Person> {
@JsonIgnore
override val rule: Rule<Person>
get() = Rule {
Validation {
Person::email { email() }
}
}
}
If you want more detail, see thing-spring-example.
Thing | Bean Validation | |
---|---|---|
How to use | Kotlin DSL | Annotation |
Custom Validation | Easy to extend | Difficult to extend |
Nested | Easy | Confuse |
Support Iterable, Array, Map | ✅ | ❌ |
Validation | ✅ | ✅ |
Normalization | ✅ | ❌ |
Redaction | ✅ | ❌ |
Can use with Spring Boot | ✅ | ✅ |
Bean 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.
Thing supports nested data. For example, if you have a Group
object that contains a person
field, you can use it as follows:
data class Person(
val name: String,
val email: String
)
data class Group(
val person: Person
): Thing<Group> {
override val rule: Rule<Group>
get() = Rule {
Validation {
Group::person {
Person::name { notEmpty() }
Person::email { email() }
}
}
}
}
Thing 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:
data class Person(
val name: String,
val email: String
)
data class Group(
val people: List<Person>
): Thing<Group> {
override val rule: Rule<Group>
get() = Rule {
Validation {
Group::people {
onEach {
Person::name { notEmpty() }
Person::email { email() }
}
}
}
}
}
You may want to add custom rules in addition to the ones provided by default. If so, you can do so as follows:
data class Foo(
val data: String
): Thing<Person> {
override val rule: Rule<Foo>
get() = Rule {
Validation {
Foo::data {
addValidator("must be `bar`") {
it == "bar"
}
}
}
}
}
Normalization
and Redaction
also addition custom rules.
data class Foo(
val data: String
): Thing<Person> {
override val rule: Rule<Foo>
get() = Rule {
Normalization {
Foo::data {
addNormalizer {
it.replace("bar", "foo")
}
}
}
Redaction {
Foo::data {
addRedactor("[REDACTED]") {
it.contains("bar")
}
}
}
}
}
If you need a common rule that you use in multiple places, you can write it like this:
fun ValidationRuleBuilder<String>.isBar() {
addValidator("must be `bar`") {
it == "bar"
}
}
fun NormalizationRuleBuilder<String>.replaceBarToFoo() {
addNormalizer {
it.replace("bar", "foo")
}
}
fun RedactionRuleBuilder<String>.redactBar() {
addRedactor("[REDACTED]") {
it.contains("bar")
}
}
data class Foo(
val data: String
): Thing<Person> {
override val rule: Rule<Foo>
get() = Rule {
Normalization {
Foo::data { replaceBarToFoo() }
}
Validation {
Foo::data { isBar() }
}
Redaction {
Foo::data { redactBar() }
}
}
}
- konform
- If you need multi-platform validator, recommend this.
- redact-pii
Thing is made available under the MIT License.