PhoenixKit.Migrations.ExpectedSchema.Resolver (phoenix_kit v2.6.0)

Copy Markdown View Source

Finds the concrete PhoenixKit.Migrations.ExpectedSchema.Behaviour implementation to consume — the single choke point every P2 consumer (the repair engine, mix phoenix_kit.repair, mix phoenix_kit.doctor, mix phoenix_kit.release_check's chain-hash assertion) goes through, so "the manifest is not generated yet" is handled identically everywhere instead of once per call site.

Why this has to exist at all

PhoenixKit.Migrations.ExpectedSchema — the tool-generated manifest — does not exist in this repository. It is produced from a real migrated scratch database (spec §5.1/§8.3) and lands with the squash PR (P3); everything in P2 has to build and run correctly before that happens. Every consumer must therefore treat "the module is absent" as an ordinary, expected runtime condition — not an exception to guard against defensively in each of them — which is exactly what resolve/0 turns it into: {:error, :not_generated}, no raise, no crash, ever.

Resolution order

  1. Application.get_env(:phoenix_kit, :expected_schema_module) — the test/override hook. Not read from config/config.exs anywhere in this library; set it with Application.put_env/3 (and clean up with on_exit/1 — this is process-global application config, not per-process state).
  2. default_module/0 (PhoenixKit.Migrations.ExpectedSchema) — the real manifest, once P3 generates it.

Either way, the resolved module must actually be loaded (Code.ensure_loaded?/1) and export objects/1, data_invariants/1, and chain_hash/0 (checked via __info__(:functions) membership, not function_exported?/3 — this codebase hit real async-test flakiness from that check running before a just-compiled test-support module was loaded into the process calling it; see PhoenixKit.MigrationTest's exports?/3 for the same fix applied previously). A misconfigured override pointing at some unrelated-but-loaded module therefore resolves to the same {:error, :not_generated} as a genuinely absent module, rather than surfacing as a confusing UndefinedFunctionError deep inside a caller — both cases mean "there is nothing usable to hand back".

Usage

case Resolver.resolve() do
  {:ok, module} ->
    module.objects(prefix)

  {:error, :not_generated} ->
    {:error, Resolver.not_generated_message()}
end

Testing against a fixture:

setup do
  Application.put_env(:phoenix_kit, :expected_schema_module, PhoenixKit.Test.FixtureExpectedSchema)
  on_exit(fn -> Application.delete_env(:phoenix_kit, :expected_schema_module) end)
end

Summary

Types

resolve/0's return shape.

Functions

The manifest module name every consumer resolves to absent an override — PhoenixKit.Migrations.ExpectedSchema, the module P3's squash PR generates. Exposed so callers/tests can reference it without repeating the literal atom (and so a future rename only has to change it here).

The exact wording every P2 consumer should surface for {:error, :not_generated}, so the message is identical whether it comes from the repair engine, mix phoenix_kit.repair, or mix phoenix_kit.doctor.

Resolves the PhoenixKit.Migrations.ExpectedSchema.Behaviour implementation to consume. Never raises.

Types

resolution()

@type resolution() :: {:ok, module()} | {:error, :not_generated}

resolve/0's return shape.

Functions

default_module()

@spec default_module() :: module()

The manifest module name every consumer resolves to absent an override — PhoenixKit.Migrations.ExpectedSchema, the module P3's squash PR generates. Exposed so callers/tests can reference it without repeating the literal atom (and so a future rename only has to change it here).

not_generated_message()

@spec not_generated_message() :: String.t()

The exact wording every P2 consumer should surface for {:error, :not_generated}, so the message is identical whether it comes from the repair engine, mix phoenix_kit.repair, or mix phoenix_kit.doctor.

resolve()

@spec resolve() :: resolution()

Resolves the PhoenixKit.Migrations.ExpectedSchema.Behaviour implementation to consume. Never raises.

Returns {:ok, module} when the resolved module is loaded and exports the full contract, {:error, :not_generated} otherwise (absent, not yet compiled, or loaded-but-not-conformant — see the moduledoc's "Resolution order" section for why those collapse to the same result).