diff --git a/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala new file mode 100644 index 00000000..e93ba93a --- /dev/null +++ b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala @@ -0,0 +1,71 @@ +package cats.derived + +import cats.Show +import shapeless3.deriving.{Continue, K0, Labelling} + +import scala.annotation.implicitNotFound +import scala.compiletime.* +import scala.deriving.Mirror + +trait ShowPretty[A] extends Show[A]: + def showLines(a: A): List[String] + def show(a: A): String = showLines(a).mkString(System.lineSeparator) + +object ShowPretty: + inline def apply[A](using A: ShowPretty[A]): A.type = A + +@implicitNotFound("""Could not derive an instance of ShowPretty[A] where A = ${A}. +Make sure that A satisfies one of the following conditions: + * it is a case class where all fields have a Show instance + * it is a sealed trait where all subclasses have a Show instance""") +type DerivedShowPretty[A] = Derived[ShowPretty[A]] +object DerivedShowPretty: + opaque type Or[A] = A => List[String] + object Or extends OrInstances: + def apply[A](instance: A => List[String]): Or[A] = instance + extension [A](or: Or[A]) def apply(a: A): List[String] = or(a) + + sealed abstract class OrInstances: + inline given [A]: Or[A] = summonFrom { + case instance: Show[A] => Or((a: A) => instance.show(a).split(System.lineSeparator).toList) + case derived: DerivedShowPretty[A] => Or(derived.instance.showLines(_)) + } + + inline def apply[A]: ShowPretty[A] = + import DerivedShowPretty.given + summonInline[DerivedShowPretty[A]].instance + + given [A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]): DerivedShowPretty[A] = + new Product[A] {} + + given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedShowPretty[A] = + new Coproduct[A] {} + + trait Product[A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]) extends ShowPretty[A]: + def showLines(a: A): List[String] = + val prefix = labelling.label + val labels = labelling.elemLabels + val n = labels.size + if n <= 0 then List(s"$prefix()") + else + var lines: List[String] = List(")") + val inner = inst.project(a)(n - 1)([t] => (show: Or[t], x: t) => show.apply(x)) + inner match + case Nil => lines = s" ${labels(n - 1)} = \"\"," :: lines + case h :: t => lines = s" ${labels(n - 1)} = $h" :: t.map(s => " " + s) ::: lines + var i = n - 2 + while i >= 0 do + val inner = inst.project(a)(i)([t] => (show: Or[t], x: t) => show.apply(x)) + inner match + case Nil => lines = s" ${labels(i)} = \"\"," :: lines + case v :: Nil => lines = s" ${labels(i)} = $v," :: lines + case h :: t => lines = s" ${labels(i)} = $h" :: t.init.map(s => " " + s) ::: s" ${t.last}," :: lines + i -= 1 + + lines = s"$prefix(" :: lines + + lines + + trait Coproduct[A](using inst: K0.CoproductInstances[Or, A]) extends ShowPretty[A]: + def showLines(a: A): List[String] = + inst.fold(a)([t] => (st: Or[t], t: t) => st.apply(t)) diff --git a/core/src/main/scala-3/cats/derived/package.scala b/core/src/main/scala-3/cats/derived/package.scala index 2bd150fa..deebb012 100644 --- a/core/src/main/scala-3/cats/derived/package.scala +++ b/core/src/main/scala-3/cats/derived/package.scala @@ -29,6 +29,7 @@ extension (x: MonoidK.type) inline def derived[F[_]]: MonoidK[F] = DerivedMonoid extension (x: Contravariant.type) inline def derived[F[_]]: Contravariant[F] = DerivedContravariant[F] extension (x: Invariant.type) inline def derived[F[_]]: Invariant[F] = DerivedInvariant[F] extension (x: PartialOrder.type) inline def derived[A]: PartialOrder[A] = DerivedPartialOrder[A] +extension (x: ShowPretty.type) inline def derived[A]: ShowPretty[A] = DerivedShowPretty[A] object semiauto: inline def eq[A]: Eq[A] = DerivedEq[A] @@ -54,6 +55,7 @@ object semiauto: inline def contravariant[F[_]]: Contravariant[F] = DerivedContravariant[F] inline def invariant[F[_]]: Invariant[F] = DerivedInvariant[F] inline def partialOrder[A]: PartialOrder[A] = DerivedPartialOrder[A] + inline def showPretty[A]: ShowPretty[A] = DerivedShowPretty[A] object auto: object eq: @@ -124,3 +126,6 @@ object auto: object partialOrder: inline given [A](using NotGiven[PartialOrder[A]]): PartialOrder[A] = DerivedPartialOrder[A] + + object showPretty: + inline given [A](using NotGiven[Show[A]]): ShowPretty[A] = DerivedShowPretty[A] diff --git a/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala new file mode 100644 index 00000000..8d51de3a --- /dev/null +++ b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala @@ -0,0 +1,193 @@ +package cats +package derived + +import cats.laws.discipline.SerializableTests +import scala.compiletime.* + +class ShowPrettySuite extends KittensSuite: + import ShowPrettySuite.* + import ShowPrettySuite.given + import TestDefns.* + + inline def showPretty[A](value: A): String = + summonInline[ShowPretty[A]].show(value) + + inline def testShowPretty(context: String): Unit = { + checkAll(s"$context.ShowPretty is Serializable", SerializableTests.serializable(summonInline[ShowPretty[IntTree]])) + + test(s"$context.ShowPretty[Foo]") { + val value = Foo(42, Option("Hello")) + val pretty = """ + |Foo( + | i = 42, + | b = Some(Hello) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[Outer]") { + val value = Outer(Inner(3)) + val pretty = """ + |Outer( + | in = Inner( + | i = 3 + | ) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[IntTree]") { + val value: IntTree = IntNode(IntLeaf(1), IntNode(IntNode(IntLeaf(2), IntLeaf(3)), IntLeaf(4))) + val pretty = """ + |IntNode( + | l = IntLeaf( + | t = 1 + | ), + | r = IntNode( + | l = IntNode( + | l = IntLeaf( + | t = 2 + | ), + | r = IntLeaf( + | t = 3 + | ) + | ), + | r = IntLeaf( + | t = 4 + | ) + | ) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[GenericAdt[Int]]") { + val value: GenericAdt[Int] = GenericAdtCase(Some(1)) + val pretty = """ + |GenericAdtCase( + | value = Some(1) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[People]") { + val value = People("Kai", ContactInfo("303-123-4567", Address("123 1st St", "New York", "NY"))) + val pretty = """ + |People( + | name = Kai, + | contactInfo = ContactInfo( + | phoneNumber = 303-123-4567, + | address = 123 1st St New York NY + | ) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[ListField]") { + val value = ListField("a", List(ListFieldChild(1))) + val pretty = """ + |ListField( + | a = a, + | b = List(ListFieldChild( + | c = 1 + | )) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[Interleaved[Int]]") { + val value = Interleaved(1, 2, 3, Vector(4, 5, 6), "789") + val pretty = """ + |Interleaved( + | i = 1, + | t = 2, + | l = 3, + | tt = Vector(4, 5, 6), + | s = 789 + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty[Tree[Int]]") { + val value: Tree[Int] = Node(Leaf(1), Node(Node(Leaf(2), Leaf(3)), Leaf(4))) + val pretty = """ + |Node( + | left = Leaf( + | value = 1 + | ), + | right = Node( + | left = Node( + | left = Leaf( + | value = 2 + | ), + | right = Leaf( + | value = 3 + | ) + | ), + | right = Leaf( + | value = 4 + | ) + | ) + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + + test(s"$context.ShowPretty respects existing instances") { + val value = Box(Bogus(42)) + val pretty = """ + |Box( + | content = Blah + |) + """.stripMargin.trim + + assertEquals(showPretty(value), pretty) + } + } + + locally { + import auto.showPretty.given + testShowPretty("auto") + } + + locally { + import semiInstances.given + testShowPretty("semiauto") + } + +object ShowPrettySuite: + import TestDefns.* + + given Show[Address] = Show.show { a => + List(a.street, a.city, a.state).mkString(" ") + } + + final case class Bogus(value: Int) + object Bogus: + given Show[Bogus] = Show.show(_ => "Blah") + + object semiInstances: + given ShowPretty[Foo] = semiauto.showPretty + given ShowPretty[Outer] = semiauto.showPretty + given ShowPretty[IntTree] = semiauto.showPretty + given ShowPretty[GenericAdt[Int]] = semiauto.showPretty + given ShowPretty[People] = semiauto.showPretty + given ShowPretty[ListFieldChild] = semiauto.showPretty + given ShowPretty[ListField] = semiauto.showPretty + given ShowPretty[Interleaved[Int]] = semiauto.showPretty + given ShowPretty[Tree[Int]] = semiauto.showPretty + given ShowPretty[Box[Bogus]] = semiauto.showPretty