Last active
January 23, 2020 16:56
-
-
Save DenisVerkhoturov/09938bec36d584e6fef2542a609aad54 to your computer and use it in GitHub Desktop.
Scala micro types using value classes and smart constructors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cats.Show | |
import cats.data.{ NonEmptyChain, NonEmptyList, Validated } | |
import cats.data.Validated.{ Invalid, Valid } | |
import scala.util.control.NoStackTrace | |
trait Wrapped[T] extends Any { | |
def unwrap: T | |
def canEqual(that: Any): Boolean = this.getClass.isInstance(that) | |
override def equals(that: Any): Boolean = canEqual(that) && this.unwrap.equals(that.asInstanceOf[Wrapped[T]].unwrap) | |
override def hashCode: Int = this.getClass.hashCode + unwrap.hashCode() | |
override def toString: String = s"${this.getClass.getSimpleName}(${unwrap.toString})" | |
} | |
object Wrapped { | |
trait Companion { | |
type Type | |
type Error | |
type Wrapper <: Wrapped[Type] | |
type ValidationResult[A] = Validated[NonEmptyChain[Error], A] | |
implicit def wrappedOrdering(implicit ord: Ordering[Type]): Ordering[Wrapper] = Ordering.by(_.unwrap) | |
protected def create(value: Type): Wrapper | |
protected def validate(value: Type): ValidationResult[Type] | |
def from(value: Type): ValidationResult[Wrapper] = | |
validate(value) match { | |
case Valid(x) => Valid(create(x)) | |
case Invalid(errors) => Invalid(errors) | |
} | |
final def apply(value: Type)(implicit evidence: Wrapped.Enable.Unsafe.type, show: Show[Error]): Wrapper = | |
validate(value) match { | |
case Valid(x) => create(x) | |
case Invalid(errors) => throw new Enable.UnsafeException(errors.toNonEmptyList) | |
} | |
} | |
object Enable { | |
implicit object Unsafe | |
final class UnsafeException[T: Show](errors: NonEmptyList[T]) extends Throwable with NoStackTrace { | |
override lazy val getMessage: String = errors.show | |
} | |
} | |
} | |
final class Email private (override val unwrap: String) extends AnyVal with Wrapped[String] | |
object Email extends Wrapped.Companion { | |
type Type = String | |
type Wrapper = Email | |
type Error = String | |
override protected def create(value: String): Email = new Email(value) | |
override def validate(value: String): ValidationResult[String] = Validated.cond( | |
value == "no-reply@example.com", | |
value, | |
NonEmptyChain.one("I dont like your email.") | |
) | |
} | |
final class UserName private (override val unwrap: String) extends AnyVal with Wrapped[String] | |
object UserName extends Wrapped.Companion { | |
type Type = String | |
type Wrapper = UserName | |
type Error = String | |
override protected def create(value: String): UserName = new UserName(value) | |
override protected def validate(value: String): ValidationResult[String] = Validated.cond( | |
value != "John De Goes", | |
value, | |
NonEmptyChain.one("We don't need any De Goeses over here!") | |
) | |
} | |
final class Password private (override val unwrap: String) extends AnyVal with Wrapped[String] | |
object Password extends Wrapped.Companion { | |
type Type = String | |
type Wrapper = Password | |
type Error = String | |
override protected def create(value: String): Password = new Password(value) | |
override protected def validate(value: String): ValidationResult[String] = Validated.cond( | |
value == "qwerty", | |
value, | |
NonEmptyChain( | |
"You won't be able to remember this password, use qwerty instead.", | |
"But actually we can have multiple reasons to reject the value" | |
) | |
) | |
} | |
/** | |
* Now since we lifted all the validation in types that user composed from and | |
* there is no special validation logic of how the values correspond to each other | |
* we are OK with `copy` and `apply` that are generated by scala. | |
*/ | |
final case class User private (email: Email, name: UserName, password: Password) | |
object Main extends App { | |
import cats.implicits._ | |
val user = (Email.from(""), UserName.from(""), Password.from("")).mapN(User) | |
import Wrapped.Enable.Unsafe | |
User(Email(""), UserName(""), Password("")) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment