-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 ).