Packages

Emit an OpenAPI 3.1.0 document from atproto lexicons.

Current section

Files

Jump to
atproto_openapi src atproto_openapi@refs.erl
Raw

src/atproto_openapi@refs.erl

-module(atproto_openapi@refs).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/atproto_openapi/refs.gleam").
-export([describe/1, canonicalize/2, component_key/1, schema_pointer/2, find_collisions/1]).
-export_type([ref/0, ref_error/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(
" Lexicon ref -> OpenAPI component-key resolution. Every `PropRef`/\n"
" `ItemRef`/`BodySchemaRef`/union member string in the AST is one of\n"
" three spellings (`#localName`, `nsid#name`, bare `nsid`); this module\n"
" reduces all three to the canonical `Ref(nsid, name)` pair (bare `nsid`\n"
" canonicalizes to `Ref(nsid, \"main\")`) and derives the `components/\n"
" schemas` key from it. OpenAPI component keys must match\n"
" `^[a-zA-Z0-9._-]+$`, so the lexicon's `#` separator cannot appear in\n"
" one; `_` is used instead, which is safe because NSID segments and def\n"
" names never contain `_`. Two distinct canonical refs landing on the\n"
" same key is a collision, reported via `find_collisions` rather than\n"
" silently merged, mirroring how `atproto_sdl`/`atproto_mlf` report NSID\n"
" collisions. See `docs/lexicon-openapi.md` for the full scheme.\n"
).
-type ref() :: {ref, binary(), binary()}.
-type ref_error() :: empty_ref |
{multiple_fragments, binary()} |
{empty_local_name, binary()} |
{key_collision, binary(), ref(), ref()}.
-file("src/atproto_openapi/refs.gleam", 51).
-spec ref_to_string(ref()) -> binary().
ref_to_string(Ref) ->
<<<<(erlang:element(2, Ref))/binary, "#"/utf8>>/binary,
(erlang:element(3, Ref))/binary>>.
-file("src/atproto_openapi/refs.gleam", 35).
-spec describe(ref_error()) -> binary().
describe(Error) ->
case Error of
empty_ref ->
<<"empty ref"/utf8>>;
{multiple_fragments, Raw} ->
<<"malformed ref (more than one '#'): "/utf8, Raw/binary>>;
{empty_local_name, Raw@1} ->
<<"malformed ref (empty name after '#'): "/utf8, Raw@1/binary>>;
{key_collision, Key, First, Second} ->
<<<<<<<<<<<<"component key collision: "/utf8,
(ref_to_string(First))/binary>>/binary,
" and "/utf8>>/binary,
(ref_to_string(Second))/binary>>/binary,
" both map to \""/utf8>>/binary,
Key/binary>>/binary,
"\""/utf8>>
end.
-file("src/atproto_openapi/refs.gleam", 58).
?DOC(
" Resolve a raw ref string against the NSID of the doc it appears in.\n"
" `#foo` resolves to `Ref(doc_id, \"foo\")`; `nsid#foo` to `Ref(nsid,\n"
" \"foo\")`; bare `nsid` to `Ref(nsid, \"main\")`.\n"
).
-spec canonicalize(binary(), binary()) -> {ok, ref()} | {error, ref_error()}.
canonicalize(Raw, Doc_id) ->
case Raw of
<<""/utf8>> ->
{error, empty_ref};
_ ->
case gleam@string:split(Raw, <<"#"/utf8>>) of
[Nsid] ->
{ok, {ref, Nsid, <<"main"/utf8>>}};
[Nsid@1, Name] ->
case Name of
<<""/utf8>> ->
{error, {empty_local_name, Raw}};
_ ->
case Nsid@1 of
<<""/utf8>> ->
{ok, {ref, Doc_id, Name}};
_ ->
{ok, {ref, Nsid@1, Name}}
end
end;
_ ->
{error, {multiple_fragments, Raw}}
end
end.
-file("src/atproto_openapi/refs.gleam", 80).
?DOC(
" The `components/schemas` key for a canonical ref: `nsid` when `name`\n"
" is `\"main\"`, else `nsid <> \"_\" <> name`.\n"
).
-spec component_key(ref()) -> binary().
component_key(Ref) ->
case erlang:element(3, Ref) of
<<"main"/utf8>> ->
erlang:element(2, Ref);
Name ->
<<<<(erlang:element(2, Ref))/binary, "_"/utf8>>/binary,
Name/binary>>
end.
-file("src/atproto_openapi/refs.gleam", 89).
?DOC(
" The `$ref` JSON pointer for a raw ref string, resolved against the\n"
" containing doc's id: `#/components/schemas/<component_key>`.\n"
).
-spec schema_pointer(binary(), binary()) -> {ok, binary()} |
{error, ref_error()}.
schema_pointer(Raw, Doc_id) ->
gleam@result:'try'(
canonicalize(Raw, Doc_id),
fun(Ref) ->
{ok, <<"#/components/schemas/"/utf8, (component_key(Ref))/binary>>}
end
).
-file("src/atproto_openapi/refs.gleam", 111).
-spec group_by_key(list(ref())) -> list({binary(), list(ref())}).
group_by_key(Refs) ->
gleam@list:fold(
Refs,
[],
fun(Acc, Ref) ->
Key = component_key(Ref),
case gleam@list:key_find(Acc, Key) of
{ok, Existing} ->
gleam@list:key_set(Acc, Key, lists:append(Existing, [Ref]));
{error, _} ->
gleam@list:key_set(Acc, Key, [Ref])
end
end
).
-file("src/atproto_openapi/refs.gleam", 98).
?DOC(
" Report every case where two distinct canonical refs land on the same\n"
" component key, rather than letting the second silently overwrite the\n"
" first in the emitted `components/schemas` map. Empty when `refs` has\n"
" no collisions.\n"
).
-spec find_collisions(list(ref())) -> list(ref_error()).
find_collisions(Refs) ->
_pipe = Refs,
_pipe@1 = gleam@list:unique(_pipe),
_pipe@2 = group_by_key(_pipe@1),
gleam@list:flat_map(
_pipe@2,
fun(Group) ->
{Key, Members} = Group,
case Members of
[First | Rest] ->
gleam@list:map(
Rest,
fun(_capture) ->
{key_collision, Key, First, _capture}
end
);
[] ->
[]
end
end
).