Repository navigation
feat(ipa): add IPA-132 OperationResponse schema rules - #1437
Conversation
10a7ed2 to
14859d3
Compare
| }, | ||
| }, | ||
| // A paginated wrapper composed with allOf, which Spectral does not flatten | ||
| PaginatedAllOfOperationResponse: { |
There was a problem hiding this comment.
Why do we allow allOf? Could you clarify that?
There was a problem hiding this comment.
Spectral resolves $refs but does not flatten allOf, so without merging the allOf branches a composed-but-compliant wrapper would be a false violation — per OAS semantics an instance must satisfy every subschema, so a results array defined in any branch is genuinely part of the schema. Root-level allOf composition is a common idiom in our spec (107 of the 1820 schemas in openapi/.raw/v2.yaml are composed this way), so the first real paginated Operations wrapper may well extend a shared pagination base like this. This test pins that case.
There was a problem hiding this comment.
My question is more about whether we expect allOf usage in the Operations endpoints
There was a problem hiding this comment.
Update on the allOf case after a deeper review pass: I've kept allOf support for the OperationResponse schema itself (now merged conjunctively — re-declared properties combine keywords and enums intersect), but dropped allOf acceptance for the paginated wrapper. The wrapper shape I blessed here would fail xgen-IPA-110-collections-response-define-results-array at error severity on /…/{id}/operations, since schemaIsPaginated doesn't traverse allOf — so IPA-132 now requires the results array directly on the wrapper, consistent with IPA-110. Teaching the IPA-110 pagination predicate allOf would change an existing error-level rule and belongs in its own PR.
There was a problem hiding this comment.
Final state after our Slack discussion: composition is not expected anywhere for the Operation status contract, so as of 7cda85d the rules enforce that directly — OperationResponse must be a flat schema (allOf/oneOf/anyOf is a violation), and the paginated wrapper must define results directly per IPA-110. The allOf merge machinery is removed.
| type: 'object', | ||
| required: ['operationId', 'operationType', 'createdAt', 'updatedAt'], | ||
| properties: { | ||
| operationId: { type: 'string' }, |
There was a problem hiding this comment.
Looking at the Technical Design document, I see that there are other required fields, in other words, core metadata. Shouldn't they be involved here as well?
There was a problem hiding this comment.
Deliberate split, each rule maps 1:1 onto a guideline ID in the IPA repo, and IPA-132-operation-response-must-include-core-metadata scopes core metadata to operationId, operationType, createdAt and updatedAt (plus customMethod declared but not required). The other required fields in the design doc are owned by their own guidelines:
status→xgen-IPA-132-operation-status-must-use-the-standard-status-enum(this PR)resultHref,retryAfterSeconds,error→ conditionally required at runtime, so statically they can only be checked as declared-and-not-required; covered byxgen-IPA-132-successful-lros-must-reference-the-result-resource,xgen-IPA-132-in-progress-operations-must-expose-retry-after-secondsandxgen-IPA-132-failed-lros-must-report-a-structured-errorin the next PR.expiresAt→ owned byIPA-132-operations-must-expire-after-a-finite-retention-period, which is review-level in the IPA repo because retention and the 404-after-expiry are backend behavior
There was a problem hiding this comment.
I recall a discussion where we said that since there are several guidelines covering required fields, the plan was to cover all of them in one validation rule, so we don't need a separate validation rule for each required-field guideline. Isn't that still the case?
There was a problem hiding this comment.
Restructured in 907d1c6 following our Slack discussion. The schema checks are now split by check type instead of by guideline, with the field lists in functionOptions: one rule for all the always-required fields (operationId, status, operationType, createdAt, updatedAt), one for the optional/conditional ones, one for enum values and one for nested structure.
Two things I kept on purpose: operations-should-expose-status-fields stays its own rule since severity is per-rule and that one is a should, and expiresAt still isn't linted because the expiry guideline is review-level. If we decide it should be required, it's a guideline change in the IPA repo first and then a one-line functionOptions addition here.
Add the three rules validating how long-running operation status is communicated through the OperationResponse schema: - xgen-IPA-132-operation-endpoints-must-return-operation-response: the single Operation endpoint must reference the OperationResponse schema, and the Operations collection endpoint must return a paginated response whose results reference it. - xgen-IPA-132-operation-status-must-use-the-standard-status-enum: the schema must report progress through a status field using exactly the enum PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED. - xgen-IPA-132-operation-response-must-include-core-metadata: operationId, operationType, createdAt and updatedAt must be defined and required, operationType must use exactly the enum CREATE, UPDATE, DELETE, CUSTOM, and customMethod must be defined but not required. The schema name, both enums and the exact-enum-match helper live in utils/longRunningOperations.js for reuse by the remaining IPA-132 rules.
Per review of the IPA-132 guideline examples, the status field must be listed as required: an OperationResponse that can legally omit status defeats polling. The status enum rule now checks required membership in addition to the exact enum, matching the updated guideline example.
Per review, the test fixtures follow the same pattern across the IPA-132 test files: the operations and schemas under test are defined once at the top of the file and reused across the test cases.
Address findings from a deeper review of the OperationResponse rules: - The status enum and core metadata rules read properties and required lists through allOf sub-schemas, which Spectral does not flatten, so an allOf-composed OperationResponse no longer produces false violations. - The Operations collection endpoint must reference a paginated wrapper schema: inline wrappers are rejected, consistent with xgen-IPA-110-collections-use-paginated-prefix, which rejects them at error severity. The results property of the wrapper may live in one of its allOf sub-schemas. - The single Operation endpoint no longer accepts an array schema whose items reference OperationResponse. - The endpoint rule checks its cheap path and media type gates before walking the document.
a42e887 to
d70c98d
Compare
…pe rules Replace the per-guideline schema rules with four declarative rules driven by functionOptions field lists, validating the OperationResponse reference schema by check type: required fields, conditionally-present optional fields, standard enum values and nested object structure. The should-level status fields rule stays separate because severity is per-rule.
- Follow responses defined as refs to components.responses instead of silently skipping them, and flag 2xx responses without a JSON schema - Reject allOf-composed paginated wrappers, consistent with the IPA-110 pagination rules, and require the results array directly on the wrapper - Merge allOf properties conjunctively: keyword maps combine instead of overwriting and enums apply intersected - Anchor property violations at their actual document location inside allOf members instead of a location that only exists after merging - Iterate response codes from the responses object so missing content is visible, moving rule exceptions to the response level - Drive the should-level status fields rule from functionOptions like its siblings, and fix the a/an grammar in its messages - Drop the unused enum constants: the ruleset functionOptions are the single source of truth for enum values
Operation records must expire, so the record must always carry its expiration date. The retention behavior itself and the 404 after expiry remain review-level, only the field declaration is statically checkable.
Drop the internal guideline-ID mappings from the rendered descriptions, clarify how allOf/oneOf/anyOf composition relates to the OperationResponse schema, and trim the enum rule's field-presence justification.
There is one and only one OperationResponse, provided as scaffolding, so the schema must define its properties directly: allOf, oneOf and anyOf composition is a violation reported by the required-fields rule, and the remaining schema rules skip composed schemas. Removes the allOf merge machinery this design decision makes unnecessary.
| const OPERATIONS_COLLECTION_ERROR_MESSAGE = `The Operations collection endpoint must return a paginated response whose results reference the ${OPERATION_RESPONSE_SCHEMA_NAME} schema.`; | ||
| const MISSING_SCHEMA_ERROR_MESSAGE = 'The Operations endpoint must define a JSON response schema.'; | ||
|
|
||
| const COMPONENT_RESPONSES_REF_PREFIX = '#/components/responses/'; |
There was a problem hiding this comment.
In our spec components/responses is reserved for error responses. Every 2xx response defines content inline or references the schema via components/schemas. Since this rule only evaluates 2xx codes, resolveResponseRef and the contentBasePath branch are unreachable. Could we drop them and read response.content directly?
There was a problem hiding this comment.
You're right — removed. resolveResponseRef and contentBasePath are gone and the rule reads response.content directly. Rule description, README, and tests updated accordingly.
…nt rule In this spec components.responses is reserved for error responses: every 2xx response defines its content inline. The endpoint rule only evaluates 2xx codes, so resolveResponseRef and the contentBasePath branch could never run. Read response.content directly instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; | ||
| import { usesComposition } from './utils/longRunningOperations.js'; | ||
|
|
||
| const COMPOSITION_ERROR_MESSAGE = |
There was a problem hiding this comment.
IPA132OperationResponseMustDefineRequiredFields owns two unrelated checks: "don't use allOf/oneOf/anyOf" and "define these six fields." Because they share a rule name, they share an exception as well, and that opens a gap.
If you ran a schema composed via allOf and missing operationId entirely through this validation, plus one exception:
"x-xgen-IPA-exception": {
"xgen-IPA-132-operation-response-must-define-required-fields": "we compose from a base schema"
}
Result: 0 warnings. The exception suppresses the composition error.
Suggested fix: extract it into its own rule, xgen-IPA-132-operation-response-must-not-use-composition, on the same OperationResponseSchema alias. That matches the naming already used here, gives composition its own exception key and its own adoption metric, and leaves each rule named for what it checks.
Please update the other rules accordingly if needed
There was a problem hiding this comment.
Good catch, extracted as suggested. Composition rejection now lives in xgen-IPA-132-operation-response-must-not-use-composition with its own exception key and adoption metric. Your bypass scenario — an allOf schema with a required-fields exception — is now a test case and produces the composition violation.
The required-fields rule owned two unrelated checks sharing one exception key, so an exception granted for composition also silenced the six required-field checks. Composition rejection now lives in xgen-IPA-132-operation-response-must-not-use-composition with its own exception key and adoption metric, and every other OperationResponse schema rule skips composed schemas relying on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yelizhenden-mdb
left a comment
There was a problem hiding this comment.
LGTM, thanks for addressing all the feedback
Proposed changes
This PR adds the IPA-132 rules validating how long-running operation status is communicated through the
OperationResponseschema. The schema checks validate the OperationResponse reference schema by check type, with the field lists declared in the ruleset'sfunctionOptions.Jira ticket: CLOUDP-429837
Rules added
All at
warnseverity, anchored on$.components.schemas.OperationResponse(sharedOperationResponseSchemaalias):xgen-IPA-132-operation-endpoints-must-return-operation-response.../operations/{operationId}) must reference theOperationResponseschema; the Operations collection endpoint must reference a paginated wrapper defining theresultsarray directly (inline andallOf-composed wrappers rejected, consistent with the IPA-110 pagination rules). Responses defined as$refs tocomponents.responsesare followed. Exceptions at the response levelxgen-IPA-132-operation-response-must-define-required-fieldsoperationId,status,operationType,createdAt,updatedAt,expiresAt, each listed as requiredxgen-IPA-132-operation-response-must-define-optional-fieldscustomMethod,error,resultHref,retryAfterSecondsdefined and not listed as required — each is present only under a runtime condition that cannot be validated staticallyxgen-IPA-132-operation-response-must-use-standard-enum-valuesstatusenum exactlyPENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED;operationTypeenum exactlyCREATE, UPDATE, DELETE, CUSTOM;error.retryStrategyenum exactlyIMMEDIATE, BACKOFF, NONExgen-IPA-132-operation-response-must-define-nested-object-structureerrorobject must definecode,messageandretryablexgen-IPA-132-operations-should-expose-status-fieldsstatusMessage,progress(withcompleted,total,unit) andestimatedCompletionTime. Kept separate because it enforces should-level guidance and severity is per-ruleEach check-type rule skips fields whose presence another rule owns, so a missing field is reported exactly once. Each rule's description lists the IPA-132 guidelines it covers.
There is one and only one
OperationResponse, provided as scaffolding, so the schema must define its properties directly:allOf/oneOf/anyOfcomposition is a violation reported by the required-fields rule, and the remaining schema rules skip composed schemas.Testing
gen-ipa-docs, prettier and eslint clean.spectral lint openapi/.raw/v2.yamlwith the full ruleset: 0xgen-IPA-132-*findings — the spec defines noOperationResponseschema yet.Checklist