From 6482280cff818027565cc51931997a3394a23db4 Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Wed, 5 Jul 2023 16:33:40 +0330 Subject: [PATCH] add manual and automatic derivation. --- build.sbt | 1 + docs/automatic-schema-derivation.md | 91 +++++++++++++ docs/manual-schema-construction.md | 192 ++++++++++++++++++++++++++++ docs/our-first-schema.md | 129 ------------------- docs/sidebars.js | 21 ++- 5 files changed, 299 insertions(+), 135 deletions(-) create mode 100644 docs/automatic-schema-derivation.md create mode 100644 docs/manual-schema-construction.md delete mode 100644 docs/our-first-schema.md diff --git a/build.sbt b/build.sbt index cb32d750c..b81d8a290 100644 --- a/build.sbt +++ b/build.sbt @@ -341,4 +341,5 @@ lazy val docs = project |sbt test |```""".stripMargin ) + .dependsOn(zioSchemaJVM, zioSchemaProtobufJVM) .enablePlugins(WebsitePlugin) diff --git a/docs/automatic-schema-derivation.md b/docs/automatic-schema-derivation.md new file mode 100644 index 000000000..b057ac814 --- /dev/null +++ b/docs/automatic-schema-derivation.md @@ -0,0 +1,91 @@ +--- +id: automatic-schema-derivation +title: "Automatic Schema Derivation" +--- + +Automatic schema derivation is the process of generating schema definitions for data types automatically, without the need to manually write them. It allows us to generate the schema for a data type based on its structure and annotations. + +Instead of manually specifying the schema for each data type, we can rely on automatic schema derivation to generate the schema for us. This approach can save time and reduce the potential for errors, especially when dealing with complex data models. + +By leveraging reflection and type introspection using macros, automatic schema derivation analyzes the structure of the data type and its fields, including their names, types, and annotations. It then generates the corresponding schema definition based on this analysis. + +ZIO streamlines schema derivation through its `zio-schema-derivation` package, which utilizes the capabilities of Scala macros to automatically derive schemas. In order to use automatic schema derivation, we neeed to add the following line to our `build.sbt` file: + +```scala +libraryDependencies += "dev.zio" %% "zio-schema-derivation" % @VERSION@ +``` + +Once again, let's revisit our domain models: + +```scala mdoc:compile-only +final case class Person(name: String, age: Int) + +sealed trait PaymentMethod + +object PaymentMethod { + final case class CreditCard(number: String, expirationMonth: Int, expirationYear: Int) extends PaymentMethod + final case class WireTransfer(accountNumber: String, bankCode: String) extends PaymentMethod +} + +final case class Customer(person: Person, paymentMethod: PaymentMethod) +``` + +We can easily use auto derivation to create schemas: + +```scala +import zio.schema._ +import zio.schema.codec._ + +final case class Person(name: String, age: Int) + +object Person { + implicit val schema: Schema[Person] = DeriveSchema.gen[Person] +} + +sealed trait PaymentMethod + +object PaymentMethod { + + implicit val schema: Schema[PaymentMethod] = + DeriveSchema.gen[PaymentMethod] + + final case class CreditCard( + number: String, + expirationMonth: Int, + expirationYear: Int + ) extends PaymentMethod + + final case class WireTransfer(accountNumber: String, bankCode: String) + extends PaymentMethod +} + +final case class Customer(person: Person, paymentMethod: PaymentMethod) + +object Customer { + implicit val schema: Schema[Customer] = DeriveSchema.gen[Customer] +} +``` + +Now we can write an example that demonstrates a roundtrip test for protobuf codecs: + +```scala +// Create a customer instance +val customer = + Customer( + person = Person("John Doe", 42), + paymentMethod = PaymentMethod.CreditCard("1000100010001000", 6, 2024) + ) + +// Create binary codec from customer +val customerCodec: BinaryCodec[Customer] = + ProtobufCodec.protobufCodec[Customer] + +// Encode the customer object +val encodedCustomer: Chunk[Byte] = customerCodec.encode(customer) + +// Decode the byte array back to the person instance +val decodedCustomer: Either[DecodeError, Customer] = + customerCodec.decode(encodedCustomer) + +assert(Right(customer) == decodedCustomer) +``` diff --git a/docs/manual-schema-construction.md b/docs/manual-schema-construction.md new file mode 100644 index 000000000..29c0848fc --- /dev/null +++ b/docs/manual-schema-construction.md @@ -0,0 +1,192 @@ +--- +id: manual-schema-construction +title: "Manual Schema Construction" +--- + +Assume we have a domain containing following models: + +```scala +object Domain { + final case class Person(name: String, age: Int) + + sealed trait PaymentMethod + + object PaymentMethod { + final case class CreditCard(number: String, expirationMonth: Int, expirationYear: Int) extends PaymentMethod + final case class WireTransfer(accountNumber: String, bankCode: String) extends PaymentMethod + } + + final case class Customer(person: Person, paymentMethod: PaymentMethod) + +} +``` + +Let's begin by creating a schema for the `Person` data type: + +```scala mdoc:silent +import zio.schema._ + +final case class Person(name: String, age: Int) + +object Person { + implicit val schema: Schema[Person] = + Schema.CaseClass2[String, Int, Person]( + id0 = TypeId.fromTypeName("Person"), + field01 = Schema.Field(name0 = "name", schema0 = Schema[String], get0 = _.name, set0 = (p, x) => p.copy(name = x)), + field02 = Schema.Field(name0 = "age", schema0 = Schema[Int], get0 = _.age, set0 = (person, age) => person.copy(age = age)), + construct0 = (name, age) => Person(name, age), + ) +} +``` + +The next step is writing schema for `PaymentMethod`: + +```scala mdoc:silent +import zio._ +import zio.schema._ + +sealed trait PaymentMethod + +object PaymentMethod { + implicit val schema: Schema[PaymentMethod] = + Schema.Enum2[CreditCard, WireTransfer, PaymentMethod]( + id = TypeId.fromTypeName("PaymentMethod"), + case1 = Schema.Case[PaymentMethod, CreditCard]( + id = "CreditCard", + schema = CreditCard.schema, + unsafeDeconstruct = pm => pm.asInstanceOf[PaymentMethod.CreditCard], + construct = cc => cc.asInstanceOf[PaymentMethod], + isCase = _.isInstanceOf[PaymentMethod.CreditCard], + annotations = Chunk.empty + ), + case2 = Schema.Case[PaymentMethod, WireTransfer]( + id = "WireTransfer", + schema = WireTransfer.schema, + unsafeDeconstruct = pm => pm.asInstanceOf[PaymentMethod.WireTransfer], + construct = wt => wt.asInstanceOf[PaymentMethod], + isCase = _.isInstanceOf[PaymentMethod.WireTransfer], + annotations = Chunk.empty + ) + ) + + final case class CreditCard( + number: String, + expirationMonth: Int, + expirationYear: Int + ) extends PaymentMethod + + object CreditCard { + implicit val schema: Schema[CreditCard] = + Schema.CaseClass3[String, Int, Int, CreditCard]( + id0 = TypeId.fromTypeName("CreditCard"), + field01 = Schema.Field[CreditCard, String]( + name0 = "number", + schema0 = Schema.primitive[String], + get0 = _.number, + set0 = (cc, n) => cc.copy(number = n) + ), + field02 = Schema.Field[CreditCard, Int]( + name0 = "expirationMonth", + schema0 = Schema.primitive[Int], + get0 = _.expirationMonth, + set0 = (cc, em) => cc.copy(expirationMonth = em) + ), + field03 = Schema.Field[CreditCard, Int]( + name0 = "expirationYear", + schema0 = Schema.primitive[Int], + get0 = _.expirationYear, + set0 = (cc, ey) => cc.copy(expirationYear = ey) + ), + construct0 = (n, em, ey) => CreditCard(n, em, ey) + ) + } + + final case class WireTransfer(accountNumber: String, bankCode: String) + extends PaymentMethod + + object WireTransfer { + implicit val schema: Schema[WireTransfer] = + Schema.CaseClass2[String, String, WireTransfer]( + id0 = TypeId.fromTypeName("WireTransfer"), + field01 = Schema.Field[WireTransfer, String]( + name0 = "accountNumber", + schema0 = Schema.primitive[String], + get0 = _.accountNumber, + set0 = (wt, an) => wt.copy(accountNumber = an) + ), + field02 = Schema.Field[WireTransfer, String]( + name0 = "bankCode", + schema0 = Schema.primitive[String], + get0 = _.bankCode, + set0 = (wt, bc) => wt.copy(bankCode = bc) + ), + construct0 = (ac, bc) => WireTransfer(ac, bc) + ) + } +} +``` + +And finally, we need to define the schema for the `Customer` data type: + +```scala mdoc:silent +import zio._ +import zio.schema._ + +final case class Customer(person: Person, paymentMethod: PaymentMethod) + +object Customer { + implicit val schema: Schema[Customer] = + Schema.CaseClass2[Person, PaymentMethod, Customer]( + id0 = TypeId.fromTypeName("Customer"), + field01 = Schema.Field[Customer, Person]( + name0 = "person", + schema0 = Person.schema, + get0 = _.person, + set0 = (c, p) => c.copy(person = p) + ), + field02 = Schema.Field[Customer, PaymentMethod]( + name0 = "paymentMethod", + schema0 = Schema[PaymentMethod], + get0 = _.paymentMethod, + set0 = (c, pm) => c.copy(paymentMethod = pm) + ), + construct0 = (p, pm) => Customer(p, pm) + ) +} +``` + +Now that we have written all the required schemas, we can proceed to create encoders and decoders (codecs) for each of our domain models. + +Let's start with writing protobuf codecs. We need to add the following line to our `build.sbt`: + +```scala +libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % @VERSION@ +``` + +Here's an example that demonstrates a roundtrip test for protobuf codecs: + +```scala mdoc:silent +import zio.schema._ +import zio.schema.codec._ +import zio.schema.codec.ProtobufCodec._ + +// Create a customer instance +val customer = + Customer( + person = Person("John Doe", 42), + paymentMethod = PaymentMethod.CreditCard("1000100010001000", 6, 2024) + ) + +// Create binary codec from customer +val customerCodec: BinaryCodec[Customer] = + ProtobufCodec.protobufCodec[Customer] + +// Encode the customer object +val encodedCustomer: Chunk[Byte] = customerCodec.encode(customer) + +// Decode the byte array back to the person instance +val decodedCustomer: Either[DecodeError, Customer] = + customerCodec.decode(encodedCustomer) + +assert(Right(customer) == decodedCustomer) +``` diff --git a/docs/our-first-schema.md b/docs/our-first-schema.md deleted file mode 100644 index 6a2b7eb53..000000000 --- a/docs/our-first-schema.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -id: our-first-schema -title: "Our First Schema" ---- - -ZIO Schema provides macros to help you create `Schema`s out of our data types. But before using the macros, we should take a look at how to do this the manual way. - -## The Domain - -As described in the [Overview](index.md) section, we define the example domain as follows: - -```scala -object Domain { - final case class Person(name: String, age: Int) - - sealed trait PaymentMethod - - object PaymentMethod { - final case class CreditCard(number: String, expirationMonth: Int, expirationYear: Int) extends PaymentMethod - final case class WireTransfer(accountNumber: String, bankCode: String) extends PaymentMethod - } - - final case class Customer(person: Person, paymentMethod: PaymentMethod) - -} -``` - -### Manual construction of a Schema - -This part is similar to other libraries that we might know, e.g. for JSON processing. Basically, we create a `Schema` for every data type in our domain: - -```scala -object ManualConstruction { - import zio.schema.Schema._ - import Domain._ - import Domain.PaymentMethod._ - - val schemaPerson: Schema[Person] = Schema.CaseClass2[String, Int, Person]( - field1 = Schema.Field[String]("name", Schema.primitive[String]), - field2 = Schema.Field[Int]("age", Schema.primitive[Int]), - construct = (name, age) => Person(name, age), - extractField1 = p => p.name, - extractField2 = p => p.age - ) - - val schemaPaymentMethodWireTransfer: Schema[WireTransfer] = Schema.CaseClass2[String, String, WireTransfer]( - field1 = Schema.Field[String]("accountNumber", Schema.primitive[String]), - field2 = Schema.Field[String]("bankCode", Schema.primitive[String]), - construct = (number, bankCode) => PaymentMethod.WireTransfer(number, bankCode), - extractField1 = p => p.accountNumber, - extractField2 = p => p.bankCode - ) - - val schemaPaymentMethodCreditCard: Schema[CreditCard] = Schema.CaseClass3[String, Int, Int, CreditCard]( - field1 = Schema.Field[String]("number", Schema.primitive[String]), - field2 = Schema.Field[Int]("expirationMonth", Schema.primitive[Int]), - field3 = Schema.Field[Int]("expirationYear", Schema.primitive[Int]), - construct = (number, expirationMonth, expirationYear) => PaymentMethod.CreditCard(number, expirationMonth, expirationYear), - extractField1 = p => p.number, - extractField2 = p => p.expirationMonth, - extractField3 = p => p.expirationYear - ) - - val schemaPaymentMethod: Schema[PaymentMethod] = Schema.Enum2( - case1 = Case[PaymentMethod.CreditCard, PaymentMethod]( - id = "CreditCard", - codec = schemaPaymentMethodCreditCard, - unsafeDeconstruct = pm => pm.asInstanceOf[PaymentMethod.CreditCard] - ), - case2 = Case[PaymentMethod.WireTransfer, PaymentMethod]( - id = "WireTransfer", - codec = schemaPaymentMethodWireTransfer, - unsafeDeconstruct = pm => pm.asInstanceOf[PaymentMethod.WireTransfer] - ) - ) - - val schemaCustomer: Schema[Customer] = Schema.CaseClass2[Person, PaymentMethod, Customer]( - field1 = Schema.Field[Person]("person", schemaPerson), - field2 = Schema.Field[PaymentMethod]("paymentMethod", schemaPaymentMethod), - construct = (person, paymentMethod) => Customer(person, paymentMethod), - extractField1 = c => c.person, - extractField2 = c => c.paymentMethod - ) -} -``` - -### Macro derivation - -Using macros, the above code gets reduced to this: - -```scala -object MacroConstruction { - import Domain._ - - val schemaPerson: Schema[Person] = DeriveSchema.gen[Person] - - val schemaPaymentMethod: Schema[PaymentMethod] = DeriveSchema.gen[PaymentMethod] - - val schemaCustomer: Schema[Customer] = DeriveSchema.gen[Customer] -} -``` - -## Applying it to Our Domain - -Let's put this all together in a small sample: - -```scala -object JsonSample extends zio.App { - import zio.schema.codec.JsonCodec - import ManualConstruction._ - import zio.stream.ZStream - - override def run(args: List[String]): UIO[ExitCode] = for { - _ <- ZIO.unit - person = Person("Michelle", 32) - personToJsonTransducer = JsonCodec.encoder[Person](schemaPerson) - _ <- ZStream(person) - .transduce(personToJsonTransducer) - .transduce(ZTransducer.utf8Decode) - .foreach(ZIO.debug) - } yield ExitCode.success -} -``` - -When we run this, we get our expected result printed out: - -```json -{"name":"Michelle","age":32} -``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 3d3940693..bf99c66cc 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -5,18 +5,27 @@ const sidebars = { label: "ZIO Schema", collapsed: false, link: { type: "doc", id: "index" }, - items: [ + items: [ "use-cases", - "our-first-schema", + { + type: "category", + label: "Writing Schema", + collapsed: true, + link: { type: "doc", id: "index" }, + items: [ + "manual-schema-construction", + "automatic-schema-derivation" + ], + }, "motivation", "getting-started", "transforming-schemas", "codecs", "protobuf-example", - "combining-different-encoders" - ] - } - ] + "combining-different-encoders", + ], + }, + ], }; module.exports = sidebars;