Skip to content

Instantly share code, notes, and snippets.

@lordnull
Last active April 22, 2022 19:30
Show Gist options
  • Save lordnull/0017acfd2a971b4abcd980d4f1e06630 to your computer and use it in GitHub Desktop.
Save lordnull/0017acfd2a971b4abcd980d4f1e06630 to your computer and use it in GitHub Desktop.
-module(from_json).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-type json_number() :: number().
-type json_string() :: binary().
-type json_null() :: null.
-type json_boolean() :: true | false.
-type json_array() :: [ json() ].
-type json_array(T) :: [ T ].
-type json_key() :: atom() | binary().
-type json_object() :: #{ json_key() => json() }.
-type json() :: json_number() | json_string() | json_null() | json_boolean() | json_array() | json_object().
-type decoder(T) :: fun((json()) -> decode_result(T)).
-type decode_result(T) :: {ok, T} | {error, iolist()}.
-export_type([ json_number/0, json_string/0, json_null/0, json_boolean/0,
json_array/0, json_array/1, json_object/0, json/0, decoder/1 ]).
-export([ decode_string/2, decode_json/2 ]).
-export([ number/0, integer/0, string/0, null/0, boolean/0]).
-export([ array/1, object/1, proplist/1]).
-export([ map/2, field/2, optional_field/3, at/2, index/2 ]).
-export([ nullable/1, maybe/1, one_of/1, as_is/0, ok/1, fail/1]).
-export([ fold_over/2, tuple/2, validate/2]).
-export([ the_atom/1, an_atom/1]).
-export([ and_then/2]).
-spec decode_string(binary(), decoder(T)) -> decode_result(T).
decode_string(String, Decoder) ->
decode_json(jsx:decode(String, [return_maps]), Decoder).
-spec decode_json(json(), decoder(T)) -> decode_result(T).
decode_json(Json, Decoder) ->
Decoder(Json).
-spec number() -> decoder(number()).
number() ->
fun number/1.
-ifdef(TEST).
number_test_() ->
[ fun() -> {ok, 3} = decode_json(3, number()) end
, fun() -> {error, _} = decode_json(null, number()) end
, fun() -> {ok, 27.5} = decode_json(27.5, number()) end
].
-endif.
number(Json) when is_integer(Json); is_float(Json) ->
{ok, Json};
number(_) ->
{error, <<"not number">>}.
-spec integer() -> decoder(integer()).
integer() ->
fun integer/1.
-ifdef(TEST).
integer_test_() ->
[ fun() -> {ok, 67} = decode_json(67, integer()) end
, fun() -> {error, _} = decode_json(35.8, integer()) end
, fun() -> {error, _} = decode_json(2.0, integer()) end
].
-endif.
integer(Json) when is_integer(Json) ->
{ok, Json};
integer(_) ->
{error, <<"not an integer">>}.
-spec string() -> decoder(binary()).
string() ->
fun string/1.
-ifdef(TEST).
string_test_() ->
[ fun() -> {ok, <<"hi!">>} = decode_json(<<"hi!">>, string()) end
, fun() -> {error, _} = decode_json(32, string()) end
, fun() -> {error, _} = decode_json(null, string()) end
].
-endif.
string(Json) when is_binary(Json) ->
{ok, Json};
string(_) ->
{error, <<"not string">>}.
-spec null() -> decoder(undefined).
null() ->
fun null/1.
-ifdef(TEST).
null_test_() ->
[ fun() -> {ok, undefined} = decode_json(null, null()) end
, fun() -> {error, _} = decode_json(<<"hi!">>, null()) end
, fun() -> {error, _} = decode_json(#{}, null()) end
].
-endif.
null(null) ->
{ok, undefined};
null(_) ->
{error, <<"not null">>}.
-spec boolean() -> decoder(boolean()).
boolean() ->
fun boolean/1.
-ifdef(TEST).
boolean_test_() ->
[ fun() -> {ok, true} = decode_json(true, boolean()) end
, fun() -> {ok, false} = decode_json(false, boolean()) end
, fun() -> {error, _} = decode_json(5, boolean()) end
].
-endif.
boolean(Json) when Json =:= true; Json =:= false ->
{ok, Json};
boolean(_) ->
{error, <<"not boolean">>}.
-spec array(decoder(T)) -> decoder([T]).
array(Decoder) ->
fun(Json) ->
array(Json, Decoder)
end.
-ifdef(TEST).
array_test_() ->
[ fun() -> {ok, [1,4,7]} = decode_json([1,4,7], array(number())) end
, fun() -> {ok, [<<"hi">>, <<"yo">>]} = decode_json([<<"hi">>, <<"yo">>], array(string())) end
, fun() -> {error, _} = decode_json([<<"hi">>], array(number())) end
, fun() -> {ok, [false]} = decode_json([false], array(boolean())) end
, fun() -> {error, _} = decode_json(#{}, array(null())) end
].
-endif.
array(Json, Validator) when is_list(Json) ->
array_map(Json, Validator, 0, []);
array(_Json, _Validator) ->
{error, <<"not array">>}.
array_map([], _Validator, _Count, Acc) ->
{ok, lists:reverse(Acc)};
array_map([Hd | Tail], Validator, Count, Acc) ->
case Validator(Hd) of
{ok, NewVal} ->
array_map(Tail, Validator, Count + 1, [NewVal | Acc]);
{error, Wut} ->
{error, [Wut, <<" in array at item ">>, integer_to_list(Count)]}
end.
-spec map(fun((A) -> B), decoder(A)) -> decoder(B).
map(ValueTransform, Decoder) ->
fun(Json) ->
case Decoder(Json) of
{ok, MidValue} ->
{ok, ValueTransform(MidValue)};
Error ->
Error
end
end.
-ifdef(TEST).
map_test_() ->
[ fun() -> {ok, pants} = decode_json(3, map(fun(_) -> pants end, number())) end
, fun() -> {error, _} = decode_json(null, map(fun(_) -> pants end, number())) end
].
-endif.
-spec nullable(decoder(T)) -> decoder(T | undefined).
nullable(Decoder) ->
fun
(null) ->
{ok, undefined};
(Json) ->
Decoder(Json)
end.
-ifdef(TEST).
nullable_test_() ->
[ fun() -> {ok, undefined} = decode_json(null, nullable(number())) end
, fun() -> {ok, 7} = decode_json(7, nullable(number())) end
, fun() -> {error, _} = decode_json(<<"hi">>, nullable(number())) end
].
-endif.
-spec object(#{json_key() => decoder(T)}) -> decoder(#{json_key() => T}).
object(DecodeMap) when is_map(DecodeMap) ->
fun(Json) ->
decode_object(Json, DecodeMap, #{})
end.
-ifdef(TEST).
object_test_() ->
Input = #{
<<"int">> => 7,
<<"str">> => <<"goober">>,
<<"bool">> => false,
<<"obj">> => #{}
}, [
fun() ->
Expected = #{<<"int">> => 7, <<"bool">> => false},
Decoder = #{<<"int">> => number(), <<"bool">> => boolean()},
{ok, Expected} = decode_json(Input, object(Decoder))
end,
fun() ->
Decoder = #{<<"int">> => string()},
{error, _} = decode_json(Input, object(Decoder))
end,
fun() ->
Decoder = #{},
{ok, #{}} = decode_json(Input, object(Decoder))
end,
fun() ->
Decoder = #{<<"obj">> => object(#{})},
{ok, #{<<"obj">> := #{}}} = decode_json(Input, object(Decoder))
end,
fun() ->
Decoder = #{<<"no-field">> => string()},
{error, _} = decode_json(Input, object(Decoder))
end ].
-endif.
decode_object(Json, DecodeMap, AccObj) when is_map(Json), is_map(DecodeMap) ->
DecodeList = maps:to_list(DecodeMap),
decode_object(Json, DecodeList, AccObj);
decode_object(_Json, [], Acc) ->
{ok, Acc};
decode_object(Json, [{Key, Decoder} | Tail], Acc) when is_map(Json) ->
case (field(Key, Decoder))(Json) of
{ok, Val} ->
decode_object(Json, Tail, Acc#{Key => Val});
{error, Wut} ->
{error, [<<"invalid value at key ">>, to_json_key(Key), " ", Wut]}
end.
to_json_key(Atom) when is_atom(Atom) ->
atom_to_binary(Atom, utf8);
to_json_key(Binary) when is_binary(Binary) ->
Binary.
-spec proplist(#{json_key() => decoder(T)}) -> decoder([{ json_key(), decoder(T)}]).
proplist(DecodeMap) when is_map(DecodeMap) ->
DecodeList = maps:to_list(DecodeMap),
fun(Json) ->
decode_to_proplist(Json, DecodeList, [])
end.
-ifdef(TEST).
proplist_test_() ->
[ fun() ->
{ok, [{<<"a">>, 3}]} = decode_json(#{<<"a">> => 3}, proplist(#{<<"a">> => number()}))
end
, fun() ->
{ok, []} = decode_json(#{<<"a">> => 3}, proplist(#{}))
end
, fun() ->
{error, _} = decode_json(#{<<"a">> => <<"hi">>}, proplist(#{<<"a">> => number()}))
end ].
-endif.
decode_to_proplist(Json, _Decoders, _Acc) when not is_map(Json) ->
{error, <<"not an object">>};
decode_to_proplist(_Json, [], Acc) ->
{ok, Acc};
decode_to_proplist(Json, [{Key, Decoder} | Tail], Acc) ->
case Decoder(maps:get(Key, Json, undefined)) of
{ok, Val} ->
decode_to_proplist(Json, Tail, [{Key, Val} | Acc]);
{error, Wut} ->
{error, [<<"error at key ">>, to_json_key(Key), " ", Wut]}
end.
-spec field(json_key(), decoder(T)) -> decoder(T).
field(Key, Decoder) ->
fun(Json) ->
decode_field(Json, Key, Decoder)
end.
-ifdef(TEST).
field_test_() ->
[ fun() -> {ok, 3} = decode_json(#{<<"a">> => 3}, field(<<"a">>, number())) end
, fun() -> {error, _} = decode_json(#{<<"a">> => 3}, field(<<"a">>, string())) end
, fun() -> {ok, 7} = decode_json(#{<<"a">> => 7}, field(a, number())) end
, fun() -> {ok, 9} = decode_json(#{a => 9}, field(<<"a">>, number())) end
].
-endif.
decode_field(Json, _Key, _Validator) when not is_map(Json) ->
{error, <<"not an object">>};
decode_field(Json, Key, Validator) when is_atom(Key) ->
BinKey = atom_to_binary(Key, utf8),
case {maps:find(Key, Json), maps:find(BinKey, Json)} of
{error, error} ->
{error, [Key, <<" is missing">>]};
{{ok, Value}, _} ->
Validator(Value);
{_, {ok, Value}} ->
Validator(Value)
end;
decode_field(Json, Key, Validator) when is_binary(Key) ->
case maps:find(Key, Json) of
error ->
Keys = maps:keys(Json),
AtomKeys = [ K || K <- Keys, is_atom(K) ],
case [ Matching || Matching <- AtomKeys, atom_to_binary(Matching, utf8) =:= Key ] of
[] ->
{error, [Key, <<" is missing">>]};
[MatchKey | _] ->
Validator(maps:get(MatchKey, Json))
end;
{ok, Value} ->
Validator(Value)
end.
-spec optional_field(json_key(), T, decoder(T)) -> decoder(T).
optional_field(Key, IfMissing, Decoder) ->
fun(Json) ->
decode_optional_field(Json, Key, IfMissing, Decoder)
end.
-ifdef(TEST).
optional_field_test_() ->
[ fun() ->
{ok, 3} = decode_json(#{}, optional_field(<<"a">>, 3, number()))
end,
fun() ->
{ok, 7} = decode_json(#{<<"a">> => 7}, optional_field(<<"a">>, 3, number()))
end,
fun() ->
{error, _} = decode_json(#{<<"a">> => <<"hi">>}, optional_field(<<"a">>, 3, number()))
end,
fun() ->
{ok, 9} = decode_json(#{<<"a">> => 9}, optional_field(a, 7, number()))
end,
fun() ->
{ok, 5} = decode_json(#{ a => 5}, optional_field(<<"a">>, 7, number()))
end
].
-endif.
decode_optional_field(Json, Key, IfMissing, Validator) ->
case decode_field(Json, Key, Validator) of
{error, [_, <<" is missing">>]} ->
{ok, IfMissing};
Else ->
Else
end.
-spec at([json_key()], decoder(T)) -> decoder(T).
at(KeyPath, Decoder) ->
fun(Json) ->
at(Json, KeyPath, [], Decoder)
end.
-ifdef(TEST).
at_test_() ->
[ fun() ->
{ok, 3} = decode_json(#{a => #{ b => 3}}, at([a,b], number()))
end,
fun() ->
{error, _} = decode_json(3, at([a,b], number()))
end,
fun() ->
{error, _} = decode_json(#{a => #{ b => <<"hi">>}}, at([a,b],number()))
end ].
-endif.
at(Json, [], _PathWalked, Validator) ->
Validator(Json);
at(Json, [LastPath], PathWalked, Decoder) when is_map(Json) ->
case Decoder(maps:get(LastPath, Json, undefined)) of
{ok, _} = Ok ->
Ok;
{error, Wut} ->
{error, [pretty_print_key([LastPath | PathWalked]), <<" did not pass decoder: ">>, Wut]}
end;
at(Json, [Key | Tail], PathWalked, Validator) when is_map(Json) ->
case maps:get(Key, Json, undefined) of
Obj when is_map(Obj) ->
at(Obj, Tail, [Key | PathWalked], Validator);
_NotObj ->
{error, [pretty_print_key([Key | PathWalked]), <<" is not an object">>]}
end;
at(_Json, _Keys, PathWalked, _Decoder) ->
{error, [pretty_print_key(PathWalked), <<" is not an object">>]}.
pretty_print_key([]) ->
[];
pretty_print_key([Key]) ->
[Key];
pretty_print_key([H | T]) ->
pretty_print_key(T, [H]).
pretty_print_key([P], Acc) ->
[P, "." | Acc];
pretty_print_key([P | Tail], Acc) ->
pretty_print_key(Tail, [P, ".", Acc]).
-spec index(non_neg_integer(), decoder(T)) -> decoder(T).
index(Idx, Decoder) ->
fun(Json) ->
index(Json, Idx, Decoder)
end.
-ifdef(TEST).
index_test_() ->
[ fun() -> {error, _} = decode_json([], index(0, number())) end
, fun() -> {ok, 3} = decode_json([1,2,3], index(2, number())) end
, fun() -> {error, _} = decode_json([1,2,<<"hi">>], index(2, number())) end
].
-endif.
index([], _Idx, _Decoder) ->
{error, <<"empty list">>};
index(Json, Idx, _Decoder) when is_list(Json), length(Json) < Idx ->
{error, <<"list too short">>};
index(Json, Idx, Decoder) when is_list(Json) ->
Decoder(lists:nth(Idx + 1, Json));
index(_Json, _Idx, _Decoder) ->
{error, <<"not a list">>}.
-spec maybe(decoder(T)) -> decoder(T | undefined).
maybe(Decoder) ->
fun(Json) ->
maybe(Json, Decoder)
end.
-ifdef(TEST).
maybe_test_() ->
[ fun() -> {ok, undefined} = decode_json(3, maybe(string())) end
, fun() -> {ok, 6} = decode_json(6, maybe(number())) end
].
-endif.
maybe(Json, Decoder) ->
case Decoder(Json) of
{error, _} ->
{ok, undefined};
Ok ->
Ok
end.
-spec one_of([decoder(T)]) -> decoder(T).
one_of(Decoders) when is_list(Decoders) ->
fun(Json) ->
one_of(Json, Decoders)
end.
-ifdef(TEST).
one_of_test_() ->
[ fun() -> {ok, 3} = decode_json(3, one_of([number(), string()])) end
, fun() -> {ok, <<"hi">>} = decode_json(<<"hi">>, one_of([number(), string()])) end
, fun() -> {error, _} = decode_json(false, one_of([number(), string()])) end
].
-endif.
one_of(_Json, []) ->
{error, <<"no valid conversions">>};
one_of(Json, [ValidatorHead | Tail]) ->
case ValidatorHead(Json) of
{error, _} ->
one_of(Json, Tail);
{ok, _} = Ok ->
Ok
end.
-spec as_is() -> decoder(json()).
as_is() ->
fun(Json) -> {ok, Json} end.
-ifdef(TEST).
as_is_test() ->
[ fun() -> {ok, 3} = decode_json(3, as_is()) end
, fun() -> {ok, null} = decode_json(null, as_is()) end
].
-endif.
-spec ok(T) -> decoder(T).
ok(Value) ->
fun(_) -> {ok, Value} end.
-ifdef(TEST).
ok_test() ->
{ok, {1,5}} = decode_json(false, ok({1,5})).
-endif.
-spec fail(binary()) -> decoder(none()).
fail(Why) ->
fun(_) -> {error, Why} end.
-ifdef(TEST).
fail_test() ->
{error, <<"ugh">>} = decode_json(true, fail(<<"ugh">>)).
-endif.
-spec fold_over(S, [{decoder(T), fun((T,S) -> S)}]) -> decoder(S).
fold_over(InitState, DecodeAndUpdates) when is_list(DecodeAndUpdates) ->
fun(Json) ->
fold_over(Json, InitState, 1, DecodeAndUpdates)
end.
-ifdef(TEST).
fold_over_test_() ->
UDs = [{number(), fun(N,S) -> N * S end} || _ <- lists:seq(1,5)],
[ fun() ->
{ok, 32} = decode_json(2, fold_over(1, UDs))
end,
fun() ->
{error, _} = decode_json(1, fold_over(1, [{string(), fun(_, _) -> ok end} | UDs]))
end ].
-endif.
fold_over(_Json, FinalState, _Step, []) ->
{ok, FinalState};
fold_over(Json, State, Step, [{Decode, Update} | Tail]) ->
case Decode(Json) of
{ok, Value} ->
fold_over(Json, Update(Value, State), Step + 1, Tail);
{error, Wut} ->
{error, [<<"fold failed at step ">>, integer_to_list(Step), <<"due to ">>, Wut]}
end.
-spec tuple(tuple(), [{pos_integer(), decoder(_)}]) -> decoder(tuple()).
tuple(InitialRecord, Decoders) ->
fun(Json) ->
tuple(Json, InitialRecord, Decoders)
end.
-ifdef(TEST).
tuple_test_() ->
[ fun() ->
{ok, {r, 7}} = decode_json(#{f => 7}, tuple({r, undefined}, [{2, field(f, number())}]))
end,
fun() ->
{ok, {r, 7}} = decode_json(7, tuple({r, undefined}, [{2, number()}]))
end,
fun() ->
{error, _} = decode_json(#{f => <<"hi">>}, tuple({r, undefined}, [{2, number()}]))
end ].
-endif.
tuple(_Json, Record, []) ->
{ok, Record};
tuple(Json, Record, [{FieldIdx, Decoder} | Tail]) ->
case Decoder(Json) of
{ok, Value} ->
tuple(Json, setelement(FieldIdx, Record, Value), Tail);
{error, Wut} ->
{error, [<<"for field idx ">>, integer_to_list(FieldIdx), " ", Wut]}
end.
-spec validate(fun((A) -> decode_result(B)), decoder(A)) -> decoder(B).
validate(Map, Decoder) ->
fun(Json) ->
validate(Json, Map, Decoder)
end.
-ifdef(TEST).
validate_test_() ->
Validate = fun(N) when N < 0 -> {error, too_low};
(N) when N < 5 -> {ok, low};
(N) when is_integer(N) -> {ok, high}
end,
[ fun() -> {ok, low} = decode_json(1, validate(Validate, number())) end
, fun() -> {ok, high} = decode_json(200, validate(Validate, number())) end
, fun() -> {error, _} = decode_json(<<"hi">>, validate(Validate, number())) end
, fun() -> {error, too_low} = decode_json(-70, validate(Validate, number())) end
].
-endif.
validate(Json, Map, Decoder) ->
case Decoder(Json) of
{ok, Value} ->
Map(Value);
Error ->
Error
end.
the_atom(Atom) ->
EncodedAtom = atom_to_binary(Atom, utf8),
Validator = fun(String) ->
if
EncodedAtom =:= String ->
{ok, Atom};
true ->
{error, [String, " does not represent ", EncodedAtom]}
end
end,
validate(Validator, string()).
-ifdef(TEST).
the_atom_test_() ->
[ fun() -> {ok, goober} = decode_json(<<"goober">>, the_atom(goober)) end
, fun() -> {error, <<"not string">>} = decode_json(5, the_atom(goober)) end
, fun() -> {error, _} = decode_json(<<"pants">>, the_atom(goober)) end
].
-endif.
an_atom(Atoms) ->
Validators = lists:map(fun(Atom) ->
the_atom(Atom)
end, Atoms),
one_of(Validators).
-ifdef(TEST).
an_atom_test_() ->
[ fun() -> {ok, goober} = decode_json(<<"goober">>, an_atom([pants, goober])) end
, fun() -> {ok, pants} = decode_json(<<"pants">>, an_atom([pants, goober])) end
, fun() -> {error, _} = decode_json(<<"hello">>, an_atom([pants, goober])) end
, fun() -> {error, _} = decode_json(5, an_atom([pants, goober])) end
].
-endif.
-ifdef(TEST).
and_then_test_() ->
[ fun() -> {error, fart} = decode_json(1, and_then(number(), fun(_) -> fail(fart) end)) end
, fun() -> {error, <<"not number">>} = decode_json(<<"a">>, and_then(number(), fun(_) -> ok(fart) end)) end
, fun() -> {ok, fart} = decode_json(1, and_then(number(), fun(1) -> ok(fart) end)) end
].
-endif.
-spec and_then(decoder(A), fun((A) -> decoder(B))) -> decoder(B).
and_then(Decoder, Generator) ->
fun(Json) ->
case Decoder(Json) of
{ok, Val} ->
NextDecoder = Generator(Val),
NextDecoder(Json);
Error ->
Error
end
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment