Repository navigation
Add OpenAPI 3.1 webhook support and per-operation security - #188
Draft
andreashasse wants to merge 5 commits into
Draft
andreashasse wants to merge 5 commits into
andreashasse wants to merge 5 commits into
Conversation
Teach spectra_openapi to emit the OpenAPI 3.1 top-level `webhooks` key.
New builders:
- webhook/2,3 creates a webhook specification, keyed by an event name
rather than a URL path, because the consumer owns the URL the API calls.
- to_openapi/4 generates a spec from both endpoints and webhooks.
endpoints_to_openapi/2,3 are unchanged and delegate with no webhooks,
so existing specs emit no `webhooks` key.
The direction is inverted relative to an endpoint: the request body is the
payload this API sends out, and the responses describe what the consumer is
expected to send back. add_response/2, with_request_body/3,4 and
with_parameter/3 accept webhooks as well as endpoints, so both build the
same way, and webhook schemas share components/schemas with endpoints -
a type used by both is emitted once.
with_parameter/3 raises {parameter_location_not_supported_on_webhook,
Name, In} for path and query parameters. The consumer owns the URL, so
there is nothing to template into and no query string this API controls;
emitting such a parameter would document something that can never be
satisfied.
Responses are optional, matching OpenAPI 3.1, and one event name can carry
several methods since a webhook value is a Path Item Object just like a
paths entry.
Known limitation, documented in the README: a global `security`
requirement is emitted at the top level and so applies to webhook
operations too, even though its meaning is inverted there. Per-operation
security is not supported yet.
Adds EUnit coverage for the top-level placement, schema sharing between an
endpoint and a webhook (asserted against the real serialized JSON so the
$ref is checked as a consumer would resolve it), several methods under one
name, header parameters, path/query rejection, responseless webhooks,
absence of the key without webhooks, and a full openapi-spec-validator
3.1 pass.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Erhgwu59rBxiYw1fVQGbcr
This was referenced Aug 24, 2026
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The new “webhook without responses” behavior is asserted but not validated against the same OpenAPI 3.1 validator used elsewhere, so compliance/tooling compatibility should be confirmed in tests.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds first-class OpenAPI 3.1 webhook generation to spectra_openapi, emitting the top-level webhooks object and enabling webhook specs to share the same builder pipeline and schema/components generation as regular endpoints.
Changes:
- Added
spectra_openapi:webhook/2,3andspectra_openapi:to_openapi/4(withendpoints_to_openapi/2,3delegating toto_openapi/4with no webhooks). - Widened builders (
add_response/2,with_request_body/3,4,with_parameter/3) to work with both endpoints and webhooks, including webhook-specific parameter-location rejection. - Added EUnit coverage for webhook emission/shape, schema sharing, and related behaviors; updated docs and changelog/todo accordingly.
File summaries
| File | Description |
|---|---|
src/spectra_openapi.erl |
Introduces webhook builders/types and extends OpenAPI generation to optionally include top-level webhooks. |
test/openapi_json_test.erl |
Adds tests covering webhook serialization, schema sharing, multi-method webhooks, parameter rules, and validator pass for webhook-bearing specs. |
README.md |
Documents new webhook API and generation entrypoint (to_openapi/4). |
CHANGELOG.md |
Notes the new webhook support and related API additions. |
todo.md |
Marks the OpenAPI 3.1 webhooks item as completed. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
webhook_without_responses_test asserted only that the operation has no responses key. But "responses are optional" is a claim about OpenAPI 3.1 itself, so the openapi-spec-validator is the thing that can falsify it - without that step, a regression where a responseless operation stopped validating would go unnoticed. Confirmed the validator does accept it, so the behaviour was right; the test just was not checking the part that matters. Raised by Copilot's review on #188.
The Doc map given to endpoint/3 and webhook/3 now accepts a security key holding a list of Security Requirement Objects. It is emitted on that operation alone and overrides the global security default; security => [] opts the operation out of the global requirement entirely. This is what lets an API authenticate inbound calls one way and sign its outgoing webhooks another - previously a global requirement decorated every webhook operation with no way to override it, which was documented as a known limitation. Webhooks still inherit the global default when they declare nothing, which is what OpenAPI specifies: top-level security applies to every operation, webhooks included. The change is two type lines, because generate_operation/2 already starts from the Doc map and copies it into the emitted operation. Adding security to endpoint_doc() without also adding it to openapi_operation() would have silently dropped it at encode time, so both were needed - a comment on endpoint_doc() now records that the two must stay in step. Adds EUnit coverage for a webhook overriding the global requirement while the top-level default and an untouched endpoint keep theirs, an endpoint overriding it too (this is a per-operation field, not a webhook-specific one), and security => [] opting a webhook out, asserted against the real serialized JSON and validated by openapi-spec-validator.
…k-support-sz755w # Conflicts: # CHANGELOG.md
…k-support-sz755w # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Emits the OpenAPI 3.1 top-level
webhookskey, and adds per-operationsecurityso webhooks can authenticate differently from the API. Closes thewebhooksitem intodo.md.What a webhook is here
A webhook is keyed by an event name rather than a URL path, because the consumer owns the URL this API calls. The direction is inverted relative to an endpoint: the request body is the payload the API sends out, and the responses describe what the consumer is expected to return.
API
endpoints_to_openapi/2,3are unchanged and delegate toto_openapi/4with no webhooks, so existing specs emit nowebhookskey (covered by a test).add_response/2,with_request_body/3,4andwith_parameter/3were widened toendpoint_spec() | webhook_spec(), so both kinds build through the same pipe rather than gaining webhook-specific twins.Per-operation security
The
Docmap now takes asecuritykey, emitted on that operation alone and overriding the global default.security => []opts an operation out entirely. This is what lets an API authenticate inbound calls one way and sign its outgoing webhooks another:Webhooks still inherit the global default when they declare nothing, which is what OpenAPI specifies — top-level
securityapplies to every operation, webhooks included. This replaces the "per-operation security is not supported yet" limitation the first draft of this PR documented.The change is two type lines, because
generate_operation/2already starts from theDocmap and copies it into the emitted operation. Addingsecuritytoendpoint_doc()without also adding it toopenapi_operation()silently dropped it at encode time — the first attempt did exactly that and a test caught it — so a comment onendpoint_doc()now records that the two must stay in step.Behaviour decisions
pathandqueryparameters are rejected on webhooks, raising{parameter_location_not_supported_on_webhook, Name, In}. The consumer owns the URL, so there is nothing to template into and no query string this API controls.headerandcookieare accepted.Operation.responsesis not required.pathsentry.components/schemas.pathsstays required inopenapi_spec(); a webhook-only spec emits"paths": {}, which is valid 3.1. A sibling document type with optionalpathswas considered and rejected: Erlang map types can't be composed, so every future root-level field (top-leveltagsandexternalDocsare both queued intodo.md) would need editing in two places, with no compiler signal on a miss.Tests
Eleven new EUnit tests: top-level placement, schema sharing between an endpoint and a webhook, several methods under one name, header parameters,
path/queryrejection, a responseless webhook, absence of the key without webhooks, a webhook overriding the global security requirement while the top-level default and an untouched endpoint keep theirs, an endpoint overriding it too,security => []opting a webhook out, and fullopenapi-spec-validator3.1 passes on webhook-bearing specs.Where a test asserts something about OpenAPI 3.1 itself — "responses are optional", "an empty requirement clears the default" — it runs the validator rather than only checking shape, since the validator is the only thing that can falsify such a claim. Schema-sharing and opt-out assert against the really-serialized JSON so
$refandsecurityare checked in the form a consumer resolves.Verified locally on the merge head, on OTP 28 to match the current
.tool-versionspin:rebar3 eunit(768 tests),rebar3 proper(9/9 properties),dialyzer,xref,hank,check_app_callsandfmt --checkall clean.elpis not installed in this container, sotype_check(eqwalizer) andlintrun in CI, across the full OTP 27/28/29 matrix.Kept current with
mainmainhas moved twice while this sat open — 0.14.0, then 0.14.1 plus the OTP/rebar3/ELP pin bump — and both are merged in here.Each time the only conflict was
CHANGELOG.md: this PR's## [Unreleased]entries against the newly released section. Both are kept, Unreleased on top;README.mdauto-merged.0.14.0 included #189, which changed schema generation so
-spectra()doc annotations survive inlining into another schema. That plausibly reaches the output these tests assert on, so the whole suite was re-run against each merge rather than assumed — no assertion has needed a change.Not in this PR
elli_openapiis pinned tospectra ~> 0.8.2, six minors behind, so it cannot consume this without a separate upgrade. Wrapper support follows in andreashasse/spectral#38 and andreashasse/phoenix_spectral#22.