Current section

Files

Jump to
oaspec src oaspec@config.erl
Raw

src/oaspec@config.erl

-module(oaspec@config).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/oaspec/config.gleam").
-export([new/6, input/1, output_server/1, output_client/1, package/1, mode/1, validate/1, parse_mode/1, with_mode/2, with_validate/2, with_output/2, validate_output_package_match/1, validate_output_dir_layout/1, error_to_string/1, load/1]).
-export_type([config/0, generate_mode/0, config_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.
-opaque config() :: {config,
binary(),
binary(),
binary(),
binary(),
generate_mode(),
boolean()}.
-type generate_mode() :: server | client | both.
-type config_error() :: {file_not_found, binary()} |
{file_read_error, binary(), binary()} |
{parse_error, binary()} |
{missing_field, binary()} |
{invalid_value, binary(), binary()}.
-file("src/oaspec/config.gleam", 28).
?DOC(
" Construct a new `Config` from its six fields. Prefer `load/1` in\n"
" production code; `new/6` is primarily for tests and ad-hoc tooling\n"
" that assembles a config in memory.\n"
).
-spec new(binary(), binary(), binary(), binary(), generate_mode(), boolean()) -> config().
new(Input, Output_server, Output_client, Package, Mode, Validate) ->
{config, Input, Output_server, Output_client, Package, Mode, Validate}.
-file("src/oaspec/config.gleam", 40).
?DOC(" Path to the OpenAPI spec this config was built for.\n").
-spec input(config()) -> binary().
input(Cfg) ->
erlang:element(2, Cfg).
-file("src/oaspec/config.gleam", 45).
?DOC(" Output directory for server-side generated files.\n").
-spec output_server(config()) -> binary().
output_server(Cfg) ->
erlang:element(3, Cfg).
-file("src/oaspec/config.gleam", 50).
?DOC(" Output directory for client-side generated files.\n").
-spec output_client(config()) -> binary().
output_client(Cfg) ->
erlang:element(4, Cfg).
-file("src/oaspec/config.gleam", 55).
?DOC(" Gleam package name (module prefix) for generated files.\n").
-spec package(config()) -> binary().
package(Cfg) ->
erlang:element(5, Cfg).
-file("src/oaspec/config.gleam", 60).
?DOC(" Generation mode: server, client, or both.\n").
-spec mode(config()) -> generate_mode().
mode(Cfg) ->
erlang:element(6, Cfg).
-file("src/oaspec/config.gleam", 65).
?DOC(" Whether guard-based runtime validation is enabled.\n").
-spec validate(config()) -> boolean().
validate(Cfg) ->
erlang:element(7, Cfg).
-file("src/oaspec/config.gleam", 86).
?DOC(" Parse a mode string into GenerateMode.\n").
-spec parse_mode(binary()) -> {ok, generate_mode()} | {error, config_error()}.
parse_mode(Mode) ->
case Mode of
<<"server"/utf8>> ->
{ok, server};
<<"client"/utf8>> ->
{ok, client};
<<"both"/utf8>> ->
{ok, both};
_ ->
{error,
{invalid_value,
<<"mode"/utf8>>,
<<"must be one of: server, client, both"/utf8>>}}
end.
-file("src/oaspec/config.gleam", 216).
?DOC(" Apply CLI overrides to a config.\n").
-spec with_mode(config(), generate_mode()) -> config().
with_mode(Config, Mode) ->
{config,
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config),
Mode,
erlang:element(7, Config)}.
-file("src/oaspec/config.gleam", 221).
?DOC(" Apply validation mode override.\n").
-spec with_validate(config(), boolean()) -> config().
with_validate(Config, Validate) ->
{config,
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config),
erlang:element(6, Config),
Validate}.
-file("src/oaspec/config.gleam", 233).
?DOC(
" Apply output base directory override.\n"
" Derives server/client paths as <dir>/<package> and <dir>/<package>_client\n"
" in `Both` mode. In client-only mode the client path drops the suffix\n"
" (Issue #262) so generated `import <package>/...` lines resolve.\n"
"\n"
" The suffix decision reads `config.mode` at call time, so apply\n"
" `with_mode/2` before `with_output/2` if both overrides are needed —\n"
" otherwise the client path will reflect the previous mode's default.\n"
).
-spec with_output(config(), gleam@option:option(binary())) -> config().
with_output(Config, Output) ->
case Output of
{some, Dir} ->
{config,
erlang:element(2, Config),
<<<<Dir/binary, "/"/utf8>>/binary,
(erlang:element(5, Config))/binary>>,
case erlang:element(6, Config) of
client ->
<<<<Dir/binary, "/"/utf8>>/binary,
(erlang:element(5, Config))/binary>>;
server ->
<<<<<<Dir/binary, "/"/utf8>>/binary,
(erlang:element(5, Config))/binary>>/binary,
"_client"/utf8>>;
both ->
<<<<<<Dir/binary, "/"/utf8>>/binary,
(erlang:element(5, Config))/binary>>/binary,
"_client"/utf8>>
end,
erlang:element(5, Config),
erlang:element(6, Config),
erlang:element(7, Config)};
none ->
Config
end.
-file("src/oaspec/config.gleam", 302).
?DOC(" Get the basename of a path (last segment after /).\n").
-spec basename(binary()) -> binary().
basename(Path) ->
_pipe = Path,
_pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
_pipe@2 = gleam@list:last(_pipe@1),
gleam@result:unwrap(_pipe@2, <<""/utf8>>).
-file("src/oaspec/config.gleam", 257).
?DOC(
" Validate that output directory basenames are valid Gleam module names\n"
" usable as import roots.\n"
"\n"
" Server output must end in `<package>` so generated imports such as\n"
" `import <package>/types` resolve. Client output may end in either\n"
" `<package>` (when client lives in its own project) or `<package>_client`\n"
" (the new default since Issue #248 — both server and client share the same\n"
" `<dir>` and need distinct basenames). Anything else is a misconfigured\n"
" package/output mismatch the user should be told about.\n"
).
-spec validate_output_package_match(config()) -> {ok, nil} |
{error, config_error()}.
validate_output_package_match(Config) ->
_pipe = case erlang:element(6, Config) of
server ->
case basename(erlang:element(3, Config)) =:= erlang:element(
5,
Config
) of
true ->
{ok, nil};
false ->
{error,
{invalid_value,
<<"output.server"/utf8>>,
<<<<<<<<"Directory basename '"/utf8,
(basename(erlang:element(3, Config)))/binary>>/binary,
"' must match package '"/utf8>>/binary,
(erlang:element(5, Config))/binary>>/binary,
"'"/utf8>>}}
end;
both ->
case basename(erlang:element(3, Config)) =:= erlang:element(
5,
Config
) of
true ->
{ok, nil};
false ->
{error,
{invalid_value,
<<"output.server"/utf8>>,
<<<<<<<<"Directory basename '"/utf8,
(basename(erlang:element(3, Config)))/binary>>/binary,
"' must match package '"/utf8>>/binary,
(erlang:element(5, Config))/binary>>/binary,
"'"/utf8>>}}
end;
client ->
{ok, nil}
end,
gleam@result:'try'(_pipe, fun(_) -> case erlang:element(6, Config) of
client ->
Client_basename = basename(erlang:element(4, Config)),
Client_suffix = <<(erlang:element(5, Config))/binary,
"_client"/utf8>>,
case (Client_basename =:= erlang:element(5, Config)) orelse (Client_basename
=:= Client_suffix) of
true ->
{ok, nil};
false ->
{error,
{invalid_value,
<<"output.client"/utf8>>,
<<<<<<<<<<<<"Directory basename '"/utf8,
Client_basename/binary>>/binary,
"' must match package '"/utf8>>/binary,
(erlang:element(5, Config))/binary>>/binary,
"' or '"/utf8>>/binary,
Client_suffix/binary>>/binary,
"'"/utf8>>}}
end;
both ->
Client_basename = basename(erlang:element(4, Config)),
Client_suffix = <<(erlang:element(5, Config))/binary,
"_client"/utf8>>,
case (Client_basename =:= erlang:element(5, Config)) orelse (Client_basename
=:= Client_suffix) of
true ->
{ok, nil};
false ->
{error,
{invalid_value,
<<"output.client"/utf8>>,
<<<<<<<<<<<<"Directory basename '"/utf8,
Client_basename/binary>>/binary,
"' must match package '"/utf8>>/binary,
(erlang:element(5, Config))/binary>>/binary,
"' or '"/utf8>>/binary,
Client_suffix/binary>>/binary,
"'"/utf8>>}}
end;
server ->
{ok, nil}
end end).
-file("src/oaspec/config.gleam", 345).
-spec misplaced_src_error(binary(), binary()) -> config_error().
misplaced_src_error(Field, Path) ->
{invalid_value,
Field,
<<<<"'"/utf8, Path/binary>>/binary,
"' contains a 'src' segment that is not the immediate parent of the package directory. Generated code lands inside src/.../<package>/, but oaspec emits imports as `<package>/...`, which the Gleam compiler resolves relative to src/, not relative to <dir>. Either set the path so that 'src' is the direct parent of the package directory (e.g. './src'), or move the output outside any 'src/' tree (e.g. './gen') and treat that directory as a standalone Gleam project root with its own gleam.toml."/utf8>>}.
-file("src/oaspec/config.gleam", 354).
-spec path_has_misplaced_src(binary()) -> boolean().
path_has_misplaced_src(Path) ->
Segments = begin
_pipe = Path,
_pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
gleam@list:filter(
_pipe@1,
fun(S) -> (S /= <<""/utf8>>) andalso (S /= <<"."/utf8>>) end
)
end,
case lists:reverse(Segments) of
[_ | Rest_reversed] ->
Parent_segments = lists:reverse(Rest_reversed),
case gleam@list:last(Parent_segments) of
{ok, <<"src"/utf8>>} ->
false;
_ ->
gleam@list:any(
Parent_segments,
fun(S@1) -> S@1 =:= <<"src"/utf8>> end
)
end;
[] ->
false
end.
-file("src/oaspec/config.gleam", 322).
?DOC(
" Validate the on-disk layout implied by `output.dir`.\n"
"\n"
" Issue #319: when `output.dir` is something like `./src/gen`, generated\n"
" code lands at `src/gen/<pkg>/types.gleam` whose Gleam module path is\n"
" `gen/<pkg>/types` — but oaspec emits `import <pkg>/types`, which the\n"
" compiler can't resolve. Catch this at config time so the user sees a\n"
" clear error instead of a wall of `Unknown module ...` from\n"
" `gleam build`.\n"
"\n"
" Heuristic: in the path leading up to the package directory, `src`\n"
" must either be the immediate parent of the package (the \"<dir> is\n"
" the project's src/\" pattern) or be absent (the standalone-Gleam-\n"
" project pattern). `src` in any other position is the foot-gun.\n"
).
-spec validate_output_dir_layout(config()) -> {ok, nil} |
{error, config_error()}.
validate_output_dir_layout(Config) ->
_pipe = case erlang:element(6, Config) of
server ->
case path_has_misplaced_src(erlang:element(3, Config)) of
false ->
{ok, nil};
true ->
{error,
misplaced_src_error(
<<"output.server"/utf8>>,
erlang:element(3, Config)
)}
end;
both ->
case path_has_misplaced_src(erlang:element(3, Config)) of
false ->
{ok, nil};
true ->
{error,
misplaced_src_error(
<<"output.server"/utf8>>,
erlang:element(3, Config)
)}
end;
client ->
{ok, nil}
end,
gleam@result:'try'(_pipe, fun(_) -> case erlang:element(6, Config) of
client ->
case path_has_misplaced_src(erlang:element(4, Config)) of
false ->
{ok, nil};
true ->
{error,
misplaced_src_error(
<<"output.client"/utf8>>,
erlang:element(4, Config)
)}
end;
both ->
case path_has_misplaced_src(erlang:element(4, Config)) of
false ->
{ok, nil};
true ->
{error,
misplaced_src_error(
<<"output.client"/utf8>>,
erlang:element(4, Config)
)}
end;
server ->
{ok, nil}
end end).
-file("src/oaspec/config.gleam", 377).
?DOC(" Convert config error to a human-readable string.\n").
-spec error_to_string(config_error()) -> binary().
error_to_string(Error) ->
case Error of
{file_not_found, Path} ->
<<<<"Config file not found: "/utf8, Path/binary>>/binary,
" (paths resolve relative to the current working directory)"/utf8>>;
{file_read_error, Path@1, Detail} ->
<<<<<<"Error reading config file "/utf8, Path@1/binary>>/binary,
": "/utf8>>/binary,
Detail/binary>>;
{parse_error, Detail@1} ->
<<"Config parse error: "/utf8, Detail@1/binary>>;
{missing_field, Field} ->
<<"Missing required config field: "/utf8, Field/binary>>;
{invalid_value, Field@1, Detail@2} ->
<<<<<<"Invalid value for "/utf8, Field@1/binary>>/binary,
": "/utf8>>/binary,
Detail@2/binary>>
end.
-file("src/oaspec/config.gleam", 393).
?DOC(" Extract a nested string value from YAML like output.server.\n").
-spec extract_nested_string(yay:node_(), binary(), binary()) -> gleam@option:option(binary()).
extract_nested_string(Root, Key1, Key2) ->
case yay:select_sugar(
Root,
<<<<Key1/binary, "."/utf8>>/binary, Key2/binary>>
) of
{ok, {node_str, Value}} ->
{some, Value};
_ ->
none
end.
-file("src/oaspec/config.gleam", 405).
?DOC(" Convert a yay YAML error to string.\n").
-spec yaml_error_to_string(yay:yaml_error()) -> binary().
yaml_error_to_string(Error) ->
case Error of
unexpected_parsing_error ->
<<"Unexpected parsing error"/utf8>>;
{parsing_error, Msg, _} ->
Msg
end.
-file("src/oaspec/config.gleam", 100).
?DOC(" Load config from a YAML file.\n").
-spec load(binary()) -> {ok, config()} | {error, config_error()}.
load(Path) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(Path),
gleam@result:map_error(_pipe, fun(E) -> case E of
enoent ->
{file_not_found, Path};
_ ->
{file_read_error,
Path,
<<"Failed to read file: "/utf8,
(simplifile:describe_error(E))/binary>>}
end end)
end,
fun(Content) ->
gleam@result:'try'(
begin
_pipe@1 = yaml_ffi:parse_string(Content),
gleam@result:map_error(
_pipe@1,
fun(E@1) ->
{parse_error,
<<"YAML parse error: "/utf8,
(yaml_error_to_string(E@1))/binary>>}
end
)
end,
fun(Docs) -> gleam@result:'try'(case Docs of
[First | _] ->
{ok, First};
[] ->
{error,
{parse_error,
<<"Empty YAML document"/utf8>>}}
end, fun(Doc) ->
Root = yay:document_root(Doc),
gleam@result:'try'(
begin
_pipe@2 = yay:extract_string(
Root,
<<"input"/utf8>>
),
gleam@result:map_error(
_pipe@2,
fun(_) ->
{missing_field, <<"input"/utf8>>}
end
)
end,
fun(Input) ->
Package = begin
_pipe@3 = yay:extract_optional_string(
Root,
<<"package"/utf8>>
),
_pipe@4 = gleam@result:unwrap(
_pipe@3,
none
),
gleam@option:unwrap(
_pipe@4,
<<"api"/utf8>>
)
end,
gleam@result:'try'(
case begin
_pipe@5 = yay:extract_optional_string(
Root,
<<"mode"/utf8>>
),
gleam@result:unwrap(_pipe@5, none)
end of
{some, <<"server"/utf8>>} ->
{ok, server};
{some, <<"client"/utf8>>} ->
{ok, client};
{some, <<"both"/utf8>>} ->
{ok, both};
none ->
{ok, both};
{some, Other} ->
{error,
{invalid_value,
<<"mode"/utf8>>,
<<<<"must be one of: server, client, both (got: "/utf8,
Other/binary>>/binary,
")"/utf8>>}}
end,
fun(Mode) ->
Output_dir = begin
_pipe@6 = extract_nested_string(
Root,
<<"output"/utf8>>,
<<"dir"/utf8>>
),
gleam@option:unwrap(
_pipe@6,
<<"./gen"/utf8>>
)
end,
Server_default = <<<<Output_dir/binary,
"/"/utf8>>/binary,
Package/binary>>,
Client_default = case Mode of
client ->
<<<<Output_dir/binary,
"/"/utf8>>/binary,
Package/binary>>;
server ->
<<<<<<Output_dir/binary,
"/"/utf8>>/binary,
Package/binary>>/binary,
"_client"/utf8>>;
both ->
<<<<<<Output_dir/binary,
"/"/utf8>>/binary,
Package/binary>>/binary,
"_client"/utf8>>
end,
Output_server = begin
_pipe@7 = extract_nested_string(
Root,
<<"output"/utf8>>,
<<"server"/utf8>>
),
gleam@option:unwrap(
_pipe@7,
Server_default
)
end,
Output_client = begin
_pipe@8 = extract_nested_string(
Root,
<<"output"/utf8>>,
<<"client"/utf8>>
),
gleam@option:unwrap(
_pipe@8,
Client_default
)
end,
gleam@result:'try'(
case yay:select_sugar(
Root,
<<"validate"/utf8>>
) of
{ok, {node_bool, true}} ->
{ok, true};
{ok,
{node_str,
<<"true"/utf8>>}} ->
{ok, true};
{ok, {node_bool, false}} ->
{ok, false};
{ok,
{node_str,
<<"false"/utf8>>}} ->
{ok, false};
{error, _} ->
case Mode of
server ->
{ok, true};
both ->
{ok, true};
client ->
{ok, false}
end;
{ok, _} ->
{error,
{invalid_value,
<<"validate"/utf8>>,
<<"must be a boolean (true or false)"/utf8>>}}
end,
fun(Validate) ->
{ok,
{config,
Input,
Output_server,
Output_client,
Package,
Mode,
Validate}}
end
)
end
)
end
)
end) end
)
end
).