From 1190d937894f3a7b33d48a5033439ce0f31c5242 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Wed, 7 Dec 2022 12:09:53 +0000 Subject: [PATCH] Extensible MetaSchema (#454) * Extensible MetaSchema * Scala3 fix * Scalafix * Type alias to case class --- .../scala-2/zio/schema/MetaSchemaSpec.scala | 201 +++-- .../test/scala/zio/schema/MigrationSpec.scala | 42 +- .../src/main/scala/zio/schema/DeriveGen.scala | 24 +- .../main/scala/zio/schema/DynamicValue.scala | 4 +- .../src/main/scala/zio/schema/Schema.scala | 2 + .../scala/zio/schema/meta/AstRenderer.scala | 167 ++++ .../schema/meta/ExtensibleMetaSchema.scala | 746 ++++++++++++++++++ .../scala/zio/schema/meta/MetaSchema.scala | 717 ----------------- .../scala/zio/schema/meta/Migration.scala | 104 ++- .../zio/schema/meta/SchemaInstances.scala | 51 ++ .../main/scala/zio/schema/meta/package.scala | 12 + 11 files changed, 1233 insertions(+), 837 deletions(-) create mode 100644 zio-schema/shared/src/main/scala/zio/schema/meta/AstRenderer.scala create mode 100644 zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala delete mode 100644 zio-schema/shared/src/main/scala/zio/schema/meta/MetaSchema.scala create mode 100644 zio-schema/shared/src/main/scala/zio/schema/meta/SchemaInstances.scala diff --git a/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala b/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala index 38576d625..72a1c532c 100644 --- a/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala +++ b/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala @@ -3,9 +3,11 @@ package zio.schema import scala.collection.immutable.ListMap import zio._ +import zio.constraintless.TypeList._ import zio.schema.CaseSet._ import zio.schema.SchemaAssertions._ -import zio.schema.meta.{ MetaSchema, NodePath } +import zio.schema.meta.ExtensibleMetaSchema.Labelled +import zio.schema.meta.{ ExtensibleMetaSchema, MetaSchema, NodePath } import zio.test._ object MetaSchemaSpec extends ZIOSpecDefault { @@ -15,7 +17,7 @@ object MetaSchemaSpec extends ZIOSpecDefault { test("primitive") { check(SchemaGen.anyPrimitive) { case s @ Schema.Primitive(typ, _) => - assertTrue(MetaSchema.fromSchema(s) == MetaSchema.Value(typ)) + assertTrue(MetaSchema.fromSchema(s) == ExtensibleMetaSchema.Value[DynamicValue :: End](typ)) } } ), @@ -23,7 +25,9 @@ object MetaSchemaSpec extends ZIOSpecDefault { test("primitive") { check(SchemaGen.anyPrimitive) { case s @ Schema.Primitive(typ, _) => - assertTrue(MetaSchema.fromSchema(s.optional) == MetaSchema.Value(typ, optional = true)) + assertTrue( + MetaSchema.fromSchema(s.optional) == ExtensibleMetaSchema.Value[DynamicValue :: End](typ, optional = true) + ) } } ), @@ -32,8 +36,11 @@ object MetaSchemaSpec extends ZIOSpecDefault { check(SchemaGen.anyPrimitive) { case s @ Schema.Primitive(typ, _) => assertTrue( - MetaSchema.fromSchema(Schema.chunk(s)) == MetaSchema - .ListNode(MetaSchema.Value(typ, path = NodePath.root / "item"), NodePath.root) + MetaSchema.fromSchema(Schema.chunk(s)) == ExtensibleMetaSchema + .ListNode( + ExtensibleMetaSchema.Value[DynamicValue :: End](typ, path = NodePath.root / "item"), + NodePath.root + ) ) } } @@ -61,12 +68,15 @@ object MetaSchemaSpec extends ZIOSpecDefault { ) ) val expectedAst = - MetaSchema.Product( + ExtensibleMetaSchema.Product( id = TypeId.parse("zio.schema.MetaSchema.Product"), path = NodePath.root, fields = Chunk( - ("a", MetaSchema.Value(StandardType.StringType, NodePath.root / "a")), - ("b", MetaSchema.Value(StandardType.IntType, NodePath.root / "b")) + Labelled( + "a", + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.StringType, NodePath.root / "a") + ), + Labelled("b", ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root / "b")) ) ) assertTrue(MetaSchema.fromSchema(schema) == expectedAst) @@ -74,16 +84,27 @@ object MetaSchemaSpec extends ZIOSpecDefault { test("case class") { val schema = Schema[SchemaGen.Arity2] val expectedAst = - MetaSchema.Product( + ExtensibleMetaSchema.Product( id = TypeId.parse("zio.schema.SchemaGen.Arity2"), path = NodePath.root, fields = Chunk( - "value1" -> MetaSchema.Value(StandardType.StringType, NodePath.root / "value1"), - "value2" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.SchemaGen.Arity1"), - path = NodePath.root / "value2", - fields = Chunk( - "value" -> MetaSchema.Value(StandardType.IntType, NodePath.root / "value2" / "value") + Labelled( + "value1", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.StringType, NodePath.root / "value1") + ), + Labelled( + "value2", + ExtensibleMetaSchema.Product( + id = TypeId.parse("zio.schema.SchemaGen.Arity1"), + path = NodePath.root / "value2", + fields = Chunk( + Labelled( + "value", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.IntType, NodePath.root / "value2" / "value") + ) + ) ) ) ) @@ -96,17 +117,17 @@ object MetaSchemaSpec extends ZIOSpecDefault { val ast = MetaSchema.fromSchema(schema) val recursiveRef: Option[MetaSchema] = ast match { - case MetaSchema.Product(_, _, elements, _) => + case ExtensibleMetaSchema.Product(_, _, elements, _) => elements.find { - case ("r", _) => true - case _ => false - }.map(_._2) + case Labelled("r", _) => true + case _ => false + }.map(_.schema) case _ => None } assertTrue( recursiveRef.exists { - case MetaSchema.Ref(pathRef, _, _) => pathRef == Chunk.empty - case _ => false + case ExtensibleMetaSchema.Ref(pathRef, _, _) => pathRef == Chunk.empty + case _ => false } ) } @@ -123,28 +144,51 @@ object MetaSchemaSpec extends ZIOSpecDefault { )(_.asInstanceOf[SchemaGen.Arity2])(_.asInstanceOf[Any])(_.isInstanceOf[Any]) ) val expectedAst = - MetaSchema.Sum( + ExtensibleMetaSchema.Sum( TypeId.Structural, path = NodePath.root, cases = Chunk( - "type1" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.SchemaGen.Arity1"), - path = NodePath.root / "type1", - fields = Chunk( - "value" -> MetaSchema.Value(StandardType.IntType, NodePath.root / "type1" / "value") + Labelled( + "type1", + ExtensibleMetaSchema.Product( + id = TypeId.parse("zio.schema.SchemaGen.Arity1"), + path = NodePath.root / "type1", + fields = Chunk( + Labelled( + "value", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.IntType, NodePath.root / "type1" / "value") + ) + ) ) ), - "type2" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.SchemaGen.Arity2"), - path = NodePath.root / "type2", - fields = Chunk( - "value1" -> MetaSchema.Value(StandardType.StringType, NodePath.root / "type2" / "value1"), - "value2" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.SchemaGen.Arity1"), - path = NodePath.root / "type2" / "value2", - fields = Chunk( - "value" -> MetaSchema - .Value(StandardType.IntType, NodePath.root / "type2" / "value2" / "value") + Labelled( + "type2", + ExtensibleMetaSchema.Product( + id = TypeId.parse("zio.schema.SchemaGen.Arity2"), + path = NodePath.root / "type2", + fields = Chunk( + Labelled( + "value1", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.StringType, NodePath.root / "type2" / "value1") + ), + Labelled( + "value2", + ExtensibleMetaSchema.Product( + id = TypeId.parse("zio.schema.SchemaGen.Arity1"), + path = NodePath.root / "type2" / "value2", + fields = Chunk( + Labelled( + "value", + ExtensibleMetaSchema + .Value[DynamicValue :: End]( + StandardType.IntType, + NodePath.root / "type2" / "value2" / "value" + ) + ) + ) + ) ) ) ) @@ -155,26 +199,49 @@ object MetaSchemaSpec extends ZIOSpecDefault { }, test("sealed trait") { val schema = Schema[Pet] - val expectedAst = MetaSchema.Sum( + val expectedAst = ExtensibleMetaSchema.Sum[DynamicValue :: End]( TypeId.parse("zio.schema.MetaSchemaSpec.Pet"), path = NodePath.root, cases = Chunk( - "Rock" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.MetaSchemaSpec.Rock"), - path = NodePath.root / "Rock", - fields = Chunk.empty + Labelled( + "Rock", + ExtensibleMetaSchema.Product[DynamicValue :: End]( + id = TypeId.parse("zio.schema.MetaSchemaSpec.Rock"), + path = NodePath.root / "Rock", + fields = Chunk.empty + ) ), - "Dog" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.MetaSchemaSpec.Dog"), - path = NodePath.root / "Dog", - fields = Chunk("name" -> MetaSchema.Value(StandardType.StringType, NodePath.root / "Dog" / "name")) + Labelled( + "Dog", + ExtensibleMetaSchema.Product[DynamicValue :: End]( + id = TypeId.parse("zio.schema.MetaSchemaSpec.Dog"), + path = NodePath.root / "Dog", + fields = Chunk( + Labelled( + "name", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.StringType, NodePath.root / "Dog" / "name") + ) + ) + ) ), - "Cat" -> MetaSchema.Product( - id = TypeId.parse("zio.schema.MetaSchemaSpec.Cat"), - path = NodePath.root / "Cat", - fields = Chunk( - "name" -> MetaSchema.Value(StandardType.StringType, NodePath.root / "Cat" / "name"), - "hasHair" -> MetaSchema.Value(StandardType.BoolType, NodePath.root / "Cat" / "hasHair") + Labelled( + "Cat", + ExtensibleMetaSchema.Product[DynamicValue :: End]( + id = TypeId.parse("zio.schema.MetaSchemaSpec.Cat"), + path = NodePath.root / "Cat", + fields = Chunk( + Labelled( + "name", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.StringType, NodePath.root / "Cat" / "name") + ), + Labelled( + "hasHair", + ExtensibleMetaSchema + .Value[DynamicValue :: End](StandardType.BoolType, NodePath.root / "Cat" / "hasHair") + ) + ) ) ) ) @@ -253,6 +320,27 @@ object MetaSchemaSpec extends ZIOSpecDefault { assert(MetaSchema.fromSchema(schema).toSchema)(hasSameSchemaStructure(schema)) } } + ), + suite("extended meta schema")( + test("represents known type as Known") { + val meta1 = ExtendedMetaSchema.fromSchema(Schema[Pet]) + val meta2 = ExtendedMetaSchema.fromSchema(Schema[DynamicValue]) + val meta3 = ExtendedMetaSchema.fromSchema(Schema[Recursive]) + + assertTrue( + meta1.isInstanceOf[ExtensibleMetaSchema.Known[_]], + meta2.isInstanceOf[ExtensibleMetaSchema.Known[_]], + meta3.isInstanceOf[ExtensibleMetaSchema.Product[_]] + ) + }, + test("materializes the original schema") { + val meta1 = ExtendedMetaSchema.fromSchema(Schema[Pet]) + val meta2 = ExtendedMetaSchema.fromSchema(Schema[DynamicValue]) + + val refEq1 = meta1.toSchema eq Schema[Pet] + val refEq2 = meta2.toSchema eq Schema[DynamicValue] + assertTrue(refEq1, refEq2) + } ) ) @@ -288,4 +376,11 @@ object MetaSchemaSpec extends ZIOSpecDefault { implicit lazy val schema: Schema[Pet] = DeriveSchema.gen[Pet] } + type ExtendedMetaSchema = ExtensibleMetaSchema[DynamicValue :: Pet :: End] + + object ExtendedMetaSchema { + + def fromSchema[A](schema: Schema[A]): ExtendedMetaSchema = + ExtensibleMetaSchema.fromSchema(schema) + } } diff --git a/tests/shared/src/test/scala/zio/schema/MigrationSpec.scala b/tests/shared/src/test/scala/zio/schema/MigrationSpec.scala index e623ad29c..15576083f 100644 --- a/tests/shared/src/test/scala/zio/schema/MigrationSpec.scala +++ b/tests/shared/src/test/scala/zio/schema/MigrationSpec.scala @@ -3,7 +3,8 @@ package zio.schema import scala.collection.immutable.ListMap import zio._ -import zio.schema.meta.{ MetaSchema, Migration, NodePath } +import zio.constraintless.TypeList._ +import zio.schema.meta.{ ExtensibleMetaSchema, MetaSchema, Migration, NodePath } import zio.schema.syntax._ import zio.test._ @@ -13,8 +14,8 @@ object MigrationSpec extends ZIOSpecDefault { suite("Derivation")( suite("Value")( test("change type") { - val from = MetaSchema.Value(StandardType.IntType, NodePath.root) - val to = MetaSchema.Value(StandardType.StringType, NodePath.root) + val from = ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root) + val to = ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.StringType, NodePath.root) assertTrue( Migration @@ -22,8 +23,9 @@ object MigrationSpec extends ZIOSpecDefault { ) }, test("optional") { - val from = MetaSchema.Value(StandardType.IntType, NodePath.root, optional = false) - val to = MetaSchema.Value(StandardType.IntType, NodePath.root, optional = true) + val from = + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = false) + val to = ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = true) assertTrue( Migration @@ -31,8 +33,10 @@ object MigrationSpec extends ZIOSpecDefault { ) }, test("require") { - val from = MetaSchema.Value(StandardType.IntType, NodePath.root, optional = true) - val to = MetaSchema.Value(StandardType.IntType, NodePath.root, optional = false) + val from = + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = true) + val to = + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = false) assertTrue( Migration @@ -40,9 +44,14 @@ object MigrationSpec extends ZIOSpecDefault { ) }, test("increment dimensions") { - val from = MetaSchema.Value(StandardType.IntType, NodePath.root, optional = true) + val from = + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = true) val to = - MetaSchema.ListNode(MetaSchema.Value(StandardType.IntType, NodePath.root, optional = true), NodePath.root) + ExtensibleMetaSchema + .ListNode( + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = true), + NodePath.root + ) assertTrue( Migration @@ -51,8 +60,12 @@ object MigrationSpec extends ZIOSpecDefault { }, test("decrement dimensions") { val from = - MetaSchema.ListNode(MetaSchema.Value(StandardType.IntType, NodePath.root, optional = true), NodePath.root) - val to = MetaSchema.Value(StandardType.IntType, NodePath.root, optional = true) + ExtensibleMetaSchema + .ListNode( + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = true), + NodePath.root + ) + val to = ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.IntType, NodePath.root, optional = true) assertTrue( Migration @@ -205,7 +218,12 @@ object MigrationSpec extends ZIOSpecDefault { test("ignore add case") { val value: Pet1 = Pet1.Dog("name") val dynamicValue = value.dynamic - assert(Migration.AddCase(NodePath.root, MetaSchema.Value(StandardType.UnitType, NodePath.root)))( + assert( + Migration.AddCase( + NodePath.root, + ExtensibleMetaSchema.Value[DynamicValue :: End](StandardType.UnitType, NodePath.root) + ) + )( transformsValueTo(value, dynamicValue) ) }, diff --git a/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala b/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala index 1bcc5d294..c2cf96731 100644 --- a/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala +++ b/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala @@ -3,7 +3,9 @@ package zio.schema import scala.collection.immutable.ListMap import zio.Chunk -import zio.schema.meta.{ MetaSchema, NodePath } +import zio.constraintless.TypeList.{ ::, End } +import zio.schema.meta.ExtensibleMetaSchema.Labelled +import zio.schema.meta.{ ExtensibleMetaSchema, MetaSchema, NodePath } import zio.test.{ Gen, Sized } object DeriveGen { @@ -533,29 +535,29 @@ object DeriveGen { private def genLazy[A](lazySchema: Schema.Lazy[A]): Gen[Sized, A] = Gen.suspend(gen(lazySchema.schema)) - private def genSchemaAstProduct(path: NodePath): Gen[Sized, MetaSchema.Product] = + private def genSchemaAstProduct(path: NodePath): Gen[Sized, ExtensibleMetaSchema.Product[DynamicValue :: End]] = for { id <- Gen.string(Gen.alphaChar).map(TypeId.parse) optional <- Gen.boolean fields <- Gen.chunkOf( Gen .string1(Gen.asciiChar) - .flatMap(name => genAst(path / name).map(fieldSchema => (name, fieldSchema))) + .flatMap(name => genAst(path / name).map(fieldSchema => Labelled(name, fieldSchema))) ) - } yield MetaSchema.Product(id, path, fields, optional) + } yield ExtensibleMetaSchema.Product(id, path, fields, optional) - private def genSchemaAstSum(path: NodePath): Gen[Sized, MetaSchema.Sum] = + private def genSchemaAstSum(path: NodePath): Gen[Sized, ExtensibleMetaSchema.Sum[DynamicValue :: End]] = for { id <- Gen.string(Gen.alphaChar).map(TypeId.parse) optional <- Gen.boolean fields <- Gen.chunkOf( Gen .string1(Gen.asciiChar) - .flatMap(name => genAst(path / name).map(fieldSchema => (name, fieldSchema))) + .flatMap(name => genAst(path / name).map(fieldSchema => Labelled(name, fieldSchema))) ) - } yield MetaSchema.Sum(id, path, fields, optional) + } yield ExtensibleMetaSchema.Sum(id, path, fields, optional) - private def genSchemaAstValue(path: NodePath): Gen[Any, MetaSchema.Value] = + private def genSchemaAstValue(path: NodePath): Gen[Any, ExtensibleMetaSchema.Value[DynamicValue :: End]] = for { valueType <- Gen.oneOf( Gen.const(StandardType.UnitType), @@ -588,12 +590,12 @@ object DeriveGen { Gen.const(StandardType.ZonedDateTimeType) ) optional <- Gen.boolean - } yield MetaSchema.Value(valueType, path, optional) + } yield ExtensibleMetaSchema.Value(valueType, path, optional) - private def genSchemaAstDynamic(path: NodePath): Gen[Any, MetaSchema.Dynamic] = + private def genSchemaAstDynamic(path: NodePath): Gen[Any, ExtensibleMetaSchema.Known[DynamicValue :: End]] = for { optional <- Gen.boolean - } yield MetaSchema.Dynamic(path, optional) + } yield ExtensibleMetaSchema.Known(DynamicValue.typeId, path, optional) private def genAst(path: NodePath): Gen[Sized, MetaSchema] = Gen.weighted( diff --git a/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala b/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala index 83bee159c..a01b74e77 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala @@ -218,9 +218,11 @@ object DynamicValue { final case class Error(message: String) extends DynamicValue + lazy val typeId: TypeId = TypeId.parse("zio.schema.DynamicValue") + lazy val schema: Schema[DynamicValue] = Schema.EnumN( - TypeId.fromTypeName("zio.schema.DynamicValue"), + typeId, CaseSet .Cons(errorCase, CaseSet.Empty[DynamicValue]()) .:+:(noneValueCase) diff --git a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala index 75564148e..bc94422c2 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Schema.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Schema.scala @@ -683,6 +683,8 @@ object Schema extends SchemaEquality { } final case class Dynamic(override val annotations: Chunk[Any] = Chunk.empty) extends Schema[DynamicValue] { + def id: TypeId = DynamicValue.typeId + override type Accessors[Lens[_, _, _], Prism[_, _, _], Traversal[_, _]] = Unit /** diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/AstRenderer.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/AstRenderer.scala new file mode 100644 index 000000000..37c09de19 --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/meta/AstRenderer.scala @@ -0,0 +1,167 @@ +package zio.schema.meta + +import zio.Chunk +import zio.constraintless.TypeList +import zio.schema.meta.ExtensibleMetaSchema.Labelled + +private[schema] object AstRenderer { + private val INDENT_STEP = 2 + + def render(ast: ExtensibleMetaSchema[_]): String = ast match { + case v @ ExtensibleMetaSchema.Value(_, _, _) => renderValue(v, 0, None) + case f @ ExtensibleMetaSchema.FailNode(_, _, _) => renderFail(f, 0, None) + case ExtensibleMetaSchema.Product(_, _, fields, optional) => + val buffer = new StringBuffer() + buffer.append(s"product") + if (optional) buffer.append("?") + buffer.append("\n").append(fields.map(renderField(_, INDENT_STEP)).mkString("\n")).toString + case ExtensibleMetaSchema.Tuple(_, left, right, optional) => + val buffer = new StringBuffer() + buffer.append(s"tuple") + if (optional) buffer.append("?") + buffer + .append("\n") + .append(Chunk(Labelled("left", left), Labelled("right", right)).map(renderField(_, INDENT_STEP)).mkString("\n")) + .toString + case ExtensibleMetaSchema.Sum(_, _, cases, optional) => + val buffer = new StringBuffer() + buffer.append(s"enum") + if (optional) buffer.append("?") + buffer.append("\n").append(cases.map(renderField(_, INDENT_STEP)).mkString("\n")).toString + case ExtensibleMetaSchema.Either(_, left, right, optional) => + val buffer = new StringBuffer() + buffer.append(s"either") + if (optional) buffer.append("?") + buffer + .append("\n") + .append(Chunk(Labelled("left", left), Labelled("right", right)).map(renderField(_, INDENT_STEP)).mkString("\n")) + .toString + case ExtensibleMetaSchema.ListNode(items, _, optional) => + val buffer = new StringBuffer() + buffer.append(s"list") + if (optional) buffer.append("?") + buffer + .append("\n") + .append(Chunk(Labelled("item", items)).map(renderField(_, INDENT_STEP)).mkString("\n")) + .toString + case ExtensibleMetaSchema.Dictionary(keys, values, _, optional) => + val buffer = new StringBuffer() + buffer.append(s"map") + if (optional) buffer.append("?") + buffer + .append("\n") + .append( + Chunk(Labelled("keys", keys), Labelled("values", values)).map(renderField(_, INDENT_STEP)).mkString("\n") + ) + .toString + case ExtensibleMetaSchema.Ref(refPath, _, optional) => + val buffer = new StringBuffer() + buffer.append(s"ref#$refPath") + if (optional) buffer.append("?") + buffer.toString + case ExtensibleMetaSchema.Known(typeId, _, optional) => + val buffer = new StringBuffer() + buffer.append(typeId.toString) + if (optional) buffer.append("?") + buffer.toString + } + + def renderField[BuiltIn <: TypeList](labelled: ExtensibleMetaSchema.Labelled[BuiltIn], indent: Int): String = { + val buffer = new StringBuffer() + labelled.schema match { + case value @ ExtensibleMetaSchema.Value(_, _, _) => + renderValue(value, indent, Some(labelled.label)) + case fail @ ExtensibleMetaSchema.FailNode(_, _, _) => + renderFail(fail, indent, Some(labelled.label)) + case ExtensibleMetaSchema.Product(_, _, fields, optional) => + pad(buffer, indent) + buffer.append(s"${labelled.label}: record") + if (optional) buffer.append("?") + buffer.append("\n").append(fields.map(renderField(_, indent + INDENT_STEP)).mkString("\n")).toString + case ExtensibleMetaSchema.Tuple(_, left, right, optional) => + pad(buffer, indent) + buffer.append(s"${labelled.label}: tuple") + if (optional) buffer.append("?") + buffer + .append("\n") + .append( + Chunk(Labelled("left", left), Labelled("right", right)) + .map(renderField(_, indent + INDENT_STEP)) + .mkString("\n") + ) + .toString + case ExtensibleMetaSchema.Sum(_, _, cases, optional) => + pad(buffer, indent) + buffer.append(s"${labelled.label}: enum") + if (optional) buffer.append("?") + buffer.append("\n").append(cases.map(renderField(_, indent + INDENT_STEP)).mkString("\n")).toString + case ExtensibleMetaSchema.Either(_, left, right, optional) => + pad(buffer, indent) + buffer.append(s"${labelled.label}: either") + if (optional) buffer.append("?") + buffer + .append("\n") + .append( + Chunk(Labelled("left", left), Labelled("right", right)) + .map(renderField(_, indent + INDENT_STEP)) + .mkString("\n") + ) + .toString + case ExtensibleMetaSchema.ListNode(items, _, optional) => + val buffer = new StringBuffer() + buffer.append(s"${labelled.label}: list") + if (optional) buffer.append("?") + buffer + .append("\n") + .append(Chunk(Labelled("item", items)).map(renderField(_, INDENT_STEP)).mkString("\n")) + .toString + case ExtensibleMetaSchema.Dictionary(keys, values, _, optional) => + val buffer = new StringBuffer() + buffer.append(s"${labelled.label}: map") + if (optional) buffer.append("?") + buffer + .append("\n") + .append( + Chunk(Labelled("keys", keys), Labelled("values", values)).map(renderField(_, INDENT_STEP)).mkString("\n") + ) + .toString + case ExtensibleMetaSchema.Ref(refPath, _, optional) => + pad(buffer, indent) + buffer.append(s"${labelled.label}: ") + if (optional) buffer.append("?") + buffer.append(s"{ref#${refPath.render}}").toString + case ExtensibleMetaSchema.Known(typeId, _, optional) => + pad(buffer, indent) + buffer.append(s"${labelled.label}: ") + buffer.append(typeId.toString) + if (optional) buffer.append("?") + buffer.toString + } + } + + def renderValue(value: ExtensibleMetaSchema.Value[_], indent: Int, label: Option[String]): String = { + val buffer = new StringBuffer() + pad(buffer, indent) + label.foreach(l => buffer.append(s"$l: ")) + if (value.optional) buffer.append("?") + buffer.append(value.valueType.tag).toString + } + + def renderFail(fail: ExtensibleMetaSchema.FailNode[_], indent: Int, label: Option[String]): String = { + val buffer = new StringBuffer() + pad(buffer, indent) + label.foreach(l => buffer.append(s"$l: ")) + if (fail.optional) buffer.append("?") + buffer.append(s"FAIL: ${fail.message}").toString + } + + private def pad(buffer: StringBuffer, indent: Int): StringBuffer = { + if (indent > 0) { + buffer.append("|") + for (_ <- 0 until indent) { + buffer.append("-") + } + } + buffer + } +} diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala new file mode 100644 index 000000000..82da97f4a --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/meta/ExtensibleMetaSchema.scala @@ -0,0 +1,746 @@ +package zio.schema.meta + +import scala.annotation.tailrec +import scala.collection.immutable.ListMap +import scala.collection.mutable + +import zio.constraintless.TypeList +import zio.prelude._ +import zio.schema._ +import zio.{ Chunk, ChunkBuilder } + +sealed trait ExtensibleMetaSchema[BuiltIn <: TypeList] { self => + def builtInInstances: SchemaInstances[BuiltIn] + + def path: NodePath + def optional: Boolean + + def toSchema: Schema[_] = { + val refMap = mutable.HashMap.empty[NodePath, Schema[_]] + ExtensibleMetaSchema.materialize(self, refMap)(builtInInstances) + } + + override def toString: String = AstRenderer.render(self) +} + +object ExtensibleMetaSchema { + import CaseSet._ + + final case class Labelled[BuiltIn <: TypeList](label: String, schema: ExtensibleMetaSchema[BuiltIn]) + + object Labelled { + private lazy val schemaAny: Schema[Labelled[TypeList.End]] = + Schema + .defer(Schema.tuple2(Schema[String], ExtensibleMetaSchema.schemaAny)) + .transform(tuple => Labelled(tuple._1, tuple._2), l => (l.label, l.schema)) + + implicit def schema[BuiltIn <: TypeList]: Schema[Labelled[BuiltIn]] = + schemaAny.asInstanceOf[Schema[Labelled[BuiltIn]]] + } + + final case class Lineage(paths: Chunk[(Int, NodePath)]) { + def :+(pair: (Int, NodePath)): Lineage = Lineage(paths :+ pair) + } + + object Lineage { + def apply(paths: (Int, NodePath)*): Lineage = Lineage(Chunk.fromIterable(paths)) + + lazy val empty: Lineage = Lineage(Chunk.empty) + + implicit lazy val schema: Schema[Lineage] = + Schema.chunk(Schema.tuple2(Schema[Int], Schema[NodePath])).transform(Lineage(_), _.paths) + } + + implicit val nodePathSchema: Schema[NodePath] = + Schema[String].repeated + .transform(NodePath(_), NodePath.unwrap) + + final case class Product[BuiltIn <: TypeList]( + id: TypeId, + override val path: NodePath, + fields: Chunk[Labelled[BuiltIn]] = Chunk.empty, + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Product { + implicit def schema[BuiltIn <: TypeList]: Schema[Product[BuiltIn]] = + schemaAny.asInstanceOf[Schema[Product[BuiltIn]]] + + private lazy val schemaAny: Schema[Product[TypeList.End]] = { + Schema.CaseClass4( + TypeId.parse("zio.schema.meta.MetaSchema.Product"), + field01 = Schema.Field("id", Schema[TypeId], get0 = _.id, set0 = (a, value: TypeId) => a.copy(id = value)), + field02 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (p, value: Chunk[String]) => p.copy(path = NodePath(value)) + ), + field03 = Schema + .Field( + "fields", + Schema[Labelled[TypeList.End]].repeated, + get0 = _.fields, + set0 = (a: Product[TypeList.End], value: Chunk[Labelled[TypeList.End]]) => a.copy(fields = value) + ), + field04 = Schema + .Field( + "optional", + Schema[Boolean], + get0 = _.optional, + set0 = (a, value: Boolean) => a.copy(optional = value) + ), + (id: TypeId, path: Chunk[String], fields: Chunk[Labelled[TypeList.End]], optional: Boolean) => + Product(id, NodePath(path), fields, optional) + ) + } + } + final case class Tuple[BuiltIn <: TypeList]( + override val path: NodePath, + left: ExtensibleMetaSchema[BuiltIn], + right: ExtensibleMetaSchema[BuiltIn], + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Tuple { + implicit def schema[BuiltIn <: TypeList]: Schema[Tuple[BuiltIn]] = schemaAny.asInstanceOf[Schema[Tuple[BuiltIn]]] + + private lazy val schemaAny: Schema[Tuple[TypeList.End]] = { + Schema.CaseClass4( + TypeId.parse("zio.schema.meta.MetaSchema.Tuple"), + field01 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field02 = Schema + .Field( + "left", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.left, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(left = value) + ), + field03 = Schema + .Field( + "right", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.right, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(right = value) + ), + field04 = Schema + .Field( + "optional", + Schema[Boolean], + get0 = _.optional, + set0 = (a, value: Boolean) => a.copy(optional = value) + ), + ( + path: Chunk[String], + left: ExtensibleMetaSchema[TypeList.End], + right: ExtensibleMetaSchema[TypeList.End], + optional: Boolean + ) => Tuple(NodePath(path), left, right, optional) + ) + } + } + + final case class Sum[BuiltIn <: TypeList]( + id: TypeId, + override val path: NodePath, + cases: Chunk[Labelled[BuiltIn]] = Chunk.empty, + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Sum { + implicit def schema[BuiltIn <: TypeList]: Schema[Sum[BuiltIn]] = schemaAny.asInstanceOf[Schema[Sum[BuiltIn]]] + + private lazy val schemaAny: Schema[Sum[TypeList.End]] = + Schema.CaseClass4( + TypeId.parse("zio.schema.meta.MetaSchema.Sum"), + field01 = Schema.Field("id", Schema[TypeId], get0 = _.id, set0 = (a, value: TypeId) => a.copy(id = value)), + field02 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field03 = Schema.Field( + "cases", + Schema[Labelled[TypeList.End]].repeated, + get0 = _.cases, + set0 = (a, value: Chunk[Labelled[TypeList.End]]) => a.copy(cases = value) + ), + field04 = Schema + .Field( + "optional", + Schema[Boolean], + get0 = _.optional, + set0 = (a, value: Boolean) => a.copy(optional = value) + ), + (id: TypeId, path: Chunk[String], fields: Chunk[Labelled[TypeList.End]], optional: Boolean) => + Sum(id, NodePath(path), fields, optional) + ) + } + + final case class Either[BuiltIn <: TypeList]( + override val path: NodePath, + left: ExtensibleMetaSchema[BuiltIn], + right: ExtensibleMetaSchema[BuiltIn], + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Either { + implicit def schema[BuiltIn <: TypeList]: Schema[Either[BuiltIn]] = schemaAny.asInstanceOf[Schema[Either[BuiltIn]]] + + private lazy val schemaAny: Schema[Either[TypeList.End]] = { + Schema.CaseClass4( + TypeId.parse("zio.schema.meta.MetaSchema.Either"), + field01 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field02 = Schema + .Field( + "left", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.left, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(left = value) + ), + field03 = Schema + .Field( + "right", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.right, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(right = value) + ), + field04 = Schema + .Field( + "optional", + Schema[Boolean], + get0 = _.optional, + set0 = (a, value: Boolean) => a.copy(optional = value) + ), + ( + path: Chunk[String], + left: ExtensibleMetaSchema[TypeList.End], + right: ExtensibleMetaSchema[TypeList.End], + optional: Boolean + ) => Either(NodePath(path), left, right, optional) + ) + } + } + + final case class FailNode[BuiltIn <: TypeList]( + message: String, + override val path: NodePath, + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object FailNode { + implicit def schema[BuiltIn <: TypeList]: Schema[FailNode[BuiltIn]] = + schemaAny.asInstanceOf[Schema[FailNode[BuiltIn]]] + + private lazy val schemaAny: Schema[FailNode[TypeList.End]] = Schema.CaseClass3( + TypeId.parse("zio.schema.meta.MetaSchema.FailNode"), + field01 = + Schema.Field("message", Schema[String], get0 = _.message, set0 = (a, value: String) => a.copy(message = value)), + field02 = Schema.Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field03 = Schema + .Field("optional", Schema[Boolean], get0 = _.optional, set0 = (a, value: Boolean) => a.copy(optional = value)), + (m: String, path: Chunk[String], optional: Boolean) => FailNode(m, NodePath(path), optional) + ) + } + + final case class ListNode[BuiltIn <: TypeList]( + item: ExtensibleMetaSchema[BuiltIn], + override val path: NodePath, + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object ListNode { + implicit def schema[BuiltIn <: TypeList]: Schema[ListNode[BuiltIn]] = + schemaAny.asInstanceOf[Schema[ListNode[BuiltIn]]] + + private lazy val schemaAny: Schema[ListNode[TypeList.End]] = Schema.CaseClass3( + TypeId.parse("zio.schema.meta.MetaSchema.ListNode"), + field01 = Schema.Field( + "item", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.item, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(item = value) + ), + field02 = Schema.Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field03 = Schema + .Field("optional", Schema[Boolean], get0 = _.optional, set0 = (a, value: Boolean) => a.copy(optional = value)), + (item: ExtensibleMetaSchema[TypeList.End], path: Chunk[String], optional: Boolean) => + ListNode(item, NodePath(path), optional) + ) + } + + final case class Dictionary[BuiltIn <: TypeList]( + keys: ExtensibleMetaSchema[BuiltIn], + values: ExtensibleMetaSchema[BuiltIn], + override val path: NodePath, + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Dictionary { + implicit def schema[BuiltIn <: TypeList]: Schema[Dictionary[BuiltIn]] = + schemaAny.asInstanceOf[Schema[Dictionary[BuiltIn]]] + + private lazy val schemaAny: Schema[Dictionary[TypeList.End]] = Schema.CaseClass4( + TypeId.parse("zio.schema.meta.MetaSchema.Dictionary"), + field01 = Schema.Field( + "keys", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.keys, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(keys = value) + ), + field02 = Schema + .Field( + "values", + Schema[ExtensibleMetaSchema[TypeList.End]], + get0 = _.values, + set0 = (a, value: ExtensibleMetaSchema[TypeList.End]) => a.copy(values = value) + ), + field03 = Schema.Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field04 = Schema + .Field("optional", Schema[Boolean], get0 = _.optional, set0 = (a, value: Boolean) => a.copy(optional = value)), + ( + keys: ExtensibleMetaSchema[TypeList.End], + values: ExtensibleMetaSchema[TypeList.End], + path: Chunk[String], + optional: Boolean + ) => Dictionary(keys, values, NodePath(path), optional) + ) + } + + final case class Value[BuiltIn <: TypeList]( + valueType: StandardType[_], + override val path: NodePath = NodePath.root, + override val optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Value { + implicit def schema[BuiltIn <: TypeList]: Schema[Value[BuiltIn]] = schemaAny.asInstanceOf[Schema[Value[BuiltIn]]] + + private lazy val schemaAny: Schema[Value[TypeList.End]] = + Schema + .CaseClass3[String, Chunk[String], Boolean, (String, Chunk[String], Boolean)]( + TypeId.parse("zio.schema.meta.MetaSchema.Value"), + field01 = + Schema.Field("valueType", Schema[String], get0 = _._1, set0 = (a, value: String) => (value, a._2, a._3)), + field02 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _._2, + set0 = (a, value: Chunk[String]) => (a._1, value, a._3) + ), + field03 = + Schema.Field("optional", Schema[Boolean], get0 = _._3, set0 = (a, value: Boolean) => (a._1, a._2, value)), + construct0 = (v, p, o) => (v, p, o) + ) + .transformOrFail(tup => fromTuple(tup), tupled) + + private def tupled[BuiltIn <: TypeList]( + value: Value[BuiltIn] + ): scala.Either[String, (String, Chunk[String], Boolean)] = + Right((value.valueType.tag, value.path, value.optional)) + + private def fromTuple[BuiltIn <: TypeList]( + tuple: (String, Chunk[String], Boolean) + )(implicit builtInInstances: SchemaInstances[BuiltIn]): scala.Either[String, Value[BuiltIn]] = tuple match { + case (s, path, optional) => + StandardType + .fromString(s) + .map(typ => Value(typ, NodePath(path), optional)) + .toRight(s"unkown standard type $s") + } + } + final case class Ref[BuiltIn <: TypeList]( + refPath: NodePath, + override val path: NodePath, + optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Ref { + implicit def schema[BuiltIn <: TypeList]: Schema[Ref[BuiltIn]] = schemaAny.asInstanceOf[Schema[Ref[BuiltIn]]] + + private lazy val schemaAny: Schema[Ref[TypeList.End]] = + Schema.CaseClass3( + TypeId.parse("zio.schema.meta.MetaSchema.Ref"), + field01 = Schema.Field( + "refPath", + Schema[String].repeated, + get0 = _.refPath, + set0 = (a, value: Chunk[String]) => a.copy(refPath = NodePath(value)) + ), + field02 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field03 = Schema + .Field( + "optional", + Schema[Boolean], + get0 = _.optional, + set0 = (a, value: Boolean) => a.copy(optional = value) + ), + (refPath: Chunk[String], path: Chunk[String], optional: Boolean) => + Ref(NodePath(refPath), NodePath(path), optional) + ) + } + + final case class Known[BuiltIn <: TypeList]( + typeId: TypeId, + override val path: NodePath, + optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) + extends ExtensibleMetaSchema[BuiltIn] + + object Known { + implicit def schema[BuiltIn <: TypeList]: Schema[Known[BuiltIn]] = + schemaAny.asInstanceOf[Schema[Known[BuiltIn]]] + + private lazy val schemaAny: Schema[Known[TypeList.End]] = + Schema.CaseClass3( + TypeId.parse("zio.schema.meta.MetaSchema.Known"), + field01 = + Schema.Field("typeId", Schema[TypeId], get0 = _.typeId, set0 = (a, value: TypeId) => a.copy(typeId = value)), + field02 = Schema + .Field( + "path", + Schema[String].repeated, + get0 = _.path, + set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) + ), + field03 = Schema + .Field( + "optional", + Schema[Boolean], + get0 = _.optional, + set0 = (a, value: Boolean) => a.copy(optional = value) + ), + (typeId: TypeId, path: Chunk[String], optional: Boolean) => Known(typeId, NodePath(path), optional) + ) + } + + final private[schema] case class NodeBuilder[BuiltIn <: TypeList]( + path: NodePath, + lineage: Lineage, + optional: Boolean = false + )(implicit val builtInInstances: SchemaInstances[BuiltIn]) { self => + private val children: ChunkBuilder[Labelled[BuiltIn]] = ChunkBuilder.make[Labelled[BuiltIn]]() + + def addLabelledSubtree(label: String, schema: Schema[_]): NodeBuilder[BuiltIn] = { + children += Labelled(label, subtree(path / label, lineage, schema)) + self + } + + def buildProduct(id: TypeId): Product[BuiltIn] = Product(id, path, children.result(), optional) + + def buildSum(id: TypeId): Sum[BuiltIn] = Sum(id, path, children.result(), optional) + } + + @tailrec + def fromSchema[A, BuiltIn <: TypeList]( + schema: Schema[A] + )(implicit builtInInstances: SchemaInstances[BuiltIn]): ExtensibleMetaSchema[BuiltIn] = + getBuiltInTypeId(builtInInstances, schema) match { + case Some(typeId) => Known(typeId, NodePath.root) + case None => + schema match { + case Schema.Primitive(typ, _) => Value(typ, NodePath.root) + case Schema.Fail(message, _) => FailNode(message, NodePath.root) + case Schema.Optional(schema, _) => subtree(NodePath.root, Lineage.empty, schema, optional = true) + case Schema.Either(left, right, _) => + Either( + NodePath.root, + subtree(NodePath.root / "left", Lineage.empty, left), + subtree(NodePath.root / "right", Lineage.empty, right) + ) + case Schema.Tuple2(left, right, _) => + Tuple( + NodePath.root, + subtree(NodePath.root / "left", Lineage.empty, left), + subtree(NodePath.root / "right", Lineage.empty, right) + ) + case Schema.Sequence(schema, _, _, _, _) => + ListNode(item = subtree(NodePath.root / "item", Lineage.empty, schema), NodePath.root) + case Schema.Map(ks, vs, _) => + Dictionary( + keys = subtree(NodePath.root / "keys", Lineage.empty, ks), + values = subtree(NodePath.root / "values", Lineage.empty, vs), + NodePath.root + ) + case Schema.Set(schema, _) => + ListNode(item = subtree(NodePath.root / "item", Lineage.empty, schema), NodePath.root) + case Schema.Transform(schema, _, _, _, _) => subtree(NodePath.root, Lineage.empty, schema) + case lzy @ Schema.Lazy(_) => fromSchema(lzy.schema) + case s: Schema.Record[A] => + s.fields + .foldLeft(NodeBuilder(NodePath.root, Lineage(s.hashCode() -> NodePath.root))) { (node, field) => + node.addLabelledSubtree(field.name, field.schema) + } + .buildProduct(s.id) + case s: Schema.Enum[A] => + s.cases + .foldLeft(NodeBuilder(NodePath.root, Lineage(s.hashCode() -> NodePath.root))) { + case (node, caseValue) => + node.addLabelledSubtree(caseValue.id, caseValue.schema) + } + .buildSum(s.id) + case Schema.Dynamic(_) => fromSchema(DynamicValue.schema) + } + } + + private[schema] def subtree[BuiltIn <: TypeList]( + path: NodePath, + lineage: Lineage, + schema: Schema[_], + optional: Boolean = false + )(implicit builtInInstances: SchemaInstances[BuiltIn]): ExtensibleMetaSchema[BuiltIn] = + lineage.paths + .find(_._1 == schema.hashCode()) + .map { + case (_, refPath) => + Ref(refPath, path, optional) + } + .getOrElse { + getBuiltInTypeId(builtInInstances, schema) match { + case Some(typeId) => Known(typeId, path, optional) + case None => + schema match { + case Schema.Primitive(typ, _) => Value(typ, path, optional) + case Schema.Optional(schema, _) => subtree(path, lineage, schema, optional = true) + case Schema.Either(left, right, _) => + Either( + path, + subtree(path / "left", lineage, left, optional = false), + subtree(path / "right", lineage, right, optional = false), + optional + ) + case Schema.Tuple2(left, right, _) => + Tuple( + path, + subtree(path / "left", lineage, left, optional = false), + subtree(path / "right", lineage, right, optional = false), + optional + ) + case Schema.Sequence(schema, _, _, _, _) => + ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) + case Schema.Map(ks, vs, _) => + Dictionary( + keys = subtree(path / "keys", Lineage.empty, ks, optional = false), + values = subtree(path / "values", Lineage.empty, vs, optional = false), + path, + optional + ) + case Schema.Set(schema @ _, _) => + ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) + case Schema.Transform(schema, _, _, _, _) => subtree(path, lineage, schema, optional) + case lzy @ Schema.Lazy(_) => subtree(path, lineage, lzy.schema, optional) + case s: Schema.Record[_] => + s.fields + .foldLeft(NodeBuilder(path, lineage :+ (s.hashCode() -> path), optional)) { (node, field) => + node.addLabelledSubtree(field.name, field.schema) + } + .buildProduct(s.id) + case s: Schema.Enum[_] => + s.cases + .foldLeft(NodeBuilder(path, lineage :+ (s.hashCode() -> path), optional)) { + case (node, caseValue) => + node.addLabelledSubtree(caseValue.id, caseValue.schema) + } + .buildSum(s.id) + case Schema.Fail(message, _) => FailNode(message, path) + case Schema.Dynamic(_) => subtree(path, lineage, DynamicValue.schema, optional) + } + } + } + + private[schema] def materialize[BuiltIn <: TypeList]( + ast: ExtensibleMetaSchema[BuiltIn], + refs: mutable.Map[NodePath, Schema[_]] + )(implicit builtInInstances: SchemaInstances[BuiltIn]): Schema[_] = { + val baseSchema = ast match { + case ExtensibleMetaSchema.Value(typ, _, _) => + Schema.Primitive(typ, Chunk.empty) + case ExtensibleMetaSchema.FailNode(msg, _, _) => Schema.Fail(msg) + case ExtensibleMetaSchema.Ref(refPath, _, _) => + Schema.defer( + refs.getOrElse(refPath, Schema.Fail(s"invalid ref path $refPath")) + ) + case ExtensibleMetaSchema.Product(id, _, elems, _) => + Schema.record( + id, + elems.map { + case Labelled(label, ast) => + Schema.Field( + label, + materialize(ast, refs).asInstanceOf[Schema[Any]], + get0 = (p: ListMap[String, _]) => p(label), + set0 = (p: ListMap[String, _], v: Any) => p.updated(label, v) + ) + }: _* + ) + case ExtensibleMetaSchema.Tuple(_, left, right, _) => + Schema.tuple2( + materialize(left, refs), + materialize(right, refs) + ) + case ExtensibleMetaSchema.Sum(id, _, elems, _) => + Schema.enumeration[Any, CaseSet.Aux[Any]]( + id, + elems.foldRight[CaseSet.Aux[Any]](CaseSet.Empty[Any]()) { + case (Labelled(label, ast), acc) => + val _case: Schema.Case[Any, Any] = Schema + .Case[Any, Any]( + label, + materialize(ast, refs).asInstanceOf[Schema[Any]], + identity[Any], + identity[Any], + _.isInstanceOf[Any], + Chunk.empty + ) + CaseSet.Cons(_case, acc) + } + ) + case ExtensibleMetaSchema.Either(_, left, right, _) => + Schema.either( + materialize(left, refs), + materialize(right, refs) + ) + case ExtensibleMetaSchema.ListNode(itemAst, _, _) => + Schema.chunk(materialize(itemAst, refs)) + case ExtensibleMetaSchema.Dictionary(keyAst, valueAst, _, _) => + Schema.Map(materialize(keyAst, refs), materialize(valueAst, refs), Chunk.empty) + case ExtensibleMetaSchema.Known(typeId, _, _) => + builtInInstances.all.collectFirst { + case record: Schema.Record[_] if record.id == typeId => record + case e: Schema.Enum[_] if e.id == typeId => e + case dyn: Schema.Dynamic if dyn.id == typeId => dyn + }.getOrElse(Schema.Fail(s"invalid known type id $typeId")) + case ast => Schema.Fail(s"AST cannot be materialized to a Schema:\n$ast") + } + + refs += ast.path -> baseSchema + + if (ast.optional) baseSchema.optional else baseSchema + } + + implicit def schema[BuiltIn <: TypeList]: Schema[ExtensibleMetaSchema[BuiltIn]] = + schemaAny.asInstanceOf[Schema[ExtensibleMetaSchema[BuiltIn]]] + + private lazy val schemaAny: Schema[ExtensibleMetaSchema[TypeList.End]] = + Schema.defer { + Schema.EnumN[ExtensibleMetaSchema[TypeList.End], CaseSet.Aux[ExtensibleMetaSchema[TypeList.End]]]( + TypeId.parse("zio.schema.meta.ExtensibleMetaSchema[TypeList.End]"), + caseOf[Value[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Value")(_.asInstanceOf[Value[TypeList.End]])( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )(_.isInstanceOf[Value[TypeList.End]]) ++ + caseOf[Sum[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Sum")(_.asInstanceOf[Sum[TypeList.End]])( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )(_.isInstanceOf[Sum[TypeList.End]]) ++ + caseOf[Either[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Either")( + _.asInstanceOf[Either[TypeList.End]] + )( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )( + _.isInstanceOf[Either[TypeList.End]] + ) ++ + caseOf[Product[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Product")( + _.asInstanceOf[Product[TypeList.End]] + )( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )( + _.isInstanceOf[Product[TypeList.End]] + ) ++ + caseOf[Tuple[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Tuple")(_.asInstanceOf[Tuple[TypeList.End]])( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )(_.isInstanceOf[Tuple[TypeList.End]]) ++ + caseOf[Ref[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Ref")(_.asInstanceOf[Ref[TypeList.End]])( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )(_.isInstanceOf[Ref[TypeList.End]]) ++ + caseOf[ListNode[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("ListNode")( + _.asInstanceOf[ListNode[TypeList.End]] + )( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )( + _.isInstanceOf[ListNode[TypeList.End]] + ) ++ + caseOf[Dictionary[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Dictionary")( + _.asInstanceOf[Dictionary[TypeList.End]] + )( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )( + _.isInstanceOf[Dictionary[TypeList.End]] + ) ++ + caseOf[Known[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Known")( + _.asInstanceOf[Known[TypeList.End]] + )( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )( + _.isInstanceOf[Known[TypeList.End]] + ) ++ + caseOf[FailNode[TypeList.End], ExtensibleMetaSchema[TypeList.End]]("Fail")( + _.asInstanceOf[FailNode[TypeList.End]] + )( + _.asInstanceOf[ExtensibleMetaSchema[TypeList.End]] + )( + _.isInstanceOf[FailNode[TypeList.End]] + ), + Chunk.empty + ) + } + + implicit val equals: Equal[MetaSchema] = Equal.default + + private def getBuiltInTypeId[BuiltIn <: TypeList]( + instances: SchemaInstances[BuiltIn], + schema: Schema[_] + ): Option[TypeId] = + if (instances.all.contains(schema)) { + schema match { + case record: Schema.Record[_] => Some(record.id) + case e: Schema.Enum[_] => Some(e.id) + case dyn: Schema.Dynamic => Some(dyn.id) + case _ => None + } + } else None +} diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/MetaSchema.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/MetaSchema.scala deleted file mode 100644 index a842ee72e..000000000 --- a/zio-schema/shared/src/main/scala/zio/schema/meta/MetaSchema.scala +++ /dev/null @@ -1,717 +0,0 @@ -package zio.schema.meta - -import scala.annotation.tailrec -import scala.collection.immutable.ListMap -import scala.collection.mutable - -import zio.prelude._ -import zio.schema._ -import zio.{ Chunk, ChunkBuilder } - -sealed trait MetaSchema { self => - def path: NodePath - def optional: Boolean - - def toSchema: Schema[_] = { - val refMap = mutable.HashMap.empty[NodePath, Schema[_]] - MetaSchema.materialize(self, refMap) - } - - override def toString: String = AstRenderer.render(self) -} - -object MetaSchema { - import CaseSet._ - - type Labelled = (String, MetaSchema) - type Lineage = Chunk[(Int, NodePath)] - - implicit val nodePathSchema: Schema[NodePath] = - Schema[String].repeated - .transform(NodePath(_), NodePath.unwrap) - - final case class Product( - id: TypeId, - override val path: NodePath, - fields: Chunk[Labelled] = Chunk.empty, - override val optional: Boolean = false - ) extends MetaSchema - - object Product { - implicit val schema: Schema[Product] = { - Schema.CaseClass4( - TypeId.parse("zio.schema.meta.MetaSchema.Product"), - field01 = Schema.Field("id", Schema[TypeId], get0 = _.id, set0 = (a, value: TypeId) => a.copy(id = value)), - field02 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (p, value: Chunk[String]) => p.copy(path = NodePath(value)) - ), - field03 = Schema - .Field( - "fields", - Schema[Labelled].repeated, - get0 = _.fields, - set0 = (a: Product, value: Chunk[Labelled]) => a.copy(fields = value) - ), - field04 = Schema - .Field( - "optional", - Schema[Boolean], - get0 = _.optional, - set0 = (a, value: Boolean) => a.copy(optional = value) - ), - (id: TypeId, path: Chunk[String], fields: Chunk[Labelled], optional: Boolean) => - Product(id, NodePath(path), fields, optional) - ) - } - } - final case class Tuple( - override val path: NodePath, - left: MetaSchema, - right: MetaSchema, - override val optional: Boolean = false - ) extends MetaSchema - - object Tuple { - implicit val schema: Schema[Tuple] = { - Schema.CaseClass4( - TypeId.parse("zio.schema.meta.MetaSchema.Tuple"), - field01 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field02 = Schema - .Field("left", Schema[MetaSchema], get0 = _.left, set0 = (a, value: MetaSchema) => a.copy(left = value)), - field03 = Schema - .Field("right", Schema[MetaSchema], get0 = _.right, set0 = (a, value: MetaSchema) => a.copy(right = value)), - field04 = Schema - .Field( - "optional", - Schema[Boolean], - get0 = _.optional, - set0 = (a, value: Boolean) => a.copy(optional = value) - ), - (path: Chunk[String], left: MetaSchema, right: MetaSchema, optional: Boolean) => - Tuple(NodePath(path), left, right, optional) - ) - } - } - - final case class Sum( - id: TypeId, - override val path: NodePath, - cases: Chunk[Labelled] = Chunk.empty, - override val optional: Boolean = false - ) extends MetaSchema - - object Sum { - implicit lazy val schema: Schema[Sum] = - Schema.CaseClass4( - TypeId.parse("zio.schema.meta.MetaSchema.Sum"), - field01 = Schema.Field("id", Schema[TypeId], get0 = _.id, set0 = (a, value: TypeId) => a.copy(id = value)), - field02 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field03 = Schema.Field( - "cases", - Schema[Labelled].repeated, - get0 = _.cases, - set0 = (a, value: Chunk[Labelled]) => a.copy(cases = value) - ), - field04 = Schema - .Field( - "optional", - Schema[Boolean], - get0 = _.optional, - set0 = (a, value: Boolean) => a.copy(optional = value) - ), - (id: TypeId, path: Chunk[String], fields: Chunk[Labelled], optional: Boolean) => - Sum(id, NodePath(path), fields, optional) - ) - } - - final case class Either( - override val path: NodePath, - left: MetaSchema, - right: MetaSchema, - override val optional: Boolean = false - ) extends MetaSchema - - object Either { - implicit val schema: Schema[Either] = { - Schema.CaseClass4( - TypeId.parse("zio.schema.meta.MetaSchema.Either"), - field01 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field02 = Schema - .Field("left", Schema[MetaSchema], get0 = _.left, set0 = (a, value: MetaSchema) => a.copy(left = value)), - field03 = Schema - .Field("right", Schema[MetaSchema], get0 = _.right, set0 = (a, value: MetaSchema) => a.copy(right = value)), - field04 = Schema - .Field( - "optional", - Schema[Boolean], - get0 = _.optional, - set0 = (a, value: Boolean) => a.copy(optional = value) - ), - (path: Chunk[String], left: MetaSchema, right: MetaSchema, optional: Boolean) => - Either(NodePath(path), left, right, optional) - ) - } - } - - final case class FailNode( - message: String, - override val path: NodePath, - override val optional: Boolean = false - ) extends MetaSchema - - object FailNode { - implicit val schema: Schema[FailNode] = Schema.CaseClass3( - TypeId.parse("zio.schema.meta.MetaSchema.FailNode"), - field01 = - Schema.Field("message", Schema[String], get0 = _.message, set0 = (a, value: String) => a.copy(message = value)), - field02 = Schema.Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field03 = Schema - .Field("optional", Schema[Boolean], get0 = _.optional, set0 = (a, value: Boolean) => a.copy(optional = value)), - (m: String, path: Chunk[String], optional: Boolean) => FailNode(m, NodePath(path), optional) - ) - } - - final case class ListNode( - item: MetaSchema, - override val path: NodePath, - override val optional: Boolean = false - ) extends MetaSchema - - object ListNode { - implicit val schema: Schema[ListNode] = Schema.CaseClass3( - TypeId.parse("zio.schema.meta.MetaSchema.ListNode"), - field01 = - Schema.Field("item", Schema[MetaSchema], get0 = _.item, set0 = (a, value: MetaSchema) => a.copy(item = value)), - field02 = Schema.Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field03 = Schema - .Field("optional", Schema[Boolean], get0 = _.optional, set0 = (a, value: Boolean) => a.copy(optional = value)), - (item: MetaSchema, path: Chunk[String], optional: Boolean) => ListNode(item, NodePath(path), optional) - ) - } - - final case class Dictionary( - keys: MetaSchema, - values: MetaSchema, - override val path: NodePath, - override val optional: Boolean = false - ) extends MetaSchema - - object Dictionary { - implicit val schema: Schema[Dictionary] = Schema.CaseClass4( - TypeId.parse("zio.schema.meta.MetaSchema.Dictionary"), - field01 = - Schema.Field("keys", Schema[MetaSchema], get0 = _.keys, set0 = (a, value: MetaSchema) => a.copy(keys = value)), - field02 = Schema - .Field("values", Schema[MetaSchema], get0 = _.values, set0 = (a, value: MetaSchema) => a.copy(values = value)), - field03 = Schema.Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field04 = Schema - .Field("optional", Schema[Boolean], get0 = _.optional, set0 = (a, value: Boolean) => a.copy(optional = value)), - (keys: MetaSchema, values: MetaSchema, path: Chunk[String], optional: Boolean) => - Dictionary(keys, values, NodePath(path), optional) - ) - } - - final case class Value( - valueType: StandardType[_], - override val path: NodePath = NodePath.root, - override val optional: Boolean = false - ) extends MetaSchema - - object Value { - implicit val schema: Schema[Value] = - Schema - .CaseClass3[String, Chunk[String], Boolean, (String, Chunk[String], Boolean)]( - TypeId.parse("zio.schema.meta.MetaSchema.Value"), - field01 = - Schema.Field("valueType", Schema[String], get0 = _._1, set0 = (a, value: String) => (value, a._2, a._3)), - field02 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _._2, - set0 = (a, value: Chunk[String]) => (a._1, value, a._3) - ), - field03 = - Schema.Field("optional", Schema[Boolean], get0 = _._3, set0 = (a, value: Boolean) => (a._1, a._2, value)), - construct0 = (v, p, o) => (v, p, o) - ) - .transformOrFail(fromTuple, tupled) - - private def tupled(value: Value): scala.Either[String, (String, Chunk[String], Boolean)] = - Right((value.valueType.tag, value.path, value.optional)) - - private def fromTuple(tuple: (String, Chunk[String], Boolean)): scala.Either[String, Value] = tuple match { - case (s, path, optional) => - StandardType - .fromString(s) - .map(typ => Value(typ, NodePath(path), optional)) - .toRight(s"unkown standard type $s") - } - } - final case class Ref( - refPath: NodePath, - override val path: NodePath, - optional: Boolean = false - ) extends MetaSchema - - object Ref { - implicit val schema: Schema[Ref] = - Schema.CaseClass3( - TypeId.parse("zio.schema.meta.MetaSchema.Ref"), - field01 = Schema.Field( - "refPath", - Schema[String].repeated, - get0 = _.refPath, - set0 = (a, value: Chunk[String]) => a.copy(refPath = NodePath(value)) - ), - field02 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field03 = Schema - .Field( - "optional", - Schema[Boolean], - get0 = _.optional, - set0 = (a, value: Boolean) => a.copy(optional = value) - ), - (refPath: Chunk[String], path: Chunk[String], optional: Boolean) => - Ref(NodePath(refPath), NodePath(path), optional) - ) - } - - final case class Dynamic( - override val path: NodePath, - optional: Boolean = false - ) extends MetaSchema - - object Dynamic { - implicit val schema: Schema[Dynamic] = - Schema.CaseClass2( - TypeId.parse("zio.schema.meta.MetaSchema.Dynamic"), - field01 = Schema - .Field( - "path", - Schema[String].repeated, - get0 = _.path, - set0 = (a, value: Chunk[String]) => a.copy(path = NodePath(value)) - ), - field02 = Schema - .Field( - "optional", - Schema[Boolean], - get0 = _.optional, - set0 = (a, value: Boolean) => a.copy(optional = value) - ), - (path: Chunk[String], optional: Boolean) => Dynamic(NodePath(path), optional) - ) - } - - final private[schema] case class NodeBuilder( - path: NodePath, - lineage: Lineage, - optional: Boolean = false - ) { self => - private val children: ChunkBuilder[Labelled] = ChunkBuilder.make[Labelled]() - - def addLabelledSubtree(label: String, schema: Schema[_]): NodeBuilder = { - children += (label -> subtree(path / label, lineage, schema)) - self - } - - def buildProduct(id: TypeId): Product = Product(id, path, children.result(), optional) - - def buildSum(id: TypeId): Sum = Sum(id, path, children.result(), optional) - } - - @tailrec - def fromSchema[A](schema: Schema[A]): MetaSchema = schema match { - case Schema.Primitive(typ, _) => Value(typ, NodePath.root) - case Schema.Fail(message, _) => FailNode(message, NodePath.root) - case Schema.Optional(schema, _) => subtree(NodePath.root, Chunk.empty, schema, optional = true) - case Schema.Either(left, right, _) => - Either( - NodePath.root, - subtree(NodePath.root / "left", Chunk.empty, left), - subtree(NodePath.root / "right", Chunk.empty, right) - ) - case Schema.Tuple2(left, right, _) => - Tuple( - NodePath.root, - subtree(NodePath.root / "left", Chunk.empty, left), - subtree(NodePath.root / "right", Chunk.empty, right) - ) - case Schema.Sequence(schema, _, _, _, _) => - ListNode(item = subtree(NodePath.root / "item", Chunk.empty, schema), NodePath.root) - case Schema.Map(ks, vs, _) => - Dictionary( - keys = subtree(NodePath.root / "keys", Chunk.empty, ks), - values = subtree(NodePath.root / "values", Chunk.empty, vs), - NodePath.root - ) - case Schema.Set(schema, _) => - ListNode(item = subtree(NodePath.root / "item", Chunk.empty, schema), NodePath.root) - case Schema.Transform(schema, _, _, _, _) => subtree(NodePath.root, Chunk.empty, schema) - case lzy @ Schema.Lazy(_) => fromSchema(lzy.schema) - case s: Schema.Record[A] => - s.fields - .foldLeft(NodeBuilder(NodePath.root, Chunk(s.hashCode() -> NodePath.root))) { (node, field) => - node.addLabelledSubtree(field.name, field.schema) - } - .buildProduct(s.id) - case s: Schema.Enum[A] => - s.cases - .foldLeft(NodeBuilder(NodePath.root, Chunk(s.hashCode() -> NodePath.root))) { - case (node, caseValue) => - node.addLabelledSubtree(caseValue.id, caseValue.schema) - } - .buildSum(s.id) - case Schema.Dynamic(_) => Dynamic(NodePath.root) - } - - private[schema] def subtree( - path: NodePath, - lineage: Lineage, - schema: Schema[_], - optional: Boolean = false - ): MetaSchema = - lineage - .find(_._1 == schema.hashCode()) - .map { - case (_, refPath) => - Ref(refPath, path, optional) - } - .getOrElse { - schema match { - case Schema.Primitive(typ, _) => Value(typ, path, optional) - case Schema.Optional(schema, _) => subtree(path, lineage, schema, optional = true) - case Schema.Either(left, right, _) => - Either( - path, - subtree(path / "left", lineage, left, optional = false), - subtree(path / "right", lineage, right, optional = false), - optional - ) - case Schema.Tuple2(left, right, _) => - Tuple( - path, - subtree(path / "left", lineage, left, optional = false), - subtree(path / "right", lineage, right, optional = false), - optional - ) - case Schema.Sequence(schema, _, _, _, _) => - ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) - case Schema.Map(ks, vs, _) => - Dictionary( - keys = subtree(path / "keys", Chunk.empty, ks, optional = false), - values = subtree(path / "values", Chunk.empty, vs, optional = false), - path, - optional - ) - case Schema.Set(schema @ _, _) => - ListNode(item = subtree(path / "item", lineage, schema, optional = false), path, optional) - case Schema.Transform(schema, _, _, _, _) => subtree(path, lineage, schema, optional) - case lzy @ Schema.Lazy(_) => subtree(path, lineage, lzy.schema, optional) - case s: Schema.Record[_] => - s.fields - .foldLeft(NodeBuilder(path, lineage :+ (s.hashCode() -> path), optional)) { (node, field) => - node.addLabelledSubtree(field.name, field.schema) - } - .buildProduct(s.id) - case s: Schema.Enum[_] => - s.cases - .foldLeft(NodeBuilder(path, lineage :+ (s.hashCode() -> path), optional)) { - case (node, caseValue) => - node.addLabelledSubtree(caseValue.id, caseValue.schema) - } - .buildSum(s.id) - case Schema.Fail(message, _) => FailNode(message, path) - case Schema.Dynamic(_) => Dynamic(path, optional) - } - } - - private[schema] def materialize(ast: MetaSchema, refs: mutable.Map[NodePath, Schema[_]]): Schema[_] = { - val baseSchema = ast match { - case MetaSchema.Value(typ, _, _) => - Schema.Primitive(typ, Chunk.empty) - case MetaSchema.FailNode(msg, _, _) => Schema.Fail(msg) - case MetaSchema.Ref(refPath, _, _) => - Schema.defer( - refs.getOrElse(refPath, Schema.Fail(s"invalid ref path $refPath")) - ) - case MetaSchema.Product(id, _, elems, _) => - Schema.record( - id, - elems.map { - case (label, ast) => - Schema.Field( - label, - materialize(ast, refs).asInstanceOf[Schema[Any]], - get0 = (p: ListMap[String, _]) => p(label), - set0 = (p: ListMap[String, _], v: Any) => p.updated(label, v) - ) - }: _* - ) - case MetaSchema.Tuple(_, left, right, _) => - Schema.tuple2( - materialize(left, refs), - materialize(right, refs) - ) - case MetaSchema.Sum(id, _, elems, _) => - Schema.enumeration[Any, CaseSet.Aux[Any]]( - id, - elems.foldRight[CaseSet.Aux[Any]](CaseSet.Empty[Any]()) { - case ((label, ast), acc) => - val _case: Schema.Case[Any, Any] = Schema - .Case[Any, Any]( - label, - materialize(ast, refs).asInstanceOf[Schema[Any]], - identity[Any], - identity[Any], - _.isInstanceOf[Any], - Chunk.empty - ) - CaseSet.Cons(_case, acc) - } - ) - case MetaSchema.Either(_, left, right, _) => - Schema.either( - materialize(left, refs), - materialize(right, refs) - ) - case MetaSchema.ListNode(itemAst, _, _) => - Schema.chunk(materialize(itemAst, refs)) - case MetaSchema.Dictionary(keyAst, valueAst, _, _) => - Schema.Map(materialize(keyAst, refs), materialize(valueAst, refs), Chunk.empty) - case MetaSchema.Dynamic(_, _) => - Schema.dynamicValue - case ast => Schema.Fail(s"AST cannot be materialized to a Schema:\n$ast") - } - - refs += ast.path -> baseSchema - - if (ast.optional) baseSchema.optional else baseSchema - } - - implicit lazy val schema: Schema[MetaSchema] = - Schema.Lazy { () => - Schema.EnumN[MetaSchema, CaseSet.Aux[MetaSchema]]( - TypeId.parse("zio.schema.meta.MetaSchema"), - caseOf[Value, MetaSchema]("Value")(_.asInstanceOf[Value])(_.asInstanceOf[MetaSchema])(_.isInstanceOf[Value]) ++ - caseOf[Sum, MetaSchema]("Sum")(_.asInstanceOf[Sum])(_.asInstanceOf[MetaSchema])(_.isInstanceOf[Sum]) ++ - caseOf[Either, MetaSchema]("Either")(_.asInstanceOf[Either])(_.asInstanceOf[MetaSchema])( - _.isInstanceOf[Either] - ) ++ - caseOf[Product, MetaSchema]("Product")(_.asInstanceOf[Product])(_.asInstanceOf[MetaSchema])( - _.isInstanceOf[Product] - ) ++ - caseOf[Tuple, MetaSchema]("Tuple")(_.asInstanceOf[Tuple])(_.asInstanceOf[MetaSchema])(_.isInstanceOf[Tuple]) ++ - caseOf[Ref, MetaSchema]("Ref")(_.asInstanceOf[Ref])(_.asInstanceOf[MetaSchema])(_.isInstanceOf[Ref]) ++ - caseOf[ListNode, MetaSchema]("ListNode")(_.asInstanceOf[ListNode])(_.asInstanceOf[MetaSchema])( - _.isInstanceOf[ListNode] - ) ++ - caseOf[Dictionary, MetaSchema]("Dictionary")(_.asInstanceOf[Dictionary])(_.asInstanceOf[MetaSchema])( - _.isInstanceOf[Dictionary] - ) ++ - caseOf[Dynamic, MetaSchema]("Dynamic")(_.asInstanceOf[Dynamic])(_.asInstanceOf[MetaSchema])( - _.isInstanceOf[Dynamic] - ) ++ - caseOf[FailNode, MetaSchema]("Fail")(_.asInstanceOf[FailNode])(_.asInstanceOf[MetaSchema])( - _.isInstanceOf[FailNode] - ), - Chunk.empty - ) - } - - implicit val equals: Equal[MetaSchema] = Equal.default -} - -private[schema] object AstRenderer { - private val INDENT_STEP = 2 - - def render(ast: MetaSchema): String = ast match { - case v @ MetaSchema.Value(_, _, _) => renderValue(v, 0, None) - case f @ MetaSchema.FailNode(_, _, _) => renderFail(f, 0, None) - case MetaSchema.Product(_, _, fields, optional) => - val buffer = new StringBuffer() - buffer.append(s"product") - if (optional) buffer.append("?") - buffer.append("\n").append(fields.map(renderField(_, INDENT_STEP)).mkString("\n")).toString - case MetaSchema.Tuple(_, left, right, optional) => - val buffer = new StringBuffer() - buffer.append(s"tuple") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("left" -> left, "right" -> right).map(renderField(_, INDENT_STEP)).mkString("\n")) - .toString - case MetaSchema.Sum(_, _, cases, optional) => - val buffer = new StringBuffer() - buffer.append(s"enum") - if (optional) buffer.append("?") - buffer.append("\n").append(cases.map(renderField(_, INDENT_STEP)).mkString("\n")).toString - case MetaSchema.Either(_, left, right, optional) => - val buffer = new StringBuffer() - buffer.append(s"either") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("left" -> left, "right" -> right).map(renderField(_, INDENT_STEP)).mkString("\n")) - .toString - case MetaSchema.ListNode(items, _, optional) => - val buffer = new StringBuffer() - buffer.append(s"list") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("item" -> items).map(renderField(_, INDENT_STEP)).mkString("\n")) - .toString - case MetaSchema.Dictionary(keys, values, _, optional) => - val buffer = new StringBuffer() - buffer.append(s"map") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("keys" -> keys, "values" -> values).map(renderField(_, INDENT_STEP)).mkString("\n")) - .toString - case MetaSchema.Ref(refPath, _, optional) => - val buffer = new StringBuffer() - buffer.append(s"ref#$refPath") - if (optional) buffer.append("?") - buffer.toString - case MetaSchema.Dynamic(_, optional) => - val buffer = new StringBuffer() - buffer.append(s"dynamic") - if (optional) buffer.append("?") - buffer.toString - } - - def renderField(value: MetaSchema.Labelled, indent: Int): String = { - val buffer = new StringBuffer() - value match { - case (label, value @ MetaSchema.Value(_, _, _)) => - renderValue(value, indent, Some(label)) - case (label, fail @ MetaSchema.FailNode(_, _, _)) => - renderFail(fail, indent, Some(label)) - case (label, MetaSchema.Product(_, _, fields, optional)) => - pad(buffer, indent) - buffer.append(s"$label: record") - if (optional) buffer.append("?") - buffer.append("\n").append(fields.map(renderField(_, indent + INDENT_STEP)).mkString("\n")).toString - case (label, MetaSchema.Tuple(_, left, right, optional)) => - pad(buffer, indent) - buffer.append(s"$label: tuple") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("left" -> left, "right" -> right).map(renderField(_, indent + INDENT_STEP)).mkString("\n")) - .toString - case (label, MetaSchema.Sum(_, _, cases, optional)) => - pad(buffer, indent) - buffer.append(s"$label: enum") - if (optional) buffer.append("?") - buffer.append("\n").append(cases.map(renderField(_, indent + INDENT_STEP)).mkString("\n")).toString - case (label, MetaSchema.Either(_, left, right, optional)) => - pad(buffer, indent) - buffer.append(s"$label: either") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("left" -> left, "right" -> right).map(renderField(_, indent + INDENT_STEP)).mkString("\n")) - .toString - case (label, MetaSchema.ListNode(items, _, optional)) => - val buffer = new StringBuffer() - buffer.append(s"$label: list") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("item" -> items).map(renderField(_, INDENT_STEP)).mkString("\n")) - .toString - case (label, MetaSchema.Dictionary(keys, values, _, optional)) => - val buffer = new StringBuffer() - buffer.append(s"$label: map") - if (optional) buffer.append("?") - buffer - .append("\n") - .append(Chunk("keys" -> keys, "values" -> values).map(renderField(_, INDENT_STEP)).mkString("\n")) - .toString - case (label, MetaSchema.Ref(refPath, _, optional)) => - pad(buffer, indent) - buffer.append(s"$label: ") - if (optional) buffer.append("?") - buffer.append(s"{ref#${refPath.render}}").toString - case (label, MetaSchema.Dynamic(_, optional)) => - pad(buffer, indent) - buffer.append(s"$label: ") - buffer.append(s"dynamic") - if (optional) buffer.append("?") - buffer.toString - } - } - - def renderValue(value: MetaSchema.Value, indent: Int, label: Option[String]): String = { - val buffer = new StringBuffer() - pad(buffer, indent) - label.foreach(l => buffer.append(s"$l: ")) - if (value.optional) buffer.append("?") - buffer.append(value.valueType.tag).toString - } - - def renderFail(fail: MetaSchema.FailNode, indent: Int, label: Option[String]): String = { - val buffer = new StringBuffer() - pad(buffer, indent) - label.foreach(l => buffer.append(s"$l: ")) - if (fail.optional) buffer.append("?") - buffer.append(s"FAIL: ${fail.message}").toString - } - - private def pad(buffer: StringBuffer, indent: Int): StringBuffer = { - if (indent > 0) { - buffer.append("|") - for (_ <- 0 until indent) { - buffer.append("-") - } - } - buffer - } -} diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/Migration.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/Migration.scala index 556fcdda3..908baae5a 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/meta/Migration.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/meta/Migration.scala @@ -2,6 +2,7 @@ package zio.schema.meta import scala.collection.immutable.ListMap +import zio.schema.meta.ExtensibleMetaSchema.Labelled import zio.schema.{ DynamicValue, StandardType } import zio.{ Chunk, ChunkBuilder } @@ -69,11 +70,11 @@ object Migration { def goProduct( f: MetaSchema, t: MetaSchema, - ffields: Chunk[(String, MetaSchema)], - tfields: Chunk[(String, MetaSchema)] + ffields: Chunk[MetaSchema.Labelled], + tfields: Chunk[MetaSchema.Labelled] ): Either[String, Chunk[Migration]] = matchedSubtrees(ffields, tfields).map { - case ((nextPath, fs), (_, ts)) => go(acc, path / nextPath, fs, ts, ignoreRefs) + case (Labelled(nextPath, fs), Labelled(_, ts)) => go(acc, path / nextPath, fs, ts, ignoreRefs) }.foldRight[Either[String, Chunk[Migration]]](Right(Chunk.empty)) { case (err @ Left(_), Right(_)) => err case (Right(_), err @ Left(_)) => err @@ -91,11 +92,11 @@ object Migration { def goSum( f: MetaSchema, t: MetaSchema, - fcases: Chunk[(String, MetaSchema)], - tcases: Chunk[(String, MetaSchema)] + fcases: Chunk[MetaSchema.Labelled], + tcases: Chunk[MetaSchema.Labelled] ): Either[String, Chunk[Migration]] = matchedSubtrees(fcases, tcases).map { - case ((nextPath, fs), (_, ts)) => go(acc, path / nextPath, fs, ts, ignoreRefs) + case (Labelled(nextPath, fs), Labelled(_, ts)) => go(acc, path / nextPath, fs, ts, ignoreRefs) }.foldRight[Either[String, Chunk[Migration]]](Right(Chunk.empty)) { case (err @ Left(_), Right(_)) => err case (Right(_), err @ Left(_)) => err @@ -111,54 +112,71 @@ object Migration { ) (fromSubtree, toSubtree) match { - case (f @ MetaSchema.FailNode(_, _, _), t @ MetaSchema.FailNode(_, _, _)) => + case (f @ ExtensibleMetaSchema.FailNode(_, _, _), t @ ExtensibleMetaSchema.FailNode(_, _, _)) => Right( if (f.message == t.message) Chunk.empty else transformShape(path, f, t) :+ UpdateFail(path, t.message) ) - case (f @ MetaSchema.Product(_, _, ffields, _), t @ MetaSchema.Product(_, _, tfields, _)) => + case (f @ ExtensibleMetaSchema.Product(_, _, ffields, _), t @ ExtensibleMetaSchema.Product(_, _, tfields, _)) => goProduct(f, t, ffields, tfields) - case (f @ MetaSchema.Tuple(_, fleft, fright, _), t @ MetaSchema.Tuple(_, tleft, tright, _)) => - val ffields = Chunk("left" -> fleft, "right" -> fright) - val tfields = Chunk("left" -> tleft, "right" -> tright) + case ( + f @ ExtensibleMetaSchema.Tuple(_, fleft, fright, _), + t @ ExtensibleMetaSchema.Tuple(_, tleft, tright, _) + ) => + val ffields = Chunk(Labelled("left", fleft), Labelled("right", fright)) + val tfields = Chunk(Labelled("left", tleft), Labelled("right", tright)) goProduct(f, t, ffields, tfields) - case (f @ MetaSchema.Product(_, _, ffields, _), t @ MetaSchema.Tuple(_, tleft, tright, _)) => - val tfields = Chunk("left" -> tleft, "right" -> tright) + case ( + f @ ExtensibleMetaSchema.Product(_, _, ffields, _), + t @ ExtensibleMetaSchema.Tuple(_, tleft, tright, _) + ) => + val tfields = Chunk(Labelled("left", tleft), Labelled("right", tright)) goProduct(f, t, ffields, tfields) - case (f @ MetaSchema.Tuple(_, fleft, fright, _), t @ MetaSchema.Product(_, _, tfields, _)) => - val ffields = Chunk("left" -> fleft, "right" -> fright) + case ( + f @ ExtensibleMetaSchema.Tuple(_, fleft, fright, _), + t @ ExtensibleMetaSchema.Product(_, _, tfields, _) + ) => + val ffields = Chunk(Labelled("left", fleft), Labelled("right", fright)) goProduct(f, t, ffields, tfields) - case (f @ MetaSchema.ListNode(fitem, _, _), t @ MetaSchema.ListNode(titem, _, _)) => - val ffields = Chunk("item" -> fitem) - val tfields = Chunk("item" -> titem) + case (f @ ExtensibleMetaSchema.ListNode(fitem, _, _), t @ ExtensibleMetaSchema.ListNode(titem, _, _)) => + val ffields = Chunk(Labelled("item", fitem)) + val tfields = Chunk(Labelled("item", titem)) goProduct(f, t, ffields, tfields) - case (MetaSchema.ListNode(fitem, _, _), titem) => + case (ExtensibleMetaSchema.ListNode(fitem, _, _), titem) => derive(fitem, titem).map(migrations => DecrementDimensions(titem.path, 1) +: migrations) - case (fitem, MetaSchema.ListNode(titem, _, _)) => + case (fitem, ExtensibleMetaSchema.ListNode(titem, _, _)) => derive(fitem, titem).map(migrations => IncrementDimensions(titem.path, 1) +: migrations) - case (f @ MetaSchema.Dictionary(fkeys, fvalues, _, _), t @ MetaSchema.Dictionary(tkeys, tvalues, _, _)) => - val ffields = Chunk("keys" -> fkeys, "values" -> fvalues) - val tfields = Chunk("keys" -> tkeys, "values" -> tvalues) + case ( + f @ ExtensibleMetaSchema.Dictionary(fkeys, fvalues, _, _), + t @ ExtensibleMetaSchema.Dictionary(tkeys, tvalues, _, _) + ) => + val ffields = Chunk(Labelled("keys", fkeys), Labelled("values", fvalues)) + val tfields = Chunk(Labelled("keys", tkeys), Labelled("values", tvalues)) goProduct(f, t, ffields, tfields) - case (f @ MetaSchema.Sum(_, _, fcases, _), t @ MetaSchema.Sum(_, _, tcases, _)) => + case (f @ ExtensibleMetaSchema.Sum(_, _, fcases, _), t @ ExtensibleMetaSchema.Sum(_, _, tcases, _)) => goSum(f, t, fcases, tcases) - case (f @ MetaSchema.Either(_, fleft, fright, _), t @ MetaSchema.Either(_, tleft, tright, _)) => - val fcases = Chunk("left" -> fleft, "right" -> fright) - val tcases = Chunk("left" -> tleft, "right" -> tright) + case ( + f @ ExtensibleMetaSchema.Either(_, fleft, fright, _), + t @ ExtensibleMetaSchema.Either(_, tleft, tright, _) + ) => + val fcases = Chunk(Labelled("left", fleft), Labelled("right", fright)) + val tcases = Chunk(Labelled("left", tleft), Labelled("right", tright)) goSum(f, t, fcases, tcases) - case (f @ MetaSchema.Sum(_, _, fcases, _), t @ MetaSchema.Either(_, tleft, tright, _)) => - val tcases = Chunk("left" -> tleft, "right" -> tright) + case (f @ ExtensibleMetaSchema.Sum(_, _, fcases, _), t @ ExtensibleMetaSchema.Either(_, tleft, tright, _)) => + val tcases = Chunk(Labelled("left", tleft), Labelled("right", tright)) goSum(f, t, fcases, tcases) - case (f @ MetaSchema.Either(_, fleft, fright, _), t @ MetaSchema.Sum(_, _, tcases, _)) => - val fcases = Chunk("left" -> fleft, "right" -> fright) + case (f @ ExtensibleMetaSchema.Either(_, fleft, fright, _), t @ ExtensibleMetaSchema.Sum(_, _, tcases, _)) => + val fcases = Chunk(Labelled("left", fleft), Labelled("right", fright)) goSum(f, t, fcases, tcases) - case (f @ MetaSchema.Value(ftype, _, _), t @ MetaSchema.Value(ttype, _, _)) if ttype != ftype => + case (f @ ExtensibleMetaSchema.Value(ftype, _, _), t @ ExtensibleMetaSchema.Value(ttype, _, _)) + if ttype != ftype => Right(transformShape(path, f, t) :+ ChangeType(path, ttype)) - case (f @ MetaSchema.Value(_, _, _), t @ MetaSchema.Value(_, _, _)) => + case (f @ ExtensibleMetaSchema.Value(_, _, _), t @ ExtensibleMetaSchema.Value(_, _, _)) => Right(transformShape(path, f, t)) - case (f @ MetaSchema.Ref(fromRef, nodePath, _), t @ MetaSchema.Ref(toRef, _, _)) if fromRef == toRef => + case (f @ ExtensibleMetaSchema.Ref(fromRef, nodePath, _), t @ ExtensibleMetaSchema.Ref(toRef, _, _)) + if fromRef == toRef => if (ignoreRefs) Right(Chunk.empty) else { val recursiveMigrations = acc @@ -206,8 +224,8 @@ object Migration { * Represents a valid label transformation. * * Not currently implemented but we can use this type to encode - * unambiguous string transformations applued to field and case labels. - * For example, convering from snake to camel case (or vica versa) + * unambiguous string transformations applied to field and case labels. + * For example, converting from snake to camel case (or vica versa) */ sealed trait LabelTransformation { def apply(label: String): Either[String, String] @@ -220,7 +238,7 @@ object Migration { to: Chunk[MetaSchema.Labelled] ): Chunk[(MetaSchema.Labelled, MetaSchema.Labelled)] = from.map { - case fromNode @ (label, _) => to.find(_._1 == label).map(toNode => fromNode -> toNode) + case fromNode @ Labelled(label, _) => to.find(_.label == label).map(toNode => fromNode -> toNode) }.collect { case Some(pair) => pair } @@ -231,8 +249,8 @@ object Migration { to: Chunk[MetaSchema.Labelled] ): Chunk[Migration] = to.foldRight[Chunk[Migration]](Chunk.empty) { - case ((nodeLabel, _), acc) if from.exists(_._1 == nodeLabel) => acc - case ((nodeLabel, ast), acc) => acc :+ AddNode(path / nodeLabel, ast) + case (Labelled(nodeLabel, _), acc) if from.exists(_.label == nodeLabel) => acc + case (Labelled(nodeLabel, ast), acc) => acc :+ AddNode(path / nodeLabel, ast) } private def caseInsertions( @@ -241,8 +259,8 @@ object Migration { to: Chunk[MetaSchema.Labelled] ): Chunk[Migration] = to.foldRight[Chunk[Migration]](Chunk.empty) { - case ((nodeLabel, _), acc) if from.exists(_._1 == nodeLabel) => acc - case ((nodeLabel, ast), acc) => acc :+ AddCase(path / nodeLabel, ast) + case (Labelled(nodeLabel, _), acc) if from.exists(_.label == nodeLabel) => acc + case (Labelled(nodeLabel, ast), acc) => acc :+ AddCase(path / nodeLabel, ast) } private def deletions( @@ -251,8 +269,8 @@ object Migration { to: Chunk[MetaSchema.Labelled] ): Chunk[Migration] = from.foldRight[Chunk[Migration]](Chunk.empty) { - case ((nodeLabel, _), acc) if !to.exists(_._1 == nodeLabel) => acc :+ DeleteNode(path / nodeLabel) - case (_, acc) => acc + case (Labelled(nodeLabel, _), acc) if !to.exists(_.label == nodeLabel) => acc :+ DeleteNode(path / nodeLabel) + case (_, acc) => acc } private def transformShape(path: NodePath, from: MetaSchema, to: MetaSchema): Chunk[Migration] = { diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/SchemaInstances.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/SchemaInstances.scala new file mode 100644 index 000000000..caa60ce49 --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/meta/SchemaInstances.scala @@ -0,0 +1,51 @@ +package zio.schema.meta + +import zio.constraintless.IsElementOf.{ Head, Tail } +import zio.constraintless.{ IsElementOf, TypeList } +import zio.schema.Schema + +/** Special version of zio-constraintless's Instances for capturing Schema instances + **/ +trait SchemaInstances[As <: TypeList] { + + def withInstance[B, D](use: Schema[B] => D)( + implicit + ev: B IsElementOf As + ): D + + def all: Set[Schema[_]] +} + +object SchemaInstances { + import TypeList._ + + implicit def instancesCons[A, As <: TypeList]( + implicit + c: Schema[A], + ev: SchemaInstances[As] + ): SchemaInstances[A :: As] = new SchemaInstances[A :: As] { + override def withInstance[B, D]( + use: Schema[B] => D + )(implicit ev2: IsElementOf[B, A :: As]): D = + ev2 match { + case Head() => + use( + c.asInstanceOf[Schema[B]] + ) // Coz we have compile time evidence that B is in fact A + case Tail(x) => ev.withInstance(use)(x) + } + + override def all: Set[Schema[_]] = ev.all + c + } + + // The definition is slightly from what mentioned in the paper where it traverses hlist + implicit lazy val instancesEnd: SchemaInstances[End] = new SchemaInstances[End] { + override def withInstance[B, D](use: Schema[B] => D)( + implicit + ev: IsElementOf[B, End] + ): D = + sys.error("hmmm") + + override def all: Set[Schema[_]] = Set.empty + } +} diff --git a/zio-schema/shared/src/main/scala/zio/schema/meta/package.scala b/zio-schema/shared/src/main/scala/zio/schema/meta/package.scala index 4077e855f..e8adee60b 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/meta/package.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/meta/package.scala @@ -1,5 +1,17 @@ package zio.schema +import zio.constraintless.TypeList._ + package object meta { type NodePath = NodePath.Type + + type MetaSchema = ExtensibleMetaSchema[DynamicValue :: End] + + object MetaSchema { + type Labelled = ExtensibleMetaSchema.Labelled[DynamicValue :: End] + + val schema: Schema[MetaSchema] = ExtensibleMetaSchema.schema[DynamicValue :: End] + + def fromSchema[A](schema: Schema[A]): MetaSchema = ExtensibleMetaSchema.fromSchema[A, DynamicValue :: End](schema) + } }