-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, 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 / and /_client\n" " in `Both` mode. In client-only mode the client path drops the suffix\n" " (Issue #262) so generated `import /...` 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), <<<>/binary, (erlang:element(5, Config))/binary>>, case erlang:element(6, Config) of client -> <<<>/binary, (erlang:element(5, Config))/binary>>; server -> <<<<<>/binary, (erlang:element(5, Config))/binary>>/binary, "_client"/utf8>>; both -> <<<<<>/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 `` so generated imports such as\n" " `import /types` resolve. Client output may end in either\n" " `` (when client lives in its own project) or `_client`\n" " (the new default since Issue #248 — both server and client share the same\n" " `` 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", 310). ?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", 326). ?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, <<<>/binary, Key2/binary>> ) of {ok, {node_str, Value}} -> {some, Value}; _ -> none end. -file("src/oaspec/config.gleam", 338). ?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 = <<<>/binary, Package/binary>>, Client_default = case Mode of client -> <<<>/binary, Package/binary>>; server -> <<<<<>/binary, Package/binary>>/binary, "_client"/utf8>>; both -> <<<<<>/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 ).