Skip to content

Commit

Permalink
Fully document parse errors
Browse files Browse the repository at this point in the history
  • Loading branch information
mlms13 committed May 24, 2019
1 parent 4f3b990 commit 18b4468
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 29 deletions.
37 changes: 9 additions & 28 deletions src/Decode_ParseError.re
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@ and objError('a) =

type failure = t(DecodeBase.failure);

let rec map = (fn, v) =>
switch (v) {
| Val(a, json) => Val(fn(a), json)
| Arr(nel) => Arr(NonEmpty.List.map(((i, a)) => (i, map(fn, a)), nel))
| Obj(nel) =>
Obj(NonEmpty.List.map(((f, o)) => (f, mapObj(fn, o)), nel))
}
and mapObj = (fn, obj) =>
switch (obj) {
| MissingField => MissingField
| InvalidField(a) => InvalidField(map(fn, a))
};

let arrPure = (pos, err) => Arr(NonEmpty.List.pure((pos, err)));
let objPure = (field, err) => Obj(NonEmpty.List.pure((field, err)));

Expand Down Expand Up @@ -112,22 +99,19 @@ module type ValError = {
let handle: DecodeBase.failure => t;
};

module ResultOf = (T: ValError) => {
module ResultOf = (Err: ValError) => {
open BsAbstract.Interface;

type r('a) = Result.t('a, t(T.t));
let mapErr = (v, fn) => Result.bimap(id, v, fn);
let fromFailure = err => map(T.handle, err);
let fromFailureResult = err => mapErr(fromFailure, err);
type r('a) = Result.t('a, t(Err.t));

module Monad: MONAD with type t('a) = r('a) = {
type t('a) = r('a);
let map = Result.map;
let apply = (f, v) =>
switch (f, v) {
| (Belt.Result.Ok(fn), Belt.Result.Ok(a)) => Result.ok(fn(a))
| (Ok(_), Error(x)) => Result.error(x)
| (Error(x), Ok(_)) => Result.error(x)
| (Ok(_), Error(_) as err) => err
| (Error(_) as err, Ok(_)) => err
| (Error(fnx), Error(ax)) => Result.error(combine(fnx, ax))
};
let pure = Result.pure;
Expand All @@ -142,15 +126,12 @@ module ResultOf = (T: ValError) => {
module TransformError: DecodeBase.TransformError with type t('a) = r('a) = {
type t('a) = r('a);

let valErr = (v, json) => Result.error(Val(T.handle(v), json));
let arrErr = pos => mapErr(x => arrPure(pos, x));
let valErr = (v, json) => Result.error(Val(Err.handle(v), json));
let arrErr = pos => Result.mapError(arrPure(pos));
let missingFieldErr = field =>
Result.error(objPure(field, MissingField));
let objErr = field => mapErr(x => objPure(field, InvalidField(x)));
let lazyAlt = (res, fn) =>
switch (res) {
| Belt.Result.Ok(v) => Belt.Result.Ok(v)
| Belt.Result.Error(_) => fn()
};
let objErr = field =>
Result.mapError(x => objPure(field, InvalidField(x)));
let lazyAlt = (res, fn) => Result.fold(_ => fn(), Result.ok, res);
};
};
180 changes: 179 additions & 1 deletion test/Decode_AsResult_OfParseError_test.re
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ describe("Simple decoders", () => {
|> toEqual(Result.error(Err.Val(`ExpectedNumber, Sample.jsonString)))
);

test("intFromNumber", () =>
test("intFromNumber (non-number)", () =>
expect(Decode.intFromNumber(Sample.jsonNull))
|> toEqual(Result.error(Err.Val(`ExpectedNumber, Sample.jsonNull)))
);

test("intFromNumber (float)", () =>
expect(Decode.intFromNumber(Sample.jsonFloat))
|> toEqual(Result.error(Err.Val(`ExpectedInt, Sample.jsonFloat)))
);
Expand All @@ -31,4 +36,177 @@ describe("Simple decoders", () => {
expect(Decode.date(Sample.jsonString))
|> toEqual(Result.error(Err.Val(`ExpectedValidDate, Sample.jsonString)))
);

test("variant", () =>
expect(Decode.variantFromString(Sample.colorFromJs, Sample.jsonString))
|> toEqual(
Result.error(Err.Val(`ExpectedValidOption, Sample.jsonString)),
)
);

test("array", () =>
expect(Decode.array(Decode.string, Sample.jsonNull))
|> toEqual(Result.error(Err.Val(`ExpectedArray, Sample.jsonNull)))
);

test("object", () =>
expect(Decode.field("x", Decode.string, Sample.jsonString))
|> toEqual(Result.error(Err.Val(`ExpectedObject, Sample.jsonString)))
);
});

describe("Inner decoders", () => {
test("array", () =>
expect(Decode.array(Decode.string, Sample.jsonArrayString))
|> toEqual(Result.ok(Sample.valArrayString))
);

test("array (failure on inner decode)", () =>
expect(Decode.array(Decode.boolean, Sample.jsonArrayString))
|> toEqual(
Result.error(
Err.Arr(
NonEmpty.List.make(
(0, Err.Val(`ExpectedBoolean, Js.Json.string("A"))),
[
(1, Err.Val(`ExpectedBoolean, Js.Json.string("B"))),
(2, Err.Val(`ExpectedBoolean, Js.Json.string("C"))),
],
),
),
),
)
);

test("field (missing)", () =>
expect(Decode.field("x", Decode.string, Sample.jsonJobCeo))
|> toEqual(
Result.error(Err.Obj(NonEmpty.List.pure(("x", Err.MissingField)))),
)
);

test("field (failure on inner decode)", () =>
expect(Decode.field("manager", Decode.string, Sample.jsonJobCeo))
|> toEqual(
Result.error(
Err.Obj(
NonEmpty.List.pure((
"manager",
Err.InvalidField(Err.Val(`ExpectedString, Sample.jsonNull)),
)),
),
),
)
);

test("oneOf", () => {
let decodeUnion =
Decode.(
oneOf(
map(Sample.unionS, string),
[
map(Sample.unionN, optional(floatFromNumber)),
map(Sample.unionB, boolean),
],
)
);

expect(decodeUnion(Sample.jsonBool))
|> toEqual(Result.ok(Sample.(B(valBool))));
});
});

describe("Large, nested decoder", () => {
let decodeJob =
Decode.(
map4(
Sample.makeJob,
field("title", string),
field("x", string),
field("title", date),
pure(None),
)
);

let decoded = decodeJob(Sample.jsonJobCeo);

let error =
Err.Obj(
NonEmpty.List.make(
("x", Err.MissingField),
[
(
"title",
Err.InvalidField(
Err.Val(`ExpectedValidDate, Js.Json.string("CEO")),
),
),
],
),
);

let objErrString = {|Failed to decode object:
Field "x" is required, but was not present
Field "title" had an invalid value: Expected a valid date but found "CEO"|};

let arrErrString = {|Failed to decode array:
At position 0: Expected string but found null|};

test("map4, field", () =>
expect(decoded) |> toEqual(Result.error(error))
);

test("toDebugString (obj)", () =>
expect(Err.failureToDebugString(error)) |> toEqual(objErrString)
);

test("toDebugString (arr)", () =>
expect(
Err.(
arrPure(0, Val(`ExpectedString, Sample.jsonNull))
|> failureToDebugString
),
)
|> toEqual(arrErrString)
);
});

// ParseErrors only know how to combine Arr+Arr and Obj+Obj. In most situations
// this is all that matters. In all other cases, the first error is chosen.
describe("Parse error combinations", () => {
let combine = (a, b) => (a, b);

let arrError =
Err.Arr(
NonEmpty.List.make(
(0, Err.Val(`ExpectedBoolean, Js.Json.string("A"))),
[
(1, Err.Val(`ExpectedBoolean, Js.Json.string("B"))),
(2, Err.Val(`ExpectedBoolean, Js.Json.string("C"))),
],
),
);

let objError = Err.objPure("x", Err.MissingField);

test("combine Val/Val", () =>
expect(Decode.(map2(combine, string, boolean, Sample.jsonNull)))
|> toEqual(Result.error(Err.Val(`ExpectedString, Sample.jsonNull)))
);

test("combine Arr/Val", () =>
expect(
Decode.(map2(combine, list(boolean), boolean, Sample.jsonArrayString)),
)
|> toEqual(Result.error(arrError))
);

test("combine Obj/Val", () =>
expect(
Decode.(
map2(combine, field("x", boolean), boolean, Sample.jsonDictEmpty)
),
)
|> toEqual(Result.error(objError))
);
});

0 comments on commit 18b4468

Please sign in to comment.