Skip to content

Commit

Permalink
add manual and automatic derivation.
Browse files Browse the repository at this point in the history
  • Loading branch information
khajavi committed Jul 5, 2023
1 parent 9733f83 commit 6482280
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 135 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,5 @@ lazy val docs = project
|sbt test
|```""".stripMargin
)
.dependsOn(zioSchemaJVM, zioSchemaProtobufJVM)
.enablePlugins(WebsitePlugin)
91 changes: 91 additions & 0 deletions docs/automatic-schema-derivation.md
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)
```
192 changes: 192 additions & 0 deletions docs/manual-schema-construction.md
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)
```
Loading

0 comments on commit 6482280

Please sign in to comment.