-
Notifications
You must be signed in to change notification settings - Fork 162
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add manual and automatic derivation.
- Loading branch information
Showing
5 changed files
with
299 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
``` |
Oops, something went wrong.