Current section
Files
Jump to
Current section
Files
src/glon.erl
-module(glon).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glon.gleam").
-export([describe/2, decode/2, string/0, integer/0, number/0, boolean/0, array/1, nullable/1, enum/1, enum_map/1, constant/1, constant_map/2, map/2, one_of/1, any_of/1, success/1, tagged_union/2, field/3, optional/3, optional_or_null/3, field_with_default/5, to_json/1, to_string/1]).
-export_type([schema_node/0, combiner_keyword/0, object_field/0, json_schema/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type schema_node() :: string_node |
integer_node |
number_node |
boolean_node |
{array_node, schema_node()} |
{nullable_node, schema_node()} |
{object_node, list(object_field())} |
{description_node, schema_node(), binary()} |
{enum_node, list(binary())} |
{const_node, binary()} |
{default_node, schema_node(), gleam@json:json()} |
{combiner_node, combiner_keyword(), list(schema_node())}.
-type combiner_keyword() :: one_of | any_of.
-type object_field() :: {object_field, binary(), schema_node(), boolean()}.
-opaque json_schema(DTC) :: {json_schema,
schema_node(),
gleam@dynamic@decode:decoder(DTC)}.
-file("src/glon.gleam", 306).
?DOC(
" Attach a `\"description\"` annotation to any schema.\n"
"\n"
" The description appears in the JSON Schema output but does not affect\n"
" decoding. Can be composed with other annotations and combinators.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema =\n"
" glon.string()\n"
" |> glon.describe(\"A person's full name\")\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\",\\\"description\\\":\\\"A person's full name\\\"}\"\n"
" ```\n"
).
-spec describe(json_schema(DUB), binary()) -> json_schema(DUB).
describe(Schema, Description) ->
{json_schema,
{description_node, erlang:element(2, Schema), Description},
erlang:element(3, Schema)}.
-file("src/glon.gleam", 711).
?DOC(
" Decode a JSON string using the schema's decoder.\n"
"\n"
" Parses the given JSON string and decodes it according to the schema.\n"
" Returns `Ok(value)` on success or `Error(DecodeError)` on failure.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.integer()\n"
" glon.decode(schema, from: \"42\")\n"
" // -> Ok(42)\n"
"\n"
" glon.decode(schema, from: \"\\\"not a number\\\"\")\n"
" // -> Error(...)\n"
" ```\n"
).
-spec decode(json_schema(DVW), binary()) -> {ok, DVW} |
{error, gleam@json:decode_error()}.
decode(Schema, Json_string) ->
gleam@json:parse(Json_string, erlang:element(3, Schema)).
-file("src/glon.gleam", 70).
?DOC(
" A schema for JSON strings.\n"
"\n"
" Produces `{\"type\": \"string\"}` and decodes JSON strings into `String`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.string()\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"hello\\\"\")\n"
" // -> Ok(\"hello\")\n"
" ```\n"
).
-spec string() -> json_schema(binary()).
string() ->
{json_schema,
string_node,
{decoder, fun gleam@dynamic@decode:decode_string/1}}.
-file("src/glon.gleam", 88).
?DOC(
" A schema for JSON integers.\n"
"\n"
" Produces `{\"type\": \"integer\"}` and decodes JSON integers into `Int`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.integer()\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"integer\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"42\")\n"
" // -> Ok(42)\n"
" ```\n"
).
-spec integer() -> json_schema(integer()).
integer() ->
{json_schema,
integer_node,
{decoder, fun gleam@dynamic@decode:decode_int/1}}.
-file("src/glon.gleam", 106).
?DOC(
" A schema for JSON numbers (floating point).\n"
"\n"
" Produces `{\"type\": \"number\"}` and decodes JSON numbers into `Float`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.number()\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"number\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"3.14\")\n"
" // -> Ok(3.14)\n"
" ```\n"
).
-spec number() -> json_schema(float()).
number() ->
{json_schema,
number_node,
{decoder, fun gleam@dynamic@decode:decode_float/1}}.
-file("src/glon.gleam", 124).
?DOC(
" A schema for JSON booleans.\n"
"\n"
" Produces `{\"type\": \"boolean\"}` and decodes JSON booleans into `Bool`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.boolean()\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"boolean\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"true\")\n"
" // -> Ok(True)\n"
" ```\n"
).
-spec boolean() -> json_schema(boolean()).
boolean() ->
{json_schema,
boolean_node,
{decoder, fun gleam@dynamic@decode:decode_bool/1}}.
-file("src/glon.gleam", 145).
?DOC(
" A schema for JSON arrays where every element matches the given inner schema.\n"
"\n"
" Produces `{\"type\": \"array\", \"items\": <inner>}` and decodes JSON arrays\n"
" into `List(t)`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.array(of: glon.string())\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"array\\\",\\\"items\\\":{\\\"type\\\":\\\"string\\\"}}\"\n"
"\n"
" glon.decode(schema, from: \"[\\\"a\\\",\\\"b\\\"]\")\n"
" // -> Ok([\"a\", \"b\"])\n"
" ```\n"
).
-spec array(json_schema(DTI)) -> json_schema(list(DTI)).
array(Inner) ->
{json_schema,
{array_node, erlang:element(2, Inner)},
gleam@dynamic@decode:list(erlang:element(3, Inner))}.
-file("src/glon.gleam", 171).
?DOC(
" A schema for values that may be `null`.\n"
"\n"
" Wraps an inner schema to also accept JSON `null`. Produces a schema with\n"
" `\"type\": [\"<inner_type>\", \"null\"]` for simple inner types, or an `anyOf`\n"
" for complex ones. Decodes into `Option(t)`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.nullable(glon.string())\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":[\\\"string\\\",\\\"null\\\"]}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"hello\\\"\")\n"
" // -> Ok(Some(\"hello\"))\n"
"\n"
" glon.decode(schema, from: \"null\")\n"
" // -> Ok(None)\n"
" ```\n"
).
-spec nullable(json_schema(DTM)) -> json_schema(gleam@option:option(DTM)).
nullable(Inner) ->
{json_schema,
{nullable_node, erlang:element(2, Inner)},
gleam@dynamic@decode:optional(erlang:element(3, Inner))}.
-file("src/glon.gleam", 272).
-spec enum_decoder(list(binary()), fun((binary()) -> DTZ)) -> gleam@dynamic@decode:decoder(DTZ).
enum_decoder(Values, Mapper) ->
{First@1, Rest@1} = case gleam@list:map(
Values,
fun(V) -> _pipe = {decoder, fun gleam@dynamic@decode:decode_string/1},
gleam@dynamic@decode:then(_pipe, fun(S) -> case S =:= V of
true ->
gleam@dynamic@decode:success(Mapper(V));
false ->
gleam@dynamic@decode:failure(
Mapper(V),
<<"one of: "/utf8,
(gleam@string:join(Values, <<", "/utf8>>))/binary>>
)
end end) end
) of
[First | Rest] -> {First, Rest};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"glon"/utf8>>,
function => <<"enum_decoder"/utf8>>,
line => 276,
value => _assert_fail,
start => 7282,
'end' => 7578,
pattern_start => 7293,
pattern_end => 7308})
end,
gleam@dynamic@decode:one_of(First@1, Rest@1).
-file("src/glon.gleam", 198).
?DOC(
" A schema that restricts values to a fixed set of strings.\n"
"\n"
" Produces `{\"type\": \"string\", \"enum\": [...]}` and decodes only strings\n"
" that appear in the given list. Decoding rejects any value not in the list.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.enum([\"red\", \"green\", \"blue\"])\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"red\\\",\\\"green\\\",\\\"blue\\\"]}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"red\\\"\")\n"
" // -> Ok(\"red\")\n"
"\n"
" glon.decode(schema, from: \"\\\"yellow\\\"\")\n"
" // -> Error(...)\n"
" ```\n"
).
-spec enum(list(binary())) -> json_schema(binary()).
enum(Values) ->
Decoder = enum_decoder(Values, fun(S) -> S end),
{json_schema, {enum_node, Values}, Decoder}.
-file("src/glon.gleam", 220).
?DOC(
" Like `enum`, but maps each string value to an arbitrary Gleam type.\n"
"\n"
" Takes a list of `#(json_string, gleam_value)` pairs. The schema output\n"
" uses the JSON strings, while the decoder maps each to its paired value.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.enum_map([\n"
" #(\"red\", Red), #(\"green\", Green), #(\"blue\", Blue),\n"
" ])\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"red\\\",\\\"green\\\",\\\"blue\\\"]}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"red\\\"\")\n"
" // -> Ok(Red)\n"
" ```\n"
).
-spec enum_map(list({binary(), DTS})) -> json_schema(DTS).
enum_map(Variants) ->
Decoder = enum_decoder(
gleam@list:map(Variants, fun(V) -> erlang:element(1, V) end),
fun(S) ->
Mapped@1 = case gleam@list:find(
Variants,
fun(V@1) -> erlang:element(1, V@1) =:= S end
) of
{ok, {_, Mapped}} -> Mapped;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"glon"/utf8>>,
function => <<"enum_map"/utf8>>,
line => 223,
value => _assert_fail,
start => 5870,
'end' => 5939,
pattern_start => 5881,
pattern_end => 5897})
end,
Mapped@1
end
),
{json_schema,
{enum_node,
gleam@list:map(Variants, fun(V@2) -> erlang:element(1, V@2) end)},
Decoder}.
-file("src/glon.gleam", 250).
?DOC(
" A schema that accepts only a single specific string value.\n"
"\n"
" Produces `{\"type\": \"string\", \"const\": \"<value>\"}` and decodes only\n"
" that exact string.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.constant(\"active\")\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\",\\\"const\\\":\\\"active\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"active\\\"\")\n"
" // -> Ok(\"active\")\n"
"\n"
" glon.decode(schema, from: \"\\\"inactive\\\"\")\n"
" // -> Error(...)\n"
" ```\n"
).
-spec constant(binary()) -> json_schema(binary()).
constant(Value) ->
Decoder = enum_decoder([Value], fun(S) -> S end),
{json_schema, {const_node, Value}, Decoder}.
-file("src/glon.gleam", 267).
?DOC(
" Like `constant`, but maps the matched string to an arbitrary Gleam value.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.constant_map(\"yes\", mapped: True)\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\",\\\"const\\\":\\\"yes\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"yes\\\"\")\n"
" // -> Ok(True)\n"
" ```\n"
).
-spec constant_map(binary(), DTW) -> json_schema(DTW).
constant_map(Value, Mapped) ->
Decoder = enum_decoder([Value], fun(_) -> Mapped end),
{json_schema, {const_node, Value}, Decoder}.
-file("src/glon.gleam", 336).
?DOC(
" Transform the decoded type of a schema without changing its JSON Schema output.\n"
"\n"
" The schema definition stays the same, but the decoder maps decoded values\n"
" through the given function. Useful for wrapping primitives in custom types\n"
" or making different schemas produce the same type for use with `one_of`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" type Email { Email(String) }\n"
"\n"
" let schema = glon.string() |> glon.map(Email)\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"type\\\":\\\"string\\\"}\"\n"
"\n"
" glon.decode(schema, from: \"\\\"a@b.com\\\"\")\n"
" // -> Ok(Email(\"a@b.com\"))\n"
" ```\n"
).
-spec map(json_schema(DUE), fun((DUE) -> DUG)) -> json_schema(DUG).
map(Schema, Transform) ->
{json_schema,
erlang:element(2, Schema),
gleam@dynamic@decode:then(
erlang:element(3, Schema),
fun(A) -> gleam@dynamic@decode:success(Transform(A)) end
)}.
-file("src/glon.gleam", 361).
?DOC(
" A schema where the value must match exactly one of the given sub-schemas.\n"
"\n"
" Produces `{\"oneOf\": [...]}`. The decoder tries each variant in order and\n"
" returns the first successful match. All variants must decode to the same\n"
" Gleam type `t` — use `map` to align types if needed.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" type Value { TextVal(String) NumVal(Int) }\n"
"\n"
" let schema = glon.one_of([\n"
" glon.string() |> glon.map(TextVal),\n"
" glon.integer() |> glon.map(NumVal),\n"
" ])\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"oneOf\\\":[{\\\"type\\\":\\\"string\\\"},{\\\"type\\\":\\\"integer\\\"}]}\"\n"
" ```\n"
).
-spec one_of(list(json_schema(DUI))) -> json_schema(DUI).
one_of(Variants) ->
{First@1, Rest@1} = case Variants of
[First | Rest] -> {First, Rest};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"glon"/utf8>>,
function => <<"one_of"/utf8>>,
line => 362,
value => _assert_fail,
start => 9780,
'end' => 9817,
pattern_start => 9791,
pattern_end => 9806})
end,
{json_schema,
{combiner_node,
one_of,
gleam@list:map(Variants, fun(V) -> erlang:element(2, V) end)},
gleam@dynamic@decode:one_of(
erlang:element(3, First@1),
gleam@list:map(Rest@1, fun(V@1) -> erlang:element(3, V@1) end)
)}.
-file("src/glon.gleam", 389).
?DOC(
" A schema where the value must match at least one of the given sub-schemas.\n"
"\n"
" Produces `{\"anyOf\": [...]}`. Behaves identically to `one_of` for decoding\n"
" (first match wins), but generates `anyOf` instead of `oneOf` in the schema.\n"
" The distinction matters for JSON Schema validation: `oneOf` requires exactly\n"
" one match, `anyOf` allows multiple.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = glon.any_of([\n"
" glon.string() |> glon.map(TextVal),\n"
" glon.integer() |> glon.map(NumVal),\n"
" ])\n"
" glon.to_string(schema)\n"
" // -> \"{\\\"anyOf\\\":[{\\\"type\\\":\\\"string\\\"},{\\\"type\\\":\\\"integer\\\"}]}\"\n"
" ```\n"
).
-spec any_of(list(json_schema(DUM))) -> json_schema(DUM).
any_of(Variants) ->
{First@1, Rest@1} = case Variants of
[First | Rest] -> {First, Rest};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"glon"/utf8>>,
function => <<"any_of"/utf8>>,
line => 390,
value => _assert_fail,
start => 10719,
'end' => 10756,
pattern_start => 10730,
pattern_end => 10745})
end,
{json_schema,
{combiner_node,
any_of,
gleam@list:map(Variants, fun(V) -> erlang:element(2, V) end)},
gleam@dynamic@decode:one_of(
erlang:element(3, First@1),
gleam@list:map(Rest@1, fun(V@1) -> erlang:element(3, V@1) end)
)}.
-file("src/glon.gleam", 471).
?DOC(
" Finish building an object schema by providing the final value.\n"
"\n"
" This is used as the last step in a chain of `field`, `optional`, or\n"
" `field_with_default` calls via `use` syntax. It produces an empty object\n"
" node that gets merged with the fields collected from the chain.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = {\n"
" use name <- glon.field(\"name\", glon.string())\n"
" use age <- glon.field(\"age\", glon.integer())\n"
" glon.success(User(name:, age:))\n"
" }\n"
" ```\n"
).
-spec success(DUU) -> json_schema(DUU).
success(Value) ->
{json_schema, {object_node, []}, gleam@dynamic@decode:success(Value)}.
-file("src/glon.gleam", 720).
-spec get_object_fields(schema_node()) -> list(object_field()).
get_object_fields(Node) ->
case Node of
{object_node, Fields} ->
Fields;
_ ->
[]
end.
-file("src/glon.gleam", 422).
?DOC(
" A schema for discriminated unions (tagged unions).\n"
"\n"
" Produces a `oneOf` schema where each variant is an object with a\n"
" discriminator field set to a constant tag value. The decoder checks\n"
" the discriminator field to select the correct variant.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" type Shape { Circle(Float) Square(Float) }\n"
"\n"
" let schema = glon.tagged_union(\"type\", [\n"
" #(\"circle\", {\n"
" use radius <- glon.field(\"radius\", glon.number())\n"
" glon.success(Circle(radius))\n"
" }),\n"
" #(\"square\", {\n"
" use side <- glon.field(\"side\", glon.number())\n"
" glon.success(Square(side))\n"
" }),\n"
" ])\n"
" ```\n"
).
-spec tagged_union(binary(), list({binary(), json_schema(DUQ)})) -> json_schema(DUQ).
tagged_union(Discriminator, Variants) ->
Schema_variants = gleam@list:map(
Variants,
fun(Variant) ->
{Tag, Schema} = Variant,
Fields = get_object_fields(erlang:element(2, Schema)),
{object_node,
[{object_field, Discriminator, {const_node, Tag}, true} |
Fields]}
end
),
{First_decoder@1, Rest_decoders@1} = case gleam@list:map(
Variants,
fun(Variant@1) ->
{Tag@1, Schema@1} = Variant@1,
gleam@dynamic@decode:field(
Discriminator,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Decoded_tag) -> case Decoded_tag =:= Tag@1 of
true ->
erlang:element(3, Schema@1);
false ->
gleam@dynamic@decode:failure(
glon_ffi:coerce_nil(),
Tag@1
)
end end
)
end
) of
[First_decoder | Rest_decoders] -> {First_decoder, Rest_decoders};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"glon"/utf8>>,
function => <<"tagged_union"/utf8>>,
line => 438,
value => _assert_fail,
start => 12189,
'end' => 12503,
pattern_start => 12200,
pattern_end => 12232})
end,
{json_schema,
{combiner_node, one_of, Schema_variants},
gleam@dynamic@decode:one_of(First_decoder@1, Rest_decoders@1)}.
-file("src/glon.gleam", 493).
?DOC(
" Declare a required object field.\n"
"\n"
" The field appears in the schema's `\"properties\"` and `\"required\"` array.\n"
" Decoding fails if the field is missing from the JSON input.\n"
"\n"
" Used with Gleam's `use` syntax to chain multiple fields together.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = {\n"
" use name <- glon.field(\"name\", glon.string())\n"
" use age <- glon.field(\"age\", glon.integer())\n"
" glon.success(User(name:, age:))\n"
" }\n"
" glon.decode(schema, from: \"{\\\"name\\\":\\\"Alice\\\",\\\"age\\\":30}\")\n"
" // -> Ok(User(name: \"Alice\", age: 30))\n"
" ```\n"
).
-spec field(binary(), json_schema(DUW), fun((DUW) -> json_schema(DUY))) -> json_schema(DUY).
field(Name, Schema, Next) ->
Rest = Next(glon_ffi:coerce_nil()),
Rest_fields = get_object_fields(erlang:element(2, Rest)),
Node = {object_node,
[{object_field, Name, erlang:element(2, Schema), true} | Rest_fields]},
Decoder = begin
gleam@dynamic@decode:field(
Name,
erlang:element(3, Schema),
fun(Value) ->
Result = Next(Value),
erlang:element(3, Result)
end
)
end,
{json_schema, Node, Decoder}.
-file("src/glon.gleam", 534).
?DOC(
" Declare an optional object field.\n"
"\n"
" The field appears in the schema's `\"properties\"` but not in `\"required\"`.\n"
" The continuation receives `Option(a)`: `Some(value)` when the field is\n"
" present, `None` when absent.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = {\n"
" use name <- glon.field(\"name\", glon.string())\n"
" use email <- glon.optional(\"email\", glon.string())\n"
" glon.success(User(name:, email:))\n"
" }\n"
" glon.decode(schema, from: \"{\\\"name\\\":\\\"Alice\\\"}\")\n"
" // -> Ok(User(name: \"Alice\", email: None))\n"
" ```\n"
).
-spec optional(
binary(),
json_schema(DVB),
fun((gleam@option:option(DVB)) -> json_schema(DVE))
) -> json_schema(DVE).
optional(Name, Schema, Next) ->
Rest = Next(glon_ffi:coerce_nil()),
Rest_fields = get_object_fields(erlang:element(2, Rest)),
Node = {object_node,
[{object_field, Name, erlang:element(2, Schema), false} | Rest_fields]},
Decoder = begin
gleam@dynamic@decode:optional_field(
Name,
none,
gleam@dynamic@decode:optional(erlang:element(3, Schema)),
fun(Value) ->
Result = Next(Value),
erlang:element(3, Result)
end
)
end,
{json_schema, Node, Decoder}.
-file("src/glon.gleam", 577).
?DOC(
" Declare an optional object field that may also be `null`.\n"
"\n"
" Like `optional`, but the schema type is wrapped with `nullable` and the\n"
" decoder treats both an absent field and a `null` value as `None`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = {\n"
" use name <- glon.field(\"name\", glon.string())\n"
" use nick <- glon.optional_or_null(\"nickname\", glon.string())\n"
" glon.success(User(name:, nickname: nick))\n"
" }\n"
" // Field absent -> None, field null -> None, field present -> Some(value)\n"
" ```\n"
).
-spec optional_or_null(
binary(),
json_schema(DVH),
fun((gleam@option:option(DVH)) -> json_schema(DVK))
) -> json_schema(DVK).
optional_or_null(Name, Schema, Next) ->
Rest = Next(glon_ffi:coerce_nil()),
Rest_fields = get_object_fields(erlang:element(2, Rest)),
Node = {object_node,
[{object_field, Name, {nullable_node, erlang:element(2, Schema)}, false} |
Rest_fields]},
Decoder = begin
gleam@dynamic@decode:optional_field(
Name,
none,
gleam@dynamic@decode:optional(erlang:element(3, Schema)),
fun(Value) ->
Result = Next(Value),
erlang:element(3, Result)
end
)
end,
{json_schema, Node, Decoder}.
-file("src/glon.gleam", 640).
?DOC(
" Declare an optional object field with a default value.\n"
"\n"
" The field appears in the schema's `\"properties\"` with a `\"default\"`\n"
" annotation, but not in `\"required\"`. When the field is absent from the\n"
" JSON input, the decoder uses the provided default value. Unlike `optional`,\n"
" the continuation receives the unwrapped type `a` directly, not `Option(a)`.\n"
"\n"
" The `encode` parameter converts the default value to `json.Json` for the\n"
" schema output. For primitives, use `json.int`, `json.string`, `json.float`,\n"
" or `json.bool`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let schema = {\n"
" use host <- glon.field(\"host\", glon.string())\n"
" use port <- glon.field_with_default(\n"
" \"port\", glon.integer(),\n"
" default: 8080, encode: json.int,\n"
" )\n"
" glon.success(Config(host:, port:))\n"
" }\n"
" glon.to_string(schema)\n"
" // -> \"{...\\\"port\\\":{\\\"type\\\":\\\"integer\\\",\\\"default\\\":8080}...}\"\n"
"\n"
" glon.decode(schema, from: \"{\\\"host\\\":\\\"localhost\\\"}\")\n"
" // -> Ok(Config(host: \"localhost\", port: 8080))\n"
"\n"
" glon.decode(schema, from: \"{\\\"host\\\":\\\"localhost\\\",\\\"port\\\":3000}\")\n"
" // -> Ok(Config(host: \"localhost\", port: 3000))\n"
" ```\n"
).
-spec field_with_default(
binary(),
json_schema(DVN),
DVN,
fun((DVN) -> gleam@json:json()),
fun((DVN) -> json_schema(DVP))
) -> json_schema(DVP).
field_with_default(Name, Schema, Default_value, Encode, Next) ->
Rest = Next(glon_ffi:coerce_nil()),
Rest_fields = get_object_fields(erlang:element(2, Rest)),
Node = {object_node,
[{object_field,
Name,
{default_node, erlang:element(2, Schema), Encode(Default_value)},
false} |
Rest_fields]},
Decoder = begin
gleam@dynamic@decode:optional_field(
Name,
Default_value,
erlang:element(3, Schema),
fun(Value) ->
Result = Next(Value),
erlang:element(3, Result)
end
)
end,
{json_schema, Node, Decoder}.
-file("src/glon.gleam", 825).
-spec get_type_name(schema_node()) -> {ok, binary()} | {error, nil}.
get_type_name(Node) ->
case Node of
string_node ->
{ok, <<"string"/utf8>>};
integer_node ->
{ok, <<"integer"/utf8>>};
number_node ->
{ok, <<"number"/utf8>>};
boolean_node ->
{ok, <<"boolean"/utf8>>};
{array_node, _} ->
{ok, <<"array"/utf8>>};
{object_node, _} ->
{ok, <<"object"/utf8>>};
{enum_node, _} ->
{ok, <<"string"/utf8>>};
{const_node, _} ->
{ok, <<"string"/utf8>>};
{description_node, Inner, _} ->
get_type_name(Inner);
{default_node, Inner@1, _} ->
get_type_name(Inner@1);
{nullable_node, _} ->
{error, nil};
{combiner_node, _, _} ->
{error, nil}
end.
-file("src/glon.gleam", 727).
-spec node_to_pairs(schema_node()) -> list({binary(), gleam@json:json()}).
node_to_pairs(Node) ->
case Node of
string_node ->
[{<<"type"/utf8>>, gleam@json:string(<<"string"/utf8>>)}];
integer_node ->
[{<<"type"/utf8>>, gleam@json:string(<<"integer"/utf8>>)}];
number_node ->
[{<<"type"/utf8>>, gleam@json:string(<<"number"/utf8>>)}];
boolean_node ->
[{<<"type"/utf8>>, gleam@json:string(<<"boolean"/utf8>>)}];
{array_node, Items} ->
[{<<"type"/utf8>>, gleam@json:string(<<"array"/utf8>>)},
{<<"items"/utf8>>, node_to_json(Items)}];
{nullable_node, Inner} ->
case get_type_name(Inner) of
{ok, Type_name} ->
Null_type = gleam@json:preprocessed_array(
[gleam@json:string(Type_name),
gleam@json:string(<<"null"/utf8>>)]
),
Inner_pairs = node_to_pairs(Inner),
_pipe = Inner_pairs,
_pipe@1 = gleam@list:filter(
_pipe,
fun(Pair) ->
erlang:element(1, Pair) /= <<"type"/utf8>>
end
),
gleam@list:prepend(_pipe@1, {<<"type"/utf8>>, Null_type});
{error, nil} ->
[{<<"anyOf"/utf8>>,
gleam@json:preprocessed_array(
[node_to_json(Inner),
gleam@json:object(
[{<<"type"/utf8>>,
gleam@json:string(
<<"null"/utf8>>
)}]
)]
)}]
end;
{object_node, Fields} ->
Properties = gleam@list:map(
Fields,
fun(F) ->
{erlang:element(2, F), node_to_json(erlang:element(3, F))}
end
),
Required = begin
_pipe@2 = Fields,
_pipe@3 = gleam@list:filter(
_pipe@2,
fun(F@1) -> erlang:element(4, F@1) end
),
gleam@list:map(
_pipe@3,
fun(F@2) -> gleam@json:string(erlang:element(2, F@2)) end
)
end,
case Required of
[] ->
[{<<"type"/utf8>>, gleam@json:string(<<"object"/utf8>>)},
{<<"properties"/utf8>>, gleam@json:object(Properties)}];
_ ->
[{<<"type"/utf8>>, gleam@json:string(<<"object"/utf8>>)},
{<<"properties"/utf8>>, gleam@json:object(Properties)},
{<<"required"/utf8>>,
gleam@json:preprocessed_array(Required)}]
end;
{enum_node, Values} ->
[{<<"type"/utf8>>, gleam@json:string(<<"string"/utf8>>)},
{<<"enum"/utf8>>,
gleam@json:preprocessed_array(
gleam@list:map(Values, fun gleam@json:string/1)
)}];
{const_node, Value} ->
[{<<"type"/utf8>>, gleam@json:string(<<"string"/utf8>>)},
{<<"const"/utf8>>, gleam@json:string(Value)}];
{description_node, Inner@1, Description} ->
lists:append(
node_to_pairs(Inner@1),
[{<<"description"/utf8>>, gleam@json:string(Description)}]
);
{default_node, Inner@2, Default} ->
lists:append(
node_to_pairs(Inner@2),
[{<<"default"/utf8>>, Default}]
);
{combiner_node, Keyword, Variants} ->
Keyword_str = case Keyword of
one_of ->
<<"oneOf"/utf8>>;
any_of ->
<<"anyOf"/utf8>>
end,
[{Keyword_str,
gleam@json:preprocessed_array(
gleam@list:map(Variants, fun node_to_json/1)
)}]
end.
-file("src/glon.gleam", 819).
-spec node_to_json(schema_node()) -> gleam@json:json().
node_to_json(Node) ->
_pipe = Node,
_pipe@1 = node_to_pairs(_pipe),
gleam@json:object(_pipe@1).
-file("src/glon.gleam", 676).
?DOC(
" Convert a schema to a `json.Json` value.\n"
"\n"
" Returns the JSON Schema representation as a `json.Json` value that can\n"
" be further processed or serialized.\n"
).
-spec to_json(json_schema(any())) -> gleam@json:json().
to_json(Schema) ->
node_to_json(erlang:element(2, Schema)).
-file("src/glon.gleam", 690).
?DOC(
" Convert a schema to a JSON string.\n"
"\n"
" Returns the JSON Schema as a serialized JSON string.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" glon.string() |> glon.to_string\n"
" // -> \"{\\\"type\\\":\\\"string\\\"}\"\n"
" ```\n"
).
-spec to_string(json_schema(any())) -> binary().
to_string(Schema) ->
_pipe = Schema,
_pipe@1 = to_json(_pipe),
gleam@json:to_string(_pipe@1).