Skip to main content

src/oaisp@cli.erl

-module(oaisp@cli).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/oaisp/cli.gleam").
-export([build_document/2, main/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(
    " `gleam run -m oaisp/cli` — the build-time CLI.\n"
    "\n"
    " `generate` orchestrates the pipeline: it runs `gleam export\n"
    " package-interface` for the resolved type information, runs `gleam run --\n"
    " --emit-endpoints` to collect the endpoint declarations the app wires into\n"
    " `add_openapi`, merges the two, and writes the document atomically.\n"
    "\n"
    " Status messages always go to stderr; with `generate -o -` the document is\n"
    " the only thing on stdout, so it can be piped to other tools.\n"
).

-file("src/oaisp/cli.gleam", 299).
-spec help_text() -> binary().
help_text() ->
    <<"oaisp — generate an OpenAPI 3.1 document from your Gleam code

USAGE:
  gleam run -m oaisp/cli <COMMAND> [OPTIONS]

COMMANDS:
  generate             Emit the OpenAPI 3.1 document

OPTIONS:
  -o, --out <PATH>              Output path (default ./openapi.json; - for stdout)
      --package-interface <PATH>  Use this package-interface.json instead of
                                  running `gleam export package-interface`
      --quiet                   Suppress status output on stderr
  -h, --help                    Print this help"/utf8>>.

-file("src/oaisp/cli.gleam", 295).
-spec log_error(binary()) -> nil.
log_error(Message) ->
    gleam_stdlib:println_error(<<"oaisp: error: "/utf8, Message/binary>>).

-file("src/oaisp/cli.gleam", 121).
?DOC(
    " Two routes with the same method and path would describe one operation in the\n"
    " document while the server runs only the first — the document and the server\n"
    " would disagree. List the offenders so the stray route is easy to find.\n"
).
-spec duplicate_routes_message(list({binary(), binary()})) -> binary().
duplicate_routes_message(Routes) ->
    Bullets = begin
        _pipe = Routes,
        _pipe@1 = gleam@list:map(
            _pipe,
            fun(Route) ->
                <<<<<<"  - "/utf8,
                            (string:uppercase(erlang:element(1, Route)))/binary>>/binary,
                        " "/utf8>>/binary,
                    (erlang:element(2, Route))/binary>>
            end
        ),
        gleam@string:join(_pipe@1, <<"\n"/utf8>>)
    end,
    <<<<"these routes share a method and path — each (method, path) must be unique, or "/utf8,
            "the generated document and your running server will drift:\n"/utf8>>/binary,
        Bullets/binary>>.

-file("src/oaisp/cli.gleam", 149).
?DOC(
    " A path parameter that doesn't match the path template leaves an invalid\n"
    " OpenAPI 3.1 document. List the offenders — the endpoint and whether a\n"
    " parameter has no placeholder, or a placeholder has no parameter — so the\n"
    " missing declaration or the typo is obvious.\n"
).
-spec path_param_mismatches_message(
    list(oaisp@internal@merge:path_param_mismatch())
) -> binary().
path_param_mismatches_message(Mismatches) ->
    Bullets = begin
        _pipe = Mismatches,
        _pipe@1 = gleam@list:map(_pipe, fun(Mismatch) -> case Mismatch of
                    {param_without_placeholder, Method, Path, Name} ->
                        <<<<<<<<<<<<<<<<"  - "/utf8, Method/binary>>/binary,
                                                    " "/utf8>>/binary,
                                                Path/binary>>/binary,
                                            ": path parameter `"/utf8>>/binary,
                                        Name/binary>>/binary,
                                    "` has no matching `{"/utf8>>/binary,
                                Name/binary>>/binary,
                            "}` in the path"/utf8>>;

                    {placeholder_without_param, Method@1, Path@1, Name@1} ->
                        <<<<<<<<<<<<"  - "/utf8, Method@1/binary>>/binary,
                                            " "/utf8>>/binary,
                                        Path@1/binary>>/binary,
                                    ": `{"/utf8>>/binary,
                                Name@1/binary>>/binary,
                            "}` in the path has no declared path parameter"/utf8>>
                end end),
        gleam@string:join(_pipe@1, <<"\n"/utf8>>)
    end,
    <<<<<<"these path parameters don't match the path template — every `in: path` "/utf8,
                "parameter must name a `{placeholder}`, and every placeholder must be "/utf8>>/binary,
            "declared:\n"/utf8>>/binary,
        Bullets/binary>>.

-file("src/oaisp/cli.gleam", 135).
?DOC(
    " A `type_ref` that doesn't resolve would leave a dangling `$ref` in the\n"
    " document. List the offenders so the typo — or the missing `pub` — is obvious.\n"
).
-spec unresolved_refs_message(list({binary(), binary()})) -> binary().
unresolved_refs_message(Refs) ->
    Bullets = begin
        _pipe = Refs,
        _pipe@1 = gleam@list:map(
            _pipe,
            fun(Ref) ->
                <<<<<<"  - "/utf8, (erlang:element(1, Ref))/binary>>/binary,
                        "."/utf8>>/binary,
                    (erlang:element(2, Ref))/binary>>
            end
        ),
        gleam@string:join(_pipe@1, <<"\n"/utf8>>)
    end,
    <<<<"these type references don't resolve against the package interface — check the "/utf8,
            "module path and name, and that the type is public:\n"/utf8>>/binary,
        Bullets/binary>>.

-file("src/oaisp/cli.gleam", 186).
?DOC(
    " A malformed `@format` directive is dropped at emit time, so the format it\n"
    " asked for never reaches the schema. List the offenders — the type and the\n"
    " line — so the typo (a missing colon, an empty field or format) is obvious.\n"
).
-spec malformed_formats_message(list({binary(), binary(), binary()})) -> binary().
malformed_formats_message(Formats) ->
    Bullets = begin
        _pipe = Formats,
        _pipe@1 = gleam@list:map(
            _pipe,
            fun(Format) ->
                {Module, Name, Line} = Format,
                <<<<<<<<<<"  - "/utf8, Module/binary>>/binary, "."/utf8>>/binary,
                            Name/binary>>/binary,
                        ": "/utf8>>/binary,
                    Line/binary>>
            end
        ),
        gleam@string:join(_pipe@1, <<"\n"/utf8>>)
    end,
    <<<<"these `@format` directives are malformed — each must read `@format <field>: "/utf8,
            "<format>`:\n"/utf8>>/binary,
        Bullets/binary>>.

-file("src/oaisp/cli.gleam", 232).
-spec decode_inputs(binary(), binary()) -> {ok,
        {gleam@package_interface:package(), oaisp@internal@emit:document()}} |
    {error, binary()}.
decode_inputs(Package_interface_json, Endpoints_json) ->
    gleam@result:'try'(
        begin
            _pipe = oaisp@internal@package_interface:decode_string(
                Package_interface_json
            ),
            gleam@result:map_error(
                _pipe,
                fun(_) -> <<"could not decode the package interface"/utf8>> end
            )
        end,
        fun(Package) ->
            gleam@result:'try'(
                begin
                    _pipe@1 = oaisp@internal@emit:parse(Endpoints_json),
                    gleam@result:map_error(
                        _pipe@1,
                        fun(_) ->
                            <<"could not parse the emitted endpoints"/utf8>>
                        end
                    )
                end,
                fun(Document) -> {ok, {Package, Document}} end
            )
        end
    ).

-file("src/oaisp/cli.gleam", 91).
?DOC(
    " Build the OpenAPI document from the two JSON inputs the pipeline gathers.\n"
    " Pure, so the heart of `generate` is testable without shelling out.\n"
).
-spec build_document(binary(), binary()) -> {ok, binary()} | {error, binary()}.
build_document(Package_interface_json, Endpoints_json) ->
    gleam@result:'try'(
        decode_inputs(Package_interface_json, Endpoints_json),
        fun(_use0) ->
            {Package, Document} = _use0,
            case oaisp@internal@merge:duplicate_routes(
                erlang:element(3, Document)
            ) of
                [] ->
                    case oaisp@internal@merge:path_param_mismatches(
                        erlang:element(3, Document)
                    ) of
                        [] ->
                            case oaisp@internal@merge:unresolved_refs(
                                erlang:element(3, Document),
                                Package
                            ) of
                                [] ->
                                    case oaisp@internal@merge:malformed_formats(
                                        erlang:element(3, Document),
                                        Package
                                    ) of
                                        [] ->
                                            {ok,
                                                oaisp@internal@merge:to_string(
                                                    erlang:element(3, Document),
                                                    erlang:element(2, Document),
                                                    Package
                                                )};

                                        Formats ->
                                            {error,
                                                malformed_formats_message(
                                                    Formats
                                                )}
                                    end;

                                Refs ->
                                    {error, unresolved_refs_message(Refs)}
                            end;

                        Mismatches ->
                            {error, path_param_mismatches_message(Mismatches)}
                    end;

                Routes ->
                    {error, duplicate_routes_message(Routes)}
            end
        end
    ).

-file("src/oaisp/cli.gleam", 259).
-spec emit_endpoints() -> {ok, binary()} | {error, binary()}.
emit_endpoints() ->
    Output = oaisp@internal@exec:run(<<"gleam run -- --emit-endpoints"/utf8>>),
    case erlang:element(2, Output) of
        0 ->
            {ok, erlang:element(3, Output)};

        Code ->
            {error,
                <<"`gleam run -- --emit-endpoints` exited with "/utf8,
                    (erlang:integer_to_binary(Code))/binary>>}
    end.

-file("src/oaisp/cli.gleam", 270).
-spec read_file(binary()) -> {ok, binary()} | {error, binary()}.
read_file(Path) ->
    _pipe = oaisp@internal@fs:read(Path),
    gleam@result:map_error(
        _pipe,
        fun(Reason) ->
            <<<<<<"could not read "/utf8, Path/binary>>/binary, ": "/utf8>>/binary,
                Reason/binary>>
        end
    ).

-file("src/oaisp/cli.gleam", 247).
-spec export_package_interface() -> {ok, binary()} | {error, binary()}.
export_package_interface() ->
    Output = oaisp@internal@exec:run(
        <<"gleam export package-interface --out "/utf8,
            "build/oaisp_package_interface.json"/utf8>>
    ),
    case erlang:element(2, Output) of
        0 ->
            read_file(<<"build/oaisp_package_interface.json"/utf8>>);

        Code ->
            {error,
                <<"`gleam export package-interface` exited with "/utf8,
                    (erlang:integer_to_binary(Code))/binary>>}
    end.

-file("src/oaisp/cli.gleam", 216).
-spec package_interface_source(
    oaisp@internal@argv:options(),
    fun((binary()) -> nil)
) -> {ok, binary()} | {error, binary()}.
package_interface_source(Options, Log) ->
    case erlang:element(3, Options) of
        {some, Path} ->
            Log(<<"reading package interface from "/utf8, Path/binary>>),
            read_file(Path);

        none ->
            Log(<<"running `gleam export package-interface`"/utf8>>),
            export_package_interface()
    end.

-file("src/oaisp/cli.gleam", 203).
-spec gather(oaisp@internal@argv:options(), fun((binary()) -> nil)) -> {ok,
        {binary(), binary()}} |
    {error, binary()}.
gather(Options, Log) ->
    gleam@result:'try'(
        package_interface_source(Options, Log),
        fun(Package_interface_json) ->
            Log(<<"collecting endpoint declarations"/utf8>>),
            gleam@result:'try'(
                emit_endpoints(),
                fun(Endpoints_json) ->
                    {ok, {Package_interface_json, Endpoints_json}}
                end
            )
        end
    ).

-file("src/oaisp/cli.gleam", 291).
-spec log_status(binary()) -> nil.
log_status(Message) ->
    gleam_stdlib:println_error(<<"oaisp: "/utf8, Message/binary>>).

-file("src/oaisp/cli.gleam", 275).
-spec logger(oaisp@internal@argv:options()) -> fun((binary()) -> nil).
logger(Options) ->
    fun(Message) -> case erlang:element(4, Options) of
            true ->
                nil;

            false ->
                log_status(Message)
        end end.

-file("src/oaisp/cli.gleam", 60).
-spec run_generate(oaisp@internal@argv:options()) -> {ok, nil} |
    {error, binary()}.
run_generate(Options) ->
    Log = logger(Options),
    gleam@result:'try'(
        gather(Options, Log),
        fun(_use0) ->
            {Package_interface_json, Endpoints_json} = _use0,
            gleam@result:'try'(
                build_document(Package_interface_json, Endpoints_json),
                fun(Document) -> case erlang:element(2, Options) of
                        to_stdout ->
                            gleam_stdlib:println(Document),
                            {ok, nil};

                        {to_file, Path} ->
                            gleam@result:'try'(
                                begin
                                    _pipe = oaisp@internal@atomic_write:write(
                                        Path,
                                        Document
                                    ),
                                    gleam@result:map_error(
                                        _pipe,
                                        fun(Reason) ->
                                            <<<<<<"could not write "/utf8,
                                                        Path/binary>>/binary,
                                                    ": "/utf8>>/binary,
                                                Reason/binary>>
                                        end
                                    )
                                end,
                                fun(_) ->
                                    Log(<<"wrote "/utf8, Path/binary>>),
                                    {ok, nil}
                                end
                            )
                    end end
            )
        end
    ).

-file("src/oaisp/cli.gleam", 284).
-spec arg_error_message(oaisp@internal@argv:error()) -> binary().
arg_error_message(Error) ->
    case Error of
        {unknown_flag, Flag} ->
            <<<<"unknown flag `"/utf8, Flag/binary>>/binary, "`"/utf8>>;

        {missing_value, Flag@1} ->
            <<<<"`"/utf8, Flag@1/binary>>/binary, "` needs a value"/utf8>>
    end.

-file("src/oaisp/cli.gleam", 43).
-spec generate(list(binary())) -> nil.
generate(Arguments) ->
    case oaisp@internal@argv:parse(Arguments) of
        {error, Error} ->
            log_error(arg_error_message(Error)),
            erlang:halt(2);

        {ok, Options} ->
            case run_generate(Options) of
                {ok, nil} ->
                    nil;

                {error, Message} ->
                    log_error(Message),
                    erlang:halt(1)
            end
    end.

-file("src/oaisp/cli.gleam", 29).
?DOC(" CLI entrypoint.\n").
-spec main() -> nil.
main() ->
    case erlang:element(4, argv:load()) of
        [<<"generate"/utf8>> | Rest] ->
            generate(Rest);

        [] ->
            gleam_stdlib:println(help_text());

        [<<"help"/utf8>>] ->
            gleam_stdlib:println(help_text());

        [<<"--help"/utf8>>] ->
            gleam_stdlib:println(help_text());

        [<<"-h"/utf8>>] ->
            gleam_stdlib:println(help_text());

        [Command | _] ->
            log_error(
                <<<<"unknown command `"/utf8, Command/binary>>/binary,
                    "`"/utf8>>
            ),
            gleam_stdlib:println_error(help_text()),
            erlang:halt(2)
    end.