diff --git a/README.md b/README.md index 5941909..96caec1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # json-extra -Facilitating JSON encoding and decoding by copying Elm's equivalent package + +Facilitating JSON encoding and decoding by copying Elm's equivalent package. + +This package aims to port +[Json.Decode](https://package.elm-lang.org/packages/elm/json/latest/Json.Decode) +and +[Json.Encode](https://package.elm-lang.org/packages/elm/json/latest/Json-Encode) +to N. diff --git a/decode/error.n b/decode/error.n new file mode 100644 index 0000000..80ed083 --- /dev/null +++ b/decode/error.n @@ -0,0 +1,10 @@ +import json + +/// A decoding error type that describes precisely how an error happened. This +/// can be used to make more elaborate visualizations of a decoding error. For +/// example, you could show a preview of the JSON object and highlight the +/// invalid parts in red. +type pub error = pub + | pub + | pub + | pub diff --git a/decode/mod.n b/decode/mod.n index 9fee001..a686b23 100644 --- a/decode/mod.n +++ b/decode/mod.n @@ -1,13 +1,10 @@ import json -/// A decoding error type that describes precisely how an error happened. This -/// can be used to make more elaborate visualizations of a decoding error. For -/// example, you could show a preview of the JSON object and highlight the -/// invalid parts in red. -type pub error = - | - | - | +let utils = imp "../utils.n" +let decodeError = imp "./error.n" + +alias pub error = decodeError.error +let failure = decodeError.failure /// A value that knows how to decode `JSON.value` values. alias pub decoder[t] = json.value -> result[t, error] @@ -21,25 +18,259 @@ let pub str: decoder[str] = [value: json.value] -> result[str, error] { } } +/// Decode a JSON string into an N `bool`. +let pub bool: decoder[bool] = [value: json.value] -> result[bool, error] { + return if let = value { + ok(bool) + } else { + err(failure("Expected a boolean value.", value)) + } +} + +/// Decode a JSON string into an N `int`. +let pub int: decoder[int] = [value: json.value] -> result[int, error] { + if let = value { + // HACK: Cast the float back to an int with `round` then back to a float by + // exponentiation. + let int: int = round(number) + if number = int ^ 1 { + return ok(int) + } else { + return err(failure("Expected an integer value.", value)) + } + } else { + return err(failure("Expected an integer value.", value)) + } +} + +/// Decode a JSON string into an N `float`. +let pub float: decoder[float] = [value: json.value] -> result[float, error] { + return if let = value { + ok(float) + } else { + err(failure("Expected a float value.", value)) + } +} + +// Decode a value that may be null into an N `maybe`. +let pub nullable = [[t] decoder: decoder[t] value: json.value] -> result[maybe[t], error] { + if value = json.null { + return ok(none) + } else { + let result = decoder(value) + return if let = result { + ok(yes(decoded)) + } else { + // TEMP: needs match + // result + err(decodeError.oneOf([])) + } + } +} + +// TODO: list +// TODO: dict +// TODO: keyValuePairs +// TODO: oneOrMore + +/// Decode a value from a specific property from a JSON object. +let pub field = [[t] key: str decoder: decoder[t] value: json.value] -> result[t, error] { + if let = value { + if let = getValue(key, map) { + let decoded = decoder(value) + return if let = decoded { + err(decodeError.field(key, error)) + } else { + decoded + } + } else { + return err(failure("Expected the property `" + key + "`.", value)) + } + } else { + return err(failure("Expected a JSON object.", value)) + } +} + +// TODO: at + +/// Decode a value from a specific property from a JSON object. +let pub index = [[t] index: int decoder: decoder[t] value: json.value] -> result[t, error] { + if let = value { + if let = itemAt(index, list) { + let decoded = decoder(value) + return if let = decoded { + err(decodeError.index(index, error)) + } else { + decoded + } + } else { + return err(failure("Expected item " + intInBase10(index) + ".", value)) + } + } else { + return err(failure("Expected a JSON array.", value)) + } +} + +/// Deals with optional fields. Examples: +/// +/// ```n +/// let json = "{ \"name\": \"Billy\", \"age\": 18 }" +/// +/// decodeString(maybe(field("age", int)), json) = ok(yes(18)) +/// decodeString(maybe(field("name", int)), json) = ok(none) +/// decodeString(maybe(field("height", float)), json) = ok(none) +/// +/// decodeString(field("age", maybe(float)), json) = ok(none) +/// ``` +let pub maybe = [ + [t] + decoder: decoder[t] + value: json.value +] -> result[maybe[t], error] { + if let = decoder(value) { + ok(yes(decoded)) + } else { + ok(none) + } +} + +/// Tries several decoders until one works. +let pub oneOf = [[t] decoders: list[decoder[t]] value: json.value] -> result[t, error] { + let results = filterMap( + [decoder: decoder[t]] -> maybe[result[t, error]] { + return yes(decoder(value)) + }, + decoders + ) + for (result in results) { + if let = result { + return ok(decoded) + } + } + let errors = (filterMap( + [result: result[t, error]] -> maybe[error] { + return if let = result { + yes(error) + } else { + none + } + }, + results + )) + |> decodeError.oneOf + |> err + return errors +} + +/// Parse the given string into a JSON value and then run the decoder on it. +/// This will fail if the string is invalid JSON or if the decoder fails. +let pub decodeString = [[t] decoder: decoder[t] jsonStr: str] -> result[t, error] { + if let = json.parseSafe(jsonStr) { + return decoder(value) + } else { + return jsonStr + |> json.string + |> failure("Invalid JSON syntax.") + |> err + } +} + +/// Run a decoder on a `json.value`. +let pub decodeValue = [[t] decoder: decoder[t] value: json.value] -> result[t, error] { + return decoder(value) +} + +let E = utils.getError + +/// Transform the output type of a decoder. Perhaps you only want the length of +/// a string: +/// +/// ```n +/// let decodeStringLength: decoder[int] = +/// map(len, str) +/// ``` +let pub map: [a, t] (a -> t) -> decoder[a] -> decoder[t] = [ + [a, t] + mapFn: a -> t + decoder: decoder[a] + value: json.value +] -> result[t, error] { + let result = decoder(value) + + return if let = result { + ok(mapFn(decoded)) + } else { + // Unfortunately I can't just return result + // result + E(result) + |> decodeError.oneOf + |> err + } +} + +/// Tries using two decoders and combines them with the given map function into +/// a single value of your choosing. +/// +/// For example, +/// +/// ```n +/// alias point = { x: float, y: float } +/// +/// let decodePoint: decoder[point] = +/// map2( +/// point, +/// field("x", float), +/// field("y", float), +/// ) +/// ``` +let pub map2: + [a, b, t] + (a -> b -> t) + -> decoder[a] + -> decoder[b] + -> decoder[t] += [ + [a, b, t] + mapFn: a -> b -> t + decoderA: decoder[a] + decoderB: decoder[b] + value: json.value +] -> result[t, error] { + let results = (decoderA(value), decoderB(value)) + + if let , = results { + return ok(mapFn(a, b)) + } else { + let (resultA, resultB) = results + let error = (E(resultA) + E(resultB)) + |> decodeError.oneOf + |> err + return error + } +} + /// Tries using three decoders and combines them with the given map function /// into a single value of your choosing. /// /// For example, +/// /// ```n /// alias person = { name: str; age: int; height: float } /// /// let decodePerson: decoder[person] = -/// map3( -/// person, -/// ... -/// ) +/// map3( +/// person, +/// field("name", str), +/// at(["info", "age"], int), +/// at(["info", "height"], float), +/// ) /// ``` let pub map3: [a, b, c, t] (a -> b -> c -> t) -> decoder[a] -> decoder[b] - -> deocder[c] + -> decoder[c] -> decoder[t] = [ [a, b, c, t] @@ -51,15 +282,231 @@ let pub map3: ] -> result[t, error] { let results = (decoderA(value), decoderB(value), decoderC(value)) - return if let , , = results { - mapFn(a, b, c) + if let , , = results { + return ok(mapFn(a, b, c)) } else { - let resultA, resultB, resultC = results + let (resultA, resultB, resultC) = results + let error = (E(resultA) + E(resultB) + E(resultC)) + |> decodeError.oneOf + |> err + return error + } +} - oneOf( - if let = resultA { [error] } else { [] } - + if let = resultB { [error] } else { [] } - + if let = resultC { [error] } else { [] } +let pub map4 = [ + [a, b, c, d, t] + mapFn: a -> b -> c -> d -> t + decoderA: decoder[a] + decoderB: decoder[b] + decoderC: decoder[c] + decoderD: decoder[d] + value: json.value +] -> result[t, error] { + let results = ( + decoderA(value), + decoderB(value), + decoderC(value), + decoderD(value) + ) + + if let , , , = results { + return ok(mapFn(a, b, c, d)) + } else { + let (resultA, resultB, resultC, resultD) = results + let error = (E(resultA) + E(resultB) + E(resultC) + E(resultD)) + |> decodeError.oneOf + |> err + return error + } +} + +let pub map5 = [ + [a, b, c, d, e, t] + mapFn: a -> b -> c -> d -> e -> t + decoderA: decoder[a] + decoderB: decoder[b] + decoderC: decoder[c] + decoderD: decoder[d] + decoderE: decoder[e] + value: json.value +] -> result[t, error] { + let results = ( + decoderA(value), + decoderB(value), + decoderC(value), + decoderD(value), + decoderE(value) + ) + + if let , , , , = results { + return ok(mapFn(a, b, c, d, e)) + } else { + let (resultA, resultB, resultC, resultD, resultE) = results + let error = ( + E(resultA) + + E(resultB) + + E(resultC) + + E(resultD) + + E(resultE) ) + |> decodeError.oneOf + |> err + return error } } + +let pub map6 = [ + [a, b, c, d, e, f, t] + mapFn: a -> b -> c -> d -> e -> f -> t + decoderA: decoder[a] + decoderB: decoder[b] + decoderC: decoder[c] + decoderD: decoder[d] + decoderE: decoder[e] + decoderF: decoder[f] + value: json.value +] -> result[t, error] { + let results = ( + decoderA(value), + decoderB(value), + decoderC(value), + decoderD(value), + decoderE(value), + decoderF(value) + ) + + if let , , , , , = results { + return ok(mapFn(a, b, c, d, e, f)) + } else { + let (resultA, resultB, resultC, resultD, resultE, resultF) = results + let error = ( + E(resultA) + + E(resultB) + + E(resultC) + + E(resultD) + + E(resultE) + + E(resultF) + ) + |> decodeError.oneOf + |> err + return error + } +} + +let pub map7 = [ + [a, b, c, d, e, f, g, t] + mapFn: a -> b -> c -> d -> e -> f -> g -> t + decoderA: decoder[a] + decoderB: decoder[b] + decoderC: decoder[c] + decoderD: decoder[d] + decoderE: decoder[e] + decoderF: decoder[f] + decoderG: decoder[g] + value: json.value +] -> result[t, error] { + let results = ( + decoderA(value), + decoderB(value), + decoderC(value), + decoderD(value), + decoderE(value), + decoderF(value), + decoderG(value) + ) + + if let , , , , , , = results { + return ok(mapFn(a, b, c, d, e, f, g)) + } else { + let ( + resultA, + resultB, + resultC, + resultD, + resultE, + resultF, + resultG + ) = results + let error = ( + E(resultA) + + E(resultB) + + E(resultC) + + E(resultD) + + E(resultE) + + E(resultF) + + E(resultG) + ) + |> decodeError.oneOf + |> err + return error + } +} + +let pub map8 = [ + [a, b, c, d, e, f, g, h, t] + mapFn: a -> b -> c -> d -> e -> f -> g -> h -> t + decoderA: decoder[a] + decoderB: decoder[b] + decoderC: decoder[c] + decoderD: decoder[d] + decoderE: decoder[e] + decoderF: decoder[f] + decoderG: decoder[g] + decoderH: decoder[h] + value: json.value +] -> result[t, error] { + let results = ( + decoderA(value), + decoderB(value), + decoderC(value), + decoderD(value), + decoderE(value), + decoderF(value), + decoderG(value), + decoderH(value) + ) + + if let ( + , + , + , + , + , + , + , + + ) = results { + return ok(mapFn(a, b, c, d, e, f, g, h)) + } else { + let ( + resultA, + resultB, + resultC, + resultD, + resultE, + resultF, + resultG, + resultH + ) = results + let error = ( + E(resultA) + + E(resultB) + + E(resultC) + + E(resultD) + + E(resultE) + + E(resultF) + + E(resultG) + + E(resultH) + ) + |> decodeError.oneOf + |> err + return error + } +} + +// TODO: lazy +// TODO: value +// TODO: null +// TODO: succeed +// TODO: fail +// TODO: andThen diff --git a/decode/test.n b/decode/test.n index 2c9eae5..25aa20d 100644 --- a/decode/test.n +++ b/decode/test.n @@ -1 +1,65 @@ let D = imp "./mod.n" +let utils = imp "../utils.n" +let id = utils.identity + +let isErr = [[o, e] result: result[o, e]] -> bool { + return if let = result { + true + } else { + false + } +} + +let assert = [name: str shouldBeTrue: bool] -> () { + if not shouldBeTrue { + print("Test failed: " + name) + } +} + +print("Begin tests.") + +D.decodeString(D.str, "true") |> isErr + |> assert("D.str fail on bool") +D.decodeString(D.str, "42") |> isErr + |> assert("D.str fail on int") +D.decodeString(D.str, "3.14") |> isErr + |> assert("D.str fail on float") +id(D.decodeString(D.str, "\"hello\"") = ok("hello")) + |> assert("D.str succeed on str") +D.decodeString(D.str, "{ \"hello\": 42 }") |> isErr + |> assert("D.str fail on obj") + +id(D.decodeString(D.bool, "true") = ok(true)) + |> assert("D.bool succeed on bool") +D.decodeString(D.bool, "42") |> isErr + |> assert("D.bool fail on int") +D.decodeString(D.bool, "3.14") |> isErr + |> assert("D.bool fail on float") +D.decodeString(D.bool, "\"hello\"") |> isErr + |> assert("D.bool fail on str") +D.decodeString(D.bool, "{ \"hello\": 42 }") |> isErr + |> assert("D.bool fail on obj") + +D.decodeString(D.int, "true") |> isErr + |> assert("D.int fail on bool") +id(D.decodeString(D.int, "42") = ok(42)) + |> assert("D.int succeed on int") +D.decodeString(D.int, "3.14") |> isErr + |> assert("D.int fail on float") +D.decodeString(D.int, "\"hello\"") |> isErr + |> assert("D.int fail on str") +D.decodeString(D.int, "{ \"hello\": 42 }") |> isErr + |> assert("D.int fail on obj") + +D.decodeString(D.float, "true") |> isErr + |> assert("D.float fail on bool") +id(D.decodeString(D.float, "42") = ok(42.0)) + |> assert("D.float succeed on int float") +id(D.decodeString(D.float, "3.14") = ok(3.14)) + |> assert("D.float succeed on float") +D.decodeString(D.float, "\"hello\"") |> isErr + |> assert("D.float fail on str") +D.decodeString(D.float, "{ \"hello\": 42 }") |> isErr + |> assert("D.float fail on obj") + +print("Tests done.") diff --git a/package.n b/package.n index de1c23c..3861ade 100644 --- a/package.n +++ b/package.n @@ -3,3 +3,5 @@ let pub name = "jsonExtra" let pub version = "0.1.0" let pub authors = ["nbuilding"] + +let pub description = "An N port of Elm's Json package." diff --git a/utils.n b/utils.n new file mode 100644 index 0000000..c086b89 --- /dev/null +++ b/utils.n @@ -0,0 +1,25 @@ +// Currently there's a bug where you can't use parentheses after return +let pub identity = [[t] value: t] -> t { + return value +} + +let pub getError = [[t, e] result: result[t, e]] -> list[e] { + return if let = result { + [error] + } else { + [] + } +} + +let pub getErrors = [[t, e] results: list[result[t, e]]] -> list[e] { + return filterMap( + [result: result[t, e]] -> maybe[e] { + return if let = result { + yes(error) + } else { + none + } + }, + results + ) +}