-module(oaspec@config). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/oaspec/config.gleam"). -export([empty_include/0, include_is_empty/1, new/6, input/1, output_server/1, output_client/1, package/1, mode/1, validate/1, include/1, parse_mode/1, with_mode/2, with_validate/2, with_include/2, with_output/2, validate_output_package_match/1, validate_output_dir_layout/1, error_to_string/1, load_all/1, load/1]). -export_type([config/0, include/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(), include()}. -type include() :: {include, list(binary()), list(binary())}. -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", 47). ?DOC( " The default include filter — matches everything. Equivalent to\n" " `oaspec.yaml` omitting the `include:` key entirely.\n" ). -spec empty_include() -> include(). empty_include() -> {include, [], []}. -file("src/oaspec/config.gleam", 52). ?DOC(" True when no filter is active (both tag and path lists empty).\n"). -spec include_is_empty(include()) -> boolean(). include_is_empty(Include) -> gleam@list:is_empty(erlang:element(2, Include)) andalso gleam@list:is_empty( erlang:element(3, Include) ). -file("src/oaspec/config.gleam", 61). ?DOC( " Construct a new `Config` from its six required fields. Prefer\n" " `load/1` in production code; `new/6` is primarily for tests and\n" " ad-hoc tooling that assembles a config in memory. The include\n" " filter defaults to `empty_include()` (match everything); use\n" " `with_include/2` to override.\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, empty_include()}. -file("src/oaspec/config.gleam", 81). ?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", 86). ?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", 91). ?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", 96). ?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", 101). ?DOC(" Generation mode: server, client, or both.\n"). -spec mode(config()) -> generate_mode(). mode(Cfg) -> erlang:element(6, Cfg). -file("src/oaspec/config.gleam", 106). ?DOC(" Whether guard-based runtime validation is enabled.\n"). -spec validate(config()) -> boolean(). validate(Cfg) -> erlang:element(7, Cfg). -file("src/oaspec/config.gleam", 111). ?DOC(" Operation include filter (tags / paths). Issue #387.\n"). -spec include(config()) -> include(). include(Cfg) -> erlang:element(8, Cfg). -file("src/oaspec/config.gleam", 132). ?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", 408). ?DOC( " Read a list-of-strings YAML field that may be absent, in which\n" " case an empty list is returned. `field_path` is the dotted name\n" " shown in any `InvalidValue` diagnostic.\n" ). -spec extract_optional_string_list(yay:node_(), binary(), binary()) -> {ok, list(binary())} | {error, config_error()}. extract_optional_string_list(Node, Field_path, Key) -> case yay:extract_string_list(Node, Key) of {ok, Values} -> {ok, Values}; {error, _} -> case yay:select_sugar(Node, Key) of {error, _} -> {ok, []}; {ok, _} -> {error, {invalid_value, Field_path, <<"must be a list of strings"/utf8>>}} end end. -file("src/oaspec/config.gleam", 385). ?DOC( " Parse the optional `include:` block. Returns `empty_include()`\n" " when the key is absent so the absence path stays cheap.\n" ). -spec parse_include(yay:node_()) -> {ok, include()} | {error, config_error()}. parse_include(Root) -> case yay:select_sugar(Root, <<"include"/utf8>>) of {error, _} -> {ok, empty_include()}; {ok, Include_node} -> gleam@result:'try'( extract_optional_string_list( Include_node, <<"include.tags"/utf8>>, <<"tags"/utf8>> ), fun(Tags) -> gleam@result:'try'( extract_optional_string_list( Include_node, <<"include.paths"/utf8>>, <<"paths"/utf8>> ), fun(Paths) -> {ok, {include, Tags, Paths}} end ) end ) end. -file("src/oaspec/config.gleam", 430). ?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), erlang:element(8, Config)}. -file("src/oaspec/config.gleam", 435). ?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, erlang:element(8, Config)}. -file("src/oaspec/config.gleam", 441). ?DOC( " Apply include-filter override (Issue #387). Useful for tests\n" " that build a `Config` in memory.\n" ). -spec with_include(config(), include()) -> config(). with_include(Config, Include) -> {config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config), Include}. -file("src/oaspec/config.gleam", 453). ?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), erlang:element(8, Config)}; none -> Config end. -file("src/oaspec/config.gleam", 554). ?DOC( " Split a package name into its slash-separated segments. Empty\n" " entries are dropped so trailing slashes and accidental double\n" " slashes are tolerated. `\"api\"` → `[\"api\"]`,\n" " `\"dco_check/github\"` → `[\"dco_check\", \"github\"]`.\n" ). -spec package_segments(binary()) -> list(binary()). package_segments(Package) -> _pipe = Package, _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>), gleam@list:filter(_pipe@1, fun(S) -> S /= <<""/utf8>> end). -file("src/oaspec/config.gleam", 563). ?DOC( " Split a filesystem path into its segments, dropping `.` and empty\n" " entries so `./src/foo/`, `src/foo`, and `./src//foo` all yield\n" " `[\"src\", \"foo\"]`.\n" ). -spec path_segments(binary()) -> list(binary()). path_segments(Path) -> _pipe = Path, _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>), gleam@list:filter( _pipe@1, fun(S) -> (S /= <<""/utf8>>) andalso (S /= <<"."/utf8>>) end ). -file("src/oaspec/config.gleam", 571). ?DOC( " Return the last `n` entries of `items`, preserving order. If\n" " `items` has fewer than `n` entries, returns `items` unchanged.\n" ). -spec last_n(list(HEW), integer()) -> list(HEW). last_n(Items, N) -> _pipe = Items, _pipe@1 = lists:reverse(_pipe), _pipe@2 = gleam@list:take(_pipe@1, N), lists:reverse(_pipe@2). -file("src/oaspec/config.gleam", 577). ?DOC( " Drop the last `n` entries of `items`, preserving order. If\n" " `items` has fewer than `n` entries, returns `[]`.\n" ). -spec drop_last_n(list(HEZ), integer()) -> list(HEZ). drop_last_n(Items, N) -> _pipe = Items, _pipe@1 = lists:reverse(_pipe), _pipe@2 = gleam@list:drop(_pipe@1, N), lists:reverse(_pipe@2). -file("src/oaspec/config.gleam", 585). ?DOC( " Append `suffix` to the LAST segment of `segments`. Empty input\n" " stays empty. `[\"a\",\"b\"]` + `\"_client\"` → `[\"a\",\"b_client\"]`. Used\n" " to derive the client-output spelling for nested packages while\n" " keeping all but the leaf intact.\n" ). -spec suffix_last_segment(list(binary()), binary()) -> list(binary()). suffix_last_segment(Segments, Suffix) -> case lists:reverse(Segments) of [] -> []; [Last | Rest] -> lists:reverse([<> | Rest]) end. -file("src/oaspec/config.gleam", 518). -spec validate_path_tail_matches_package( binary(), binary(), list(binary()), boolean() ) -> {ok, nil} | {error, config_error()}. validate_path_tail_matches_package(Field, Path, Pkg, Allow_client_suffix) -> Tail = last_n(path_segments(Path), erlang:length(Pkg)), Pkg_with_suffix = suffix_last_segment(Pkg, <<"_client"/utf8>>), case {Tail =:= Pkg, Allow_client_suffix andalso (Tail =:= Pkg_with_suffix)} of {true, _} -> {ok, nil}; {_, true} -> {ok, nil}; {false, false} -> {error, {invalid_value, Field, case Allow_client_suffix of true -> <<<<<<<<<<<<"Output path tail '"/utf8, (gleam@string:join( Tail, <<"/"/utf8>> ))/binary>>/binary, "' must match package '"/utf8>>/binary, (gleam@string:join( Pkg, <<"/"/utf8>> ))/binary>>/binary, "' or '"/utf8>>/binary, (gleam@string:join( Pkg_with_suffix, <<"/"/utf8>> ))/binary>>/binary, "'"/utf8>>; false -> <<<<<<<<"Output path tail '"/utf8, (gleam@string:join( Tail, <<"/"/utf8>> ))/binary>>/binary, "' must match package '"/utf8>>/binary, (gleam@string:join(Pkg, <<"/"/utf8>>))/binary>>/binary, "'"/utf8>> end}} end. -file("src/oaspec/config.gleam", 485). ?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" "\n" " Nested packages (Issue #387): a `package` containing `/` such as\n" " `dco_check/github` declares an N-segment Gleam module path. The path\n" " tail compared against the package is N segments deep — the LAST N\n" " segments of the output path must equal the package's segments. The\n" " `_client` suffix attaches to the LAST package segment only, matching\n" " the single-segment behaviour (`dco_check/github` →\n" " `dco_check/github_client`, never `dco_check_client/github`).\n" ). -spec validate_output_package_match(config()) -> {ok, nil} | {error, config_error()}. validate_output_package_match(Config) -> Pkg = package_segments(erlang:element(5, Config)), case Pkg of [] -> {ok, nil}; _ -> _pipe = case erlang:element(6, Config) of server -> validate_path_tail_matches_package( <<"output.server"/utf8>>, erlang:element(3, Config), Pkg, false ); both -> validate_path_tail_matches_package( <<"output.server"/utf8>>, erlang:element(3, Config), Pkg, false ); client -> {ok, nil} end, gleam@result:'try'( _pipe, fun(_) -> case erlang:element(6, Config) of client -> validate_path_tail_matches_package( <<"output.client"/utf8>>, erlang:element(4, Config), Pkg, true ); both -> validate_path_tail_matches_package( <<"output.client"/utf8>>, erlang:element(4, Config), Pkg, true ); server -> {ok, nil} end end ) end. -file("src/oaspec/config.gleam", 642). -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/...//, but oaspec emits imports as `/...`, which the Gleam compiler resolves relative to src/, not relative to . 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", 651). -spec path_has_misplaced_src(binary(), integer()) -> boolean(). path_has_misplaced_src(Path, Package_segment_count) -> Parent_segments = drop_last_n(path_segments(Path), Package_segment_count), Earlier_parents = drop_last_n(Parent_segments, 1), case gleam@list:last(Parent_segments) of {ok, <<"src"/utf8>>} -> gleam@list:any(Earlier_parents, fun(S) -> S =:= <<"src"/utf8>> end); _ -> gleam@list:any( Parent_segments, fun(S@1) -> S@1 =:= <<"src"/utf8>> end ) end. -file("src/oaspec/config.gleam", 613). ?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//types.gleam` whose Gleam module path is\n" " `gen//types` — but oaspec emits `import /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's top-level\n" " directory, `src` must either be the immediate parent (the \"\n" " is the project's src/\" pattern) or be absent (the standalone-\n" " Gleam-project pattern). `src` in any other position is the\n" " foot-gun.\n" "\n" " Nested packages (Issue #387): for a package like `dco_check/github`\n" " the \"package directory\" is the last 2 path segments, and the rule\n" " applies to whatever sits BEFORE that pair. So `./src/dco_check/github`\n" " has parent chain `[src]` (immediate parent `src` → ok), and\n" " `./pkg/src/foo/dco_check/github` has parent chain\n" " `[pkg, src, foo]` (last is `foo` and `src` appears earlier → bad).\n" ). -spec validate_output_dir_layout(config()) -> {ok, nil} | {error, config_error()}. validate_output_dir_layout(Config) -> Segment_count = erlang:length(package_segments(erlang:element(5, Config))), case Segment_count of 0 -> {ok, nil}; _ -> _pipe = case erlang:element(6, Config) of server -> case path_has_misplaced_src( erlang:element(3, Config), Segment_count ) 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), Segment_count ) 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), Segment_count ) 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), Segment_count ) of false -> {ok, nil}; true -> {error, misplaced_src_error( <<"output.client"/utf8>>, erlang:element(4, Config) )} end; server -> {ok, nil} end end ) end. -file("src/oaspec/config.gleam", 668). ?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", 684). ?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", 308). ?DOC( " Parse a single target node — either the YAML root (legacy\n" " single-target config) or one element of the `targets:` sequence\n" " — into a fully-formed `Config`. The shared `input` / `mode` /\n" " `validate` come from the top-level YAML and are baked into the\n" " returned value.\n" ). -spec parse_target_node(yay:node_(), binary(), generate_mode(), boolean()) -> {ok, config()} | {error, config_error()}. parse_target_node(Node, Input, Mode, Validate) -> Package = oaspec@internal@openapi@parser_value:string_default( Node, <<"package"/utf8>>, <<"api"/utf8>> ), Output_dir = begin _pipe = extract_nested_string(Node, <<"output"/utf8>>, <<"dir"/utf8>>), gleam@option:unwrap(_pipe, <<"./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@1 = extract_nested_string( Node, <<"output"/utf8>>, <<"server"/utf8>> ), gleam@option:unwrap(_pipe@1, Server_default) end, Output_client = begin _pipe@2 = extract_nested_string( Node, <<"output"/utf8>>, <<"client"/utf8>> ), gleam@option:unwrap(_pipe@2, Client_default) end, gleam@result:'try'( parse_include(Node), fun(Include) -> {ok, {config, Input, Output_server, Output_client, Package, Mode, Validate, Include}} end ). -file("src/oaspec/config.gleam", 361). -spec parse_target_item( yay:node_(), integer(), binary(), generate_mode(), boolean() ) -> {ok, config()} | {error, config_error()}. parse_target_item(Node, Idx, Input, Mode, Validate) -> case oaspec@internal@openapi@parser_value:optional_string( Node, <<"package"/utf8>> ) of none -> {error, {invalid_value, <<<<"targets["/utf8, (erlang:integer_to_binary(Idx))/binary>>/binary, "].package"/utf8>>, <<"each target must declare a package name"/utf8>>}}; {some, _} -> parse_target_node(Node, Input, Mode, Validate) end. -file("src/oaspec/config.gleam", 696). ?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", 718). -spec extension_of(binary()) -> gleam@option:option(binary()). extension_of(Path) -> Last_segment = case gleam@list:last(gleam@string:split(Path, <<"/"/utf8>>)) of {ok, Seg} -> Seg; {error, _} -> Path end, case gleam@string:split(Last_segment, <<"."/utf8>>) of [_] -> none; [_ | Rest] -> case gleam@list:last(Rest) of {ok, Ext} -> {some, Ext}; {error, _} -> none end; [] -> none end. -file("src/oaspec/config.gleam", 710). ?DOC( " Issue #524: detect a config path whose extension is clearly not\n" " YAML so we can short-circuit with a friendly diagnostic before\n" " the YAML parser hits a shape mismatch and crashes. Returns the\n" " extension (lowercased, without the dot) when the file is plainly\n" " non-YAML; `None` for `.yaml` / `.yml` / no extension (which we\n" " pass through to yay so it can still surface a real parse error\n" " for malformed content).\n" ). -spec non_yaml_extension(binary()) -> gleam@option:option(binary()). non_yaml_extension(Path) -> Lowered = string:lowercase(Path), case extension_of(Lowered) of {some, <<"yaml"/utf8>>} -> none; {some, <<"yml"/utf8>>} -> none; none -> none; {some, Ext} -> {some, Ext} end. -file("src/oaspec/config.gleam", 177). ?DOC( " Load every target declared by an oaspec.yaml. A config without a\n" " `targets:` key is treated as a single implicit target whose\n" " fields come from the top-level `package:`, `output:`, and\n" " `include:` keys (the legacy single-target shape). A config with\n" " a `targets:` sequence yields one `Config` per element, each\n" " carrying its own `package` / `output_*` / `include` while\n" " sharing the top-level `input:`, `mode:`, and `validate:` values.\n" ). -spec load_all(binary()) -> {ok, list(config())} | {error, config_error()}. load_all(Path) -> gleam@result:'try'(case non_yaml_extension(Path) of {some, Ext} -> {error, {parse_error, <<<<"config files must be YAML; got '."/utf8, Ext/binary>>/binary, "' extension. Try renaming to '.yaml' (or '.yml')."/utf8>>}}; none -> {ok, nil} end, fun(_) -> 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) -> gleam@result:'try'( case oaspec@internal@openapi@parser_value:optional_string( Root, <<"mode"/utf8>> ) 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) -> 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) -> case yay:select_sugar( Root, <<"targets"/utf8>> ) of {error, _} -> gleam@result:'try'( parse_target_node( Root, Input, Mode, Validate ), fun(Cfg) -> {ok, [Cfg]} end ); {ok, {node_seq, Items}} -> case Items of [] -> {error, {invalid_value, <<"targets"/utf8>>, <<"must declare at least one target"/utf8>>}}; _ -> _pipe@3 = Items, _pipe@4 = gleam@list:index_map( _pipe@3, fun( Item, Idx ) -> parse_target_item( Item, Idx, Input, Mode, Validate ) end ), gleam@result:all( _pipe@4 ) end; {ok, _} -> {error, {invalid_value, <<"targets"/utf8>>, <<"must be a sequence of target objects"/utf8>>}} end end ) end ) end ) end) end ) end ) end). -file("src/oaspec/config.gleam", 153). ?DOC( " Load config from a YAML file.\n" "\n" " Returns the single target the YAML declares. When the YAML has a\n" " `targets:` array with more than one entry, this errors out with\n" " a multi-target hint — callers that need to handle the multi-\n" " target case must use `load_all/1` instead. For ordinary\n" " single-target configs (no `targets:` key, or a `targets:` array\n" " with exactly one entry) the result is the same as before.\n" ). -spec load(binary()) -> {ok, config()} | {error, config_error()}. load(Path) -> gleam@result:'try'(load_all(Path), fun(Targets) -> case Targets of [Single] -> {ok, Single}; [_, _ | _] -> {error, {invalid_value, <<"targets"/utf8>>, <<"config declares multiple targets; use config.load_all/1 to retrieve the full list"/utf8>>}}; [] -> {error, {invalid_value, <<"targets"/utf8>>, <<"must declare at least one target"/utf8>>}} end end).