stringify_values/1 had a catch-all that coerced any non-map payload to
%{}, silently dropping all form fields on the files: :form path. Narrow
the catch-all to nil only (the legitimate payload || %{} case); any other
non-map now raises FunctionClauseError instead of vanishing on the wire.
Add prepare_body/3 form-field tests covering the map, nil, and
non-map-raises cases.
Adds `Dexcord.Api.Coverage`, a testable core that diffs the compiled
endpoint route table against a pinned snapshot of Discord's official
OpenAPI spec (`priv/discord_openapi.json`). The spec is a coverage
CHECKLIST, not codegen input: it is refreshed only by the deliberate
`mix dexcord.coverage.refresh`, so upstream churn never breaks CI
spontaneously.
- `mix dexcord.coverage` compiles then exits non-zero on any in-scope
endpoint missing from the declared surface (AC1.8/AC1.9).
- `mix dexcord.coverage.refresh` pins the spec via :httpc + castore,
adding inets/ssl to the code path since Mix prunes them.
- `priv/coverage_allowlist.exs` records out-of-scope spec routes with a
comment per family; reconciled against the real pin (Social SDK
lobbies/partner-sdk, OIDC, application-by-id, scheduled-event
exceptions, join requests, thread search, monetization).
- `.gitea/workflows/ci.yml` runs the suite + gate (Actions must be
enabled on the Gitea remote).
- ratelimit_test.exs gains AC1.10 spot-checks proving the new Phase 5
route shapes collapse to bounded, id-collapsed keys.
- [Critical] Facade delegate set is now derived from static, compile-time-stable
data (each group's __endpoints__/0 route table) instead of the group's
module_info(:exports). module_info/1 is an auto-generated function that the
Elixir compiler tracer does not record as a compile-time dependency, so no
compile-order edge was established between Dexcord.Api and the group modules;
under parallel/incremental compilation the export table was read incompletely,
silently dropping delegates (e.g. Dexcord.Api.get_gateway_bot/0 undefined after
a routine recompile). Arities are derived from segment param count + body flag
(each head's opts \\ [] yields base and base+1), with get_channel_messages/3
named explicitly as sugar. Verified: delegate always present in the compiled
beam; 8x incremental-recompile loop of facade + gateway_integration all green.
- [Important] api_facade_test.exs is now deterministic: it asserts the facade's
exported set equals the same statically-derived expected set the generator
uses (no module_info), and ensures Dexcord.Api + groups are loaded before
function_exported? probes (which do not auto-load), removing a false-negative
async load race. Historically-load-bearing arity assertions kept.
- [Minor] decode_response/2 now normalizes returns:nil endpoints to {:ok, nil}
even when the server sends a non-204 JSON body, instead of leaking the raw
decoded map through the passthrough clause. Added a unit test.
Redeclare the full existing endpoint surface with the Dexcord.Api.Endpoint
DSL across six group modules (Channels, Messages, Users, Interactions,
Commands, Misc) and delete the old named endpoint functions plus their
private helpers (message_query/maybe_param/maybe_put) from Dexcord.Api,
keeping request/4 and the rest of the request pipeline untouched (AC1.7).
Dexcord.Api re-exports every group endpoint head as a generated defdelegate
driven by module_info(:exports), so Dexcord.Api.<endpoint> keeps working for
all 9 internal call sites and external callers. create_dm is registered by
hand (spec + snowflake sugar) because the generated create_dm(body, opts \\ [])
head would collide with create_dm/1.
Endpoints are now typed: get_current_user -> Dexcord.User, create/edit_message
-> Dexcord.Message, get_channel/start_thread -> Dexcord.Channel,
get_current_application -> Dexcord.ApplicationInfo, bulk_overwrite_* ->
[Dexcord.ApplicationCommand], etc. get_gateway_bot and
create_interaction_response stay :raw. Patch registrar to match
%Dexcord.ApplicationInfo{} and widen Config.put_application_id/1 to accept
integer ids.
Behavior changes (documented in tests):
- get_channel_messages/2 no longer collapses multiple anchors with a
before>after>around precedence; it forwards every declared query key, so
callers pass exactly one anchor (the /3 locator arity still enforces this).
- start_thread_with_message extra fields now travel in the body argument
rather than a trailing opts list.
- *_id path params are validated via Snowflake.cast/1; integration-test
fixtures updated from non-numeric placeholders to numeric snowflake ids.
Add test/dexcord/api_facade_test.exs asserting the facade cannot silently drop
any group endpoint export.
Claude-Session: https://claude.ai/code/session_01T4vgXtQLs2wfNwn6B1m4hm
- Formatting: add field/hydrate/discord_struct to locals_without_parens
in .formatter.exs so the no-paren data-DSL style is the formatted form;
mix format then wrapped the long codec encode_value/2 clause. Both
previously-unformatted files now satisfy mix format --check-formatted.
- Empty-struct guard: __before_compile__ now raises ArgumentError when a
module does 'use Dexcord.Struct' with no field/hydrate declarations,
instead of silently generating a bare defstruct []. Added a compile-time
test mirroring the existing Code.compile_string validation tests.