SvEx.Source.ConfigExs (SvEx v0.4.2)

Places a contribution inside a config/*.exs, by that file's own rules.

A config file is not an appendable file. Every mix phx.new project ends config/config.exs with import_config "#{config_env()}.exs" under a comment saying it must stay there "so it overrides the configuration defined above" — so a block appended past it outranks the project's own dev.exs, test.exs and prod.exs. Measured on priv/meta/baseline_api: the contribution landed at line 45 with the import at line 43, and silently won.

Source text in, source text out, no filesystem — the shape SvEx.Source.MixExs established, over the same Sourceror.get_range/1 + patch_string/2 engine. Sourceror.to_string/1 is never called: reprinting a file the project owns is a first-order R6 violation.

Not to be confused with SvEx.Config, which reads target.exs — a different file describing a different thing. The collision is legible rather than dangerous, and is left unresolved deliberately.

Summary

Functions

Splices block above source's import_config call.

Splices block inside source's if config_env() == env do guard.

Replaces key inside the config statement addressed by path.

Types

error()

@type error() ::
  {:no_env_guard, atom()}
  | {:no_statement, [atom() | module()]}
  | {:no_key, atom()}
  | {:unparseable, term()}

Functions

insert_before_import(source, block)

@spec insert_before_import(binary(), binary()) :: {:ok, binary()} | {:error, error()}

Splices block above source's import_config call.

Above its LEADING COMMENTS, not merely above the call: the comment phx.new writes there explains why the import is last, and splicing between the two would separate a comment from the statement it describes.

A file with no import_config — every dev.exs, test.exs, prod.exs and every mix new config — appends instead, which is correct there because nothing follows that the block could override.

Idempotent by substring, as :contributes has always been: where the block sits does not change whether it is already present.

The SPLICE owns the separation, not the block: whatever trailing newlines the block carries, exactly one blank line follows it. A block reconstructed from hunk lines and one shipped as a literal suffix differ in that trailing whitespace, and derive replays this function to decide where a contribution was observed — so the two must produce identical bytes or that replay reports a false negative.

insert_in_env(source, env, block)

@spec insert_in_env(binary(), atom(), binary()) :: {:ok, binary()} | {:error, error()}

Splices block inside source's if config_env() == env do guard.

A file with no guard for that environment returns {:error, {:no_env_guard, env}} and writes nothing. It does NOT fall back to appending, which insert_before_import/2 does and which is correct there: appending here would put a production-only setting outside the guard and make it a default in every environment, secrets included.

put_key(source, path, key, value)

@spec put_key(binary(), [atom() | module()], atom(), binary()) ::
  {:ok, binary()} | {:error, error()}

Replaces key inside the config statement addressed by path.

path is the statement's leading arguments — [:app] for config :app, key: value, [:app, App.Repo] for config :app, App.Repo, key: value. value is SOURCE TEXT, as SvEx.Source.MixExs.put_project_key/3's is.

An absent statement or an absent key is an error, never an insert. "Override this value" and "introduce this key" are different intentions, and conflating them is how a typo becomes a new config key nobody reads.

This is the "replacement at a matched site" shape §4 of the deletion spec named — the key survives and its value changes — and it is the reason those cases need not become a conflict region a human resolves on every install.