From cd5bcb25e68db6206abd818b2b314395af35c6df Mon Sep 17 00:00:00 2001
From: Lee Jarvis <lee@jrvs.uk>
Date: Mon, 11 Nov 2024 14:53:01 +0000
Subject: [PATCH 1/3] Add basic parsing JSON example

---
 universal/test/formats/parsing_json.gleam | 40 +++++++++++++++++++++++
 1 file changed, 40 insertions(+)
 create mode 100644 universal/test/formats/parsing_json.gleam

diff --git a/universal/test/formats/parsing_json.gleam b/universal/test/formats/parsing_json.gleam
new file mode 100644
index 0000000..ce912ac
--- /dev/null
+++ b/universal/test/formats/parsing_json.gleam
@@ -0,0 +1,40 @@
+//// # Parsing JSON
+////
+//// The gleam_json library can be used to decode JSON.
+////
+//// ## Dependencies
+////
+//// - https://hex.pm/packages/gleam_json
+
+import gleam/dict
+import gleam/dynamic
+import gleam/json
+import gleeunit/should
+
+type User(name, age, is_admin) {
+  User(name, age, is_admin)
+}
+
+pub fn main_test() {
+  // Basic string key-value pair
+  "{\"name\": \"cookbook\"}"
+  |> json.decode(dynamic.dict(dynamic.string, dynamic.string))
+  |> should.equal(Ok(dict.from_list([#("name", "cookbook")])))
+
+  // String key-value pair with list of integers
+  "{\"scores\": [1, 2, 3]}"
+  |> json.decode(dynamic.dict(dynamic.string, dynamic.list(of: dynamic.int)))
+  |> should.equal(Ok(dict.from_list([#("scores", [1, 2, 3])])))
+
+  // Custom decoder
+  let decoder =
+    dynamic.decode3(
+      User,
+      dynamic.field("name", dynamic.string),
+      dynamic.field("age", dynamic.int),
+      dynamic.field("is_admin", dynamic.bool),
+    )
+  "{\"name\": \"Alice\", \"age\": 42, \"is_admin\": true}"
+  |> json.decode(decoder)
+  |> should.equal(Ok(User("Alice", 42, True)))
+}

From 8c61653d1c61901503fe971fe370a1e3826106be Mon Sep 17 00:00:00 2001
From: Lee Jarvis <lee@jrvs.uk>
Date: Tue, 12 Nov 2024 10:58:49 +0000
Subject: [PATCH 2/3] Use decode/zero

---
 universal/gleam.toml                      |  1 +
 universal/manifest.toml                   |  2 ++
 universal/test/formats/parsing_json.gleam | 44 +++++++++++++----------
 3 files changed, 28 insertions(+), 19 deletions(-)

diff --git a/universal/gleam.toml b/universal/gleam.toml
index daaff51..78e0511 100644
--- a/universal/gleam.toml
+++ b/universal/gleam.toml
@@ -22,6 +22,7 @@ lustre = ">= 4.4.4 and < 5.0.0"
 gleam_crypto = ">= 1.3.0 and < 2.0.0"
 tom = ">= 1.1.0 and < 2.0.0"
 envoy = ">= 1.0.2 and < 2.0.0"
+decode = ">= 0.4.1"
 
 [dev-dependencies]
 gleeunit = ">= 1.0.0 and < 2.0.0"
diff --git a/universal/manifest.toml b/universal/manifest.toml
index 69ac9f9..225d20b 100644
--- a/universal/manifest.toml
+++ b/universal/manifest.toml
@@ -2,6 +2,7 @@
 # You typically do not need to edit this file
 
 packages = [
+  { name = "decode", version = "0.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "90C83E830B380EAF64A0A20D0116C4C173AD753594AF1A37E692C1A699244244" },
   { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
   { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
   { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
@@ -18,6 +19,7 @@ packages = [
 ]
 
 [requirements]
+decode = { version = ">= 0.4.1" }
 envoy = { version = ">= 1.0.2 and < 2.0.0" }
 gleam_crypto = { version = ">= 1.3.0 and < 2.0.0" }
 gleam_json = { version = ">= 1.0.1 and < 2.0.0" }
diff --git a/universal/test/formats/parsing_json.gleam b/universal/test/formats/parsing_json.gleam
index ce912ac..f2c1634 100644
--- a/universal/test/formats/parsing_json.gleam
+++ b/universal/test/formats/parsing_json.gleam
@@ -6,35 +6,41 @@
 ////
 //// - https://hex.pm/packages/gleam_json
 
-import gleam/dict
-import gleam/dynamic
+import decode/zero
 import gleam/json
 import gleeunit/should
 
-type User(name, age, is_admin) {
-  User(name, age, is_admin)
+pub type User {
+  User(name: String, age: Int, is_admin: Bool)
 }
 
 pub fn main_test() {
-  // Basic string key-value pair
+  // Simple types
+  let decoder = {
+    use name <- zero.field("name", zero.string)
+    zero.success(name)
+  }
   "{\"name\": \"cookbook\"}"
-  |> json.decode(dynamic.dict(dynamic.string, dynamic.string))
-  |> should.equal(Ok(dict.from_list([#("name", "cookbook")])))
+  |> json.decode(zero.run(_, decoder))
+  |> should.equal(Ok("cookbook"))
 
-  // String key-value pair with list of integers
+  // Lists
+  let decoder = {
+    use scores <- zero.field("scores", zero.list(zero.int))
+    zero.success(scores)
+  }
   "{\"scores\": [1, 2, 3]}"
-  |> json.decode(dynamic.dict(dynamic.string, dynamic.list(of: dynamic.int)))
-  |> should.equal(Ok(dict.from_list([#("scores", [1, 2, 3])])))
+  |> json.decode(zero.run(_, decoder))
+  |> should.equal(Ok([1, 2, 3]))
 
-  // Custom decoder
-  let decoder =
-    dynamic.decode3(
-      User,
-      dynamic.field("name", dynamic.string),
-      dynamic.field("age", dynamic.int),
-      dynamic.field("is_admin", dynamic.bool),
-    )
+  // Records
+  let decoder = {
+    use name <- zero.field("name", zero.string)
+    use age <- zero.field("age", zero.int)
+    use is_admin <- zero.field("is_admin", zero.bool)
+    zero.success(User(name:, age:, is_admin:))
+  }
   "{\"name\": \"Alice\", \"age\": 42, \"is_admin\": true}"
-  |> json.decode(decoder)
+  |> json.decode(zero.run(_, decoder))
   |> should.equal(Ok(User("Alice", 42, True)))
 }

From 5c76b8764ae88741bccd6e18f4e061b755ddb6c9 Mon Sep 17 00:00:00 2001
From: Lee Jarvis <lee@jrvs.uk>
Date: Tue, 12 Nov 2024 11:27:47 +0000
Subject: [PATCH 3/3] Add optionals example

---
 universal/test/formats/parsing_json.gleam | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/universal/test/formats/parsing_json.gleam b/universal/test/formats/parsing_json.gleam
index f2c1634..4e7ffc6 100644
--- a/universal/test/formats/parsing_json.gleam
+++ b/universal/test/formats/parsing_json.gleam
@@ -8,6 +8,7 @@
 
 import decode/zero
 import gleam/json
+import gleam/option
 import gleeunit/should
 
 pub type User {
@@ -33,6 +34,18 @@ pub fn main_test() {
   |> json.decode(zero.run(_, decoder))
   |> should.equal(Ok([1, 2, 3]))
 
+  // Optionals
+  let decoder = {
+    use name <- zero.field("name", zero.optional(zero.string))
+    zero.success(name)
+  }
+  "{\"name\": \"cookbook\"}"
+  |> json.decode(zero.run(_, decoder))
+  |> should.equal(Ok(option.Some("cookbook")))
+  "{\"name\": null}"
+  |> json.decode(zero.run(_, decoder))
+  |> should.equal(Ok(option.None))
+
   // Records
   let decoder = {
     use name <- zero.field("name", zero.string)