Current section
Files
Jump to
Current section
Files
src/nori@capability.erl
-module(nori@capability).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/nori/capability.gleam").
-export([blocking/1, issue_to_string/1, check/1]).
-export_type([issue/0, severity/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.
?MODULEDOC(
" Capability registry — detect OpenAPI features nori does not yet support.\n"
"\n"
" Walk a parsed `Document` and surface anything the codegen would either\n"
" silently drop, generate broken output for, or interpret incorrectly.\n"
" Run this before `generate` (or alongside `validate`) so users hit a\n"
" clear error instead of mysterious runtime / compile failures.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(doc) = nori.parse_file(\"api.yaml\")\n"
" case nori/capability.check(doc) {\n"
" Ok(_) -> generate(doc)\n"
" Error(issues) -> {\n"
" list.each(issues, fn(i) { io.println(capability.issue_to_string(i)) })\n"
" panic as \"spec uses unsupported features\"\n"
" }\n"
" }\n"
" ```\n"
).
-type issue() :: {issue, binary(), binary(), binary(), severity()}.
-type severity() :: blocking | warning.
-file("src/nori/capability.gleam", 80).
?DOC(" Convenience: only the blocking issues.\n").
-spec blocking(list(issue())) -> list(issue()).
blocking(Issues) ->
gleam@list:filter(Issues, fun(I) -> erlang:element(5, I) =:= blocking end).
-file("src/nori/capability.gleam", 85).
?DOC(" Format a single issue for CLI output.\n").
-spec issue_to_string(issue()) -> binary().
issue_to_string(Issue) ->
Badge = case erlang:element(5, Issue) of
blocking ->
<<"✗"/utf8>>;
warning ->
<<"!"/utf8>>
end,
<<<<<<<<<<<<<<" "/utf8, Badge/binary>>/binary, " "/utf8>>/binary,
(erlang:element(2, Issue))/binary>>/binary,
" at "/utf8>>/binary,
(erlang:element(3, Issue))/binary>>/binary,
"\n "/utf8>>/binary,
(erlang:element(4, Issue))/binary>>.
-file("src/nori/capability.gleam", 104).
-spec check_webhooks(nori@document:document()) -> list(issue()).
check_webhooks(Doc) ->
case gleam@dict:is_empty(erlang:element(7, Doc)) of
true ->
[];
false ->
[{issue,
<<"webhooks"/utf8>>,
<<"#/webhooks"/utf8>>,
<<"OpenAPI 3.1 webhooks are parsed but no codegen target emits webhook handlers yet."/utf8>>,
blocking}]
end.
-file("src/nori/capability.gleam", 133).
-spec check_schema_discriminator(nori@schema:schema(), binary()) -> list(issue()).
check_schema_discriminator(Schema, Name) ->
case erlang:element(59, Schema) of
none ->
[];
{some, _} ->
[{issue,
<<"discriminator"/utf8>>,
<<"#/components/schemas/"/utf8, Name/binary>>,
<<"Polymorphic union narrowing via discriminator is not yet implemented; oneOf/anyOf members will be exposed without runtime tag dispatch."/utf8>>,
blocking}]
end.
-file("src/nori/capability.gleam", 122).
-spec check_components(gleam@option:option(nori@components:components())) -> list(issue()).
check_components(Components) ->
case Components of
none ->
[];
{some, C} ->
gleam@dict:fold(
erlang:element(2, C),
[],
fun(Acc, Name, Schema) ->
lists:append(Acc, check_schema_discriminator(Schema, Name))
end
)
end.
-file("src/nori/capability.gleam", 194).
-spec check_callbacks(nori@operation:operation(), binary()) -> list(issue()).
check_callbacks(Op, Op_loc) ->
case gleam@dict:is_empty(erlang:element(10, Op)) of
true ->
[];
false ->
[{issue,
<<"callbacks"/utf8>>,
<<Op_loc/binary, "/callbacks"/utf8>>,
<<"OpenAPI callbacks are parsed but no codegen target emits callback registration yet."/utf8>>,
blocking}]
end.
-file("src/nori/capability.gleam", 225).
-spec check_parameter_style(
gleam@option:option(nori@parameter:parameter_style()),
binary(),
binary()
) -> list(issue()).
check_parameter_style(Style, Loc, Param_name) ->
case Style of
none ->
[];
{some, style_deep_object} ->
[{issue,
<<"parameter style: deepObject"/utf8>>,
<<Loc/binary, "/style"/utf8>>,
<<<<"Parameter '"/utf8, Param_name/binary>>/binary,
"' uses style 'deepObject', which nori does not serialize. Only 'simple' and 'form' are supported."/utf8>>,
blocking}];
{some, style_pipe_delimited} ->
[{issue,
<<"parameter style: pipeDelimited"/utf8>>,
<<Loc/binary, "/style"/utf8>>,
<<<<"Parameter '"/utf8, Param_name/binary>>/binary,
"' uses style 'pipeDelimited', which nori does not serialize."/utf8>>,
blocking}];
{some, style_space_delimited} ->
[{issue,
<<"parameter style: spaceDelimited"/utf8>>,
<<Loc/binary, "/style"/utf8>>,
<<<<"Parameter '"/utf8, Param_name/binary>>/binary,
"' uses style 'spaceDelimited', which nori does not serialize."/utf8>>,
blocking}];
{some, _} ->
[]
end.
-file("src/nori/capability.gleam", 208).
-spec check_parameters(
list(nori@reference:ref(nori@parameter:parameter())),
binary()
) -> list(issue()).
check_parameters(Params, Op_loc) ->
_pipe = Params,
_pipe@1 = gleam@list:index_map(_pipe, fun(P, Idx) -> {Idx, P} end),
gleam@list:flat_map(
_pipe@1,
fun(Pair) ->
{Idx@1, Ref} = Pair,
case Ref of
{reference, _} ->
[];
{inline, Param} ->
check_parameter_style(
erlang:element(8, Param),
<<<<Op_loc/binary, "/parameters/"/utf8>>/binary,
(erlang:integer_to_binary(Idx@1))/binary>>,
erlang:element(2, Param)
)
end
end
).
-file("src/nori/capability.gleam", 266).
-spec check_request_body(
gleam@option:option(nori@reference:ref(nori@request_body:request_body())),
binary()
) -> list(issue()).
check_request_body(Body_ref, Op_loc) ->
case Body_ref of
none ->
[];
{some, {reference, _}} ->
[];
{some, {inline, Body}} ->
_pipe = maps:keys(erlang:element(3, Body)),
gleam@list:flat_map(_pipe, fun(Content_type) -> case Content_type of
<<"multipart/form-data"/utf8>> ->
[{issue,
<<"requestBody content: multipart/form-data"/utf8>>,
<<Op_loc/binary,
"/requestBody/content/multipart~1form-data"/utf8>>,
<<"File uploads / multipart bodies are not generated. nori's TS and Gleam clients hardcode application/json."/utf8>>,
blocking}];
<<"application/x-www-form-urlencoded"/utf8>> ->
[{issue,
<<"requestBody content: x-www-form-urlencoded"/utf8>>,
<<Op_loc/binary,
"/requestBody/content/application~1x-www-form-urlencoded"/utf8>>,
<<"URL-encoded form bodies are not generated."/utf8>>,
blocking}];
_ ->
[]
end end)
end.
-file("src/nori/capability.gleam", 187).
-spec check_operation(nori@operation:operation(), binary()) -> list(issue()).
check_operation(Op, Op_loc) ->
Callback_issues = check_callbacks(Op, Op_loc),
Parameter_issues = check_parameters(erlang:element(7, Op), Op_loc),
Body_issues = check_request_body(erlang:element(8, Op), Op_loc),
lists:append([Callback_issues, Parameter_issues, Body_issues]).
-file("src/nori/capability.gleam", 305).
-spec sort_by_severity(list(issue())) -> list(issue()).
sort_by_severity(Issues) ->
Blockers = gleam@list:filter(
Issues,
fun(I) -> erlang:element(5, I) =:= blocking end
),
Warnings = gleam@list:filter(
Issues,
fun(I@1) -> erlang:element(5, I@1) =:= warning end
),
lists:append(Blockers, Warnings).
-file("src/nori/capability.gleam", 313).
?DOC(
" Escape a path segment for JSON pointer per RFC 6901\n"
" (`/` → `~1`, `~` → `~0`).\n"
).
-spec escape_pointer(binary()) -> binary().
escape_pointer(Segment) ->
_pipe = Segment,
_pipe@1 = gleam@string:replace(_pipe, <<"~"/utf8>>, <<"~0"/utf8>>),
gleam@string:replace(_pipe@1, <<"/"/utf8>>, <<"~1"/utf8>>).
-file("src/nori/capability.gleam", 165).
-spec check_path_item(nori@operation:path_item(), binary()) -> list(issue()).
check_path_item(Item, Path) ->
Path_loc = <<"#/paths/"/utf8, (escape_pointer(Path))/binary>>,
Operations = [{<<"get"/utf8>>, erlang:element(5, Item)},
{<<"put"/utf8>>, erlang:element(6, Item)},
{<<"post"/utf8>>, erlang:element(7, Item)},
{<<"delete"/utf8>>, erlang:element(8, Item)},
{<<"options"/utf8>>, erlang:element(9, Item)},
{<<"head"/utf8>>, erlang:element(10, Item)},
{<<"patch"/utf8>>, erlang:element(11, Item)},
{<<"trace"/utf8>>, erlang:element(12, Item)}],
gleam@list:flat_map(
Operations,
fun(Pair) ->
{Method, Op} = Pair,
case Op of
none ->
[];
{some, Operation} ->
check_operation(
Operation,
<<<<Path_loc/binary, "/"/utf8>>/binary, Method/binary>>
)
end
end
).
-file("src/nori/capability.gleam", 151).
-spec check_paths(nori@document:document()) -> list(issue()).
check_paths(Doc) ->
case erlang:element(6, Doc) of
none ->
[];
{some, Paths} ->
gleam@dict:fold(
Paths,
[],
fun(Acc, Path, Item_ref) -> case Item_ref of
{reference, _} ->
Acc;
{inline, Item} ->
lists:append(Acc, check_path_item(Item, Path))
end end
)
end.
-file("src/nori/capability.gleam", 64).
?DOC(
" Walk the document and collect every unsupported-feature occurrence.\n"
"\n"
" Returns the document unchanged if nothing was flagged, otherwise a list of\n"
" issues sorted by severity (blocking first).\n"
).
-spec check(nori@document:document()) -> {ok, nori@document:document()} |
{error, list(issue())}.
check(Doc) ->
Issues = begin
_pipe = [check_webhooks(Doc),
check_components(erlang:element(8, Doc)),
check_paths(Doc)],
lists:append(_pipe)
end,
case Issues of
[] ->
{ok, Doc};
_ ->
{error, sort_by_severity(Issues)}
end.