diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9c297c3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## Unreleased version +### Added +* `Belt.Result.t` support diff --git a/__tests__/test.re b/__tests__/test.re index fa94675..d77d668 100644 --- a/__tests__/test.re +++ b/__tests__/test.re @@ -14,6 +14,7 @@ open Belt.Result; */ [@decco] type a('a) = array('a); [@decco] type l('a) = list('a); [@decco] type o('a) = option('a); +[@decco] type r('v, 'e) = Belt.Result.t('v, 'e); [@decco] type simpleVar('a) = 'a; [@decco] type j = Js.Json.t; [@decco] type optionList = l(o(s)); @@ -345,9 +346,9 @@ describe("option", () => { describe("o_encode", () => { test("none", () => o_encode(s_encode, None) - |> Js.Json.classify - |> expect - |> toBe(Js.Json.JSONNull) + |> Js.Json.classify + |> expect + |> toBe(Js.Json.JSONNull) ); test("some", () => { @@ -374,6 +375,83 @@ describe("option", () => { }); }); +describe("result", () => { + let enc = r_encode(s_encode, i_encode); + let dec = r_decode(s_decode, i_decode); + + describe("r_encode", () => { + test("ok", () => + Belt.Result.Ok("oaky") + |> enc + |> Js.Json.stringify + |> expect + |> toBe("[\"Ok\",\"oaky\"]") + ); + + test("error", () => { + Belt.Result.Error(404) + |> enc + |> Js.Json.stringify + |> expect + |> toBe("[\"Error\",404]") + }); + }); + + describe("r_decode", () => { + describe("good", () => { + let json = "[\"Ok\",\"yess\"]" |> Js.Json.parseExn; + testGoodDecode("ok", dec, json, Ok("yess")); + + let json = "[\"Error\",911]" |> Js.Json.parseExn; + testGoodDecode("error", dec, json, Error(911)); + }); + + describe("bad", () => { + let json = Js.Json.number(12.); + testBadDecode("not an array", dec, json, { + path: "", + message: "Not an array", + value: json + }); + + let json = "[]" |> Js.Json.parseExn; + testBadDecode("length != 2", dec, json, { + path: "", + message: "Expected exactly 2 values in array", + value: json + }); + + let json = "[0,1]" |> Js.Json.parseExn; + testBadDecode("constructor not a string", dec, json, { + path: "", + message: "Not a string", + value: Js.Json.number(0.) + }); + + let json = "[\"bad\",1]" |> Js.Json.parseExn; + testBadDecode("unrecognized constructor", dec, json, { + path: "", + message: "Expected either \"Ok\" or \"Error\"", + value: Js.Json.string("bad") + }); + + let json = "[\"Ok\",1]" |> Js.Json.parseExn; + testBadDecode("bad Ok decode", dec, json, { + path: "", + message: "Not a string", + value: Js.Json.number(1.) + }); + + let json = "[\"Error\",null]" |> Js.Json.parseExn; + testBadDecode("bad Error decode", dec, json, { + path: "", + message: "Not a number", + value: Js.Json.null + }); + }); + }); +}); + describe("falseable", () => { describe("falseable_encode", () => { test("none", () => diff --git a/ppx/bin/Codecs.re b/ppx/bin/Codecs.re index c8bca83..fdfad46 100644 --- a/ppx/bin/Codecs.re +++ b/ppx/bin/Codecs.re @@ -71,6 +71,10 @@ and generateConstrCodecs = ({ doEncode, doDecode }, { Location.txt: identifier, doEncode ? Some([%expr Decco.optionToJson]) : None, doDecode ? Some([%expr Decco.optionFromJson]) : None ) + | Ldot(Ldot(Lident("Belt"), "Result"), "t") => ( + doEncode ? Some([%expr Decco.resultToJson]) : None, + doDecode ? Some([%expr Decco.resultFromJson]) : None + ) | Ldot(Ldot(Lident("Js"), "Json"), "t") => ( doEncode ? Some([%expr (v) => v]) : None, doDecode ? Some([%expr (v) => Belt.Result.Ok(v)]) : None, diff --git a/src/Decco.re b/src/Decco.re index d4c2743..d892b54 100644 --- a/src/Decco.re +++ b/src/Decco.re @@ -119,6 +119,34 @@ let optionFromJson = (decoder, json) => | _ => decoder(json) |> map(_, v => Some(v)) }; +let resultToJson = (okEncoder, errorEncoder, result) => + switch result { + | Ok(v) => [| Js.Json.string("Ok"), okEncoder(v) |] + | Error(e) => [| Js.Json.string("Error"), errorEncoder(e) |] + } + |> Js.Json.array; + +let resultFromJson = (okDecoder, errorDecoder, json) => + switch (Js.Json.decodeArray(json)) { + | Some([| variantConstructorId, payload |]) => + switch (Js.Json.decodeString(variantConstructorId)) { + | Some("Ok") => + okDecoder(payload) + -> Belt.Result.map(v => Ok(v)) + + | Some("Error") => + switch(errorDecoder(payload)) { + | Ok(v) => Ok(Error(v)) + | Error(e) => Error(e) + } + + | Some(_) => error("Expected either \"Ok\" or \"Error\"", variantConstructorId) + | None => error("Not a string", variantConstructorId) + } + | Some(_) => error("Expected exactly 2 values in array", json) + | None => error("Not an array", json) + }; + module Codecs { include Codecs; let string = (stringToJson, stringFromJson);