Skip to content

Commit

Permalink
Close #325 - [refined4s-chimney] Add refined4s.modules.chimney.deriva…
Browse files Browse the repository at this point in the history
…tion.generic.auto for auto derivation for Chimney
  • Loading branch information
kevin-lee committed Aug 4, 2024
1 parent 09ba446 commit a27c1a0
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package refined4s.modules.chimney.derivation.generic

import io.scalaland.chimney
import io.scalaland.chimney.{PartialTransformer, Transformer}
import refined4s.{Coercible, RefinedCtor}

/** @author Kevin Lee
* @since 2024-08-03
*/
trait auto {

given unwrapNewTypeForChimney[Type, A](using coercible: Coercible[Type, A]): Transformer[Type, A] = coercible(_)

given unwrapAndWrapNewTypeForChimney[B, A, C](using coercible1: Coercible[B, A], coercible2: Coercible[A, C]): Transformer[B, C] =
b => coercible2(coercible1(b))

given wrapRefinedForChimney[A, Type](
using refinedCtor: RefinedCtor[Type, A]
): PartialTransformer[A, Type] = PartialTransformer[A, Type] { value =>
chimney.partial.Result.fromEitherString(refinedCtor.create(value))
}

given wrapRefinedToRefinedForChimney[A, Type1, Type2](
using refinedCtor: RefinedCtor[Type2, A],
coercible: Coercible[Type1, A],
): PartialTransformer[Type1, Type2] = PartialTransformer[Type1, Type2] { value =>
chimney.partial.Result.fromEitherString(refinedCtor.create(coercible(value)))
}

}
object auto extends auto
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package refined4s.modules.chimney.derivation.generic

import cats.syntax.all.*
import hedgehog.*
import hedgehog.extra.refined4s.gens.NumGens
import hedgehog.runner.*
import io.scalaland.chimney
import refined4s.modules.chimney.derivation.generic.auto.given
import refined4s.types.all.PosInt
import refined4s.{Newtype, Refined}

/** @author Kevin Lee
* @since 2024-08-04
*/
object autoSpec extends Properties {
import TestTypes.*

override def tests: List[Test] = List(
property("test Newtype", testNewtype),
property("test Newtype[Refined]", testRefinedNewtype),
property("test Refined (partial into)", testRefined),
property("test case class", testCaseClass),
property("test case class (partial into)", testCaseClassPartial),
)

def testNewtype: Property =
for {
nameString <- Gen
.string(Gen.alpha, Range.linear(1, 10))
.list(Range.singleton(2))
.map(_.mkString(" "))
.log("nameString")
} yield {
import io.scalaland.chimney.dsl.*
val expected = Bar.Label(nameString)

val name = Foo.Name(nameString)
val actual = name.into[Bar.Label].transform
expected ==== actual
}

def testRefinedNewtype: Property =
for {
idNum <- NumGens.genPosIntMaxTo(PosInt(Int.MaxValue)).log("idNum")
id = Foo.Id(idNum)
} yield {
import io.scalaland.chimney.dsl.*
val expected = Bar.Code(idNum)

val actual = id.into[Bar.Code].transform
expected ==== actual
}

def testRefined: Property =
for {
usernameString <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("usernameString")
domainString <- Gen.string(Gen.alpha, Range.linear(2, 3)).list(Range.linear(2, 3)).map(_.mkString(".")).log("domainString")
emailString = s"$usernameString@$domainString"

} yield {
import io.scalaland.chimney.dsl.*
val expected = chimney.partial.Result.fromValue(Bar.Email.unsafeFrom(emailString))

val email: Foo.Email = Foo.Email.unsafeFrom(emailString)

val actual = email.intoPartial[Bar.Email].transform
expected ==== actual
}

def testCaseClass: Property =
for {
idNum <- NumGens.genPosIntMaxTo(PosInt(Int.MaxValue)).log("idNum")
nameString <- Gen
.string(Gen.alpha, Range.linear(1, 10))
.list(Range.singleton(2))
.map(_.mkString(" "))
.log("nameString")

id = Something.Id(idNum)
name = Something.Name(nameString)
} yield {
import io.scalaland.chimney.dsl.*

val expected = SomethingElse(SomethingElse.Code(idNum), SomethingElse.InnerThing(SomethingElse.Label(nameString)))

val actual = Something(id, Something.InnerThing(name)).into[SomethingElse].transform
expected ==== actual
}

def testCaseClassPartial: Property =
for {
idNum <- NumGens.genPosIntMaxTo(PosInt(Int.MaxValue)).log("idNum")
nameString <- Gen
.string(Gen.alpha, Range.linear(1, 10))
.list(Range.singleton(2))
.map(_.mkString(" "))
.log("nameString")

usernameString <- Gen.string(Gen.alpha, Range.linear(3, 10)).log("usernameString")
domainString <- Gen.string(Gen.alpha, Range.linear(2, 3)).list(Range.linear(2, 3)).map(_.mkString(".")).log("domainString")
emailString = s"$usernameString@$domainString"

id = Foo.Id(idNum)
name = Foo.Name(nameString)
email = Foo.Email.unsafeFrom(emailString)
} yield {
import io.scalaland.chimney.dsl.*

val expected =
chimney.partial.Result.fromValue(Bar(Bar.Code(idNum), Bar.Baz(Bar.Label(nameString), Bar.Email.unsafeFrom(emailString))))

val actual = Foo(id, Foo.Baz(name, email)).intoPartial[Bar].transform
expected ==== actual
}

}

object TestTypes {
val emailRegEx =
"""([a-zA-Z0-9]+([-_\.\+]+[a-zA-Z0-9]+)*@[a-zA-Z0-9]+([-_]+[a-zA-Z0-9]+)*(?:[.][a-zA-Z0-9]+([-_]+[a-zA-Z0-9]+)*)+)""".r

final case class Foo(id: Foo.Id, baz: Foo.Baz)
object Foo {
type Id = Id.Type
object Id extends Newtype[PosInt]

type Name = Name.Type
object Name extends Newtype[String]

type Email = Email.Type
object Email extends Refined[String] {

override def invalidReason(a: String): String = s"Invalid email: $a"

override def predicate(a: String): Boolean = emailRegEx.findFirstMatchIn(a).isDefined
}

final case class Baz(name: Name, email: Email)
}

final case class Bar(id: Bar.Code, baz: Bar.Baz)
object Bar {
type Code = Code.Type
object Code extends Newtype[PosInt]

type Label = Label.Type
object Label extends Newtype[String]

type Email = Email.Type
object Email extends Refined[String] {

override def invalidReason(a: String): String = s"Invalid email: $a"

override def predicate(a: String): Boolean = emailRegEx.findFirstMatchIn(a).isDefined
}

final case class Baz(name: Label, email: Email)
}

final case class Something(id: Something.Id, innerThing: Something.InnerThing)
object Something {
type Id = Id.Type
object Id extends Newtype[PosInt]

type Name = Name.Type
object Name extends Newtype[String]

final case class InnerThing(name: Name)
}

final case class SomethingElse(id: SomethingElse.Code, innerThing: SomethingElse.InnerThing)
object SomethingElse {
type Code = Code.Type
object Code extends Newtype[PosInt]

type Label = Label.Type
object Label extends Newtype[String]

final case class InnerThing(name: Label)
}

}

0 comments on commit a27c1a0

Please sign in to comment.