Current section

Files

Jump to
oaspec src oaspec@openapi@hoist.erl
Raw

src/oaspec@openapi@hoist.erl

-module(oaspec@openapi@hoist).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/oaspec/openapi/hoist.gleam").
-export([hoist/1]).
-export_type([hoist_state/0]).
-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 hoist_state() :: {hoist_state,
gleam@dict:dict(binary(), oaspec@openapi@schema:schema_ref()),
gleam@dict:dict(binary(), nil),
gleam@dict:dict(binary(), nil)}.
-file("src/oaspec/openapi/hoist.gleam", 80).
?DOC(" Determine if a SchemaObject is complex and should be hoisted.\n").
-spec needs_hoisting(oaspec@openapi@schema:schema_object()) -> boolean().
needs_hoisting(Schema_obj) ->
case Schema_obj of
{object_schema, _, _, _, _, _, _} ->
true;
{all_of_schema, _, _} ->
true;
{one_of_schema, _, _, _} ->
true;
{any_of_schema, _, _} ->
true;
{array_schema, _, {inline, Inner}, _, _, _} ->
needs_hoisting(Inner);
_ ->
false
end.
-file("src/oaspec/openapi/hoist.gleam", 119).
?DOC(" Try incrementing suffixes until a unique name is found.\n").
-spec find_unique_name(binary(), integer(), hoist_state()) -> {binary(),
hoist_state()}.
find_unique_name(Base_name, Suffix, State) ->
Candidate = <<Base_name/binary, (erlang:integer_to_binary(Suffix))/binary>>,
Type_name = oaspec@util@naming:schema_to_type_name(Candidate),
case gleam@dict:has_key(erlang:element(3, State), Candidate) orelse gleam@dict:has_key(
erlang:element(4, State),
Type_name
) of
false ->
State@1 = {hoist_state,
erlang:element(2, State),
gleam@dict:insert(erlang:element(3, State), Candidate, nil),
gleam@dict:insert(erlang:element(4, State), Type_name, nil)},
{Candidate, State@1};
true ->
find_unique_name(Base_name, Suffix + 1, State)
end.
-file("src/oaspec/openapi/hoist.gleam", 92).
?DOC(" Generate a unique schema name, handling collisions by appending 2, 3, etc.\n").
-spec make_unique_name(binary(), hoist_state()) -> {binary(), hoist_state()}.
make_unique_name(Base_name, State) ->
Type_name = oaspec@util@naming:schema_to_type_name(Base_name),
case gleam@dict:has_key(erlang:element(3, State), Base_name) orelse gleam@dict:has_key(
erlang:element(4, State),
Type_name
) of
false ->
State@1 = {hoist_state,
erlang:element(2, State),
gleam@dict:insert(erlang:element(3, State), Base_name, nil),
gleam@dict:insert(erlang:element(4, State), Type_name, nil)},
{Base_name, State@1};
true ->
find_unique_name(Base_name, 2, State)
end.
-file("src/oaspec/openapi/hoist.gleam", 149).
?DOC(
" Hoist a single SchemaRef if it is inline and complex.\n"
" Returns the (possibly replaced) SchemaRef and updated state.\n"
).
-spec hoist_schema_ref(
oaspec@openapi@schema:schema_ref(),
binary(),
binary(),
hoist_state()
) -> {oaspec@openapi@schema:schema_ref(), hoist_state()}.
hoist_schema_ref(Schema_ref, Name_prefix, Name_suffix, State) ->
case Schema_ref of
{reference, _} ->
{Schema_ref, State};
{inline, Schema_obj} ->
case needs_hoisting(Schema_obj) of
false ->
{Schema_ref, State};
true ->
Base_name = <<(oaspec@util@naming:to_pascal_case(
Name_prefix
))/binary,
(oaspec@util@naming:to_pascal_case(Name_suffix))/binary>>,
{Hoisted_obj, State@1} = hoist_within_schema(
Schema_obj,
Base_name,
State
),
{Unique_name, State@2} = make_unique_name(
Base_name,
State@1
),
State@3 = {hoist_state,
gleam@dict:insert(
erlang:element(2, State@2),
Unique_name,
{inline, Hoisted_obj}
),
erlang:element(3, State@2),
erlang:element(4, State@2)},
Ref = <<"#/components/schemas/"/utf8, Unique_name/binary>>,
{{reference, Ref}, State@3}
end
end.
-file("src/oaspec/openapi/hoist.gleam", 189).
?DOC(" Recursively hoist within a SchemaObject's children.\n").
-spec hoist_within_schema(
oaspec@openapi@schema:schema_object(),
binary(),
hoist_state()
) -> {oaspec@openapi@schema:schema_object(), hoist_state()}.
hoist_within_schema(Schema_obj, Name_prefix, State) ->
case Schema_obj of
{object_schema,
Description,
Properties,
Required,
Additional_properties,
Additional_properties_untyped,
Nullable} ->
{New_props, State@3} = begin
_pipe = maps:to_list(Properties),
gleam@list:fold(
_pipe,
{maps:new(), State},
fun(Acc, Entry) ->
{Props_acc, State@1} = Acc,
{Prop_name, Prop_ref} = Entry,
{Hoisted_ref, State@2} = hoist_schema_ref(
Prop_ref,
Name_prefix,
Prop_name,
State@1
),
{gleam@dict:insert(Props_acc, Prop_name, Hoisted_ref),
State@2}
end
)
end,
{New_ap, State@5} = case Additional_properties of
{some, Ap_ref} ->
{Hoisted, State@4} = hoist_schema_ref(
Ap_ref,
Name_prefix,
<<"Value"/utf8>>,
State@3
),
{{some, Hoisted}, State@4};
none ->
{none, State@3}
end,
Result = {object_schema,
Description,
New_props,
Required,
New_ap,
Additional_properties_untyped,
Nullable},
{Result, State@5};
{array_schema, Description@1, Items, Min_items, Max_items, Nullable@1} ->
{Hoisted_items, State@6} = hoist_schema_ref(
Items,
Name_prefix,
<<"Item"/utf8>>,
State
),
{{array_schema,
Description@1,
Hoisted_items,
Min_items,
Max_items,
Nullable@1},
State@6};
{one_of_schema, Description@2, Schemas, Discriminator} ->
{Hoisted_schemas_rev, State@9} = gleam@list:index_fold(
Schemas,
{[], State},
fun(Acc@1, S_ref, Idx) ->
{Schemas_acc, State@7} = Acc@1,
Suffix = <<"Variant"/utf8,
(erlang:integer_to_binary(Idx))/binary>>,
{Hoisted@1, State@8} = hoist_schema_ref(
S_ref,
Name_prefix,
Suffix,
State@7
),
{[Hoisted@1 | Schemas_acc], State@8}
end
),
{{one_of_schema,
Description@2,
lists:reverse(Hoisted_schemas_rev),
Discriminator},
State@9};
{any_of_schema, Description@3, Schemas@1} ->
{Hoisted_schemas_rev@1, State@12} = gleam@list:index_fold(
Schemas@1,
{[], State},
fun(Acc@2, S_ref@1, Idx@1) ->
{Schemas_acc@1, State@10} = Acc@2,
Suffix@1 = <<"Variant"/utf8,
(erlang:integer_to_binary(Idx@1))/binary>>,
{Hoisted@2, State@11} = hoist_schema_ref(
S_ref@1,
Name_prefix,
Suffix@1,
State@10
),
{[Hoisted@2 | Schemas_acc@1], State@11}
end
),
{{any_of_schema,
Description@3,
lists:reverse(Hoisted_schemas_rev@1)},
State@12};
{all_of_schema, Description@4, Schemas@2} ->
{Hoisted_schemas_rev@2, State@15} = gleam@list:index_fold(
Schemas@2,
{[], State},
fun(Acc@3, S_ref@2, Idx@2) ->
{Schemas_acc@2, State@13} = Acc@3,
Suffix@2 = <<"Part"/utf8,
(erlang:integer_to_binary(Idx@2))/binary>>,
{Hoisted@3, State@14} = hoist_schema_ref(
S_ref@2,
Name_prefix,
Suffix@2,
State@13
),
{[Hoisted@3 | Schemas_acc@2], State@14}
end
),
{{all_of_schema,
Description@4,
lists:reverse(Hoisted_schemas_rev@2)},
State@15};
_ ->
{Schema_obj, State}
end.
-file("src/oaspec/openapi/hoist.gleam", 305).
?DOC(" Hoist schemas within component schemas.\n").
-spec hoist_component_schemas(
gleam@dict:dict(binary(), oaspec@openapi@schema:schema_ref()),
hoist_state()
) -> {gleam@dict:dict(binary(), oaspec@openapi@schema:schema_ref()),
hoist_state()}.
hoist_component_schemas(Schemas, State) ->
_pipe = maps:to_list(Schemas),
gleam@list:fold(
_pipe,
{maps:new(), State},
fun(Acc, Entry) ->
{Result, State@1} = Acc,
{Name, Schema_ref} = Entry,
case Schema_ref of
{inline, Schema_obj} ->
{Hoisted_obj, State@2} = hoist_within_schema(
Schema_obj,
Name,
State@1
),
{gleam@dict:insert(Result, Name, {inline, Hoisted_obj}),
State@2};
{reference, _} ->
{gleam@dict:insert(Result, Name, Schema_ref), State@1}
end
end
).
-file("src/oaspec/openapi/hoist.gleam", 404).
?DOC(" Hoist schemas within a RequestBody.\n").
-spec hoist_request_body(
oaspec@openapi@spec:request_body(),
binary(),
hoist_state()
) -> {oaspec@openapi@spec:request_body(), hoist_state()}.
hoist_request_body(Rb, Op_id, State) ->
{New_content, State@3} = begin
_pipe = maps:to_list(erlang:element(3, Rb)),
gleam@list:fold(
_pipe,
{maps:new(), State},
fun(Acc, Entry) ->
{Result, State@1} = Acc,
{Media_type_name, Media_type} = Entry,
case erlang:element(2, Media_type) of
{some, Schema_ref} ->
{Hoisted, State@2} = hoist_schema_ref(
Schema_ref,
Op_id,
<<"Request"/utf8>>,
State@1
),
Mt = {media_type, {some, Hoisted}},
{gleam@dict:insert(Result, Media_type_name, Mt),
State@2};
none ->
{gleam@dict:insert(Result, Media_type_name, Media_type),
State@1}
end
end
)
end,
{{request_body, erlang:element(2, Rb), New_content, erlang:element(4, Rb)},
State@3}.
-file("src/oaspec/openapi/hoist.gleam", 428).
?DOC(" Hoist schemas within response definitions.\n").
-spec hoist_responses(
gleam@dict:dict(binary(), oaspec@openapi@spec:response()),
binary(),
hoist_state()
) -> {gleam@dict:dict(binary(), oaspec@openapi@spec:response()), hoist_state()}.
hoist_responses(Responses, Op_id, State) ->
_pipe = maps:to_list(Responses),
gleam@list:fold(
_pipe,
{maps:new(), State},
fun(Acc, Entry) ->
{Result, State@1} = Acc,
{Status_code, Response} = Entry,
{New_content, State@4} = begin
_pipe@1 = maps:to_list(erlang:element(3, Response)),
gleam@list:fold(
_pipe@1,
{maps:new(), State@1},
fun(Ct_acc, Ct_entry) ->
{Ct_result, State@2} = Ct_acc,
{Media_type_name, Media_type} = Ct_entry,
case erlang:element(2, Media_type) of
{some, Schema_ref} ->
Suffix = <<"Response"/utf8,
(oaspec@util@naming:to_pascal_case(
Status_code
))/binary>>,
{Hoisted, State@3} = hoist_schema_ref(
Schema_ref,
Op_id,
Suffix,
State@2
),
Mt = {media_type, {some, Hoisted}},
{gleam@dict:insert(
Ct_result,
Media_type_name,
Mt
),
State@3};
none ->
{gleam@dict:insert(
Ct_result,
Media_type_name,
Media_type
),
State@2}
end
end
)
end,
Hoisted_response = {response,
erlang:element(2, Response),
New_content},
{gleam@dict:insert(Result, Status_code, Hoisted_response), State@4}
end
).
-file("src/oaspec/openapi/hoist.gleam", 382).
?DOC(" Hoist schemas within a single Operation.\n").
-spec hoist_operation(oaspec@openapi@spec:operation(), binary(), hoist_state()) -> {oaspec@openapi@spec:operation(),
hoist_state()}.
hoist_operation(Operation, Op_id, State) ->
{Request_body, State@2} = case erlang:element(7, Operation) of
none ->
{none, State};
{some, Rb} ->
{Hoisted_rb, State@1} = hoist_request_body(Rb, Op_id, State),
{{some, Hoisted_rb}, State@1}
end,
{Responses, State@3} = hoist_responses(
erlang:element(8, Operation),
Op_id,
State@2
),
Result = {operation,
erlang:element(2, Operation),
erlang:element(3, Operation),
erlang:element(4, Operation),
erlang:element(5, Operation),
erlang:element(6, Operation),
Request_body,
Responses,
erlang:element(9, Operation),
erlang:element(10, Operation)},
{Result, State@3}.
-file("src/oaspec/openapi/hoist.gleam", 357).
?DOC(" Hoist schemas in an optional operation.\n").
-spec hoist_maybe_operation(
gleam@option:option(oaspec@openapi@spec:operation()),
binary(),
binary(),
hoist_state()
) -> {gleam@option:option(oaspec@openapi@spec:operation()), hoist_state()}.
hoist_maybe_operation(Maybe_op, Method, Path, State) ->
case Maybe_op of
none ->
{none, State};
{some, Operation} ->
Op_id = case erlang:element(2, Operation) of
{some, Id} ->
Id;
none ->
<<<<Method/binary, "_"/utf8>>/binary,
(begin
_pipe = gleam@string:replace(
Path,
<<"/"/utf8>>,
<<"_"/utf8>>
),
_pipe@1 = gleam@string:replace(
_pipe,
<<"{"/utf8>>,
<<""/utf8>>
),
gleam@string:replace(
_pipe@1,
<<"}"/utf8>>,
<<""/utf8>>
)
end)/binary>>
end,
{Hoisted_op, State@1} = hoist_operation(Operation, Op_id, State),
{{some, Hoisted_op}, State@1}
end.
-file("src/oaspec/openapi/hoist.gleam", 338).
?DOC(" Hoist schemas within a single PathItem.\n").
-spec hoist_path_item(oaspec@openapi@spec:path_item(), binary(), hoist_state()) -> {oaspec@openapi@spec:path_item(),
hoist_state()}.
hoist_path_item(Path_item, Path, State) ->
{Get, State@1} = hoist_maybe_operation(
erlang:element(4, Path_item),
<<"get"/utf8>>,
Path,
State
),
{Post, State@2} = hoist_maybe_operation(
erlang:element(5, Path_item),
<<"post"/utf8>>,
Path,
State@1
),
{Put, State@3} = hoist_maybe_operation(
erlang:element(6, Path_item),
<<"put"/utf8>>,
Path,
State@2
),
{Delete, State@4} = hoist_maybe_operation(
erlang:element(7, Path_item),
<<"delete"/utf8>>,
Path,
State@3
),
{Patch, State@5} = hoist_maybe_operation(
erlang:element(8, Path_item),
<<"patch"/utf8>>,
Path,
State@4
),
Result = {path_item,
erlang:element(2, Path_item),
erlang:element(3, Path_item),
Get,
Post,
Put,
Delete,
Patch,
erlang:element(9, Path_item)},
{Result, State@5}.
-file("src/oaspec/openapi/hoist.gleam", 324).
?DOC(" Walk all paths and operations, hoisting inline schemas.\n").
-spec hoist_paths(
gleam@dict:dict(binary(), oaspec@openapi@spec:path_item()),
hoist_state()
) -> {gleam@dict:dict(binary(), oaspec@openapi@spec:path_item()), hoist_state()}.
hoist_paths(Paths, State) ->
_pipe = maps:to_list(Paths),
gleam@list:fold(
_pipe,
{maps:new(), State},
fun(Acc, Entry) ->
{Result, State@1} = Acc,
{Path, Path_item} = Entry,
{Hoisted_item, State@2} = hoist_path_item(Path_item, Path, State@1),
{gleam@dict:insert(Result, Path, Hoisted_item), State@2}
end
).
-file("src/oaspec/openapi/hoist.gleam", 30).
?DOC(
" Hoist inline complex schemas into components.schemas, replacing them with $ref.\n"
" This is a pre-processing pass that runs after parsing and before validation.\n"
" The function is idempotent: running it twice produces the same result.\n"
).
-spec hoist(oaspec@openapi@spec:open_api_spec()) -> oaspec@openapi@spec:open_api_spec().
hoist(Spec) ->
Existing_schemas = case erlang:element(5, Spec) of
{some, Components} ->
erlang:element(2, Components);
none ->
maps:new()
end,
Existing_names = begin
_pipe = maps:keys(Existing_schemas),
_pipe@1 = gleam@list:map(_pipe, fun(K) -> {K, nil} end),
maps:from_list(_pipe@1)
end,
Existing_type_names = begin
_pipe@2 = maps:keys(Existing_schemas),
_pipe@3 = gleam@list:map(
_pipe@2,
fun(K@1) -> {oaspec@util@naming:schema_to_type_name(K@1), nil} end
),
maps:from_list(_pipe@3)
end,
State = {hoist_state, maps:new(), Existing_names, Existing_type_names},
{Hoisted_component_schemas, State@1} = hoist_component_schemas(
Existing_schemas,
State
),
{Hoisted_paths, State@2} = hoist_paths(erlang:element(4, Spec), State@1),
Final_schemas = maps:merge(
Hoisted_component_schemas,
erlang:element(2, State@2)
),
Components@1 = case erlang:element(5, Spec) of
{some, C} ->
{some,
{components,
Final_schemas,
erlang:element(3, C),
erlang:element(4, C),
erlang:element(5, C),
erlang:element(6, C)}};
none ->
case gleam@dict:is_empty(erlang:element(2, State@2)) of
true ->
none;
false ->
{some,
{components,
Final_schemas,
maps:new(),
maps:new(),
maps:new(),
maps:new()}}
end
end,
{open_api_spec,
erlang:element(2, Spec),
erlang:element(3, Spec),
Hoisted_paths,
Components@1,
erlang:element(6, Spec),
erlang:element(7, Spec)}.