Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON decoders chapter #68

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1b37b65
Decode from JsonData ADT to Student ADT.
monmcguigan Jul 7, 2024
f56f150
Merge pull request #1 from monmcguigan/wip-json-decoding
monmcguigan Jul 7, 2024
181e4c0
Improve getListStudents function
monmcguigan Jul 7, 2024
4617335
Improve checkStudentsObj function
monmcguigan Jul 7, 2024
ce3a786
Use JsonDecoder alias and list decoder.
monmcguigan Jul 9, 2024
0729c05
Remove unused functions.
monmcguigan Jul 9, 2024
8a639c1
Consolidate errors for decoding, add Boolean to JsonData
monmcguigan Jul 14, 2024
21d14e1
Merge pull request #2 from monmcguigan/improvements
monmcguigan Jul 14, 2024
600c866
Comment out boolean.
monmcguigan Jul 14, 2024
263e2d4
Merge pull request #3 from monmcguigan/decoding
monmcguigan Jul 14, 2024
25c62fa
Checkpoint: condensed StudentHandler that decodes as expected.
monmcguigan Jul 14, 2024
fc4d12b
Checkpoint: Fixed error types so I can have type annotations.
monmcguigan Jul 14, 2024
a464212
Checkpoint: Add Boolean to JsonData and Bool decoder.
monmcguigan Jul 14, 2024
0581670
Checkpoint: Decoding testing and Student handling refinements.
monmcguigan Jul 14, 2024
8c87672
Improve consistency in StudentHandler.
monmcguigan Jul 15, 2024
1e0b64f
Rename error tag.
monmcguigan Jul 15, 2024
3850049
Bring in most recent changes to deocder lib and encoder WIP
monmcguigan Jul 31, 2024
6e0d7a5
OneOf decoder implementation.
monmcguigan Aug 2, 2024
1b10ca7
Bring in changes from PR
monmcguigan Aug 4, 2024
0ad4bba
Tag union decoder, using type discriminators.
monmcguigan Aug 8, 2024
3abafd0
Updates for talk
monmcguigan Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions json/DataTypes/DecodeStudent.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module [readStudents]

import Student exposing [Student, Module]
import Decoding exposing [JsonDecoder, array, field, string, number, bool, map2, map3, or, oneOf, tag]

Students : List Student
Modules : List Module

readStudents : JsonDecoder Students
readStudents = \json ->
studentsDecoder = array readStudent
(field "students" studentsDecoder) json

readStudent : JsonDecoder Student
readStudent = \json ->
nameField = field "name" string
gradeField = field "grade" number
currentStudent = map3 nameField readModules gradeField
\name, modules, grade -> CurrentStudent { name, modules, grade }
graduatedStudent = map3 nameField readModules gradeField
\name, modules, grade -> GraduatedStudent { name, modules, grade }
studentDecoders =
Dict.empty {}
|> Dict.insert "currentStudent" currentStudent
|> Dict.insert "graduatedStudent" graduatedStudent
(tag studentDecoders) json

readModules : JsonDecoder Modules
readModules = \json ->
modulesDecoder = array readModule
(field "modules" modulesDecoder) json

readModule : JsonDecoder Module
readModule = \json ->
nameField = field "name" string
creditsField = field "credits" number
enrolledField = field "enrolled" bool
f = \name, credits, enrolled -> { name, credits, enrolled }
module = map3 nameField creditsField enrolledField f
module json
80 changes: 80 additions & 0 deletions json/DataTypes/DecodeStudentLong.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
module [readJson]

import JsonData exposing [Json]
import Student exposing [Student, Module]
import Decoding exposing [typeToStr, DecodingErrors]

Students : List Student

readJson : Json -> Result Students DecodingErrors
readJson = \json ->
when json is
Object obj ->
when Dict.get obj "students" is
Ok students -> readStudents students
Err KeyNotFound -> Err (FieldNotFound "students")

_ -> Err (WrongJsonType "$(typeToStr json)")

readStudents : Json -> Result Students DecodingErrors
readStudents = \json ->
when json is
Array students ->
List.mapTry students readStudent

_ -> Err (WrongJsonType "$(typeToStr json)")

readStudent : Json -> Result Student DecodingErrors
readStudent = \json ->
when json is
Object obj ->
when Dict.get obj "name" is
Ok (String name) ->
when Dict.get obj "modules" is
Ok moduleArr ->
when readModules moduleArr is
Ok modules ->
when Dict.get obj "grade" is
Ok (Number grade) ->
when Dict.get obj "#type" is
Ok (String "currentStudent") -> Ok (CurrentStudent { name, modules, grade })
Ok (String "graduatedStudent") -> Ok (GraduatedStudent { name, modules, grade })
_ -> Err (FieldNotFound "#type")

