Skip to content

Commit

Permalink
Add the Getting started tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
plokhotnyuk committed Nov 22, 2024
1 parent 3d8ca0f commit 160b312
Show file tree
Hide file tree
Showing 2 changed files with 334 additions and 5 deletions.
207 changes: 202 additions & 5 deletions docs/tutorials/1-getting-started.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,204 @@
Goal: show the power of auto-derivation
# Getting started
Let's start our adventure with jsoniter-scala from parsing and serialization of some complex data structure of nested
collections and case classes.

Example:
1.
Nested case classes with a couple of collections (`Seq` and `Map`)
## Prerequisites
In all tutorials we will use [scala-cli](https://scala-cli.virtuslab.org) scripts, so you'll need to install it upfront.

Challenge: find concrete (non-abstract) collection from standard Scala library that is not supported yet by auto-derivation
You can use any text editor to copy and paste provided code snippets. Please, also, check Scala CLI Cookbooks
documentation if you use [VSCode](https://scala-cli.virtuslab.org/docs/cookbooks/ide/vscode),
[Intellij IDEA](https://scala-cli.virtuslab.org/docs/cookbooks/ide/intellij), or
[emacs](https://scala-cli.virtuslab.org/docs/cookbooks/ide/emacs) editors.

As an example, if you use [IntelliJ IDEA](https://www.jetbrains.com/idea/) with the latest version of Scala plugin then
you can try its [improved support for Scala CLI projects](https://blog.jetbrains.com/scala/2024/11/13/intellij-scala-plugin-2024-3-is-out/#scala-cli).

Latest versions of jsoniter-scala libraries require JDK 11 or above. You can use any of its distributions.

In all tutorials we will use Scala 3 and start from a file with the following dependencies and imports:

```scala
//> using scala 3
//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.31.3"
//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.31.3"

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._
```

Please just copy and paste code snippets from the subsequent steps and run by pressing some `Run` button/keystroke in
your IDE or run it using `scala-cli <file_name.scala>` command from the terminal.

Make sure to preserve indentation when copying and pasting.

## Definition of a complex data structure

Let's imagine that we need to generate a JSON representation of some report for some US shop.

Below is a definition of its data structures and instantiation of some sample data:

```scala
enum Category extends Enum[Category]:
case Electronics, Fashion, HomeGoods

case class Product(id: Long, name: String, category: Category, price: BigDecimal, description: String)

enum OrderStatus extends Enum[OrderStatus]:
case Pending, Shipped, Delivered, Cancelled

case class OrderItem(product: Product, quantity: Int)

case class Order(id: Long, customer: Customer, items: List[OrderItem], status: OrderStatus)

case class Customer(id: Long, name: String, email: String, address: Address)

case class Address(street: String, city: String, state: String, zip: String)

enum PaymentType extends Enum[PaymentType]:
case CreditCard, PayPal

case class PaymentMethod(`type`: PaymentType, details: Map[String, String]/*e.g. card number, expiration date*/)

case class Payment(method: PaymentMethod, amount: BigDecimal, timestamp: java.time.Instant)

case class OrderPayment(order: Order, payment: Payment)

val product1 = Product(
id = 1L,
name = "Apple iPhone 16",
category = Category.Electronics,
price = BigDecimal(999.99),
description = "A high-end smartphone with advanced camera and AI capabilities"
)
val product2 = Product(
id = 2L,
name = "Nike Air Max 270",
category = Category.Fashion,
price = BigDecimal(129.99),
description = "A stylish and comfortable sneaker with a full-length air unit"
)
val product3 = Product(
id = 3L,
name = "KitchenAid Stand Mixer",
category = Category.HomeGoods,
price = BigDecimal(299.99),
description = "A versatile and powerful stand mixer for baking and cooking"
)
val customer1 = Customer(
id = 1L,
name = "John Doe",
email = "john.doe@example.com",
address = Address(
street = "123 Main St",
city = "Anytown",
state = "CA",
zip = "12345"
)
)
val customer2 = Customer(
id = 2L,
name = "Jane Smith",
email = "jane.smith@example.com",
address = Address(
street = "456 Elm St",
city = "Othertown",
state = "NY",
zip = "67890"
)
)
val order1 = Order(
id = 1L,
customer = customer1,
items = List(
OrderItem(product1, 1),
OrderItem(product2, 2)
),
status = OrderStatus.Pending
)
val order2 = Order(
id = 2L,
customer = customer2,
items = List(
OrderItem(product3, 1)
),
status = OrderStatus.Shipped
)
val paymentMethod1 = PaymentMethod(
`type` = PaymentType.CreditCard,
details = Map(
"card_number" -> "1234-5678-9012-3456",
"expiration_date" -> "12/2026"
)
)
val paymentMethod2 = PaymentMethod(
`type` = PaymentType.PayPal,
details = Map(
"paypal_id" -> "jane.smith@example.com"
)
)
val payment1 = Payment(
method = paymentMethod1,
amount = BigDecimal(1259.97),
timestamp = java.time.Instant.parse("2025-01-03T12:30:45Z")
)
val payment2 = Payment(
method = paymentMethod2,
amount = BigDecimal(299.99),
timestamp = java.time.Instant.parse("2025-01-15T19:10:55Z")
)
val orderPayment1 = OrderPayment(
order = order1,
payment = payment1
)
val orderPayment2 = OrderPayment(
order = order2,
payment = payment2
)
val report = List(orderPayment1, orderPayment2)
```

## Defining the codec

To derive a codec we will use the following line:
```scala
given JsonValueCodec[List[OrderPayment]] = JsonCodecMaker.make
```

## Serialization

Now we are ready to serialize list the report. Just need to define some entry point method and call `writeToString`.
We will also print resulting JSON to the system output to see it as a script running result on the screen:

```scala
@main def gettingStarted: Unit =
val json = writeToString(report)
println(json)
```

From this moment you can run the script and see a long JSON string on the screen.

## Parsing

Having the JSON string in memory you can parse it using following lines that should be pasted to the end of the
`gettingStarted` method:
```scala
val parsedReport = readFromString(json)
println(parsedReport)
```

Now you can rerun the script and get additionally printed `toString` representation of a report parsed form JSON string.

If something gone wrong you can pick the final version of [a script for this tutorial](1-getting-started.scala) and
then run it in the terminal.

## Challenge
Find any concrete (non-abstract) collection from standard Scala library that is not supported by auto-derivation
of `JsonCodecMaker.make` macros yet.

## Recap
In this tutorial we learned basics for parsing and serialization of complex nested data structures using `scala-cli`.
Having definition of data structures and their instances in memory we need to:
1. Add dependency usage and package imports of `core` and `macros` modules of jsoniter-scala
2. Define `given` type-class instance of `JsonValueCodec` for top-level data structure
3. Call `writeToString` to serialize an instance of complex data structure to JSON representation
4. Call `readFromString` to parse a complex data structure from JSON representation
132 changes: 132 additions & 0 deletions docs/tutorials/1-getting-started.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//> using scala 3
//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.31.3"
//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.31.3"

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._

enum Category extends Enum[Category]:
case Electronics, Fashion, HomeGoods

case class Product(id: Long, name: String, category: Category, price: BigDecimal, description: String)

enum OrderStatus extends Enum[OrderStatus]:
case Pending, Shipped, Delivered, Cancelled

case class OrderItem(product: Product, quantity: Int)

case class Order(id: Long, customer: Customer, items: List[OrderItem], status: OrderStatus)

case class Customer(id: Long, name: String, email: String, address: Address)

case class Address(street: String, city: String, state: String, zip: String)

enum PaymentType extends Enum[PaymentType]:
case CreditCard, PayPal

case class PaymentMethod(`type`: PaymentType, details: Map[String, String]/*e.g. card number, expiration date*/)

case class Payment(method: PaymentMethod, amount: BigDecimal, timestamp: java.time.Instant)

case class OrderPayment(order: Order, payment: Payment)

val product1 = Product(
id = 1L,
name = "Apple iPhone 16",
category = Category.Electronics,
price = BigDecimal(999.99),
description = "A high-end smartphone with advanced camera and AI capabilities"
)
val product2 = Product(
id = 2L,
name = "Nike Air Max 270",
category = Category.Fashion,
price = BigDecimal(129.99),
description = "A stylish and comfortable sneaker with a full-length air unit"
)
val product3 = Product(
id = 3L,
name = "KitchenAid Stand Mixer",
category = Category.HomeGoods,
price = BigDecimal(299.99),
description = "A versatile and powerful stand mixer for baking and cooking"
)
val customer1 = Customer(
id = 1L,
name = "John Doe",
email = "john.doe@example.com",
address = Address(
street = "123 Main St",
city = "Anytown",
state = "CA",
zip = "12345"
)
)
val customer2 = Customer(
id = 2L,
name = "Jane Smith",
email = "jane.smith@example.com",
address = Address(
street = "456 Elm St",
city = "Othertown",
state = "NY",
zip = "67890"
)
)
val order1 = Order(
id = 1L,
customer = customer1,
items = List(
OrderItem(product1, 1),
OrderItem(product2, 2)
),
status = OrderStatus.Pending
)
val order2 = Order(
id = 2L,
customer = customer2,
items = List(
OrderItem(product3, 1)
),
status = OrderStatus.Shipped
)
val paymentMethod1 = PaymentMethod(
`type` = PaymentType.CreditCard,
details = Map(
"card_number" -> "1234-5678-9012-3456",
"expiration_date" -> "12/2026"
)
)
val paymentMethod2 = PaymentMethod(
`type` = PaymentType.PayPal,
details = Map(
"paypal_id" -> "jane.smith@example.com"
)
)
val payment1 = Payment(
method = paymentMethod1,
amount = BigDecimal(1259.97),
timestamp = java.time.Instant.parse("2025-01-03T12:30:45Z")
)
val payment2 = Payment(
method = paymentMethod2,
amount = BigDecimal(299.99),
timestamp = java.time.Instant.parse("2025-01-15T19:10:55Z")
)
val orderPayment1 = OrderPayment(
order = order1,
payment = payment1
)
val orderPayment2 = OrderPayment(
order = order2,
payment = payment2
)
val report = List(orderPayment1, orderPayment2)

given JsonValueCodec[List[OrderPayment]] = JsonCodecMaker.make

@main def gettingStarted: Unit =
val json = writeToString(report)
println(json)
val parsedReport = readFromString(json)
println(parsedReport)

0 comments on commit 160b312

Please sign in to comment.