Skip to content

Instantly share code, notes, and snippets.

@arturaz
Created May 16, 2024 13:27
Show Gist options
  • Save arturaz/46d6c225bae1dfe3aecf2e93db9751a6 to your computer and use it in GitHub Desktop.
Save arturaz/46d6c225bae1dfe3aecf2e93db9751a6 to your computer and use it in GitHub Desktop.
package app.prelude.utils
import neotype.Newtype
import io.scalaland.chimney.PartialTransformer
import alleycats.Empty
import app.prelude.Prelude.upickle_
import io.scalaland.chimney.partial.Result.Errors
import io.scalaland.chimney.Transformer
/** Provides a newtype that is unvalidated.
*
* Usually when dealing with input forms we have to have a type that is not valid until user inputs things into it.
*
* This is an example of how to do it.
* {{{
* type OrganizationName = OrganizationName.Type
* object OrganizationName extends Newtype[String] {
* override def validate(input: String): Boolean | String = {
* if (input.isBlank()) s"Organization name cannot be blank"
* else if (input != input.trim()) s"Organization name cannot contain leading or trailing whitespace"
* else true
* }
* }
*
* type OrganizationNameForm = OrganizationNameForm.Type
* object OrganizationNameForm extends UnvalidatedNewtypeOf(OrganizationName) {
* // This line is optional, see the documentation for [[UnvalidatedNewtypeOf#TValidatedWrapper]]
* override type TValidatedWrapper = OrganizationName
* }
* }}}
*
* `OrganizationNameForm` will get a `.validate` extension method that will turn it into an `OrganizationName`.
*/
trait UnvalidatedNewtypeOf[
TValidatedUnderlying,
TValidatedWrapperCompanion <: Newtype[TValidatedUnderlying],
](val companion: TValidatedWrapperCompanion)(using
val transformer: PartialTransformer[TValidatedUnderlying, companion.Type],
empty: Empty[TValidatedUnderlying],
rw: upickle_.ReadWriter[TValidatedUnderlying],
) {
opaque type Type = TValidatedUnderlying
/** You can do this to improve generated type signatures:
*
* {{{
* object OrganizationNameForm extends UnvalidatedNewtypeOf(OrganizationName) {
* override type TValidatedWrapper = OrganizationName
* }
* }}}
*
* Without this, the generated type signature would be:
* {{{
* val a = OrganizationNameForm("test")
* val b: Either[String, OrganizationNameForm.companion.Type] = a.validate
* }}}
*
* With it, the generated type signature would be:
* {{{
* val a = OrganizationNameForm("test")
* val b: Either[String, OrganizationNameForm.Type] = a.validate
* }}}
*/
type TValidatedWrapper = companion.Type
def apply(underlying: TValidatedUnderlying): Type = underlying
/** [[Transformer]] instead of [[Conversion]] that goes from raw type to the newtype because [[Conversion]]s are
* implicit and we don't want that.
*/
given Transformer[TValidatedUnderlying, Type] = a => a
/** Implicit [[Conversion]] from the newtype to the raw type. */
given Conversion[Type, TValidatedUnderlying] = a => a
given Empty[Type] = Empty(apply(empty.empty))
given upickle_.ReadWriter[Type] = rw.bimap(_.unwrap, apply)
given partialTransformer: PartialTransformer[Type, TValidatedWrapper] = PartialTransformer(transformer.transform(_))
given Validatable[Type] with
override def validate(value: Type): Option[Errors] = partialTransformer.transform(value).asEither.left.toOption
extension (a: Type) {
def unwrap: TValidatedUnderlying = a
/** Validates the value and returns the wrapped value if successful.
*
* Returns [[String]] on failure because [[neotype.Newtype.make]] returns an [[Either]] of [[String]].
*/
def validate: Either[String, TValidatedWrapper] =
transformer.transform(a).asEither.left.map(_.errors.head.message.asString)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment