Skip to main content

src/version_bump@config.erl

-module(version_bump@config).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/config.gleam").
-export([default/0, parse_package_json_config/1, parse_gleam_toml_config/1, parse_toml_config/1, parse_json_config/1, load/1]).
-export_type([plugin_spec/0, branch_config/0, config/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(
    " Configuration shapes plus the entry point for loading config from a project.\n"
    " The `load` implementation (reading .releaserc / release.config / package.json)\n"
    " is fleshed out by the config module work; the type is owned here so every\n"
    " other module can depend on it without a cycle.\n"
).

-type plugin_spec() :: {plugin_spec,
        binary(),
        gleam@dict:dict(binary(), binary())}.

-type branch_config() :: {branch_config,
        binary(),
        gleam@option:option(binary()),
        gleam@option:option(binary()),
        gleam@option:option(binary())}.

-type config() :: {config,
        gleam@option:option(binary()),
        binary(),
        list(branch_config()),
        list(plugin_spec()),
        boolean(),
        boolean(),
        version_bump@semver:versioning_mode()}.

-file("src/version_bump/config.gleam", 58).
?DOC(
    " The default configuration for a Gleam project: commit analysis, release\n"
    " notes, publishing to Hex, and a GitHub release, over the conventional release\n"
    " branches. (This is the Gleam-first analogue of semantic-release's defaults,\n"
    " which publish to npm; swap `hex` for `npm` to release a JavaScript package.)\n"
).
-spec default() -> config().
default() ->
    {config,
        none,
        <<"v${version}"/utf8>>,
        [{branch_config, <<"main"/utf8>>, none, none, none},
            {branch_config, <<"master"/utf8>>, none, none, none},
            {branch_config,
                <<"next"/utf8>>,
                {some, <<"next"/utf8>>},
                none,
                none},
            {branch_config,
                <<"beta"/utf8>>,
                none,
                {some, <<"beta"/utf8>>},
                none},
            {branch_config,
                <<"alpha"/utf8>>,
                none,
                {some, <<"alpha"/utf8>>},
                none}],
        [{plugin_spec, <<"commit-analyzer"/utf8>>, maps:new()},
            {plugin_spec, <<"release-notes-generator"/utf8>>, maps:new()},
            {plugin_spec, <<"hex"/utf8>>, maps:new()},
            {plugin_spec, <<"git"/utf8>>, maps:new()},
            {plugin_spec, <<"github"/utf8>>, maps:new()}],
        false,
        true,
        stable}.

-file("src/version_bump/config.gleam", 83).
?DOC(" Map the boolean `initial_development` config key to a `VersioningMode`.\n").
-spec versioning_mode_of(boolean()) -> version_bump@semver:versioning_mode().
versioning_mode_of(Initial_development) ->
    case Initial_development of
        true ->
            initial_development;

        false ->
            stable
    end.

-file("src/version_bump/config.gleam", 154).
?DOC(
    " Read a file, returning `Some(contents)` when it can be read and `None`\n"
    " otherwise. Unreadable or missing files are treated as absent so the loader\n"
    " falls through to the next candidate (and ultimately to `default()`).\n"
).
-spec read_if_present(binary()) -> gleam@option:option(binary()).
read_if_present(Path) ->
    case simplifile:read(Path) of
        {ok, Contents} ->
            {some, Contents};

        {error, _} ->
            none
    end.

-file("src/version_bump/config.gleam", 163).
?DOC(
    " Join a directory and a filename with a single `/` separator, tolerating a\n"
    " trailing slash on the directory.\n"
).
-spec join_path(binary(), binary()) -> binary().
join_path(Dir, Name) ->
    case gleam_stdlib:string_ends_with(Dir, <<"/"/utf8>>) of
        true ->
            <<Dir/binary, Name/binary>>;

        false ->
            <<<<Dir/binary, "/"/utf8>>/binary, Name/binary>>
    end.

-file("src/version_bump/config.gleam", 135).
-spec try_sources(
    binary(),
    list({binary(),
        fun((binary()) -> {ok, config()} |
            {error, version_bump@error:release_error()})})
) -> gleam@option:option({ok, config()} |
    {error, version_bump@error:release_error()}).
try_sources(Cwd, Sources) ->
    case Sources of
        [] ->
            none;

        [{Filename, Parser} | Rest] ->
            Path = join_path(Cwd, Filename),
            case read_if_present(Path) of
                {some, Contents} ->
                    {some, Parser(Contents)};

                none ->
                    try_sources(Cwd, Rest)
            end
    end.

-file("src/version_bump/config.gleam", 326).
-spec describe_decode_error(gleam@dynamic@decode:decode_error()) -> binary().
describe_decode_error(Err) ->
    <<<<<<<<"expected "/utf8, (erlang:element(2, Err))/binary>>/binary,
                ", found "/utf8>>/binary,
            (erlang:element(3, Err))/binary>>/binary,
        (case erlang:element(4, Err) of
            [] ->
                <<""/utf8>>;

            Path ->
                <<" at "/utf8, (gleam@string:join(Path, <<"."/utf8>>))/binary>>
        end)/binary>>.

-file("src/version_bump/config.gleam", 315).
-spec describe_json_error(gleam@json:decode_error()) -> binary().
describe_json_error(Err) ->
    case Err of
        unexpected_end_of_input ->
            <<"unexpected end of input"/utf8>>;

        {unexpected_byte, Byte} ->
            <<"unexpected byte "/utf8, Byte/binary>>;

        {unexpected_sequence, Seq} ->
            <<"unexpected sequence "/utf8, Seq/binary>>;

        {unable_to_decode, Errors} ->
            <<"unable to decode: "/utf8,
                (gleam@string:join(
                    gleam@list:map(Errors, fun describe_decode_error/1),
                    <<"; "/utf8>>
                ))/binary>>
    end.

-file("src/version_bump/config.gleam", 306).
?DOC(
    " Decode any scalar JSON value (string / bool / int / float) into its string\n"
    " form. Values that are not scalars decode to an empty string.\n"
).
-spec scalar_to_string_decoder() -> gleam@dynamic@decode:decoder(binary()).
scalar_to_string_decoder() ->
    gleam@dynamic@decode:one_of(
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        [begin
                _pipe = {decoder, fun gleam@dynamic@decode:decode_bool/1},
                gleam@dynamic@decode:map(_pipe, fun gleam@bool:to_string/1)
            end,
            begin
                _pipe@1 = {decoder, fun gleam@dynamic@decode:decode_int/1},
                gleam@dynamic@decode:map(
                    _pipe@1,
                    fun erlang:integer_to_binary/1
                )
            end,
            begin
                _pipe@2 = {decoder, fun gleam@dynamic@decode:decode_float/1},
                gleam@dynamic@decode:map(
                    _pipe@2,
                    fun gleam_stdlib:float_to_string/1
                )
            end,
            gleam@dynamic@decode:success(<<""/utf8>>)]
    ).

-file("src/version_bump/config.gleam", 300).
?DOC(
    " Plugin options are kept as a string-keyed dictionary of stringified scalar\n"
    " values. Non-scalar values (nested objects / arrays) are skipped, since the\n"
    " core keeps options as strings and individual plugins reparse what they need.\n"
).
-spec options_decoder() -> gleam@dynamic@decode:decoder(gleam@dict:dict(binary(), binary())).
options_decoder() ->
    gleam@dynamic@decode:dict(
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        scalar_to_string_decoder()
    ).

-file("src/version_bump/config.gleam", 285).
?DOC(
    " A plugin entry is either a bare string (the module name with no options) or\n"
    " a two-element array of `[name, options]`.\n"
).
-spec plugin_decoder() -> gleam@dynamic@decode:decoder(plugin_spec()).
plugin_decoder() ->
    String_plugin = begin
        _pipe = {decoder, fun gleam@dynamic@decode:decode_string/1},
        gleam@dynamic@decode:map(
            _pipe,
            fun(Name) -> {plugin_spec, Name, maps:new()} end
        )
    end,
    Array_plugin = begin
        gleam@dynamic@decode:field(
            0,
            {decoder, fun gleam@dynamic@decode:decode_string/1},
            fun(Name@1) ->
                gleam@dynamic@decode:optional_field(
                    1,
                    maps:new(),
                    options_decoder(),
                    fun(Options) ->
                        gleam@dynamic@decode:success(
                            {plugin_spec, Name@1, Options}
                        )
                    end
                )
            end
        )
    end,
    gleam@dynamic@decode:one_of(String_plugin, [Array_plugin]).

-file("src/version_bump/config.gleam", 271).
?DOC(
    " `prerelease` may be given as a string (the identifier) or as `true`, in\n"
    " which case the branch name is used as the identifier. We can only know the\n"
    " name in the object case, so a bare `true` maps to an empty identifier that\n"
    " branch resolution can later default from the name.\n"
).
-spec prerelease_decoder() -> gleam@dynamic@decode:decoder(gleam@option:option(binary())).
prerelease_decoder() ->
    As_bool = begin
        _pipe = {decoder, fun gleam@dynamic@decode:decode_bool/1},
        gleam@dynamic@decode:map(_pipe, fun(B) -> case B of
                    true ->
                        {some, <<""/utf8>>};

                    false ->
                        none
                end end)
    end,
    gleam@dynamic@decode:one_of(
        gleam@dynamic@decode:optional(
            {decoder, fun gleam@dynamic@decode:decode_string/1}
        ),
        [As_bool]
    ).

-file("src/version_bump/config.gleam", 241).
?DOC(
    " A branch entry is either a bare string (the branch name) or an object with\n"
    " a `name` plus optional `channel`, `prerelease`, and `range` keys.\n"
).
-spec branch_decoder() -> gleam@dynamic@decode:decoder(branch_config()).
branch_decoder() ->
    String_branch = begin
        _pipe = {decoder, fun gleam@dynamic@decode:decode_string/1},
        gleam@dynamic@decode:map(
            _pipe,
            fun(Name) -> {branch_config, Name, none, none, none} end
        )
    end,
    Object_branch = begin
        gleam@dynamic@decode:field(
            <<"name"/utf8>>,
            {decoder, fun gleam@dynamic@decode:decode_string/1},
            fun(Name@1) ->
                gleam@dynamic@decode:optional_field(
                    <<"channel"/utf8>>,
                    none,
                    gleam@dynamic@decode:optional(
                        {decoder, fun gleam@dynamic@decode:decode_string/1}
                    ),
                    fun(Channel) ->
                        gleam@dynamic@decode:optional_field(
                            <<"prerelease"/utf8>>,
                            none,
                            prerelease_decoder(),
                            fun(Prerelease) ->
                                gleam@dynamic@decode:optional_field(
                                    <<"range"/utf8>>,
                                    none,
                                    gleam@dynamic@decode:optional(
                                        {decoder,
                                            fun gleam@dynamic@decode:decode_string/1}
                                    ),
                                    fun(Range) ->
                                        gleam@dynamic@decode:success(
                                            {branch_config,
                                                Name@1,
                                                Channel,
                                                Prerelease,
                                                Range}
                                        )
                                    end
                                )
                            end
                        )
                    end
                )
            end
        )
    end,
    gleam@dynamic@decode:one_of(String_branch, [Object_branch]).

-file("src/version_bump/config.gleam", 199).
?DOC(
    " Decode a `Config`, starting from `default()` and overriding each field that\n"
    " is present in the document.\n"
).
-spec config_decoder() -> gleam@dynamic@decode:decoder(config()).
config_decoder() ->
    D = default(),
    gleam@dynamic@decode:optional_field(
        <<"repositoryUrl"/utf8>>,
        erlang:element(2, D),
        gleam@dynamic@decode:optional(
            {decoder, fun gleam@dynamic@decode:decode_string/1}
        ),
        fun(Repository_url) ->
            gleam@dynamic@decode:optional_field(
                <<"tagFormat"/utf8>>,
                erlang:element(3, D),
                {decoder, fun gleam@dynamic@decode:decode_string/1},
                fun(Tag_format) ->
                    gleam@dynamic@decode:optional_field(
                        <<"branches"/utf8>>,
                        erlang:element(4, D),
                        gleam@dynamic@decode:list(branch_decoder()),
                        fun(Branches) ->
                            gleam@dynamic@decode:optional_field(
                                <<"plugins"/utf8>>,
                                erlang:element(5, D),
                                gleam@dynamic@decode:list(plugin_decoder()),
                                fun(Plugins) ->
                                    gleam@dynamic@decode:optional_field(
                                        <<"dryRun"/utf8>>,
                                        erlang:element(6, D),
                                        {decoder,
                                            fun gleam@dynamic@decode:decode_bool/1},
                                        fun(Dry_run) ->
                                            gleam@dynamic@decode:optional_field(
                                                <<"ci"/utf8>>,
                                                erlang:element(7, D),
                                                {decoder,
                                                    fun gleam@dynamic@decode:decode_bool/1},
                                                fun(Ci) ->
                                                    gleam@dynamic@decode:optional_field(
                                                        <<"initialDevelopment"/utf8>>,
                                                        false,
                                                        {decoder,
                                                            fun gleam@dynamic@decode:decode_bool/1},
                                                        fun(Initial_development) ->
                                                            gleam@dynamic@decode:success(
                                                                {config,
                                                                    Repository_url,
                                                                    Tag_format,
                                                                    Branches,
                                                                    Plugins,
                                                                    Dry_run,
                                                                    Ci,
                                                                    versioning_mode_of(
                                                                        Initial_development
                                                                    )}
                                                            )
                                                        end
                                                    )
                                                end
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/version_bump/config.gleam", 184).
?DOC(
    " Parse the `\"release\"` object out of a `package.json` document. If there is\n"
    " no `\"release\"` key the project simply has no semantic-release config there,\n"
    " so the defaults are returned.\n"
).
-spec parse_package_json_config(binary()) -> {ok, config()} |
    {error, version_bump@error:release_error()}.
parse_package_json_config(Json_string) ->
    Release_decoder = begin
        gleam@dynamic@decode:optional_field(
            <<"release"/utf8>>,
            default(),
            config_decoder(),
            fun(Release) -> gleam@dynamic@decode:success(Release) end
        )
    end,
    _pipe = gleam@json:parse(Json_string, Release_decoder),
    gleam@result:map_error(
        _pipe,
        fun(Err) ->
            {config_error,
                <<"invalid package.json: "/utf8,
                    (describe_json_error(Err))/binary>>}
        end
    ).

-file("src/version_bump/config.gleam", 535).
-spec toml_scalar_to_string(tom:toml()) -> gleam@option:option(binary()).
toml_scalar_to_string(Value) ->
    case Value of
        {string, S} ->
            {some, S};

        {bool, B} ->
            {some, gleam@bool:to_string(B)};

        {int, I} ->
            {some, erlang:integer_to_binary(I)};

        {float, F} ->
            {some, gleam_stdlib:float_to_string(F)};

        _ ->
            none
    end.

-file("src/version_bump/config.gleam", 525).
-spec toml_table_string(gleam@dict:dict(binary(), tom:toml()), binary()) -> gleam@option:option(binary()).
toml_table_string(Fields, Key) ->
    case gleam_stdlib:map_get(Fields, Key) of
        {ok, Value} ->
            toml_scalar_to_string(Value);

        {error, _} ->
            none
    end.

-file("src/version_bump/config.gleam", 516).
-spec toml_options(gleam@dict:dict(binary(), tom:toml())) -> gleam@dict:dict(binary(), binary()).
toml_options(Fields) ->
    gleam@dict:fold(
        Fields,
        maps:new(),
        fun(Acc, Key, Value) -> case toml_scalar_to_string(Value) of
                {some, S} ->
                    gleam@dict:insert(Acc, Key, S);

                none ->
                    Acc
            end end
    ).

-file("src/version_bump/config.gleam", 496).
-spec toml_plugin(tom:toml()) -> plugin_spec().
toml_plugin(Value) ->
    case Value of
        {string, Name} ->
            {plugin_spec, Name, maps:new()};

        {array, Items} ->
            case Items of
                [{string, Name@1}, {inline_table, Opts}] ->
                    {plugin_spec, Name@1, toml_options(Opts)};

                [{string, Name@1}, {table, Opts}] ->
                    {plugin_spec, Name@1, toml_options(Opts)};

                [{string, Name@2}] ->
                    {plugin_spec, Name@2, maps:new()};

                _ ->
                    {plugin_spec, <<""/utf8>>, maps:new()}
            end;

        {inline_table, Fields} ->
            {plugin_spec,
                begin
                    _pipe = toml_table_string(Fields, <<"name"/utf8>>),
                    gleam@option:unwrap(_pipe, <<""/utf8>>)
                end,
                maps:new()};

        {table, Fields} ->
            {plugin_spec,
                begin
                    _pipe = toml_table_string(Fields, <<"name"/utf8>>),
                    gleam@option:unwrap(_pipe, <<""/utf8>>)
                end,
                maps:new()};

        _ ->
            {plugin_spec, <<""/utf8>>, maps:new()}
    end.

-file("src/version_bump/config.gleam", 430).
?DOC(" Prefix a key path with the `[tools.version_bump]` table location.\n").
-spec sr(list(binary())) -> list(binary()).
sr(Keys) ->
    lists:append([<<"tools"/utf8>>, <<"version_bump"/utf8>>], Keys).

-file("src/version_bump/config.gleam", 469).
?DOC(
    " Merge any `[tools.version_bump.plugin_options.<name>]` sub-tables into the\n"
    " matching plugin specs' options.\n"
).
-spec apply_plugin_options(
    list(plugin_spec()),
    gleam@dict:dict(binary(), tom:toml())
) -> list(plugin_spec()).
apply_plugin_options(Plugins, Table) ->
    gleam@list:map(
        Plugins,
        fun(Spec) ->
            case tom:get_table(
                Table,
                sr([<<"plugin_options"/utf8>>, erlang:element(2, Spec)])
            ) of
                {ok, Opts} ->
                    {plugin_spec,
                        erlang:element(2, Spec),
                        maps:merge(erlang:element(3, Spec), toml_options(Opts))};

                {error, _} ->
                    Spec
            end
        end
    ).

-file("src/version_bump/config.gleam", 482).
-spec toml_branch(tom:toml()) -> branch_config().
toml_branch(Value) ->
    case Value of
        {string, Name} ->
            {branch_config, Name, none, none, none};

        {inline_table, Fields} ->
            {branch_config,
                begin
                    _pipe = toml_table_string(Fields, <<"name"/utf8>>),
                    gleam@option:unwrap(_pipe, <<""/utf8>>)
                end,
                toml_table_string(Fields, <<"channel"/utf8>>),
                toml_table_string(Fields, <<"prerelease"/utf8>>),
                toml_table_string(Fields, <<"range"/utf8>>)};

        {table, Fields} ->
            {branch_config,
                begin
                    _pipe = toml_table_string(Fields, <<"name"/utf8>>),
                    gleam@option:unwrap(_pipe, <<""/utf8>>)
                end,
                toml_table_string(Fields, <<"channel"/utf8>>),
                toml_table_string(Fields, <<"prerelease"/utf8>>),
                toml_table_string(Fields, <<"range"/utf8>>)};

        _ ->
            {branch_config, <<""/utf8>>, none, none, none}
    end.

-file("src/version_bump/config.gleam", 452).
?DOC(
    " The host for a gleam.toml `[repository]` field, by its `type` (falling back to\n"
    " an explicit `host` key for `type = \"custom\"`).\n"
).
-spec repository_host(gleam@dict:dict(binary(), tom:toml())) -> gleam@option:option(binary()).
repository_host(Table) ->
    case tom:get_string(Table, [<<"repository"/utf8>>, <<"type"/utf8>>]) of
        {ok, <<"github"/utf8>>} ->
            {some, <<"github.com"/utf8>>};

        {ok, <<"gitlab"/utf8>>} ->
            {some, <<"gitlab.com"/utf8>>};

        {ok, <<"bitbucket"/utf8>>} ->
            {some, <<"bitbucket.org"/utf8>>};

        {ok, <<"codeberg"/utf8>>} ->
            {some, <<"codeberg.org"/utf8>>};

        {ok, <<"sourcehut"/utf8>>} ->
            {some, <<"git.sr.ht"/utf8>>};

        _ ->
            case tom:get_string(Table, [<<"repository"/utf8>>, <<"host"/utf8>>]) of
                {ok, Host} ->
                    {some, Host};

                {error, _} ->
                    none
            end
    end.

-file("src/version_bump/config.gleam", 436).
?DOC(
    " Derive a repository URL from gleam.toml's standard `[repository]` field, e.g.\n"
    " `repository = { type = \"github\", user = \"u\", repo = \"r\" }` -> the GitHub URL.\n"
).
-spec derive_repository_url(gleam@dict:dict(binary(), tom:toml())) -> gleam@option:option(binary()).
derive_repository_url(Table) ->
    case {tom:get_string(Table, [<<"repository"/utf8>>, <<"user"/utf8>>]),
        tom:get_string(Table, [<<"repository"/utf8>>, <<"repo"/utf8>>])} of
        {{ok, User}, {ok, Repo}} ->
            case repository_host(Table) of
                {some, Host} ->
                    {some,
                        <<<<<<<<<<"https://"/utf8, Host/binary>>/binary,
                                        "/"/utf8>>/binary,
                                    User/binary>>/binary,
                                "/"/utf8>>/binary,
                            Repo/binary>>};

                none ->
                    none
            end;

        {_, _} ->
            none
    end.

-file("src/version_bump/config.gleam", 390).
?DOC(
    " Parse the `[tools.version_bump]` table out of a `gleam.toml` — the\n"
    " Gleam-native config location. Keys use snake_case (`tag_format`, `dry_run`,\n"
    " `branches`, `plugins`). `repository_url` is taken from the table if present,\n"
    " otherwise derived from gleam.toml's standard `[repository]` field. Per-plugin\n"
    " options live in `[tools.version_bump.plugin_options.<name>]` sub-tables.\n"
    "\n"
    " A `gleam.toml` with no `[tools.version_bump]` table still yields a working\n"
    " config (defaults plus the derived repository URL). Exposed for testing.\n"
).
-spec parse_gleam_toml_config(binary()) -> {ok, config()} |
    {error, version_bump@error:release_error()}.
parse_gleam_toml_config(Toml_string) ->
    gleam@result:'try'(
        begin
            _pipe = tom:parse(Toml_string),
            gleam@result:map_error(
                _pipe,
                fun(_) -> {config_error, <<"invalid gleam.toml"/utf8>>} end
            )
        end,
        fun(Table) ->
            D = default(),
            Repository_url = case tom:get_string(
                Table,
                sr([<<"repository_url"/utf8>>])
            ) of
                {ok, Url} ->
                    {some, Url};

                {error, _} ->
                    gleam@option:'or'(
                        derive_repository_url(Table),
                        erlang:element(2, D)
                    )
            end,
            Tag_format = begin
                _pipe@1 = tom:get_string(Table, sr([<<"tag_format"/utf8>>])),
                gleam@result:unwrap(_pipe@1, erlang:element(3, D))
            end,
            Dry_run = begin
                _pipe@2 = tom:get_bool(Table, sr([<<"dry_run"/utf8>>])),
                gleam@result:unwrap(_pipe@2, erlang:element(6, D))
            end,
            Ci = begin
                _pipe@3 = tom:get_bool(Table, sr([<<"ci"/utf8>>])),
                gleam@result:unwrap(_pipe@3, erlang:element(7, D))
            end,
            Initial_development = begin
                _pipe@4 = tom:get_bool(
                    Table,
                    sr([<<"initial_development"/utf8>>])
                ),
                gleam@result:unwrap(_pipe@4, false)
            end,
            Branches = case tom:get_array(Table, sr([<<"branches"/utf8>>])) of
                {ok, Items} ->
                    gleam@list:map(Items, fun toml_branch/1);

                {error, _} ->
                    erlang:element(4, D)
            end,
            Plugins = case tom:get_array(Table, sr([<<"plugins"/utf8>>])) of
                {ok, Items@1} ->
                    apply_plugin_options(
                        gleam@list:map(Items@1, fun toml_plugin/1),
                        Table
                    );

                {error, _} ->
                    erlang:element(5, D)
            end,
            {ok,
                {config,
                    Repository_url,
                    Tag_format,
                    Branches,
                    Plugins,
                    Dry_run,
                    Ci,
                    versioning_mode_of(Initial_development)}}
        end
    ).

-file("src/version_bump/config.gleam", 341).
?DOC(
    " Parse a TOML configuration document, merging recognised keys over the\n"
    " defaults. Exposed for testing.\n"
).
-spec parse_toml_config(binary()) -> {ok, config()} |
    {error, version_bump@error:release_error()}.
parse_toml_config(Toml_string) ->
    gleam@result:'try'(
        begin
            _pipe = tom:parse(Toml_string),
            gleam@result:map_error(
                _pipe,
                fun(_) -> {config_error, <<"invalid TOML config"/utf8>>} end
            )
        end,
        fun(Table) ->
            D = default(),
            Repository_url = case tom:get_string(
                Table,
                [<<"repositoryUrl"/utf8>>]
            ) of
                {ok, Url} ->
                    {some, Url};

                {error, _} ->
                    erlang:element(2, D)
            end,
            Tag_format = begin
                _pipe@1 = tom:get_string(Table, [<<"tagFormat"/utf8>>]),
                gleam@result:unwrap(_pipe@1, erlang:element(3, D))
            end,
            Dry_run = begin
                _pipe@2 = tom:get_bool(Table, [<<"dryRun"/utf8>>]),
                gleam@result:unwrap(_pipe@2, erlang:element(6, D))
            end,
            Ci = begin
                _pipe@3 = tom:get_bool(Table, [<<"ci"/utf8>>]),
                gleam@result:unwrap(_pipe@3, erlang:element(7, D))
            end,
            Initial_development = begin
                _pipe@4 = tom:get_bool(Table, [<<"initialDevelopment"/utf8>>]),
                gleam@result:unwrap(_pipe@4, false)
            end,
            Branches = case tom:get_array(Table, [<<"branches"/utf8>>]) of
                {ok, Items} ->
                    gleam@list:map(Items, fun toml_branch/1);

                {error, _} ->
                    erlang:element(4, D)
            end,
            Plugins = case tom:get_array(Table, [<<"plugins"/utf8>>]) of
                {ok, Items@1} ->
                    gleam@list:map(Items@1, fun toml_plugin/1);

                {error, _} ->
                    erlang:element(5, D)
            end,
            {ok,
                {config,
                    Repository_url,
                    Tag_format,
                    Branches,
                    Plugins,
                    Dry_run,
                    Ci,
                    versioning_mode_of(Initial_development)}}
        end
    ).

-file("src/version_bump/config.gleam", 174).
?DOC(
    " Parse a JSON configuration document, merging any recognised keys over the\n"
    " defaults. Unknown keys are ignored. Exposed for testing.\n"
).
-spec parse_json_config(binary()) -> {ok, config()} |
    {error, version_bump@error:release_error()}.
parse_json_config(Json_string) ->
    _pipe = gleam@json:parse(Json_string, config_decoder()),
    gleam@result:map_error(
        _pipe,
        fun(Err) ->
            {config_error,
                <<"invalid JSON config: "/utf8,
                    (describe_json_error(Err))/binary>>}
        end
    ).

-file("src/version_bump/config.gleam", 123).
?DOC(
    " Walk the candidate sources in order, returning the parsed config (or parse\n"
    " error) for the first source that exists, or `None` if none exist.\n"
).
-spec find_and_parse(binary()) -> gleam@option:option({ok, config()} |
    {error, version_bump@error:release_error()}).
find_and_parse(Cwd) ->
    Json_files = gleam@list:map(
        [<<".releaserc.json"/utf8>>,
            <<".releaserc"/utf8>>,
            <<"release.config.json"/utf8>>],
        fun(Name) -> {Name, fun parse_json_config/1} end
    ),
    Sources = lists:append(
        Json_files,
        [{<<".releaserc.toml"/utf8>>, fun parse_toml_config/1},
            {<<"gleam.toml"/utf8>>, fun parse_gleam_toml_config/1},
            {<<"package.json"/utf8>>, fun parse_package_json_config/1}]
    ),
    try_sources(Cwd, Sources).

-file("src/version_bump/config.gleam", 114).
?DOC(
    " Load configuration from the project rooted at `cwd`, falling back to\n"
    " `default()` when no config file is present.\n"
    "\n"
    " The lookup order is:\n"
    "   1. `.releaserc.json`\n"
    "   2. `.releaserc`                       (parsed as JSON)\n"
    "   3. `release.config.json`\n"
    "   4. `.releaserc.toml`                  (parsed as TOML)\n"
    "   5. `[tools.version_bump]` in `gleam.toml` (the Gleam-native location;\n"
    "      also derives `repository_url` from the `[repository]` field)\n"
    "   6. the `\"release\"` key of `package.json`\n"
    "\n"
    " The first file that exists and parses wins; its values are merged over\n"
    " `default()`. Since every Gleam project has a `gleam.toml`, step 5 means a\n"
    " Gleam package needs no separate config file: with no `[tools.version_bump]`\n"
    " table it still releases using the defaults plus the derived repository URL.\n"
    " When nothing is found, `Ok(default())` is returned.\n"
).
-spec load(binary()) -> {ok, config()} |
    {error, version_bump@error:release_error()}.
load(Cwd) ->
    case find_and_parse(Cwd) of
        {some, Result} ->
            Result;

        none ->
            {ok, default()}
    end.