Packages

Code First GraphQL library for Gleam

Current section

Files

Jump to
mochi src mochi@decoders.erl
Raw

src/mochi@decoders.erl

-module(mochi@decoders).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/mochi/decoders.gleam").
-export([build_with/3, optional_string/2, optional_int/2, optional_bool/2, list_filtering/3]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Small ergonomic helpers for the decoder callback that\n"
" `mochi/types.{build}` requires on every object type.\n"
"\n"
" mochi round-trips field values through `Dynamic` during query\n"
" execution, which means each `ObjectType` needs a\n"
" `fn(Dynamic) -> Result(t, String)` decoder. Writing those by hand\n"
" produces a lot of mirror-image boilerplate — list fields with\n"
" per-item decoding, optional fields with sensible defaults, and the\n"
" standard `case decode.run { Ok -> Ok; Error -> Error(\"Failed to\n"
" decode <Type>\") }` wrap.\n"
"\n"
" These helpers exist for one reason: to make a typical schema's\n"
" `build` callback short and readable without giving up any of\n"
" `gleam_stdlib`'s `decode` typing. They're additive — every\n"
" existing decoder keeps working unchanged.\n"
"\n"
" All helpers follow `gleam_stdlib`'s `decode` continuation-passing\n"
" convention so they compose with `use` bindings:\n"
"\n"
" ```gleam\n"
" use email <- md.optional_string(\"email\")\n"
" ```\n"
"\n"
" > **Output decoders only.** The `optional_*` helpers conflate\n"
" > \"field absent\" with \"field present but defaulted.\" That's\n"
" > correct for `mochi/types.{build}` callbacks (mochi only invokes\n"
" > them on schema-conforming output values) but wrong for input\n"
" > validation — never use these to decode mutation arguments where\n"
" > \"user sent nothing\" must differ from \"user sent an empty value.\"\n"
" > For input validation, use `gleam_stdlib`'s `decode.field` /\n"
" > `decode.optional_field` directly so you control the default.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import gleam/dynamic/decode\n"
" import mochi/decoders as md\n"
" import mochi/schema\n"
" import mochi/types\n"
"\n"
" pub fn user_type() -> schema.ObjectType {\n"
" types.object(\"User\")\n"
" |> types.id(\"id\", fn(u: User) { u.id })\n"
" |> types.string(\"email\", fn(u: User) { u.email })\n"
" |> types.int(\"age\", fn(u: User) { u.age })\n"
" |> types.list_object(\"friends\", \"User\", fn(u) { ... })\n"
" |> types.build(decode_user)\n"
" }\n"
"\n"
" fn decode_user(dyn) {\n"
" let decoder = {\n"
" use id <- decode.field(\"id\", decode.string)\n"
" use email <- md.optional_string(\"email\")\n"
" use age <- md.optional_int(\"age\")\n"
" use friends <- md.list_filtering(\"friends\", decode_user)\n"
" decode.success(User(id:, email:, age:, friends:))\n"
" }\n"
" md.build_with(decoder, \"User\", dyn)\n"
" }\n"
" ```\n"
).
-file("src/mochi/decoders.gleam", 107).
-spec describe_errors(list(gleam@dynamic@decode:decode_error())) -> binary().
describe_errors(Errors) ->
case Errors of
[] ->
<<""/utf8>>;
[{decode_error, Expected, Found, Path} | _] ->
Where = case Path of
[] ->
<<""/utf8>>;
_ ->
<<" at "/utf8,
(gleam@string:join(Path, <<"."/utf8>>))/binary>>
end,
<<<<<<<<": expected "/utf8, Expected/binary>>/binary, Where/binary>>/binary,
", got "/utf8>>/binary,
Found/binary>>
end.
-file("src/mochi/decoders.gleam", 95).
?DOC(
" Run a `decode.Decoder(t)` against `dyn`, returning the\n"
" `Result(t, String)` shape that `mochi/types.{build}` expects.\n"
" `type_name` is interpolated into the error message — keep it the\n"
" same as the GraphQL object type (e.g. `\"CapabilityGraph\"`) so\n"
" error logs are searchable.\n"
"\n"
" On failure, the first decoder error is included in the message\n"
" so debugging doesn't degrade to \"something failed somewhere in\n"
" this 14-field record\":\n"
"\n"
" ```\n"
" \"Failed to decode User: expected String at name, got Int\"\n"
" ```\n"
"\n"
" Collapses the standard four-line wrap:\n"
"\n"
" ```gleam\n"
" case decode.run(dyn, decoder) {\n"
" Ok(v) -> Ok(v)\n"
" Error(_) -> Error(\"Failed to decode CapabilityGraph\")\n"
" }\n"
" ```\n"
"\n"
" into a single call:\n"
"\n"
" ```gleam\n"
" build_with(decoder, \"CapabilityGraph\", dyn)\n"
" ```\n"
).
-spec build_with(
gleam@dynamic@decode:decoder(NVV),
binary(),
gleam@dynamic:dynamic_()
) -> {ok, NVV} | {error, binary()}.
build_with(Decoder, Type_name, Dyn) ->
case gleam@dynamic@decode:run(Dyn, Decoder) of
{ok, V} ->
{ok, V};
{error, Errors} ->
{error,
<<<<"Failed to decode "/utf8, Type_name/binary>>/binary,
(describe_errors(Errors))/binary>>}
end.
-file("src/mochi/decoders.gleam", 132).
?DOC(
" Decode an optional string field, defaulting to the empty string\n"
" when absent. Continuation-passing form for `use`-binding:\n"
"\n"
" ```gleam\n"
" use email <- md.optional_string(\"email\")\n"
" ```\n"
"\n"
" Equivalent to `decode.optional_field(name, \"\", decode.string, next)`\n"
" but reads better at call sites that have many such fields.\n"
"\n"
" Output decoder only — see the module-level note. Don't use this\n"
" for input validation where \"absent\" must differ from \"empty\".\n"
).
-spec optional_string(
binary(),
fun((binary()) -> gleam@dynamic@decode:decoder(NWA))
) -> gleam@dynamic@decode:decoder(NWA).
optional_string(Name, Next) ->
gleam@dynamic@decode:optional_field(
Name,
<<""/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
Next
).
-file("src/mochi/decoders.gleam", 141).
?DOC(
" Decode an optional integer field, defaulting to `0` when absent.\n"
" Output decoder only — see the module-level note.\n"
).
-spec optional_int(
binary(),
fun((integer()) -> gleam@dynamic@decode:decoder(NWD))
) -> gleam@dynamic@decode:decoder(NWD).
optional_int(Name, Next) ->
gleam@dynamic@decode:optional_field(
Name,
0,
{decoder, fun gleam@dynamic@decode:decode_int/1},
Next
).
-file("src/mochi/decoders.gleam", 150).
?DOC(
" Decode an optional bool field, defaulting to `False` when absent.\n"
" Output decoder only — see the module-level note.\n"
).
-spec optional_bool(
binary(),
fun((boolean()) -> gleam@dynamic@decode:decoder(NWG))
) -> gleam@dynamic@decode:decoder(NWG).
optional_bool(Name, Next) ->
gleam@dynamic@decode:optional_field(
Name,
false,
{decoder, fun gleam@dynamic@decode:decode_bool/1},
Next
).
-file("src/mochi/decoders.gleam", 180).
?DOC(
" Decode an optional list field where each item is a `Dynamic` that\n"
" must be passed through a per-item decoder. Items that fail their\n"
" per-item decode are **silently dropped**; missing or non-list\n"
" values resolve to the empty list.\n"
"\n"
" Useful for top-level \"list of object\" GraphQL fields where one\n"
" malformed row shouldn't kill the whole response. Behavior:\n"
"\n"
" - Field absent or null → `[]`\n"
" - Field present, item ok → included in result\n"
" - Field present, item decode fails → dropped silently\n"
"\n"
" The `item` callback is the same shape as a `mochi/types.{build}`\n"
" callback (`fn(Dynamic) -> Result(t, String)`), so existing\n"
" per-type decoders can be passed directly:\n"
"\n"
" ```gleam\n"
" use nodes <- list_filtering(\"nodes\", decode_node)\n"
" use edges <- list_filtering(\"edges\", decode_edge)\n"
" ```\n"
"\n"
" Replaces the four-line `decode.optional_field` + `list.filter_map`\n"
" pattern with a single `use`-binding.\n"
).
-spec list_filtering(
binary(),
fun((gleam@dynamic:dynamic_()) -> {ok, NWJ} | {error, binary()}),
fun((list(NWJ)) -> gleam@dynamic@decode:decoder(NWN))
) -> gleam@dynamic@decode:decoder(NWN).
list_filtering(Name, Item, Next) ->
gleam@dynamic@decode:optional_field(
Name,
[],
gleam@dynamic@decode:list(
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
fun(Dyns) ->
Items = gleam@list:filter_map(Dyns, fun(D) -> case Item(D) of
{ok, V} ->
{ok, V};
{error, _} ->
{error, nil}
end end),
Next(Items)
end
).