Current section

Files

Jump to
oas_generator src oas@generator@schema.erl
Raw

src/oas@generator@schema.erl

-module(oas@generator@schema).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([custom_type/5, encode_fn/1, to_decoder/2, gen_top_decoder_needs_name/3, to_decode_fn/2, to_encoder/2, fields_to_encode_body/4, to_encode_fn/2, generate/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.
?MODULEDOC(
" This module builds ASTs from schemas.\n"
" It does not do any file generation\n"
).
-file("src/oas/generator/schema.gleam", 116).
?DOC(
" Generate type aliases from OpenAPI lifted schemas.\n"
" All objects are converted to names\n"
).
-spec to_type(
oas@generator@lift:schema(integer()),
oas@generator@misc:module_()
) -> glance:type().
to_type(Lifted, Module) ->
case Lifted of
{named, <<"#/components/schemas/"/utf8, Inner/binary>>} ->
Mod = case Module of
schema ->
none;
_ ->
{some, <<"schema"/utf8>>}
end,
{named_type, oas@generator@ast:name_for_gleam_type(Inner), Mod, []};
{named, Ref} ->
erlang:error(#{gleam_error => panic,
message => (<<"not referencing schema component ref: "/utf8,
Ref/binary>>),
module => <<"oas/generator/schema"/utf8>>,
function => <<"to_type"/utf8>>,
line => 126});
{primitive, Primitive} ->
case Primitive of
boolean ->
{named_type, <<"Bool"/utf8>>, none, []};
integer ->
{named_type, <<"Int"/utf8>>, none, []};
number ->
{named_type, <<"Float"/utf8>>, none, []};
string ->
{named_type, <<"String"/utf8>>, none, []};
null ->
{named_type, <<"Null"/utf8>>, none, []};
always ->
{named_type, <<"Json"/utf8>>, {some, <<"json"/utf8>>}, []};
never ->
{named_type, <<"Never"/utf8>>, none, []}
end;
{array, Items} ->
{named_type, <<"List"/utf8>>, none, [to_type(Items, Module)]};
{tuple, Items@1} ->
{tuple_type,
gleam@list:map(
Items@1,
fun(_capture) -> to_type(_capture, Module) end
)};
{compound, Index} ->
Type_ = <<"Internal"/utf8,
(erlang:integer_to_binary(Index))/binary>>,
{named_type, Type_, none, []};
{dictionary, Values} ->
{named_type,
<<"Dict"/utf8>>,
{some, <<"dict"/utf8>>},
[{named_type, <<"String"/utf8>>, none, []},
to_type(Values, Module)]};
unsupported ->
{named_type, <<"Dynamic"/utf8>>, {some, <<"dynamic"/utf8>>}, []}
end.
-file("src/oas/generator/schema.gleam", 77).
?DOC(
" Generate a custom type from an object type.\n"
" TODO make private\n"
).
-spec custom_type(
binary(),
list({binary(), {oas@generator@lift:schema(integer()), boolean()}}),
gleam@option:option(oas@generator@lift:schema(integer())),
list(binary()),
oas@generator@misc:module_()
) -> glance:custom_type().
custom_type(Name, Properties, Additional, Required, Module) ->
Fields = gleam@list:map(
Properties,
fun(Property) ->
{Key, {Schema, Nullable}} = Property,
Type_ = to_type(Schema, Module),
{labelled_variant_field,
case not gleam@list:contains(Required, Key) orelse Nullable of
false ->
Type_;
true ->
{named_type, <<"Option"/utf8>>, none, [Type_]}
end,
oas@generator@ast:name_for_gleam_field_or_var(Key)}
end
),
Fields@1 = case Additional of
{some, Schema@1} ->
Key@1 = <<"additionalProperties"/utf8>>,
Type_@1 = to_type(Schema@1, Module),
Extra = {labelled_variant_field,
{named_type,
<<"Dict"/utf8>>,
{some, <<"dict"/utf8>>},
[{named_type, <<"String"/utf8>>, none, []}, Type_@1]},
oas@generator@ast:name_for_gleam_field_or_var(Key@1)},
lists:append(Fields, [Extra]);
none ->
Fields
end,
Name@1 = oas@generator@ast:name_for_gleam_type(Name),
{custom_type, Name@1, public, false, [], [{variant, Name@1, Fields@1}]}.
-file("src/oas/generator/schema.gleam", 335).
-spec encode_fn(binary()) -> binary().
encode_fn(Name) ->
oas@generator@ast:name_for_gleam_field_or_var(
<<Name/binary, "_encode"/utf8>>
).
-file("src/oas/generator/schema.gleam", 510).
-spec decoder(binary()) -> binary().
decoder(Name) ->
oas@generator@ast:name_for_gleam_field_or_var(
<<Name/binary, "_decoder"/utf8>>
).
-file("src/oas/generator/schema.gleam", 515).
?DOC(" Not used anywhere else\n").
-spec alias(binary(), glance:type()) -> glance:type_alias().
alias(To, Type_) ->
{type_alias, To, public, [], Type_}.
-file("src/oas/generator/schema.gleam", 519).
-spec always_decode() -> glance:expression().
always_decode() ->
oas@generator@ast:call2(
<<"decode"/utf8>>,
<<"new_primitive_decoder"/utf8>>,
{string, <<"Nil"/utf8>>},
{fn,
[{fn_parameter, {discarded, <<""/utf8>>}, none}],
none,
[{expression,
{call,
{variable, <<"Ok"/utf8>>},
[{unlabelled_field, {variable, <<"Nil"/utf8>>}}]}}]}
).
-file("src/oas/generator/schema.gleam", 534).
-spec never_decode() -> glance:expression().
never_decode() ->
oas@generator@ast:call2(
<<"decode"/utf8>>,
<<"new_primitive_decoder"/utf8>>,
{string, <<"Never"/utf8>>},
{fn,
[{fn_parameter, {discarded, <<""/utf8>>}, none}],
none,
[{expression,
{call,
{variable, <<"Error"/utf8>>},
[{unlabelled_field, {variable, <<"Nil"/utf8>>}}]}}]}
).
-file("src/oas/generator/schema.gleam", 549).
-spec dynamic_decode() -> glance:expression().
dynamic_decode() ->
oas@generator@ast:call2(
<<"decode"/utf8>>,
<<"new_primitive_decoder"/utf8>>,
{string, <<"Dynamic"/utf8>>},
{fn,
[{fn_parameter, {named, <<"raw"/utf8>>}, none}],
none,
[{expression,
{call,
{variable, <<"Ok"/utf8>>},
[{unlabelled_field, {variable, <<"raw"/utf8>>}}]}}]}
).
-file("src/oas/generator/schema.gleam", 454).
-spec to_decoder(
oas@generator@lift:schema(integer()),
oas@generator@misc:module_()
) -> glance:expression().
to_decoder(Lifted, Module) ->
case Lifted of
{named, <<"#/components/schemas/"/utf8, Name/binary>>} ->
Func = oas@generator@ast:name_for_gleam_field_or_var(
<<Name/binary, "_decoder"/utf8>>
),
case Module of
schema ->
{call, {variable, Func}, []};
_ ->
oas@generator@ast:call0(<<"schema"/utf8>>, Func)
end;
{named, Ref} ->
erlang:error(#{gleam_error => panic,
message => (<<"not referencing schema component ref: "/utf8,
Ref/binary>>),
module => <<"oas/generator/schema"/utf8>>,
function => <<"to_decoder"/utf8>>,
line => 464});
{primitive, Primitive} ->
case Primitive of
boolean ->
oas@generator@ast:access(<<"decode"/utf8>>, <<"bool"/utf8>>);
integer ->
oas@generator@ast:access(<<"decode"/utf8>>, <<"int"/utf8>>);
number ->
oas@generator@ast:access(
<<"decode"/utf8>>,
<<"float"/utf8>>
);
string ->
oas@generator@ast:access(
<<"decode"/utf8>>,
<<"string"/utf8>>
);
null ->
always_decode();
always ->
oas@generator@ast:call0(
<<"utils"/utf8>>,
<<"dynamic_to_json"/utf8>>
);
never ->
never_decode()
end;
{tuple, Items} ->
Uses = gleam@list:index_map(
Items,
fun(Item, Index) ->
{use,
[{pattern_variable,
<<"e"/utf8,
(erlang:integer_to_binary(Index))/binary>>}],
oas@generator@ast:call1(
<<"decode"/utf8>>,
<<"then"/utf8>>,
to_decoder(Item, Module)
)}
end
),
Vars = gleam@list:index_map(
Items,
fun(_, Index@1) ->
{variable,
<<"e"/utf8, (erlang:integer_to_binary(Index@1))/binary>>}
end
),
{block,
lists:append(
Uses,
[{expression,
oas@generator@ast:call1(
<<"decode"/utf8>>,
<<"success"/utf8>>,
{tuple, Vars}
)}]
)};
{array, Items@1} ->
oas@generator@ast:call1(
<<"decode"/utf8>>,
<<"list"/utf8>>,
to_decoder(Items@1, Module)
);
{compound, Index@2} ->
Func@1 = <<<<"internal_"/utf8,
(erlang:integer_to_binary(Index@2))/binary>>/binary,
"_decoder"/utf8>>,
{call, {variable, Func@1}, []};
{dictionary, Values} ->
oas@generator@ast:call2(
<<"decode"/utf8>>,
<<"dict"/utf8>>,
oas@generator@ast:access(<<"decode"/utf8>>, <<"string"/utf8>>),
to_decoder(Values, Module)
);
unsupported ->
dynamic_decode()
end.
-file("src/oas/generator/schema.gleam", 355).
-spec gen_top_decoder_needs_name(
binary(),
oas@generator@lift:schema(oas@generator@lift:fields()),
oas@generator@misc:module_()
) -> list(glance:statement()).
gen_top_decoder_needs_name(Name, Top, Module) ->
case Top of
{named, N} ->
[{expression, to_decoder({named, N}, Module)}];
{primitive, P} ->
[{expression, to_decoder({primitive, P}, Module)}];
{array, Items} ->
[{expression, to_decoder({array, Items}, Module)}];
{tuple, Items@1} ->
case to_decoder({tuple, Items@1}, Module) of
{block, Statements} ->
Statements;
Other ->
[{expression, Other}]
end;
{compound, {fields, Properties, Additional, Required}} ->
Type_ = oas@generator@ast:name_for_gleam_type(Name),
Zipped = gleam@list:map(
Properties,
fun(Property) ->
{Key, {Schema, Nullable}} = Property,
Is_optional = not gleam@list:contains(Required, Key) orelse Nullable,
Field_decoder = to_decoder(Schema, Module),
{{use,
[{pattern_variable,
oas@generator@ast:name_for_gleam_field_or_var(
Key
)}],
case Is_optional of
false ->
oas@generator@ast:call2(
<<"decode"/utf8>>,
<<"field"/utf8>>,
{string, Key},
Field_decoder
);
true ->
{call,
oas@generator@ast:access(
<<"decode"/utf8>>,
<<"optional_field"/utf8>>
),
[{unlabelled_field, {string, Key}},
{unlabelled_field,
{variable, <<"None"/utf8>>}},
{unlabelled_field,
oas@generator@ast:call1(
<<"decode"/utf8>>,
<<"optional"/utf8>>,
Field_decoder
)}]}
end},
{labelled_field,
oas@generator@ast:name_for_gleam_field_or_var(Key),
{variable,
oas@generator@ast:name_for_gleam_field_or_var(
Key
)}}}
end
),
Zipped@1 = case Additional of
{some, Schema@1} ->
Key@1 = <<"additionalProperties"/utf8>>,
lists:append(
Zipped,
[{{use,
[{pattern_variable,
oas@generator@ast:name_for_gleam_field_or_var(
Key@1
)}],
oas@generator@ast:call2(
<<"utils"/utf8>>,
<<"decode_additional"/utf8>>,
{list,
gleam@list:map(
Properties,
fun(P@1) ->
{K, _} = P@1,
{string, K}
end
),
none},
to_decoder(Schema@1, Module)
)},
{labelled_field,
oas@generator@ast:name_for_gleam_field_or_var(
Key@1
),
{variable,
oas@generator@ast:name_for_gleam_field_or_var(
Key@1
)}}}]
);
none ->
Zipped
end,
{Fields, Cons} = gleam@list:unzip(Zipped@1),
Final = {expression,
oas@generator@ast:call1(
<<"decode"/utf8>>,
<<"success"/utf8>>,
case erlang:length(Cons) of
0 ->
{variable, Type_};
_ ->
{call, {variable, Type_}, Cons}
end
)},
lists:append(Fields, [Final]);
{dictionary, Values} ->
[{expression, to_decoder({dictionary, Values}, Module)}];
unsupported ->
[{expression, to_decoder(unsupported, Module)}]
end.
-file("src/oas/generator/schema.gleam", 340).
-spec to_decode_fn(
{binary(), oas@generator@lift:schema(oas@generator@lift:fields())},
oas@generator@misc:module_()
) -> glance:function_().
to_decode_fn(Entry, Module) ->
{Name, Top} = Entry,
Body = gen_top_decoder_needs_name(Name, Top, Module),
{function, decoder(Name), public, [], none, Body, {span, 0, 0}}.
-file("src/oas/generator/schema.gleam", 320).
-spec encode_tuple_body(
list(oas@generator@lift:schema(integer())),
glance:expression(),
oas@generator@misc:module_()
) -> glance:expression().
encode_tuple_body(Items, Arg, Module) ->
oas@generator@ast:call1(
<<"utils"/utf8>>,
<<"merge"/utf8>>,
{list,
gleam@list:index_map(
Items,
fun(Item, Index) ->
{call,
to_encoder(Item, Module),
[{unlabelled_field, {tuple_index, Arg, Index}}]}
end
),
none}
).
-file("src/oas/generator/schema.gleam", 259).
?DOC(" This handles a lifted encoder\n").
-spec to_encoder(
oas@generator@lift:schema(integer()),
oas@generator@misc:module_()
) -> glance:expression().
to_encoder(Lifted, Module) ->
case Lifted of
{named, <<"#/components/schemas/"/utf8, Inner/binary>>} ->
Func = encode_fn(Inner),
case Module of
schema ->
{variable, Func};
_ ->
oas@generator@ast:access(<<"schema"/utf8>>, Func)
end;
{named, _} ->
erlang:error(#{gleam_error => panic,
message => <<"unexpected ref"/utf8>>,
module => <<"oas/generator/schema"/utf8>>,
function => <<"to_encoder"/utf8>>,
line => 268});
{primitive, boolean} ->
oas@generator@ast:access(<<"json"/utf8>>, <<"bool"/utf8>>);
{primitive, integer} ->
oas@generator@ast:access(<<"json"/utf8>>, <<"int"/utf8>>);
{primitive, number} ->
oas@generator@ast:access(<<"json"/utf8>>, <<"float"/utf8>>);
{primitive, string} ->
oas@generator@ast:access(<<"json"/utf8>>, <<"string"/utf8>>);
{primitive, null} ->
{fn,
[{fn_parameter,
{discarded, <<""/utf8>>},
{some, {named_type, <<"Nil"/utf8>>, none, []}}}],
none,
[{expression,
oas@generator@ast:call0(
<<"json"/utf8>>,
<<"null"/utf8>>
)}]};
{primitive, always} ->
{fn,
[{fn_parameter, {named, <<"data"/utf8>>}, none}],
none,
[{expression, {variable, <<"data"/utf8>>}}]};
{primitive, never} ->
{fn,
[{fn_parameter, {discarded, <<"data"/utf8>>}, none}],
none,
[{expression,
{panic,
{some,
{string,
<<"never value cannot be encoded"/utf8>>}}}}]};
{array, Items} ->
{fn_capture,
none,
oas@generator@ast:access(<<"json"/utf8>>, <<"array"/utf8>>),
[],
[{unlabelled_field, to_encoder(Items, Module)}]};
{tuple, Items@1} ->
{fn,
[{fn_parameter, {named, <<"data"/utf8>>}, none}],
none,
[{expression,
encode_tuple_body(
Items@1,
{variable, <<"data"/utf8>>},
Module
)}]};
{compound, Index} ->
{variable,
encode_fn(
<<"internal_"/utf8,
(erlang:integer_to_binary(Index))/binary>>
)};
{dictionary, Values} ->
{fn_capture,
none,
oas@generator@ast:access(<<"utils"/utf8>>, <<"dict"/utf8>>),
[],
[{unlabelled_field, to_encoder(Values, Module)}]};
unsupported ->
{fn,
[{fn_parameter, {named, <<"data"/utf8>>}, none}],
none,
[{expression, {variable, <<"data"/utf8>>}}]}
end.
-file("src/oas/generator/schema.gleam", 211).
-spec fields_to_encode_body(
list({binary(), {oas@generator@lift:schema(integer()), boolean()}}),
list(binary()),
gleam@option:option(oas@generator@lift:schema(integer())),
oas@generator@misc:module_()
) -> glance:expression().
fields_to_encode_body(Properties, Required, Additional, Module) ->
oas@generator@ast:call1(
<<"utils"/utf8>>,
<<"object"/utf8>>,
{list,
gleam@list:map(
Properties,
fun(Property) ->
{Key, {Schema, Nullable}} = Property,
Arg = oas@generator@ast:access(
<<"data"/utf8>>,
oas@generator@ast:name_for_gleam_field_or_var(Key)
),
Cast = to_encoder(Schema, Module),
Value = case not gleam@list:contains(Required, Key) orelse Nullable of
false ->
{call, Cast, [{unlabelled_field, Arg}]};
true ->
oas@generator@ast:call2(
<<"json"/utf8>>,
<<"nullable"/utf8>>,
Arg,
Cast
)
end,
{tuple, [{string, Key}, Value]}
end
),
case Additional of
{some, Values} ->
{some,
oas@generator@ast:call1(
<<"dict"/utf8>>,
<<"to_list"/utf8>>,
oas@generator@ast:call2(
<<"dict"/utf8>>,
<<"map_values"/utf8>>,
oas@generator@ast:access(
<<"data"/utf8>>,
<<"additional_properties"/utf8>>
),
{fn,
[{fn_parameter,
{discarded, <<"key"/utf8>>},
none},
{fn_parameter,
{named, <<"value"/utf8>>},
none}],
none,
[{expression,
{call,
to_encoder(Values, Module),
[{unlabelled_field,
{variable,
<<"value"/utf8>>}}]}}]}
)
)};
none ->
none
end}
).
-file("src/oas/generator/schema.gleam", 156).
?DOC(
" This handles top to encoder\n"
" TODO make this private\n"
).
-spec to_encode_fn(
{binary(), oas@generator@lift:schema(oas@generator@lift:fields())},
oas@generator@misc:module_()
) -> glance:function_().
to_encode_fn(Entry, Module) ->
{Name, Top} = Entry,
Type_ = oas@generator@ast:name_for_gleam_type(Name),
Arg = {variable, <<"data"/utf8>>},
Exp = case Top of
{named, N} ->
_pipe = to_encoder({named, N}, Module),
{call, _pipe, [{unlabelled_field, Arg}]};
{primitive, null} ->
oas@generator@ast:call0(<<"json"/utf8>>, <<"null"/utf8>>);
{primitive, always} ->
Arg;
{primitive, never} ->
{panic, {some, {string, <<"never value cannot be encoded"/utf8>>}}};
{primitive, P} ->
_pipe@1 = to_encoder({primitive, P}, Module),
{call, _pipe@1, [{unlabelled_field, Arg}]};
{array, Items} ->
oas@generator@ast:call2(
<<"json"/utf8>>,
<<"array"/utf8>>,
Arg,
to_encoder(Items, Module)
);
{tuple, Items@1} ->
encode_tuple_body(Items@1, Arg, Module);
{compound, {fields, Properties, Additional, Required}} ->
fields_to_encode_body(Properties, Required, Additional, Module);
{dictionary, Values} ->
oas@generator@ast:call2(
<<"utils"/utf8>>,
<<"dict"/utf8>>,
Arg,
to_encoder(Values, Module)
);
unsupported ->
Arg
end,
Ignored = case Top of
{primitive, null} ->
true;
{primitive, never} ->
true;
{compound, {fields, Properties@1, none, _}} ->
gleam@list:is_empty(Properties@1);
_ ->
false
end,
Assignment = case Ignored of
true ->
{discarded, <<"data"/utf8>>};
false ->
{named, <<"data"/utf8>>}
end,
{function,
encode_fn(Name),
public,
[{function_parameter,
none,
Assignment,
{some, {named_type, Type_, none, []}}}],
none,
[{expression, Exp}],
{span, 0, 0}}.
-file("src/oas/generator/schema.gleam", 15).
?DOC(" From a dictionary of schemas generate all custom types, type aliases, encoders and decoders.\n").
-spec generate(gleam@dict:dict(binary(), oas:schema())) -> {list(glance:custom_type()),
list(glance:type_alias()),
list(glance:function_())}.
generate(Schemas) ->
Module = schema,
{Internal, Named} = gleam@list:map_fold(
begin
_pipe = Schemas,
maps:to_list(_pipe)
end,
[],
fun(Acc, Entry) ->
{Name, Schema} = Entry,
{Top, _, Acc@1} = oas@generator@lift:do_lift({inline, Schema}, Acc),
{Acc@1, {Name, Top}}
end
),
Named@1 = lists:append(
Named,
begin
_pipe@1 = Internal,
_pipe@2 = lists:reverse(_pipe@1),
gleam@list:index_map(
_pipe@2,
fun(Fields, Index) ->
{<<"internal_"/utf8,
(erlang:integer_to_binary(Index))/binary>>,
{compound, Fields}}
end
)
end
),
gleam@list:fold(
Named@1,
{[], [], []},
fun(Acc@2, Entry@1) ->
{Name@1, Top@1} = Entry@1,
{Custom_types, Type_aliases, Fns} = Acc@2,
Fns@1 = lists:append(
Fns,
[to_encode_fn({Name@1, Top@1}, Module),
to_decode_fn({Name@1, Top@1}, Module)]
),
Name@2 = oas@generator@ast:name_for_gleam_type(Name@1),
{Custom_types@1, Type_aliases@1} = case Top@1 of
{named, Name@3} ->
Type_ = to_type({named, Name@3}, Module),
{Custom_types, [alias(Name@3, Type_) | Type_aliases]};
{primitive, Primitive} ->
Type_@1 = to_type({primitive, Primitive}, Module),
{Custom_types, [alias(Name@2, Type_@1) | Type_aliases]};
{array, Items} ->
Type_@2 = to_type({array, Items}, Module),
{Custom_types, [alias(Name@2, Type_@2) | Type_aliases]};
{tuple, Items@1} ->
Type_@3 = to_type({tuple, Items@1}, Module),
{Custom_types, [alias(Name@2, Type_@3) | Type_aliases]};
{compound, {fields, Properties, Additional, Required}} ->
Type_@4 = custom_type(
Name@2,
Properties,
Additional,
Required,
Module
),
{[Type_@4 | Custom_types], Type_aliases};
{dictionary, Values} ->
Type_@5 = to_type({dictionary, Values}, Module),
{Custom_types, [alias(Name@2, Type_@5) | Type_aliases]};
unsupported ->
Type_@6 = to_type(unsupported, Module),
{Custom_types, [alias(Name@2, Type_@6) | Type_aliases]}
end,
{Custom_types@1, Type_aliases@1, Fns@1}
end
).