_ -> Err (FieldNotFound "grade")

Err e -> Err e

_ -> Err (FieldNotFound "modules")

_ -> Err (FieldNotFound "name")

_ -> Err (WrongJsonType "$(typeToStr json)")

readModules : Json -> Result (List Module) DecodingErrors
readModules = \json ->
when json is
Array modules ->
List.mapTry modules readModule

_ -> Err (WrongJsonType "$(typeToStr json)")

readModule : Json -> Result Module DecodingErrors
readModule = \json ->
when json is
Object fields ->
when Dict.get fields "name" is
Ok (String name) ->
when Dict.get fields "credits" is
Ok (Number credits) ->
when Dict.get fields "enrolled" is
Ok (Boolean enrolled) ->
Ok ({ name, credits, enrolled })

_ -> Err (FieldNotFound "enrolled")

_ -> Err (FieldNotFound "credits")

_ -> Err (FieldNotFound "name")

_ -> Err (WrongJsonType "$(typeToStr json)")
176 changes: 176 additions & 0 deletions json/DataTypes/Decoding.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
module [string, number, bool, array, field, map, map2, map3, andThen, or, oneOf, tag, JsonDecoder, DecodingErrors, typeToStr]
import JsonData exposing [Json]

DecodingErrors : [
FieldNotFound Str,
WrongJsonType Str,
KeyNotFound,
OneOfFailed Str,
DecoderNotFound Str
]

JsonDecoder t : Json -> Result t DecodingErrors
monmcguigan marked this conversation as resolved.
Show resolved Hide resolved
# check how complicated opaque types

string : JsonDecoder Str
string = \json ->
when json is
String str -> Ok str
_ -> Err (WrongJsonType "$(typeToStr json)")

number : JsonDecoder U64
number = \json ->
when json is
Number num -> Ok num
_ -> Err (WrongJsonType "$(typeToStr json)")

bool : JsonDecoder Bool
bool = \json ->
when json is
Boolean b -> Ok b
_ -> Err (WrongJsonType "$(typeToStr json)")

array : JsonDecoder a -> JsonDecoder (List a)
array = \decoderA ->
\json ->
when json is
Array jsonValues -> List.mapTry jsonValues decoderA
_ -> Err (WrongJsonType "$(typeToStr json)")

field : Str, JsonDecoder a -> JsonDecoder a
field = \fieldName, decoder ->
\json ->
when json is
Object dict ->
result = Dict.get dict fieldName
Result.try result (\a -> decoder a)

_ -> Err (WrongJsonType "$(typeToStr json)")

# If the result is Ok,
# transforms the entire result by running a conversion function on the value the Ok holds.
# Then returns that new result. If the result is Err, this has no effect. Use onErr to transform an Err.
# try : Result a err, (a -> Result b err) -> Result b err

map : JsonDecoder a, (a -> b) -> JsonDecoder b
map = \decoderA, f ->
\data -> Result.map (decoderA data) f

map2 : JsonDecoder a, JsonDecoder b, (a, b -> c) -> JsonDecoder c
map2 = \decoderA, decoderB, f ->
\data ->
when (decoderA data, decoderB data) is
(Ok a, Ok b) -> Ok (f a b)
(Err a, _) -> Err a
(_, Err b) -> Err b

map3 : JsonDecoder a, JsonDecoder b, JsonDecoder c, (a, b, c -> d) -> JsonDecoder d
map3 = \decoderA, decoderB, decoderC, f ->
\data ->
when (decoderA data, decoderB data, decoderC data) is
(Ok a, Ok b, Ok c) -> Ok (f a b c)
(Err a, _, _) -> Err a
(_, Err b, _) -> Err b
(_, _, Err c) -> Err c


# how to write mapN
# is there a way for me to define a function that has n number of input fields does roc let me do this

andThen : (a -> JsonDecoder b), JsonDecoder a -> JsonDecoder b
andThen = \toB, aDecoder ->
\data ->
when aDecoder data is
Ok a -> (toB a) data
Err a -> Err a

or : JsonDecoder a, JsonDecoder a -> JsonDecoder a
or = \decoderA, decoderB ->
\data ->
decoderA data
|> Result.onErr \_ -> decoderB data

oneOf : List (JsonDecoder a) -> JsonDecoder a
oneOf = \decoders ->
\json ->
check = \ds ->
when ds is
[head, .. as tail] ->
when head json is
Ok res -> Ok res
Err _ -> check tail

