Packages

Generate Gleam types, JSON codecs, and a typed XRPC client from atproto lexicon files.

Current section

Files

Jump to
atproto_codegen src atproto_codegen@plugin.erl
Raw

src/atproto_codegen@plugin.erl

-module(atproto_codegen@plugin).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/atproto_codegen/plugin.gleam").
-export([run/4, assemble/2]).
-export_type([output_file/0, codegen_error/0, plugin_context/0, plugin/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(
" Minimal plugin host.\n"
"\n"
" A `Plugin` receives a `PluginContext` (the config, the decoded lexicon\n"
" docs, the lowered view, and every upstream plugin's own output so far)\n"
" and returns the `OutputFile`s it contributes. An `OutputFile.parts` is\n"
" not a whole file: it is that plugin's ordered contribution, one entry\n"
" per `lower.CodecUnit` in the module (see `lower.codec_units`). Plugins\n"
" that cover the same units (today: `types` and `decode-json`, one def or\n"
" synthesized union per unit) therefore produce equal-length `parts` lists\n"
" for the same path.\n"
"\n"
" `run` executes the registry in dependency order (a plugin runs after\n"
" everything in its `depends_on`) and hands each plugin the outputs placed\n"
" so far, keyed by plugin name. `assemble` then merges, for a given path,\n"
" the named plugins' `parts` lists by zipping them position for position\n"
" in the caller-supplied order and joining the result with a blank line:\n"
" this is what reproduces the original emitter's interleaving of a def's\n"
" type/fields/encoder with its decoder, and a union's type/encoder with\n"
" its decoder, without either plugin knowing about the other's text.\n"
"\n"
" A plugin that owns an entire module instead of contributing per-unit\n"
" `parts` emits a `WholeFile(path, content)` for that path: `assemble`\n"
" writes its `content` verbatim, with no zipping and no blank-line joining.\n"
" This is for a plugin (e.g. `xrpc-client`) whose output is one complete\n"
" module (its own doc, imports, everything) that no other plugin\n"
" interleaves with, rather than a per-`CodecUnit` slice of a shared module.\n"
" Two collisions are rejected as `ConflictingWholeFile(path)`: a path\n"
" claimed as `WholeFile` that also has `parts` from any plugin, and a path\n"
" claimed as `WholeFile` by more than one plugin.\n"
"\n"
" Order resolution is a static, dependency-respecting placement: it is not\n"
" a general topological sort with cycle diagnostics, just enough to catch\n"
" an unknown dependency name or a cycle across the handful of plugins this\n"
" package registers.\n"
).
-type output_file() :: {output_file, binary(), list(binary())} |
{whole_file, binary(), binary()}.
-type codegen_error() :: {unknown_dependency, binary(), binary()} |
{unresolved_order, list(binary())} |
{mismatched_sections, binary()} |
{conflicting_whole_file, binary()} |
{plugin_failure, binary(), binary()}.
-type plugin_context() :: {plugin_context,
atproto_codegen@config:config(),
list(atproto_lexicon@ast:lexicon_doc()),
list(atproto_codegen@lower:flat_lexicon()),
gleam@dict:dict(binary(), list(output_file()))}.
-type plugin() :: {plugin,
binary(),
list(binary()),
fun((plugin_context()) -> {ok, list(output_file())} |
{error, codegen_error()})}.
-file("src/atproto_codegen/plugin.gleam", 110).
-spec place(list(plugin()), list(plugin())) -> {ok, list(plugin())} |
{error, codegen_error()}.
place(Remaining, Placed) ->
case Remaining of
[] ->
{ok, lists:reverse(Placed)};
_ ->
Placed_names = gleam@list:map(
Placed,
fun(P) -> erlang:element(2, P) end
),
{Ready, Rest} = gleam@list:partition(
Remaining,
fun(P@1) ->
gleam@list:all(
erlang:element(3, P@1),
fun(_capture) ->
gleam@list:contains(Placed_names, _capture)
end
)
end
),
case Ready of
[] ->
{error,
{unresolved_order,
gleam@list:map(
Remaining,
fun(P@2) -> erlang:element(2, P@2) end
)}};
_ ->
place(Rest, lists:append(lists:reverse(Ready), Placed))
end
end.
-file("src/atproto_codegen/plugin.gleam", 94).
-spec check_known_deps(list(plugin()), list(binary())) -> {ok, nil} |
{error, codegen_error()}.
check_known_deps(Plugins, Names) ->
_pipe = Plugins,
gleam@list:try_each(
_pipe,
fun(Plugin) -> _pipe@1 = erlang:element(3, Plugin),
gleam@list:try_each(
_pipe@1,
fun(Dep) -> case gleam@list:contains(Names, Dep) of
true ->
{ok, nil};
false ->
{error,
{unknown_dependency,
erlang:element(2, Plugin),
Dep}}
end end
) end
).
-file("src/atproto_codegen/plugin.gleam", 88).
-spec resolve_order(list(plugin())) -> {ok, list(plugin())} |
{error, codegen_error()}.
resolve_order(Plugins) ->
Names = gleam@list:map(Plugins, fun(P) -> erlang:element(2, P) end),
gleam@result:'try'(
check_known_deps(Plugins, Names),
fun(_) -> place(Plugins, []) end
).
-file("src/atproto_codegen/plugin.gleam", 74).
-spec run(
list(plugin()),
atproto_codegen@config:config(),
list(atproto_lexicon@ast:lexicon_doc()),
list(atproto_codegen@lower:flat_lexicon())
) -> {ok, gleam@dict:dict(binary(), list(output_file()))} |
{error, codegen_error()}.
run(Plugins, Cfg, Docs, Lexicons) ->
gleam@result:'try'(
resolve_order(Plugins),
fun(Order) ->
gleam@list:try_fold(
Order,
maps:new(),
fun(Acc, Plugin) ->
Ctx = {plugin_context, Cfg, Docs, Lexicons, Acc},
gleam@result:'try'(
(erlang:element(4, Plugin))(Ctx),
fun(Outputs) ->
{ok,
gleam@dict:insert(
Acc,
erlang:element(2, Plugin),
Outputs
)}
end
)
end
)
end
).
-file("src/atproto_codegen/plugin.gleam", 227).
-spec zip_flat(list(list(binary()))) -> list(binary()).
zip_flat(Lists) ->
case gleam@list:any(Lists, fun(L) -> L =:= [] end) of
true ->
[];
false ->
Heads = gleam@list:filter_map(Lists, fun gleam@list:first/1),
Tails = gleam@list:map(
Lists,
fun(L@1) -> gleam@list:drop(L@1, 1) end
),
lists:append(Heads, zip_flat(Tails))
end.
-file("src/atproto_codegen/plugin.gleam", 210).
-spec interleave(binary(), list(list(binary()))) -> {ok, list(binary())} |
{error, codegen_error()}.
interleave(Path, Sections) ->
Non_empty = gleam@list:filter(Sections, fun(S) -> S /= [] end),
case Non_empty of
[] ->
{ok, []};
[First | Rest] ->
Width = erlang:length(First),
case gleam@list:all(
Rest,
fun(S@1) -> erlang:length(S@1) =:= Width end
) of
false ->
{error, {mismatched_sections, Path}};
true ->
{ok, zip_flat(Non_empty)}
end
end.
-file("src/atproto_codegen/plugin.gleam", 149).
-spec file_path(output_file()) -> binary().
file_path(File) ->
case File of
{output_file, Path, _} ->
Path;
{whole_file, Path@1, _} ->
Path@1
end.
-file("src/atproto_codegen/plugin.gleam", 192).
-spec assemble_parts(
gleam@dict:dict(binary(), list(output_file())),
list(binary()),
binary()
) -> {ok, {binary(), binary()}} | {error, codegen_error()}.
assemble_parts(Outputs, Order, Path) ->
gleam@result:'try'(
gleam@list:try_map(
Order,
fun(Name) ->
Files = begin
_pipe = gleam_stdlib:map_get(Outputs, Name),
gleam@result:unwrap(_pipe, [])
end,
case gleam@list:find(
Files,
fun(File) -> file_path(File) =:= Path end
) of
{ok, {output_file, _, Parts}} ->
{ok, Parts};
{ok, {whole_file, _, _}} ->
{ok, []};
{error, nil} ->
{ok, []}
end
end
),
fun(Sections) ->
gleam@result:'try'(
interleave(Path, Sections),
fun(Merged) ->
{ok, {Path, gleam@string:join(Merged, <<"\n\n"/utf8>>)}}
end
)
end
).
-file("src/atproto_codegen/plugin.gleam", 156).
-spec assemble_path(
gleam@dict:dict(binary(), list(output_file())),
list(binary()),
binary()
) -> {ok, {binary(), binary()}} | {error, codegen_error()}.
assemble_path(Outputs, Order, Path) ->
Entries = gleam@list:flat_map(
Order,
fun(Name) -> _pipe = gleam_stdlib:map_get(Outputs, Name),
_pipe@1 = gleam@result:unwrap(_pipe, []),
gleam@list:filter(
_pipe@1,
fun(File) -> file_path(File) =:= Path end
) end
),
Wholes = gleam@list:filter_map(Entries, fun(File@1) -> case File@1 of
{whole_file, _, Content} ->
{ok, Content};
{output_file, _, _} ->
{error, nil}
end end),
Has_parts = gleam@list:any(Entries, fun(File@2) -> case File@2 of
{output_file, _, _} ->
true;
{whole_file, _, _} ->
false
end end),
case Wholes of
[] ->
assemble_parts(Outputs, Order, Path);
[Content@1] ->
case Has_parts of
true ->
{error, {conflicting_whole_file, Path}};
false ->
{ok, {Path, Content@1}}
end;
_ ->
{error, {conflicting_whole_file, Path}}
end.
-file("src/atproto_codegen/plugin.gleam", 135).
?DOC(
" Merge, for every path any of `order`'s plugins produced, their outputs\n"
" into the final file text. A path claimed by a `WholeFile` yields its\n"
" content verbatim; otherwise the plugins' `parts` lists are zipped\n"
" position for position (in `order`) and joined the way the original\n"
" emitter joined its chunks.\n"
).
-spec assemble(gleam@dict:dict(binary(), list(output_file())), list(binary())) -> {ok,
list({binary(), binary()})} |
{error, codegen_error()}.
assemble(Outputs, Order) ->
_pipe = Order,
_pipe@3 = gleam@list:flat_map(
_pipe,
fun(Name) -> _pipe@1 = gleam_stdlib:map_get(Outputs, Name),
_pipe@2 = gleam@result:unwrap(_pipe@1, []),
gleam@list:map(_pipe@2, fun file_path/1) end
),
_pipe@4 = gleam@list:unique(_pipe@3),
gleam@list:try_map(
_pipe@4,
fun(_capture) -> assemble_path(Outputs, Order, _capture) end
).