Add query params, function docs, and configurable OpenAPI metadata - #9
Conversation
- **4-arity handlers**: action signatures now take (PathArgs, QueryArgs,
Headers, Body) matching the phoenix_spectral convention. Query params
are decoded from the URL query string using elli_request:get_args/1,
type-checked via spectra, and passed as the second argument.
- **OpenAPI query parameter generation**: `to_endpoint` emits `in: query`
parameters for each field in the QueryArgs map type; required fields
(`:=`) are marked required, optional (`=>`) are not.
- **Function doc support**: `-spectra(#{summary => ..., description => ...})`
placed before a `-spec` populates the OpenAPI operation's summary and
description fields via `spectra_openapi:endpoint/3`.
- **Configurable OpenAPI metadata**: `setup_routes/2` now accepts a full
`openapi_metadata()` map (servers, contact, license, description, etc.).
`elli_openapi_handler` accepts `{MetaData, Routes}` as callback_args
for backward-compatible metadata configuration.
- **Fix double-encoding bug**: `generate_openapi_spec` now uses
`pre_encoded` option so spectra returns a raw map, preventing
double-encoding of the JSON output.
- Demo updated with `list_users/4` showing optional query params (page,
per_page). All 28 tests pass, eqwalizer clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates elli_openapi to support typed query parameters, enrich generated OpenAPI operations with function doc metadata, and make OpenAPI metadata configurable while addressing a JSON double-encoding issue introduced by newer spectra versions.
Changes:
- Extend handler signature to 4-arity
(PathArgs, QueryArgs, Headers, Body)and add query param decoding + OpenAPIin: querygeneration. - Add support for
-spectra(#{summary/description/deprecated => ...})function doc annotations to populate OpenAPI operation metadata. - Allow passing full OpenAPI metadata via
setup_routes/2and{MetaData, Routes}callback args; updatespectradependency to0.8.2and usepre_encodedto avoid double JSON encoding.
Reviewed changes
Copilot reviewed 4 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/elli_openapi_integration_SUITE.erl | Updates routes to 4-arity, adds query param + function doc OpenAPI tests, and adds list-users integration tests. |
| src/elli_openapi_handler.erl | Adds {MetaData, Routes} startup support and refactors module loading before route setup. |
| src/elli_openapi_demo.erl | Updates demo handlers/specs to 4-arity, adds query param demo endpoint, and adds -spectra doc annotations. |
| src/elli_openapi.erl | Implements query param decoding, emits query params into OpenAPI, adds configurable metadata, and fixes OpenAPI generation encoding via pre_encoded. |
| rebar.config | Bumps spectra dependency constraint to ~> 0.8.2. |
| rebar.lock | Locks spectra to 0.8.2 with updated hashes. |
Comments suppressed due to low confidence (1)
test/elli_openapi_integration_SUITE.erl:287
get_user_not_found/1no longer exercises the/api/users/{userId}handler; it requests/api/nonexistent, so the 404 is coming from the router rather than the get_user endpoint. Either rename this testcase to reflect what it validates (unknown route) or update it to hit a path that matches the get_user route and returns 404 from the handler.
get_user_not_found(Config) ->
Url = url(Config, "/api/nonexistent"),
?assertMatch(
{ok, {{_, 404, _}, _Headers, _ResponseBody}},
http_get(Url, [{"authorization", "Bearer token123"}])
),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| handle_event(elli_startup, [], {MetaData, Routes}) when is_map(MetaData) -> | ||
| ensure_modules_loaded(Routes), | ||
| elli_openapi:setup_routes(MetaData, Routes), | ||
| ok; | ||
| handle_event(elli_startup, [], Routes) -> | ||
| ensure_modules_loaded(Routes), | ||
| elli_openapi:setup_routes(Routes), | ||
| ok; |
There was a problem hiding this comment.
New behavior in handle_event/3 supports callback_args as {MetaData, Routes}, but the integration suite still only covers the legacy Routes-only startup path. Consider adding a CT case that starts Elli with {MetaData, Routes} and asserts the generated spec reflects the provided metadata (e.g. title/version/servers) to prevent regressions.
- Add search_users/4 with required query param `query := binary()` - Add GET /api/search route to integration test suite - Add search_users_missing_required_query_param test (verifies 400 when required query param is absent) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace nested case pyramid with a flat maybe block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
(PathArgs, QueryArgs, Headers, Body), matching thephoenix_spectralconvention. Query params are extracted from the URL viaelli_request:get_args/1, type-checked with spectra, and emitted asin: queryparameters in the generated OpenAPI spec.-spectra(#{summary => ..., description => ...})placed before a-spec(before the first function definition) populates the OpenAPI operation'ssummary/description/deprecatedviaspectra_openapi:endpoint/3.setup_routes/2now accepts a fullopenapi_metadata()map (title, version, servers, contact, license, description).elli_openapi_handleralso accepts{MetaData, Routes}ascallback_argsfor backwards-compatible configuration.generate_openapi_specnow uses thepre_encodedoption so spectra returns a raw Erlang map, avoiding double JSON encoding that was introduced in spectra 0.8.x.Test plan
make build-test)list_users/4demo handler with optionalpage/per_pagequery params works end-to-endpage=notanumber) return 400-spectra()doc annotations appear assummary/descriptionin generated specGET /api/usersroute with query params showsparameterswithin: queryin spec🤖 Generated with Claude Code