Reputation: 1
In the doc about Validation, when speaking of Smart constructor, it's suggested to use them on data class, but making this possible:
object EmptyAuthorName
data class Author private constructor(val name: String) {
companion object {
operator fun invoke(name: String): Either<EmptyAuthorName, Author> = either {
ensure(name.isNotEmpty()) { EmptyAuthorName }
Author(name)
}
}
}
Author("fo")
.map { it.copy(name = "") }
.run { println(this) } // Either.Right(Author(name=))
The EmptyAuthorName constraint wasn't respected. Really bad isn't it?
Redefining copy for the data class isn't possible...
Currently my only way out is to not use data class.
Am i missing something?
Upvotes: 0
Views: 434
Reputation:
The primary constructor of a data class is always available via the copy method. You either have to make sure that the result of the primary constructor is valid, or you must not use a data class.
In this case, the validation logic seems to be in a wrapper around the data class, not in the data class itself. That means you can do the validation on an instance of Author
, instead of "faking" a constructor call. Then you can call it after operations like map
, and you'll always get a properly validated Either
.
data class Author(val name: String) {
fun validate(): Either<EmptyAuthorName, Author> = either {
ensure(name.isNotEmpty()) { EmptyAuthorName }
this
}
}
Author("fo").validate()
.map { it.copy(name = "").validate() }
.run { println(this) } // Either.Left(EmptyAuthorName)
Another upside of doing it this way is that your code becomes easier to understand. In your example, you have a function that looks like an Author
constructor, but it doesn't return an instance of Author
(but an instance of Either
). That's confusing. Making a function that clearly specifies that it validates and returns a validated result is better and easier to understand.
Upvotes: 2