[] -> Err (OneOfFailed "No decoders provided to oneOf")
check decoders

tag : Dict Str (JsonDecoder a) -> JsonDecoder a
tag = \decoders ->
\json ->
type = (field "#type" string) json
when type is
Ok tagName ->
when Dict.get decoders tagName is
Ok decoder -> decoder json
_ -> Err(DecoderNotFound "Could not get decoder for $(tagName) type discriminator")

_ -> Err (FieldNotFound "Could not find #type discriminator")

typeToStr : Json -> Str
typeToStr = \json ->
when json is
String _ -> "String"
Number _ -> "Number"
Boolean _ -> "Boolean"
Object _ -> "Object"
Array _ -> "Array"

# TODO - Product decoders
# product=\a, b, c, decoderA, decoderB, decoderC, f ->
record : Str, Str, Str, JsonDecoder a, JsonDecoder b, JsonDecoder c, ((a, b, c) -> d) -> JsonDecoder d
altPr : List (Str, JsonDecoder a)

# TESTS
mathsMod =
Dict.empty {}
|> Dict.insert "name" (String "Maths 101")
|> Dict.insert "credits" (Number 200)
|> Dict.insert "enrolled" (Boolean Bool.true)
|> Object

phyMod =
Dict.empty {}
|> Dict.insert "name" (String "Physics 101")
|> Dict.insert "credits" (Number 200)
|> Object

nameDecoder = field "name" string
creditsDecoder = field "credits" number
enrolledDecoder = field "enrolled" bool

# map tests
expect (map nameDecoder \name -> { name: name }) phyMod == Ok ({ name: "Physics 101" })
expect (map enrolledDecoder \name -> { name: name }) phyMod == Err (KeyNotFound)

# map2 tests
expect (map2 nameDecoder creditsDecoder \name, credits -> { name: name, credits: credits }) (phyMod) == Ok ({ name: "Physics 101", credits: 200 })

# map3 tests
expect (map3 nameDecoder creditsDecoder enrolledDecoder \name, credits, enrolled -> { name: name, credits: credits, enrolled: enrolled }) (mathsMod) == Ok ({ name: "Maths 101", credits: 200, enrolled: Bool.true })

# list tests
myList = Array [String "hello", String "world"]
expect (array string) myList == Ok (["hello", "world"])
expect (array string) mathsMod == Err (WrongJsonType "Object")

# field tests
expect nameDecoder mathsMod == Ok ("Maths 101")
expect nameDecoder myList == Err (WrongJsonType "Array")
expect (field "blah" string) mathsMod == Err KeyNotFound

# primitive types
expect string (String "hello") == Ok ("hello")
expect string (Number 123) == Err (WrongJsonType "Number")
expect bool (Boolean Bool.true) == Ok Bool.true
expect number (Number 400) == Ok (400)
# expect null (Null) == Ok ("null")
myotherList = Array [String "s", Nummber 56]
41 changes: 41 additions & 0 deletions json/DataTypes/Encoding.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module []
import JsonData exposing [Json]

JsonEncoder t : t -> Json

string : JsonEncoder Str
string = \str ->
String str

number : JsonEncoder U64
number = \num ->
Number num

bool : JsonEncoder Bool
bool = \b ->
Boolean b

list : JsonEncoder a -> JsonEncoder (List a)
list = \encoderA ->
\data ->
json = List.map data encoderA
Array json

# field : Str, JsonEncoder a -> JsonEncoder (Str, Json)
field = \fieldName, encoderA ->
\data ->
j = encoderA data
(fieldName, j)
# TESTS
# primitives
expect string "hello" == (String "hello")
expect number 50 == (Number 50)
expect bool Bool.true == (Boolean Bool.true)
# there is no null type in Roc

# list tests
strs = ["hello", "hello again"]
expect (list string) strs == Array [String "hello", String "hello again"]

# field encoder
expect (field "fieldName" string) "fieldValue" == ("fieldName", String "fieldValue")
10 changes: 10 additions & 0 deletions json/DataTypes/JsonData.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module [Json]

Json : [
String Str,
Number U64,
Boolean Bool,
Object (Dict Str Json),
Array (List Json),
]

20 changes: 20 additions & 0 deletions json/DataTypes/Student.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module [Student, Module]

Student : [
CurrentStudent {
name : Str,
modules : List (Module),
grade : U64
},
GraduatedStudent {
name : Str,
modules : List (Module),
grade : U64
}
]

Module : {
name : Str,
credits : U64,
enrolled : Bool
}
Loading