Skip to content

Instantly share code, notes, and snippets.

@DenisVerkhoturov
Last active January 23, 2020 16:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DenisVerkhoturov/09938bec36d584e6fef2542a609aad54 to your computer and use it in GitHub Desktop.
Save DenisVerkhoturov/09938bec36d584e6fef2542a609aad54 to your computer and use it in GitHub Desktop.
Scala micro types using value classes and smart constructors
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