From eef771e4b303db88ea82f45770bfe96be3b3b58f Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sun, 9 Aug 2026 04:04:20 +0300 Subject: [PATCH 1/5] test: trace the corpus to the matrix and close coverage gaps --- compilers/openapi/conformance_matrix_test.go | 248 +++++ compilers/openapi/conformance_test.go | 379 ++++++-- docs/architecture.md | 5 +- docs/ir-spec-matrix.md | 118 +-- .../inline-hoist-positions.golden.json | 683 +++++++++++++ .../openapi/inline-hoist-positions.yaml | 71 ++ .../openapi/param-style-matrix.golden.json | 911 ++++++++++++++++++ .../openapi/param-style-matrix.yaml | 64 ++ 8 files changed, 2354 insertions(+), 125 deletions(-) create mode 100644 compilers/openapi/conformance_matrix_test.go create mode 100644 testdata/conformance/openapi/inline-hoist-positions.golden.json create mode 100644 testdata/conformance/openapi/inline-hoist-positions.yaml create mode 100644 testdata/conformance/openapi/param-style-matrix.golden.json create mode 100644 testdata/conformance/openapi/param-style-matrix.yaml diff --git a/compilers/openapi/conformance_matrix_test.go b/compilers/openapi/conformance_matrix_test.go new file mode 100644 index 00000000..2d109884 --- /dev/null +++ b/compilers/openapi/conformance_matrix_test.go @@ -0,0 +1,248 @@ +// This file is a package-level suite, not a per-source-file test: it reads +// docs/ir-spec-matrix.md and measures the whole committed corpus against it, so +// it pairs with no single source file. +package openapi_test // external test package — exercises only the public API + +import ( + "os" + "regexp" + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// matrixPath is the capability matrix, addressed relative to this test file. +const matrixPath = "../../docs/ir-spec-matrix.md" + +// matrixHeaderPrefix opens the one keyed capability table in the matrix. +const matrixHeaderPrefix = "| Key |" + +// matrixOpenAPIColumn is the header of the column this compiler answers to. +const matrixOpenAPIColumn = "OpenAPI 3.x" + +// matrixKeyPattern is the shape a row key must have: a lowercase slug. Keys are +// spelled in Go string literals and in a markdown table, so anything looser +// makes two spellings of one row possible. +var matrixKeyPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) + +// matrixRow is one capability row: its stable key, the capability it names, and +// the cell saying how OpenAPI expresses it. +type matrixRow struct { + key string + capability string + openAPI string +} + +// TestMatrix_RowsCarryUniqueSlugKeys pins the half of the contract that lives in +// the document: every row has a key, the keys are unique, and every OpenAPI cell +// opens with one of the legend's three markers. +// +// The marker check is what makes the coverage test below trustworthy. It reads +// an unmarked cell as neither expressible nor absent, so a row whose marker was +// lost in an edit would drop out of the corpus contract without failing +// anything — the silent direction of the failure, which is why it is rejected +// here rather than defaulted. +func TestMatrix_RowsCarryUniqueSlugKeys(t *testing.T) { + t.Parallel() + rows := readMatrixRows(t) + seen := make(map[string]string, len(rows)) + for _, row := range rows { + assert.Regexp(t, matrixKeyPattern, row.key, "row %q needs a lowercase-slug key", row.capability) + assert.NotContains(t, seen, row.key, + "rows %q and %q share the key %q", seen[row.key], row.capability, row.key) + seen[row.key] = row.capability + openAPIExpressible(t, row) + } + assert.Len(t, seen, len(rows), "every row contributes a distinct key") +} + +// TestConformance_EveryExpressibleMatrixRowIsWitnessed is the corpus contract +// CLAUDE.md and docs/architecture.md state in prose — "one minimal spec per +// ir-spec-matrix.md row per format that can express it" — as something that can +// disagree with the tree. +// +// It runs row → spec. The reverse direction, spec → row, is +// TestConformance_TableNamesEveryCorpusSpec's: between them, a spec cannot be +// added without a table row and a row cannot be added to the matrix without +// either a witnessing spec or a written reason there is none yet. +// +// A row OpenAPI cannot express must have no witness either. That direction +// catches the matrix and the corpus disagreeing the other way round: a spec +// naming such a row means one of the two is wrong about the source format, and +// leaving it unchecked would let the corpus quietly redefine the matrix. +func TestConformance_EveryExpressibleMatrixRowIsWitnessed(t *testing.T) { + t.Parallel() + witnesses := matrixRowWitnesses(t) + uncovered := matrixRowsUncovered() + for _, row := range readMatrixRows(t) { + if !openAPIExpressible(t, row) { + assert.NotContains(t, witnesses, row.key, + "matrix row %q is marked absent for OpenAPI, yet %v witness it", + row.key, witnesses[row.key]) + continue + } + if reason, excused := uncovered[row.key]; excused { + assert.NotEmpty(t, reason, "matrixRowsUncovered must say why %q has no spec", row.key) + assert.NotContains(t, witnesses, row.key, + "matrix row %q is listed uncovered yet witnessed by %v; delete its line", + row.key, witnesses[row.key]) + continue + } + assert.Contains(t, witnesses, row.key, + "matrix row %q (%s) has no conformance spec: add one naming it in conformanceCases, "+ + "or list it in matrixRowsUncovered with the reason it has none", row.key, row.capability) + } +} + +// TestConformance_MatrixRowNamesResolve requires every key spelled in Go to name +// a row the document declares. Without it a renamed or deleted matrix row leaves +// the corpus still claiming to cover it, and the coverage test above stays green +// because it only ever asks about rows the document still has. +func TestConformance_MatrixRowNamesResolve(t *testing.T) { + t.Parallel() + rows := readMatrixRows(t) + declared := make([]string, 0, len(rows)) + for _, row := range rows { + declared = append(declared, row.key) + } + require.NotEmpty(t, declared, "the matrix must declare rows to resolve against") + + for key, specs := range matrixRowWitnesses(t) { + assert.Contains(t, declared, key, + "corpus specs %v witness matrix row %q, which ir-spec-matrix.md does not declare", specs, key) + } + for key := range matrixRowsUncovered() { + assert.Contains(t, declared, key, + "matrixRowsUncovered names matrix row %q, which ir-spec-matrix.md does not declare", key) + } +} + +// matrixRowsUncovered names every OpenAPI-expressible matrix row the corpus does +// not witness, each with the reason it has none. Closing a gap means deleting +// its line here and naming the row from a case: a row that is both listed and +// witnessed fails, so the list cannot outlive the gap it describes. +func matrixRowsUncovered() map[string]string { + return map[string]string{ + "open-enums": "OpenAPI has no open-enum keyword; the matrix's ⚠ is the " + + "anyOf: [{enum: [...]}, {type: string}] idiom, which lowers as an ordinary union " + + "and needs a spec pinning that the enum branch survives beside the open one", + "long-running-operations": "OpenAPI states it only through vendor extensions, so a spec " + + "for this row would assert what extensions-x already asserts — that x-* survives — " + + "until the IR models polling as something a spec can read back", + "idempotency": "OpenAPI conveys it through HTTP verb semantics alone, and the method " + + "string http-binding pins is the whole of what a spec could read; there is no " + + "idempotency declaration to capture until the IR infers one as policy", + } +} + +// matrixRowWitnesses inverts the corpus table into row key → the specs naming +// it, rejecting a case that names one row twice. +func matrixRowWitnesses(t *testing.T) map[string][]string { + t.Helper() + witnesses := map[string][]string{} + for _, tc := range conformanceCases() { + for i, key := range tc.rows { + assert.NotContains(t, tc.rows[:i], key, "case %q names matrix row %q twice", tc.file, key) + witnesses[key] = append(witnesses[key], tc.file) + } + } + assert.NotEmpty(t, witnesses, "the corpus table must witness matrix rows") + return witnesses +} + +// openAPIExpressible reports whether the OpenAPI cell claims the capability is +// expressible, failing the test on a cell that opens with no legend marker. +func openAPIExpressible(t *testing.T, row matrixRow) bool { + t.Helper() + switch { + case strings.HasPrefix(row.openAPI, "✅"), strings.HasPrefix(row.openAPI, "⚠"): + return true + case strings.HasPrefix(row.openAPI, "—"): + return false + default: + t.Errorf("matrix row %q: OpenAPI cell %q opens with none of the legend markers ✅ ⚠ —", + row.key, row.openAPI) + return false + } +} + +// readMatrixRows parses the capability table into one matrixRow per data row. +func readMatrixRows(t *testing.T) []matrixRow { + t.Helper() + lines := matrixTableLines(t) + header := splitMatrixCells(lines[0]) + require.Equal(t, "Key", header[0], "the capability table's first column is the row key") + openAPIAt := slices.Index(header, matrixOpenAPIColumn) + require.Positive(t, openAPIAt, "the capability table needs a %q column", matrixOpenAPIColumn) + + rows := make([]matrixRow, 0, len(lines)) + for _, line := range lines[2:] { + cells := splitMatrixCells(line) + require.Len(t, cells, len(header), "row %q has a different cell count than the header", line) + rows = append(rows, matrixRow{ + key: strings.Trim(cells[0], "`"), + capability: cells[1], + openAPI: cells[openAPIAt], + }) + } + require.NotEmpty(t, rows, "the capability table must hold data rows") + return rows +} + +// matrixTableLines returns the keyed capability table: its header, its separator +// and every contiguous row under it. +// +// It requires the document to hold exactly one such table. The alternative — take +// the first — would let a second keyed table be added and read by nothing, which +// is the same class of silence this whole file exists to remove. +func matrixTableLines(t *testing.T) []string { + t.Helper() + data, err := os.ReadFile(matrixPath) + require.NoError(t, err) + + var table []string + inTable := false + for _, line := range strings.Split(string(data), "\n") { + switch { + case strings.HasPrefix(line, matrixHeaderPrefix): + require.Empty(t, table, "ir-spec-matrix.md holds more than one keyed capability table") + inTable = true + case !strings.HasPrefix(line, "|"): + inTable = false + } + if inTable { + table = append(table, line) + } + } + require.Greater(t, len(table), 2, "the keyed table needs a header, a separator and rows") + require.True(t, strings.HasPrefix(table[1], "|---"), "the header is followed by its separator") + return table +} + +// splitMatrixCells splits one markdown table row into trimmed cells, honouring +// the \| escape a cell uses for a literal pipe — the Erlang union spelling has +// one, and splitting naively would give that row an extra cell. +func splitMatrixCells(line string) []string { + body := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(line), "|"), "|") + var cells []string + var cell strings.Builder + escaped := false + for _, r := range body { + switch { + case escaped: + cell.WriteRune(r) + escaped = false + case r == '\\': + escaped = true + case r == '|': + cells = append(cells, strings.TrimSpace(cell.String())) + cell.Reset() + default: + cell.WriteRune(r) + } + } + return append(cells, strings.TrimSpace(cell.String())) +} diff --git a/compilers/openapi/conformance_test.go b/compilers/openapi/conformance_test.go index 17ae7d91..4d20afae 100644 --- a/compilers/openapi/conformance_test.go +++ b/compilers/openapi/conformance_test.go @@ -125,81 +125,93 @@ func corpusSpecNames(t *testing.T) []string { } // conformanceCase pairs one corpus spec with the assertion that says what -// capturing its capability losslessly means. +// capturing its capability losslessly means, and with the capability rows of +// ir-spec-matrix.md it witnesses. +// +// rows may be empty. The corpus also holds specs pinning a construct the matrix +// has no row for — a JSON Schema dialect keyword, an XML hint, a residue that +// must survive with no IR home — and a row invented to receive one of those +// would make the matrix describe the corpus instead of the source formats. The +// direction the contract runs in is row → spec, checked in +// conformance_matrix_test.go; the reverse direction is already covered, by +// TestConformance_TableNamesEveryCorpusSpec. type conformanceCase struct { file string assert func(*testing.T, *ir.Document, []ir.Diagnostic) + rows []string } -// conformanceCases is the corpus table: one row per spec, each naming the file -// and the assertion that reads it. +// conformanceCases is the corpus table: one row per spec, each naming the file, +// the assertion that reads it, and the matrix rows it witnesses. func conformanceCases() []conformanceCase { return []conformanceCase{ - {"named-types", assertNamedTypes}, - {"neutral-naming", assertNeutralNaming}, - {"empty-names", assertEmptyNames}, - {"inline-types", assertInlineTypes}, - {"component-reuse", assertComponentReuse}, - {"allof-inheritance", assertAllOfInheritance}, - {"allof-mixins", assertAllOfMixins}, - {"allof-inline-merge", assertAllOfInlineMerge}, - {"allof-required-only", assertAllOfRequiredOnly}, - {"allof-oneof-cooccurrence", assertAllOfOneOfCooccurrence}, - {"allof-inline-residue", assertAllOfInlineResidue}, - {"allof-ref-branch-siblings", assertAllOfRefBranchSiblings}, - {"allof-boolean-branch", assertAllOfBooleanBranch}, - {"oneof-discriminated", assertOneOfDiscriminated}, - {"discriminator-inheritance", assertDiscriminatorInheritance}, - {"discriminator-default-mapping", assertDiscriminatorDefaultMapping}, - {"unhomed-keywords", assertUnhomedKeywords}, - {"codeclared-keywords", assertCoDeclaredKeywords}, - {"anyof-untagged", assertAnyOfUntagged}, - {"negation-not", assertNegationNot}, - {"dependent-required", assertDependentRequired}, - {"dialect-keywords", assertDialectKeywords}, - {"dynamic-ref", assertDynamicRef}, - {"enum-string", assertEnumString}, - {"enum-numeric", assertEnumNumeric}, - {"scalar-format", assertScalarFormat}, - {"encoding-byte", assertEncodingByte}, - {"content-vocabulary", assertContentVocabulary}, - {"xml-hints", assertXMLHints}, - {"nullability-four-states", assertNullabilityFourStates}, - {"nullable-30", assertNullable30}, - {"nullable-31-ref", assertNullable31Ref}, - {"nullable-enum-31", assertNullableEnum31}, - {"defaults", assertDefaults}, - {"yaml-timestamp-scalars", assertYAMLTimestampScalars}, - {"constraints", assertConstraints}, - {"numeric-precision", assertNumericPrecision}, - {"readonly-writeonly", assertReadOnlyWriteOnly}, - {"recursive", assertRecursive}, - {"maps", assertMaps}, - {"tuples-prefixitems", assertTuples}, - {"literal-const", assertLiteralConst}, - {"tags-grouping", assertTagsGrouping}, - {"http-binding", assertHTTPBinding}, - {"param-styles", assertParamStyles}, - {"param-xml-residue", assertParamXMLResidue}, - {"param-ref-inheritance", assertParamRefInheritance}, - {"header-content-schema", assertHeaderContentSchema}, - {"multi-content", assertMultiContent}, - {"multipart-encoding", assertMultipartEncoding}, - {"file-body", assertFileBody}, - {"sequential-media", assertSequentialMedia}, - {"per-status-errors", assertPerStatusErrors}, - {"response-links", assertResponseLinks}, - {"webhooks", assertWebhooks}, - {"callbacks", assertCallbacks}, - {"deprecation", assertDeprecation}, - {"examples", assertExamples}, - {"docs-summary-desc", assertDocsSummaryDesc}, - {"extensions-x", assertExtensionsX}, - {"inline-annotations", assertInlineAnnotations}, - {"inline-residue", assertInlineResidue}, - {"servers-variables", assertServersVariables}, - {"security-schemes", assertSecuritySchemes}, - {"security-or-and", assertSecurityOrAnd}, + {"named-types", assertNamedTypes, []string{"named-objects"}}, + {"neutral-naming", assertNeutralNaming, []string{"wire-name-distinct"}}, + {"empty-names", assertEmptyNames, []string{"wire-name-distinct"}}, + {"inline-types", assertInlineTypes, []string{"inline-anonymous"}}, + {"component-reuse", assertComponentReuse, []string{"named-objects", "inline-anonymous"}}, + {"allof-inheritance", assertAllOfInheritance, []string{"inheritance"}}, + {"allof-mixins", assertAllOfMixins, []string{"intersection"}}, + {"allof-inline-merge", assertAllOfInlineMerge, []string{"intersection"}}, + {"allof-required-only", assertAllOfRequiredOnly, []string{"intersection"}}, + {"allof-oneof-cooccurrence", assertAllOfOneOfCooccurrence, []string{"intersection", "untagged-unions"}}, + {"allof-inline-residue", assertAllOfInlineResidue, []string{"intersection"}}, + {"allof-ref-branch-siblings", assertAllOfRefBranchSiblings, []string{"intersection", "untagged-unions"}}, + {"allof-boolean-branch", assertAllOfBooleanBranch, []string{"intersection"}}, + {"oneof-discriminated", assertOneOfDiscriminated, []string{"tagged-unions"}}, + {"discriminator-inheritance", assertDiscriminatorInheritance, []string{"tagged-unions", "inheritance"}}, + {"discriminator-default-mapping", assertDiscriminatorDefaultMapping, []string{"tagged-unions"}}, + {"unhomed-keywords", assertUnhomedKeywords, []string{"constraints"}}, + {"codeclared-keywords", assertCoDeclaredKeywords, []string{"intersection", "literal-types", "enums-string"}}, + {"anyof-untagged", assertAnyOfUntagged, []string{"untagged-unions"}}, + {"negation-not", assertNegationNot, []string{"negation"}}, + {"dependent-required", assertDependentRequired, []string{"constraints"}}, + {"dialect-keywords", assertDialectKeywords, nil}, + {"dynamic-ref", assertDynamicRef, []string{"recursive-types"}}, + {"enum-string", assertEnumString, []string{"enums-string"}}, + {"enum-numeric", assertEnumNumeric, []string{"enums-numeric"}}, + {"scalar-format", assertScalarFormat, []string{"custom-scalars"}}, + {"encoding-byte", assertEncodingByte, []string{"encoding-hints"}}, + {"content-vocabulary", assertContentVocabulary, []string{"encoding-hints"}}, + {"xml-hints", assertXMLHints, nil}, + {"nullability-four-states", assertNullabilityFourStates, []string{"optionality-vs-nullability"}}, + {"nullable-30", assertNullable30, []string{"optionality-vs-nullability"}}, + {"nullable-31-ref", assertNullable31Ref, []string{"optionality-vs-nullability"}}, + {"nullable-enum-31", assertNullableEnum31, []string{"optionality-vs-nullability", "enums-string"}}, + {"defaults", assertDefaults, []string{"defaults"}}, + {"yaml-timestamp-scalars", assertYAMLTimestampScalars, []string{"defaults", "literal-types"}}, + {"constraints", assertConstraints, []string{"constraints"}}, + {"numeric-precision", assertNumericPrecision, []string{"constraints", "defaults", "literal-types"}}, + {"readonly-writeonly", assertReadOnlyWriteOnly, []string{"visibility"}}, + {"recursive", assertRecursive, []string{"recursive-types"}}, + {"maps", assertMaps, []string{"maps"}}, + {"tuples-prefixitems", assertTuples, []string{"tuples", "positional-encoding"}}, + {"literal-const", assertLiteralConst, []string{"literal-types"}}, + {"tags-grouping", assertTagsGrouping, []string{"operation-grouping"}}, + {"http-binding", assertHTTPBinding, []string{"http-binding"}}, + {"param-styles", assertParamStyles, []string{"param-styles"}}, + {"param-style-matrix", assertParamStyleMatrix, []string{"param-styles"}}, + {"param-xml-residue", assertParamXMLResidue, nil}, + {"param-ref-inheritance", assertParamRefInheritance, []string{"defaults", "deprecation", "docs-summary-description"}}, + {"header-content-schema", assertHeaderContentSchema, []string{"multi-content"}}, + {"multi-content", assertMultiContent, []string{"multi-content"}}, + {"multipart-encoding", assertMultipartEncoding, []string{"multipart-encoding"}}, + {"file-body", assertFileBody, []string{"encoding-hints"}}, + {"sequential-media", assertSequentialMedia, []string{"streaming-server"}}, + {"per-status-errors", assertPerStatusErrors, []string{"per-status-errors"}}, + {"response-links", assertResponseLinks, []string{"pagination"}}, + {"webhooks", assertWebhooks, []string{"events-channels", "server-initiated-messages"}}, + {"callbacks", assertCallbacks, []string{"callbacks"}}, + {"inline-hoist-positions", assertInlineHoistPositions, []string{"inline-anonymous"}}, + {"deprecation", assertDeprecation, []string{"deprecation"}}, + {"examples", assertExamples, []string{"examples"}}, + {"docs-summary-desc", assertDocsSummaryDesc, []string{"docs-summary-description"}}, + {"extensions-x", assertExtensionsX, []string{"vendor-extensions"}}, + {"inline-annotations", assertInlineAnnotations, []string{"vendor-extensions", "inline-anonymous"}}, + {"inline-residue", assertInlineResidue, []string{"inline-anonymous"}}, + {"servers-variables", assertServersVariables, []string{"servers"}}, + {"security-schemes", assertSecuritySchemes, []string{"auth-schemes"}}, + {"security-or-and", assertSecurityOrAnd, []string{"per-op-auth"}}, } } @@ -524,6 +536,104 @@ func assertInlineTypes(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { assert.Equal(t, "shipping", inline.Name.Hint) } +// assertInlineHoistPositions pins the six operation-side positions an inline +// composite can be declared at, against the ID each one's source pointer +// derives. +// +// inline-types.yaml pins one position, a schema property; the inlinePositions +// table in compilers/openapi/internal/schema pins the nine that package reaches +// on its own. Neither reaches a parameter, a response header, a webhook or a +// callback, which are lowered a layer up — and the callback operation body had +// no anonymous node anywhere in the corpus, so a hoist that mis-derived its ID +// changed no golden at all. +// +// Both directions are asserted. The referring site must point at the derived ID, +// which is what a moved position changes; and the six targets must be six +// distinct nodes, which is what a hoist collapsing identical bodies onto one +// node changes. The fixture writes the same body at all six so that second +// failure is reachable at all. +func assertInlineHoistPositions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + got := inlineHoistPositionRefs(t, doc) + if diff := cmp.Diff(inlineHoistPositionIDs(), got); diff != "" { + t.Errorf("hoisted node per source position (-want +got):\n%s", diff) + } + + owner := map[ir.TypeID]string{} + for position, id := range got { + assert.NotContains(t, owner, id, + "the %s and %s positions share one node, %s", owner[id], position, id) + owner[id] = position + + node, found := doc.Types[id] + require.True(t, found, "%s: nothing is interned at %s", position, id) + model, ok := node.(*ir.Model) + require.True(t, ok, "%s: the inline object hoisted as a model", position) + assert.True(t, model.Anonymous, "%s: a minted node is anonymous", position) + assert.Len(t, model.Properties, 1, "%s: it kept the body it was declared with", position) + } +} + +// inlineHoistPositionIDs is the ID each position's source pointer derives, +// spelled out so a node that moves to another pointer is a diff rather than a +// silently different document. +func inlineHoistPositionIDs() map[string]ir.TypeID { + const order = "t/anon/paths/~1orders/post" + return map[string]ir.TypeID{ + "parameter schema": order + "/parameters/0/schema", + "response-header schema": order + "/responses/200/headers/X-Order-Trace/schema", + "request-body property": order + "/requestBody/content/application~1json/schema/properties/shipping", + "response-body property": order + "/responses/200/content/application~1json/schema/properties/receipt", + "webhook body": "t/anon/webhooks/onShipped/post/requestBody/content/application~1json/schema", + "callback body": order + "/callbacks/onProgress/{$request.body#~1callbackUrl}" + + "/post/requestBody/content/application~1json/schema", + } +} + +// inlineHoistPositionRefs reads back what each declaring site actually refers +// to. It walks from the operation rather than from doc.Types so a node interned +// at the right ID but wired to nothing still fails. +func inlineHoistPositionRefs(t *testing.T, doc *ir.Document) map[string]ir.TypeID { + t.Helper() + order, ok := opByName(doc, "placeOrder") + require.True(t, ok) + audit, ok := paramByName(order, "audit") + require.True(t, ok, "the operation declares its inline-schema parameter") + require.Len(t, order.Responses, 1) + require.Len(t, order.Responses[0].Headers, 1, "the response declares its inline-schema header") + webhook, ok := opByName(doc, "onShipped") + require.True(t, ok, "the webhook operation is registered") + callback, ok := opByName(doc, "onProgress") + require.True(t, ok, "the callback operation is registered") + + return map[string]ir.TypeID{ + "parameter schema": audit.Type.Target, + "response-header schema": order.Responses[0].Headers[0].Type.Target, + "request-body property": inlinePropTarget(t, doc, bodyTarget(t, order.Request), "shipping"), + "response-body property": inlinePropTarget(t, doc, bodyTarget(t, order.Responses[0].Payload), "receipt"), + "webhook body": bodyTarget(t, webhook.Request), + "callback body": bodyTarget(t, callback.Request), + } +} + +// bodyTarget returns the type a single-media-type payload refers to. +func bodyTarget(t *testing.T, payload *ir.Payload) ir.TypeID { + t.Helper() + require.NotNil(t, payload, "the operation declares a body") + require.Len(t, payload.Contents, 1, "the body declares one media type") + return payload.Contents[0].Type.Target +} + +// inlinePropTarget returns the type the named property of the model at id refers +// to — the body-property positions, one level inside a body root. +func inlinePropTarget(t *testing.T, doc *ir.Document, id ir.TypeID, wire string) ir.TypeID { + t.Helper() + model, ok := doc.Types[id].(*ir.Model) + require.True(t, ok, "the body at %s hoisted as a model", id) + prop, ok := propByWire(model, wire) + require.True(t, ok, "the body declares the property %q", wire) + return prop.Type.Target +} + // assertComponentReuse covers the non-schema half of `$ref`: OpenAPI lets a // parameter, requestBody, response, header, callback, or whole path item be // declared once under components and referenced from many operations. Each @@ -1452,6 +1562,139 @@ func assertAllowEmptyValueKept(t *testing.T, op ir.Operation) { assert.JSONEq(t, `true`, string(entry.Value)) } +// paramWire is what a parameter's location-dependent serialization resolves to: +// where it binds, the style it serializes with, and whether it explodes. The +// three travel together because OpenAPI settles them together — the location +// picks the style when none is written, and the style picks explode. +type paramWire struct { + Location ir.HTTPLocation + Style string + Explode bool +} + +// assertParamStyleMatrix pins the whole of OpenAPI's location-dependent +// serialization table at once, against a literal map rather than a spot check +// per location. +// +// The table used to be readable for a whole location without any golden +// noticing: deleting cookie from the arm that defaults it to form left the +// entire suite green, because no committed spec declared a cookie parameter. +// Comparing the resolved (location, style, explode) of every parameter closes +// that by construction — a location that loses its arm changes rows here. +func assertParamStyleMatrix(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + op, ok := opByName(doc, "styleMatrix") + require.True(t, ok) + require.Len(t, op.Bindings.HTTP, 1) + + got := make(map[string]paramWire, len(op.Bindings.HTTP[0].ParamBindings)) + for _, pb := range op.Bindings.HTTP[0].ParamBindings { + require.NotNil(t, pb.Explode, "%s: explode resolves to a value, never to nothing", pb.Param) + got[pb.Param] = paramWire{Location: pb.Location, Style: pb.Style, Explode: *pb.Explode} + } + if diff := cmp.Diff(paramStyleMatrixWant(), got); diff != "" { + t.Errorf("resolved parameter serialization (-want +got):\n%s", diff) + } + + assertParamStyleMatrixIsComplete(t, got) + assertQuerystringParam(t, op) +} + +// paramStyleMatrixWant is what param-style-matrix.yaml must resolve to: the nine +// legal (in, style) pairs with explode written both ways, plus the five +// locations with style omitted so the per-location default is what answers. +func paramStyleMatrixWant() map[string]paramWire { + const ( + path = ir.HTTPLocationPath + query = ir.HTTPLocationQuery + header = ir.HTTPLocationHeader + cookie = ir.HTTPLocationCookie + querystring = ir.HTTPLocationQuerystring + ) + return map[string]paramWire{ + "pathMatrixExplode": {path, "matrix", true}, + "pathMatrixNoExplode": {path, "matrix", false}, + "pathMatrixDefaultExplode": {path, "matrix", false}, + "pathLabelExplode": {path, "label", true}, + "pathLabelNoExplode": {path, "label", false}, + "pathSimpleExplode": {path, "simple", true}, + "pathSimpleNoExplode": {path, "simple", false}, + "pathDefaulted": {path, "simple", false}, + "queryFormExplode": {query, "form", true}, + "queryFormNoExplode": {query, "form", false}, + "querySpaceDelimitedExplode": {query, "spaceDelimited", true}, + "querySpaceDelimitedNoExplode": {query, "spaceDelimited", false}, + "queryPipeDelimitedExplode": {query, "pipeDelimited", true}, + "queryPipeDelimitedNoExplode": {query, "pipeDelimited", false}, + "queryDeepObjectExplode": {query, "deepObject", true}, + "queryDeepObjectNoExplode": {query, "deepObject", false}, + "queryDeepObjectDefaultExplode": {query, "deepObject", false}, + "queryDefaulted": {query, "form", true}, + "headerSimpleExplode": {header, "simple", true}, + "headerSimpleNoExplode": {header, "simple", false}, + "headerDefaulted": {header, "simple", false}, + "cookieFormExplode": {cookie, "form", true}, + "cookieFormNoExplode": {cookie, "form", false}, + "cookieDefaulted": {cookie, "form", true}, + // The compiler's own answer at a location that declares none — see + // assertQuerystringParam. + "querystringWhole": {querystring, "form", true}, + } +} + +// assertParamStyleMatrixIsComplete derives what the fixture reached and checks +// it against OpenAPI's own count rather than against itself: path takes +// simple|label|matrix, query form|spaceDelimited|pipeDelimited|deepObject, +// header simple and cookie form — nine pairs, eighteen with explode both ways. +// querystring is the fifth location and takes no style at all, so it is left out +// rather than counted as a tenth pair. +// +// The literal table above would still pass if a row were deleted from both it +// and the spec; this is what notices that, because the shrunk fixture no longer +// reaches nine. +func assertParamStyleMatrixIsComplete(t *testing.T, got map[string]paramWire) { + t.Helper() + type pair struct { + location ir.HTTPLocation + style string + } + pairs := map[pair]bool{} + triples := map[paramWire]bool{} + for _, w := range got { + if w.Location == ir.HTTPLocationQuerystring { + continue + } + pairs[pair{w.Location, w.Style}] = true + triples[w] = true + } + assert.Len(t, pairs, 9, "every legal (in, style) pair is exercised") + assert.Len(t, triples, 18, "and each of them with explode both ways") +} + +// assertQuerystringParam covers the 3.2 location that binds the whole query +// string. It may declare neither style nor schema, so the media type its +// one-entry content map names is the whole of its stated serialization. +// +// The style and explode asserted here are the compiler's, not the source's: it +// runs querystring through the same default arm as query and stamps form/true on +// a location the specification gives no style to (GitHub #334). They are pinned +// rather than left unasserted so that fixing #334 reddens this case and its +// golden instead of changing the IR in silence. +func assertQuerystringParam(t *testing.T, op ir.Operation) { + t.Helper() + var binding ir.HTTPParamBinding + for _, pb := range op.Bindings.HTTP[0].ParamBindings { + if pb.Location == ir.HTTPLocationQuerystring { + binding = pb + } + } + require.Equal(t, "querystringWhole", binding.Param, "the querystring parameter binds") + assert.Equal(t, "application/x-www-form-urlencoded", binding.ContentType, + "its media type is where its serialization is actually stated") + assert.Equal(t, "form", binding.Style, "today's synthesized style — GitHub #334") + require.NotNil(t, binding.Explode) + assert.True(t, *binding.Explode, "today's synthesized explode — GitHub #334") +} + // assertParamRefInheritance pins ir-design §14 at a parameter whose schema is a // $ref: docs, deprecation and default come from the referent when the use site is // silent, and from the use site when it is not. Constraints inherit at neither diff --git a/docs/architecture.md b/docs/architecture.md index 2fe2ac37..7ea8d277 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -287,7 +287,10 @@ stderr; the CLI renders diagnostics. snapshot-compared. IR changes show up as reviewable diffs. - **Capability conformance corpus**: one minimal spec per row of `ir-spec-matrix.md` per format that can express it, asserting the IR captures it losslessly. This is the regression net that - keeps "lossless by default" honest as compilers are added. + keeps "lossless by default" honest as compilers are added. The mapping is checked rather than + described: every matrix row carries a stable key, each corpus spec names the keys it witnesses, + and a row a format can express must be witnessed by a spec or listed as not-yet-covered with a + reason (`compilers/openapi/conformance_matrix_test.go`). - **Round-trip property**: `parse → serialize → deserialize → deep-equal` for every corpus document. - **Oracle sweep** (`internal/harness`): every corpus spec is driven through the oracles in order diff --git a/docs/ir-spec-matrix.md b/docs/ir-spec-matrix.md index 5a2bfabc..9b10b515 100644 --- a/docs/ir-spec-matrix.md +++ b/docs/ir-spec-matrix.md @@ -9,62 +9,68 @@ protocols. Legend: ✅ native concept · ⚠ expressible indirectly · — absent -| Capability | OpenAPI 3.x | Swagger 2.0 | TypeSpec | Smithy 2.0 | GraphQL | AsyncAPI | Protobuf | Erlang/OTP | -|---|---|---|---|---|---|---|---|---| -| Named object types | ✅ components.schemas | ✅ definitions | ✅ model | ✅ structure | ✅ type/input | ✅ schemas | ✅ message | ✅ -record/-type | -| Inline/anonymous types | ✅ | ✅ | ✅ | — (all named) | ⚠ | ✅ | ⚠ nested | ✅ type exprs | -| Inheritance / base types | ⚠ allOf | ⚠ allOf | ✅ extends | ⚠ mixins | ⚠ interfaces (conformance, not inheritance) | ⚠ allOf | — | — | -| Mixins / spread | — | — | ✅ spread | ✅ mixins | — | ⚠ traits | — | — | -| Tagged unions | ⚠ oneOf+discriminator | — | ✅ discriminated union | ✅ union | ⚠ union+__typename · ✅ @oneOf inputs (draft) | ⚠ oneOf | ✅ oneof | ✅ tagged tuples | -| Untagged unions | ✅ oneOf/anyOf | — | ✅ union | — | — | ✅ oneOf | — | ✅ \| | -| Intersection | ✅ allOf | ✅ allOf | ⚠ & (model is) | — | — | ✅ allOf | — | — | -| Negation | ✅ not | — | — | — | — | ✅ not | — | — | -| Enums (string) | ✅ | ✅ | ✅ named members | ✅ enum | ✅ | ✅ | ⚠ | ⚠ atom unions | -| Enums (numeric, valued) | ✅ | ✅ | ✅ | ✅ intEnum | — | ✅ | ✅ | ⚠ int unions | -| Open enums (unknown values allowed) | ⚠ anyOf trick | — | ⚠ union w/ string | ✅ (enums are open by default) | — | ⚠ | ✅ open (proto3/editions) / closed (proto2, per-enum feature) | ⚠ atom() fallback | -| Custom scalars | ⚠ type+format | ⚠ | ✅ scalar extends | ⚠ traits | ✅ scalar | ⚠ | — | ✅ -type/-opaque | -| Wire encoding hints (@encode / format) | ✅ format | ✅ format | ✅ @encode | ✅ timestampFormat | — | ✅ | ✅ fixed/zigzag/packed/delimited | — (ETF fixed) | -| Field wire IDs (numeric tags) | — | — | — | — | — | — | ✅ field numbers | ⚠ tuple positions | -| Wire name ≠ model name | ✅ (property key) | ✅ | ✅ @encodedName | ✅ jsonName (incl. union members) | — | ✅ | ✅ json_name | — | -| Optionality vs nullability distinct | ✅ (3.1) | ⚠ | ✅ | ⚠ presence only (null via @sparse collections) | ✅ | ✅ | ⚠ presence (3-state: implicit/explicit/required) | ⚠ :=/=> + 'undefined' | -| Defaults | ✅ | ✅ | ✅ | ✅ | ✅ args + input fields | ✅ | ✅ proto2 | ⚠ record fields | -| Constraints (min/max/pattern…) | ✅ | ✅ | ✅ decorators | ✅ traits | ⚠ directives (convention only) | ✅ | ⚠ protovalidate | ⚠ ranges, bit sizes | -| readOnly/writeOnly / visibility | ✅ | ✅ readOnly | ✅ @visibility classes | — | ✅ input vs output types | ⚠ (JSON Schema readOnly) | — | — | -| Recursive types | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Maps / additionalProperties | ✅ | ✅ | ✅ Record | ✅ map | — | ✅ | ✅ map | ✅ :=/=> | -| Tuples | ✅ prefixItems (3.1) | — | ✅ | — | — | ✅ | — | ✅ native | -| Literal types | ✅ const | ⚠ single enum | ✅ | — | — | ✅ | — | ✅ atoms/ints | -| Operations grouped by service/interface | ✅ tags (3.2 parent/kind) | ⚠ tags | ✅ interface/namespace | ✅ service/resource | ✅ Query/Mutation/Subscription | ⚠ | ✅ service | ✅ module | -| Resource hierarchy (CRUDL) | — | — | ⚠ @autoRoute | ✅ resource (incl. put, instance vs collection ops) | — | — | — | — | -| HTTP binding (method/path/status) | ✅ | ✅ | ✅ @route/@get… | ✅ http traits | — | ⚠ ws binding | ⚠ transcoding | — | -| Param styles (explode, matrix…) | ✅ style/explode | ⚠ collectionFormat | ✅ | ✅ | — | — | — | — | -| Multiple content types per body | ✅ | ⚠ consumes | ✅ @header contentType | ⚠ | — | ✅ | — | — | -| Multipart/form encoding | ✅ encoding | ✅ formData | ✅ multipart | — | — | — | — | — | -| Per-status error types | ✅ responses | ✅ | ✅ @error models | ✅ errors list (client/server fault) | — | — | ⚠ status codes | ⚠ {error, R} variants | -| Streaming: server (SSE/chunk) | ✅ itemSchema/sequential media types (3.2) | — | ✅ streams | ✅ eventstream | ✅ subscription | ✅ | ✅ stream | ⚠ info streams | -| Streaming: client / bidi | — | — | ✅ client · ⚠ bidi | ✅ | — | ✅ | ✅ | ⚠ cast/info flows | -| Events / pub-sub channels | ✅ webhooks (3.1) | — | ✅ events/sse | — | ✅ subscriptions | ✅ channels | — | ✅ gen_event/info | -| Callbacks / request-reply | ✅ callbacks | — | — | — | — | ✅ reply (static + dynamic address) | — | ⚠ From-reply | -| Pagination (first-class) | ⚠ x-* / links | — | ✅ @list/@pageItems + prev/first/last links | ✅ paginated trait | ⚠ connections | — | ⚠ AIP-158 | — | -| Long-running operations | ⚠ x-* | — | ⚠ Azure.Core @pollingOperation | ⚠ smithy.waiters | — | — | ⚠ google.longrunning | ⚠ send_request | -| Idempotency | ⚠ verb semantics | ⚠ | — | ✅ idempotent/@idempotencyToken | — | — | ✅ idempotency_level | — | -| Auth schemes | ✅ securitySchemes | ✅ | ✅ @useAuth | ✅ auth traits | — | ✅ (wide: SASL/X509/userPassword; attaches to servers) | ⚠ | — | -| Per-op auth override (AND/OR) | ✅ security | ✅ | ✅ | ⚠ OR only, priority-ordered | — | ✅ | — | — | -| Servers / endpoints | ✅ servers+vars (3.2 named) | ✅ host | ✅ @server | ⚠ @endpoint hostPrefix only | — | ✅ named servers+protocols+security | — | ⚠ nodes/registry | -| Protocol bindings (kafka/amqp/…) | — | — | — | — | — | ✅ bindings | — | ✅ behaviours | -| Versioning (added/removed) | — | — | ✅ @added/@removed | ⚠ @since | — | — | — | — | -| Deprecation w/ message | ✅ deprecated | ✅ | ✅ #deprecated | ✅ @deprecated | ✅ @deprecated(reason) | ✅ | ✅ | ✅ -deprecated | -| Examples | ✅ | ✅ | ✅ @example/@opExample | ✅ trait (input/output/error scenarios) | — | ✅ (header+payload pairs) | — | — | -| Docs: summary + description | ✅ | ✅ | ✅ @doc/@summary | ✅ @documentation | ✅ description | ✅ | ✅ comments | ✅ -doc/EDoc | -| Vendor extensions / traits / directives | ✅ x-* | ✅ x-* | ✅ decorators | ✅ traits | ✅ directives (ordered, repeatable) | ✅ x-* | ✅ options | ⚠ module attributes | -| One-way (fire-and-forget) operations | — | — | — | — | — | ✅ send w/o reply | — | ✅ cast | -| Positional wire encoding (records as tuples) | ⚠ prefixItems | — | ⚠ tuples | — | — | ⚠ items array | — | ✅ records/tuples | -| Symbol/atom literal values | — | — | — | — | — | — | — | ✅ atoms | -| Unsolicited server-initiated messages | ⚠ webhooks | — | — | — | ⚠ subscriptions | ✅ channels | — | ✅ info | -| Multi-format payload schemas | — | — | — | — | — | ✅ schemaFormat (Avro/Protobuf/RAML) | — | — | -| Third-party field extensions / extension ranges | — | — | — | — | — | — | ✅ extend/extensions | — | -| Field arguments (parameterized fields) | — | — | — | — | ✅ | — | — | — | -| Client-selectable response shape | — | — | — | — | ✅ selection sets | — | — | — | +The **Key** column is the row's stable identifier, and it is read by machine as well as by eye: +each conformance corpus spec names the keys it witnesses, and a test requires every row a format +can express to be witnessed by a spec or listed as not-yet-covered with a reason. Keys are +therefore append-only in spirit — renaming one, or deleting a row a spec still names, fails that +test. Adding a row that a format can express fails it too, until the row is witnessed or excluded. + +| Key | Capability | OpenAPI 3.x | Swagger 2.0 | TypeSpec | Smithy 2.0 | GraphQL | AsyncAPI | Protobuf | Erlang/OTP | +|---|---|---|---|---|---|---|---|---|---| +| `named-objects` | Named object types | ✅ components.schemas | ✅ definitions | ✅ model | ✅ structure | ✅ type/input | ✅ schemas | ✅ message | ✅ -record/-type | +| `inline-anonymous` | Inline/anonymous types | ✅ | ✅ | ✅ | — (all named) | ⚠ | ✅ | ⚠ nested | ✅ type exprs | +| `inheritance` | Inheritance / base types | ⚠ allOf | ⚠ allOf | ✅ extends | ⚠ mixins | ⚠ interfaces (conformance, not inheritance) | ⚠ allOf | — | — | +| `mixins` | Mixins / spread | — | — | ✅ spread | ✅ mixins | — | ⚠ traits | — | — | +| `tagged-unions` | Tagged unions | ⚠ oneOf+discriminator | — | ✅ discriminated union | ✅ union | ⚠ union+__typename · ✅ @oneOf inputs (draft) | ⚠ oneOf | ✅ oneof | ✅ tagged tuples | +| `untagged-unions` | Untagged unions | ✅ oneOf/anyOf | — | ✅ union | — | — | ✅ oneOf | — | ✅ \| | +| `intersection` | Intersection | ✅ allOf | ✅ allOf | ⚠ & (model is) | — | — | ✅ allOf | — | — | +| `negation` | Negation | ✅ not | — | — | — | — | ✅ not | — | — | +| `enums-string` | Enums (string) | ✅ | ✅ | ✅ named members | ✅ enum | ✅ | ✅ | ⚠ | ⚠ atom unions | +| `enums-numeric` | Enums (numeric, valued) | ✅ | ✅ | ✅ | ✅ intEnum | — | ✅ | ✅ | ⚠ int unions | +| `open-enums` | Open enums (unknown values allowed) | ⚠ anyOf trick | — | ⚠ union w/ string | ✅ (enums are open by default) | — | ⚠ | ✅ open (proto3/editions) / closed (proto2, per-enum feature) | ⚠ atom() fallback | +| `custom-scalars` | Custom scalars | ⚠ type+format | ⚠ | ✅ scalar extends | ⚠ traits | ✅ scalar | ⚠ | — | ✅ -type/-opaque | +| `encoding-hints` | Wire encoding hints (@encode / format) | ✅ format | ✅ format | ✅ @encode | ✅ timestampFormat | — | ✅ | ✅ fixed/zigzag/packed/delimited | — (ETF fixed) | +| `field-wire-ids` | Field wire IDs (numeric tags) | — | — | — | — | — | — | ✅ field numbers | ⚠ tuple positions | +| `wire-name-distinct` | Wire name ≠ model name | ✅ (property key) | ✅ | ✅ @encodedName | ✅ jsonName (incl. union members) | — | ✅ | ✅ json_name | — | +| `optionality-vs-nullability` | Optionality vs nullability distinct | ✅ (3.1) | ⚠ | ✅ | ⚠ presence only (null via @sparse collections) | ✅ | ✅ | ⚠ presence (3-state: implicit/explicit/required) | ⚠ :=/=> + 'undefined' | +| `defaults` | Defaults | ✅ | ✅ | ✅ | ✅ | ✅ args + input fields | ✅ | ✅ proto2 | ⚠ record fields | +| `constraints` | Constraints (min/max/pattern…) | ✅ | ✅ | ✅ decorators | ✅ traits | ⚠ directives (convention only) | ✅ | ⚠ protovalidate | ⚠ ranges, bit sizes | +| `visibility` | readOnly/writeOnly / visibility | ✅ | ✅ readOnly | ✅ @visibility classes | — | ✅ input vs output types | ⚠ (JSON Schema readOnly) | — | — | +| `recursive-types` | Recursive types | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `maps` | Maps / additionalProperties | ✅ | ✅ | ✅ Record | ✅ map | — | ✅ | ✅ map | ✅ :=/=> | +| `tuples` | Tuples | ✅ prefixItems (3.1) | — | ✅ | — | — | ✅ | — | ✅ native | +| `literal-types` | Literal types | ✅ const | ⚠ single enum | ✅ | — | — | ✅ | — | ✅ atoms/ints | +| `operation-grouping` | Operations grouped by service/interface | ✅ tags (3.2 parent/kind) | ⚠ tags | ✅ interface/namespace | ✅ service/resource | ✅ Query/Mutation/Subscription | ⚠ | ✅ service | ✅ module | +| `resource-hierarchy` | Resource hierarchy (CRUDL) | — | — | ⚠ @autoRoute | ✅ resource (incl. put, instance vs collection ops) | — | — | — | — | +| `http-binding` | HTTP binding (method/path/status) | ✅ | ✅ | ✅ @route/@get… | ✅ http traits | — | ⚠ ws binding | ⚠ transcoding | — | +| `param-styles` | Param styles (explode, matrix…) | ✅ style/explode | ⚠ collectionFormat | ✅ | ✅ | — | — | — | — | +| `multi-content` | Multiple content types per body | ✅ | ⚠ consumes | ✅ @header contentType | ⚠ | — | ✅ | — | — | +| `multipart-encoding` | Multipart/form encoding | ✅ encoding | ✅ formData | ✅ multipart | — | — | — | — | — | +| `per-status-errors` | Per-status error types | ✅ responses | ✅ | ✅ @error models | ✅ errors list (client/server fault) | — | — | ⚠ status codes | ⚠ {error, R} variants | +| `streaming-server` | Streaming: server (SSE/chunk) | ✅ itemSchema/sequential media types (3.2) | — | ✅ streams | ✅ eventstream | ✅ subscription | ✅ | ✅ stream | ⚠ info streams | +| `streaming-client` | Streaming: client / bidi | — | — | ✅ client · ⚠ bidi | ✅ | — | ✅ | ✅ | ⚠ cast/info flows | +| `events-channels` | Events / pub-sub channels | ✅ webhooks (3.1) | — | ✅ events/sse | — | ✅ subscriptions | ✅ channels | — | ✅ gen_event/info | +| `callbacks` | Callbacks / request-reply | ✅ callbacks | — | — | — | — | ✅ reply (static + dynamic address) | — | ⚠ From-reply | +| `pagination` | Pagination (first-class) | ⚠ x-* / links | — | ✅ @list/@pageItems + prev/first/last links | ✅ paginated trait | ⚠ connections | — | ⚠ AIP-158 | — | +| `long-running-operations` | Long-running operations | ⚠ x-* | — | ⚠ Azure.Core @pollingOperation | ⚠ smithy.waiters | — | — | ⚠ google.longrunning | ⚠ send_request | +| `idempotency` | Idempotency | ⚠ verb semantics | ⚠ | — | ✅ idempotent/@idempotencyToken | — | — | ✅ idempotency_level | — | +| `auth-schemes` | Auth schemes | ✅ securitySchemes | ✅ | ✅ @useAuth | ✅ auth traits | — | ✅ (wide: SASL/X509/userPassword; attaches to servers) | ⚠ | — | +| `per-op-auth` | Per-op auth override (AND/OR) | ✅ security | ✅ | ✅ | ⚠ OR only, priority-ordered | — | ✅ | — | — | +| `servers` | Servers / endpoints | ✅ servers+vars (3.2 named) | ✅ host | ✅ @server | ⚠ @endpoint hostPrefix only | — | ✅ named servers+protocols+security | — | ⚠ nodes/registry | +| `protocol-bindings` | Protocol bindings (kafka/amqp/…) | — | — | — | — | — | ✅ bindings | — | ✅ behaviours | +| `versioning` | Versioning (added/removed) | — | — | ✅ @added/@removed | ⚠ @since | — | — | — | — | +| `deprecation` | Deprecation w/ message | ✅ deprecated | ✅ | ✅ #deprecated | ✅ @deprecated | ✅ @deprecated(reason) | ✅ | ✅ | ✅ -deprecated | +| `examples` | Examples | ✅ | ✅ | ✅ @example/@opExample | ✅ trait (input/output/error scenarios) | — | ✅ (header+payload pairs) | — | — | +| `docs-summary-description` | Docs: summary + description | ✅ | ✅ | ✅ @doc/@summary | ✅ @documentation | ✅ description | ✅ | ✅ comments | ✅ -doc/EDoc | +| `vendor-extensions` | Vendor extensions / traits / directives | ✅ x-* | ✅ x-* | ✅ decorators | ✅ traits | ✅ directives (ordered, repeatable) | ✅ x-* | ✅ options | ⚠ module attributes | +| `one-way-operations` | One-way (fire-and-forget) operations | — | — | — | — | — | ✅ send w/o reply | — | ✅ cast | +| `positional-encoding` | Positional wire encoding (records as tuples) | ⚠ prefixItems | — | ⚠ tuples | — | — | ⚠ items array | — | ✅ records/tuples | +| `symbol-literals` | Symbol/atom literal values | — | — | — | — | — | — | — | ✅ atoms | +| `server-initiated-messages` | Unsolicited server-initiated messages | ⚠ webhooks | — | — | — | ⚠ subscriptions | ✅ channels | — | ✅ info | +| `multi-format-payloads` | Multi-format payload schemas | — | — | — | — | — | ✅ schemaFormat (Avro/Protobuf/RAML) | — | — | +| `field-extension-ranges` | Third-party field extensions / extension ranges | — | — | — | — | — | — | ✅ extend/extensions | — | +| `field-arguments` | Field arguments (parameterized fields) | — | — | — | — | ✅ | — | — | — | +| `selection-sets` | Client-selectable response shape | — | — | — | — | ✅ selection sets | — | — | — | ## Consequences for the IR diff --git a/testdata/conformance/openapi/inline-hoist-positions.golden.json b/testdata/conformance/openapi/inline-hoist-positions.golden.json new file mode 100644 index 00000000..5ced11d6 --- /dev/null +++ b/testdata/conformance/openapi/inline-hoist-positions.golden.json @@ -0,0 +1,683 @@ +{ + "irVersion": "0.3.0", + "name": "InlineHoistPositions", + "version": "1.0.0", + "docs": {}, + "services": [ + { + "id": "s/openapi/0", + "name": { + "source": "InlineHoistPositions", + "canonical": "inline_hoist_positions" + }, + "docs": {}, + "groups": [ + { + "name": { + "hint": "default" + }, + "docs": {}, + "operations": [ + { + "id": "op/openapi/paths/~1orders/post", + "name": { + "source": "placeOrder", + "canonical": "place_order" + }, + "docs": {}, + "params": [ + { + "name": { + "source": "audit", + "canonical": "audit" + }, + "type": { + "target": "t/anon/paths/~1orders/post/parameters/0/schema", + "nullable": false + }, + "required": false, + "docs": {} + } + ], + "request": { + "contents": [ + { + "mediaType": "application/json", + "type": { + "target": "t/anon/paths/~1orders/post/requestBody/content/application~1json/schema", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "200" + }, + "conditions": { + "statusCodes": [ + { + "from": 200, + "to": 200 + } + ] + }, + "payload": { + "contents": [ + { + "mediaType": "application/json", + "type": { + "target": "t/anon/paths/~1orders/post/responses/200/content/application~1json/schema", + "nullable": false + } + } + ] + }, + "headers": [ + { + "id": "p/openapi/paths/~1orders/post/responses/200/headers/X-Order-Trace", + "name": { + "source": "X-Order-Trace", + "canonical": "x_order_trace" + }, + "wireName": "X-Order-Trace", + "type": { + "target": "t/anon/paths/~1orders/post/responses/200/headers/X-Order-Trace/schema", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/headers/X-Order-Trace" + } + } + ], + "docs": { + "description": "ok" + } + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "http": [ + { + "method": "POST", + "uriTemplate": "/orders", + "sharedRoute": false, + "paramBindings": [ + { + "param": "audit", + "location": "query", + "wireName": "audit", + "style": "form", + "explode": true, + "allowReserved": false + } + ], + "requestContentTypes": [ + "application/json" + ], + "checksumRequired": false, + "isWebhook": false, + "callbacks": [ + { + "expression": "{$request.body#/callbackUrl}", + "operations": [ + "op/openapi/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post" + ] + } + ] + } + ] + }, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post" + } + }, + { + "id": "op/openapi/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post", + "name": { + "source": "onProgress", + "canonical": "on_progress" + }, + "docs": {}, + "request": { + "contents": [ + { + "mediaType": "application/json", + "type": { + "target": "t/anon/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post/requestBody/content/application~1json/schema", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "204" + }, + "conditions": { + "statusCodes": [ + { + "from": 204, + "to": 204 + } + ] + }, + "docs": { + "description": "no content" + } + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "http": [ + { + "method": "POST", + "uriTemplate": "{$request.body#/callbackUrl}", + "sharedRoute": false, + "requestContentTypes": [ + "application/json" + ], + "checksumRequired": false, + "isWebhook": false + } + ] + }, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post" + } + } + ] + }, + { + "name": { + "hint": "webhooks" + }, + "docs": {}, + "operations": [ + { + "id": "op/openapi/webhooks/onShipped/post", + "name": { + "source": "onShipped", + "canonical": "on_shipped" + }, + "docs": {}, + "request": { + "contents": [ + { + "mediaType": "application/json", + "type": { + "target": "t/anon/webhooks/onShipped/post/requestBody/content/application~1json/schema", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "204" + }, + "conditions": { + "statusCodes": [ + { + "from": 204, + "to": 204 + } + ] + }, + "docs": { + "description": "no content" + } + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "http": [ + { + "method": "POST", + "uriTemplate": "onShipped", + "sharedRoute": false, + "requestContentTypes": [ + "application/json" + ], + "checksumRequired": false, + "isWebhook": true + } + ] + }, + "provenance": { + "source": 0, + "pointer": "/webhooks/onShipped/post" + } + } + ] + } + ], + "auth": null, + "provenance": { + "source": 0 + } + } + ], + "types": { + "t/anon/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post/requestBody/content/application~1json/schema": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post/requestBody/content/application~1json/schema", + "name": { + "hint": "onProgress_request" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post/requestBody/content/application~1json/schema" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post/requestBody/content/application~1json/schema/properties/v", + "name": { + "source": "v", + "canonical": "v" + }, + "wireName": "v", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/callbacks/onProgress/{$request.body#~1callbackUrl}/post/requestBody/content/application~1json/schema/properties/v" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1orders/post/parameters/0/schema": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/parameters/0/schema", + "name": { + "hint": "audit" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/parameters/0/schema" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/parameters/0/schema/properties/v", + "name": { + "source": "v", + "canonical": "v" + }, + "wireName": "v", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/parameters/0/schema/properties/v" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1orders/post/requestBody/content/application~1json/schema": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/requestBody/content/application~1json/schema", + "name": { + "hint": "placeOrder_request" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/requestBody/content/application~1json/schema" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping", + "name": { + "source": "shipping", + "canonical": "shipping" + }, + "wireName": "shipping", + "type": { + "target": "t/anon/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping", + "name": { + "hint": "shipping" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping/properties/v", + "name": { + "source": "v", + "canonical": "v" + }, + "wireName": "v", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/requestBody/content/application~1json/schema/properties/shipping/properties/v" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1orders/post/responses/200/content/application~1json/schema": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/responses/200/content/application~1json/schema", + "name": { + "hint": "response" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/content/application~1json/schema" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt", + "name": { + "source": "receipt", + "canonical": "receipt" + }, + "wireName": "receipt", + "type": { + "target": "t/anon/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt", + "name": { + "hint": "receipt" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt/properties/v", + "name": { + "source": "v", + "canonical": "v" + }, + "wireName": "v", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/content/application~1json/schema/properties/receipt/properties/v" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1orders/post/responses/200/headers/X-Order-Trace/schema": { + "kind": "model", + "id": "t/anon/paths/~1orders/post/responses/200/headers/X-Order-Trace/schema", + "name": { + "hint": "X-Order-Trace" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/headers/X-Order-Trace/schema" + }, + "properties": [ + { + "id": "p/openapi/paths/~1orders/post/responses/200/headers/X-Order-Trace/schema/properties/v", + "name": { + "source": "v", + "canonical": "v" + }, + "wireName": "v", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/paths/~1orders/post/responses/200/headers/X-Order-Trace/schema/properties/v" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/webhooks/onShipped/post/requestBody/content/application~1json/schema": { + "kind": "model", + "id": "t/anon/webhooks/onShipped/post/requestBody/content/application~1json/schema", + "name": { + "hint": "onShipped_request" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/webhooks/onShipped/post/requestBody/content/application~1json/schema" + }, + "properties": [ + { + "id": "p/openapi/webhooks/onShipped/post/requestBody/content/application~1json/schema/properties/v", + "name": { + "source": "v", + "canonical": "v" + }, + "wireName": "v", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "/webhooks/onShipped/post/requestBody/content/application~1json/schema/properties/v" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + } + }, + "servers": [ + { + "name": { + "hint": "server" + }, + "urlTemplate": "/", + "description": {}, + "auth": null + } + ], + "sources": [ + { + "format": "openapi@3.1", + "path": "inline-hoist-positions.yaml", + "hash": "4a5d339bb37e2395112857818e2899507f5b06e02bee858105a5379b1ef7183d" + } + ] +} diff --git a/testdata/conformance/openapi/inline-hoist-positions.yaml b/testdata/conformance/openapi/inline-hoist-positions.yaml new file mode 100644 index 00000000..2445a910 --- /dev/null +++ b/testdata/conformance/openapi/inline-hoist-positions.yaml @@ -0,0 +1,71 @@ +# Every position outside a component schema where an inline composite is hoisted +# into a node of its own. inline-types.yaml pins one of them, the schema property; +# compilers/openapi/internal/schema's inlinePositions table pins the ones the +# schema package can reach on its own. These six are the operation-side rest: +# a parameter's schema, a response header's schema, a property inside a request +# body, a property inside a response body, a webhook operation's body, and a +# callback operation's body. +# +# The body is byte-identical at all six. A hoist that keyed on shape, or that let +# whichever position lowered first own the node, would collapse them into one and +# every ID below would move; distinct pointers are the only thing keeping them +# apart. +# +# Declaration order is part of the fixture. Webhooks are written before paths, and +# inside the operation callbacks before responses before requestBody before +# parameters — the reverse of the order the lowering walks them in. A golden that +# declared them in lowering order could not tell a pointer-derived ID from one +# minted in encounter order, because the two agree there. +openapi: 3.1.0 +info: {title: InlineHoistPositions, version: "1.0.0"} +webhooks: + onShipped: + post: + operationId: onShipped + requestBody: + required: true + content: + application/json: + schema: {type: object, properties: {v: {type: string}}} + responses: + "204": {description: no content} +paths: + /orders: + post: + operationId: placeOrder + callbacks: + onProgress: + '{$request.body#/callbackUrl}': + post: + operationId: onProgress + requestBody: + required: true + content: + application/json: + schema: {type: object, properties: {v: {type: string}}} + responses: + "204": {description: no content} + responses: + "200": + description: ok + headers: + X-Order-Trace: + schema: {type: object, properties: {v: {type: string}}} + content: + application/json: + schema: + type: object + properties: + receipt: {type: object, properties: {v: {type: string}}} + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + shipping: {type: object, properties: {v: {type: string}}} + parameters: + - name: audit + in: query + schema: {type: object, properties: {v: {type: string}}} diff --git a/testdata/conformance/openapi/param-style-matrix.golden.json b/testdata/conformance/openapi/param-style-matrix.golden.json new file mode 100644 index 00000000..efe7cedf --- /dev/null +++ b/testdata/conformance/openapi/param-style-matrix.golden.json @@ -0,0 +1,911 @@ +{ + "irVersion": "0.3.0", + "name": "ParamStyleMatrix", + "version": "1.0.0", + "docs": {}, + "services": [ + { + "id": "s/openapi/0", + "name": { + "source": "ParamStyleMatrix", + "canonical": "param_style_matrix" + }, + "docs": {}, + "groups": [ + { + "name": { + "hint": "default" + }, + "docs": {}, + "operations": [ + { + "id": "op/openapi/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get", + "name": { + "source": "styleMatrix", + "canonical": "style_matrix" + }, + "docs": {}, + "params": [ + { + "name": { + "source": "pathMatrixExplode", + "canonical": "path_matrix_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/0/schema", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathMatrixNoExplode", + "canonical": "path_matrix_no_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/1/schema", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathMatrixDefaultExplode", + "canonical": "path_matrix_default_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/2/schema", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathLabelExplode", + "canonical": "path_label_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/3/schema", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathLabelNoExplode", + "canonical": "path_label_no_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/4/schema", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathSimpleExplode", + "canonical": "path_simple_explode" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathSimpleNoExplode", + "canonical": "path_simple_no_explode" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "pathDefaulted", + "canonical": "path_defaulted" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": true, + "docs": {} + }, + { + "name": { + "source": "queryFormExplode", + "canonical": "query_form_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/8/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryFormNoExplode", + "canonical": "query_form_no_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/9/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "querySpaceDelimitedExplode", + "canonical": "query_space_delimited_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/10/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "querySpaceDelimitedNoExplode", + "canonical": "query_space_delimited_no_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/11/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryPipeDelimitedExplode", + "canonical": "query_pipe_delimited_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/12/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryPipeDelimitedNoExplode", + "canonical": "query_pipe_delimited_no_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/13/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryDeepObjectExplode", + "canonical": "query_deep_object_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/14/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryDeepObjectNoExplode", + "canonical": "query_deep_object_no_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/15/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryDeepObjectDefaultExplode", + "canonical": "query_deep_object_default_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/16/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "queryDefaulted", + "canonical": "query_defaulted" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "headerSimpleExplode", + "canonical": "header_simple_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/18/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "headerSimpleNoExplode", + "canonical": "header_simple_no_explode" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "headerDefaulted", + "canonical": "header_defaulted" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "cookieFormExplode", + "canonical": "cookie_form_explode" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/21/schema", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "cookieFormNoExplode", + "canonical": "cookie_form_no_explode" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "cookieDefaulted", + "canonical": "cookie_defaulted" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "docs": {} + }, + { + "name": { + "source": "querystringWhole", + "canonical": "querystring_whole" + }, + "type": { + "target": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/24/content/application~1x-www-form-urlencoded/schema", + "nullable": false + }, + "required": false, + "docs": {} + } + ], + "responses": [ + { + "name": { + "hint": "200" + }, + "conditions": { + "statusCodes": [ + { + "from": 200, + "to": 200 + } + ] + }, + "docs": { + "description": "ok" + } + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "http": [ + { + "method": "GET", + "uriTemplate": "/matrix/{pathMatrixExplode}/{pathMatrixNoExplode}/{pathMatrixDefaultExplode}/{pathLabelExplode}/{pathLabelNoExplode}/{pathSimpleExplode}/{pathSimpleNoExplode}/{pathDefaulted}", + "sharedRoute": false, + "paramBindings": [ + { + "param": "pathMatrixExplode", + "location": "path", + "wireName": "pathMatrixExplode", + "style": "matrix", + "explode": true, + "allowReserved": false + }, + { + "param": "pathMatrixNoExplode", + "location": "path", + "wireName": "pathMatrixNoExplode", + "style": "matrix", + "explode": false, + "allowReserved": false + }, + { + "param": "pathMatrixDefaultExplode", + "location": "path", + "wireName": "pathMatrixDefaultExplode", + "style": "matrix", + "explode": false, + "allowReserved": false + }, + { + "param": "pathLabelExplode", + "location": "path", + "wireName": "pathLabelExplode", + "style": "label", + "explode": true, + "allowReserved": false + }, + { + "param": "pathLabelNoExplode", + "location": "path", + "wireName": "pathLabelNoExplode", + "style": "label", + "explode": false, + "allowReserved": false + }, + { + "param": "pathSimpleExplode", + "location": "path", + "wireName": "pathSimpleExplode", + "style": "simple", + "explode": true, + "allowReserved": false + }, + { + "param": "pathSimpleNoExplode", + "location": "path", + "wireName": "pathSimpleNoExplode", + "style": "simple", + "explode": false, + "allowReserved": false + }, + { + "param": "pathDefaulted", + "location": "path", + "wireName": "pathDefaulted", + "style": "simple", + "explode": false, + "allowReserved": false + }, + { + "param": "queryFormExplode", + "location": "query", + "wireName": "queryFormExplode", + "style": "form", + "explode": true, + "allowReserved": false + }, + { + "param": "queryFormNoExplode", + "location": "query", + "wireName": "queryFormNoExplode", + "style": "form", + "explode": false, + "allowReserved": false + }, + { + "param": "querySpaceDelimitedExplode", + "location": "query", + "wireName": "querySpaceDelimitedExplode", + "style": "spaceDelimited", + "explode": true, + "allowReserved": false + }, + { + "param": "querySpaceDelimitedNoExplode", + "location": "query", + "wireName": "querySpaceDelimitedNoExplode", + "style": "spaceDelimited", + "explode": false, + "allowReserved": false + }, + { + "param": "queryPipeDelimitedExplode", + "location": "query", + "wireName": "queryPipeDelimitedExplode", + "style": "pipeDelimited", + "explode": true, + "allowReserved": false + }, + { + "param": "queryPipeDelimitedNoExplode", + "location": "query", + "wireName": "queryPipeDelimitedNoExplode", + "style": "pipeDelimited", + "explode": false, + "allowReserved": false + }, + { + "param": "queryDeepObjectExplode", + "location": "query", + "wireName": "queryDeepObjectExplode", + "style": "deepObject", + "explode": true, + "allowReserved": false + }, + { + "param": "queryDeepObjectNoExplode", + "location": "query", + "wireName": "queryDeepObjectNoExplode", + "style": "deepObject", + "explode": false, + "allowReserved": false + }, + { + "param": "queryDeepObjectDefaultExplode", + "location": "query", + "wireName": "queryDeepObjectDefaultExplode", + "style": "deepObject", + "explode": false, + "allowReserved": false + }, + { + "param": "queryDefaulted", + "location": "query", + "wireName": "queryDefaulted", + "style": "form", + "explode": true, + "allowReserved": false + }, + { + "param": "headerSimpleExplode", + "location": "header", + "wireName": "headerSimpleExplode", + "style": "simple", + "explode": true, + "allowReserved": false + }, + { + "param": "headerSimpleNoExplode", + "location": "header", + "wireName": "headerSimpleNoExplode", + "style": "simple", + "explode": false, + "allowReserved": false + }, + { + "param": "headerDefaulted", + "location": "header", + "wireName": "headerDefaulted", + "style": "simple", + "explode": false, + "allowReserved": false + }, + { + "param": "cookieFormExplode", + "location": "cookie", + "wireName": "cookieFormExplode", + "style": "form", + "explode": true, + "allowReserved": false + }, + { + "param": "cookieFormNoExplode", + "location": "cookie", + "wireName": "cookieFormNoExplode", + "style": "form", + "explode": false, + "allowReserved": false + }, + { + "param": "cookieDefaulted", + "location": "cookie", + "wireName": "cookieDefaulted", + "style": "form", + "explode": true, + "allowReserved": false + }, + { + "param": "querystringWhole", + "location": "querystring", + "wireName": "querystringWhole", + "style": "form", + "explode": true, + "allowReserved": false, + "contentType": "application/x-www-form-urlencoded" + } + ], + "checksumRequired": false, + "isWebhook": false + } + ] + }, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get" + } + } + ] + } + ], + "auth": null, + "provenance": { + "source": 0 + } + } + ], + "types": { + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/0/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/0/schema", + "name": { + "hint": "pathMatrixExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/0/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/1/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/1/schema", + "name": { + "hint": "pathMatrixNoExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/1/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/10/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/10/schema", + "name": { + "hint": "querySpaceDelimitedExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/10/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/11/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/11/schema", + "name": { + "hint": "querySpaceDelimitedNoExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/11/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/12/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/12/schema", + "name": { + "hint": "queryPipeDelimitedExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/12/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/13/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/13/schema", + "name": { + "hint": "queryPipeDelimitedNoExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/13/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/14/schema": { + "kind": "model", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/14/schema", + "name": { + "hint": "queryDeepObjectExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/14/schema" + }, + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/15/schema": { + "kind": "model", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/15/schema", + "name": { + "hint": "queryDeepObjectNoExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/15/schema" + }, + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/16/schema": { + "kind": "model", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/16/schema", + "name": { + "hint": "queryDeepObjectDefaultExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/16/schema" + }, + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/18/schema": { + "kind": "model", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/18/schema", + "name": { + "hint": "headerSimpleExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/18/schema" + }, + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/2/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/2/schema", + "name": { + "hint": "pathMatrixDefaultExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/2/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/21/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/21/schema", + "name": { + "hint": "cookieFormExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/21/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/24/content/application~1x-www-form-urlencoded/schema": { + "kind": "model", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/24/content/application~1x-www-form-urlencoded/schema", + "name": { + "hint": "querystringWhole" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/24/content/application~1x-www-form-urlencoded/schema" + }, + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/3/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/3/schema", + "name": { + "hint": "pathLabelExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/3/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/4/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/4/schema", + "name": { + "hint": "pathLabelNoExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/4/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/8/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/8/schema", + "name": { + "hint": "queryFormExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/8/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/9/schema": { + "kind": "list", + "id": "t/anon/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/9/schema", + "name": { + "hint": "queryFormNoExplode" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "/paths/~1matrix~1{pathMatrixExplode}~1{pathMatrixNoExplode}~1{pathMatrixDefaultExplode}~1{pathLabelExplode}~1{pathLabelNoExplode}~1{pathSimpleExplode}~1{pathSimpleNoExplode}~1{pathDefaulted}/get/parameters/9/schema" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + } + }, + "servers": [ + { + "name": { + "hint": "server" + }, + "urlTemplate": "/", + "description": {}, + "auth": null + } + ], + "sources": [ + { + "format": "openapi@3.2", + "path": "param-style-matrix.yaml", + "hash": "fafa8c2a49971685f6daab565009e1f71c06795144c0cc296cce1499ea48e7a0" + } + ] +} diff --git a/testdata/conformance/openapi/param-style-matrix.yaml b/testdata/conformance/openapi/param-style-matrix.yaml new file mode 100644 index 00000000..72763b96 --- /dev/null +++ b/testdata/conformance/openapi/param-style-matrix.yaml @@ -0,0 +1,64 @@ +openapi: 3.2.0 +info: {title: ParamStyleMatrix, version: "1.0.0"} +# The whole legal (in, style, explode) product of the OpenAPI Parameter object. +# `style` is location-dependent — path takes simple|label|matrix, query takes +# form|spaceDelimited|pipeDelimited|deepObject, header takes simple, cookie takes +# form, and querystring takes none — so the nine pairs, each with explode written +# both ways, are eighteen bindings. Beside them sit the five locations with style +# omitted, which is the only way the per-location default table is read at all. +# +# param-styles.yaml is the neighbouring spec and a different question: it covers +# the flags that are not part of that table (allowReserved, allowEmptyValue, a +# content-typed parameter, a path-item-level parameter shared by two operations). +paths: + /matrix/{pathMatrixExplode}/{pathMatrixNoExplode}/{pathMatrixDefaultExplode}/{pathLabelExplode}/{pathLabelNoExplode}/{pathSimpleExplode}/{pathSimpleNoExplode}/{pathDefaulted}: + get: + operationId: styleMatrix + parameters: + # in: path — simple | label | matrix. + - {name: pathMatrixExplode, in: path, required: true, style: matrix, explode: true, schema: {type: array, items: {type: string}}} + - {name: pathMatrixNoExplode, in: path, required: true, style: matrix, explode: false, schema: {type: array, items: {type: string}}} + # An explicit non-form style with explode omitted: the flag defaults off + # from the style, not from the location. + - {name: pathMatrixDefaultExplode, in: path, required: true, style: matrix, schema: {type: array, items: {type: string}}} + - {name: pathLabelExplode, in: path, required: true, style: label, explode: true, schema: {type: array, items: {type: string}}} + - {name: pathLabelNoExplode, in: path, required: true, style: label, explode: false, schema: {type: array, items: {type: string}}} + - {name: pathSimpleExplode, in: path, required: true, style: simple, explode: true, schema: {type: string}} + - {name: pathSimpleNoExplode, in: path, required: true, style: simple, explode: false, schema: {type: string}} + # Style omitted: the path default is simple, and simple does not explode. + - {name: pathDefaulted, in: path, required: true, schema: {type: string}} + + # in: query — form | spaceDelimited | pipeDelimited | deepObject. + - {name: queryFormExplode, in: query, style: form, explode: true, schema: {type: array, items: {type: string}}} + - {name: queryFormNoExplode, in: query, style: form, explode: false, schema: {type: array, items: {type: string}}} + - {name: querySpaceDelimitedExplode, in: query, style: spaceDelimited, explode: true, schema: {type: array, items: {type: string}}} + - {name: querySpaceDelimitedNoExplode, in: query, style: spaceDelimited, explode: false, schema: {type: array, items: {type: string}}} + - {name: queryPipeDelimitedExplode, in: query, style: pipeDelimited, explode: true, schema: {type: array, items: {type: string}}} + - {name: queryPipeDelimitedNoExplode, in: query, style: pipeDelimited, explode: false, schema: {type: array, items: {type: string}}} + - {name: queryDeepObjectExplode, in: query, style: deepObject, explode: true, schema: {type: object}} + - {name: queryDeepObjectNoExplode, in: query, style: deepObject, explode: false, schema: {type: object}} + - {name: queryDeepObjectDefaultExplode, in: query, style: deepObject, schema: {type: object}} + # Style omitted: the query default is form, and form explodes. + - {name: queryDefaulted, in: query, schema: {type: string}} + + # in: header — simple only. + - {name: headerSimpleExplode, in: header, style: simple, explode: true, schema: {type: object}} + - {name: headerSimpleNoExplode, in: header, style: simple, explode: false, schema: {type: string}} + - {name: headerDefaulted, in: header, schema: {type: string}} + + # in: cookie — form only. This is the location whose default arm no + # committed golden used to read, so a table that lost it stayed green. + - {name: cookieFormExplode, in: cookie, style: form, explode: true, schema: {type: array, items: {type: string}}} + - {name: cookieFormNoExplode, in: cookie, style: form, explode: false, schema: {type: string}} + - {name: cookieDefaulted, in: cookie, schema: {type: string}} + + # in: querystring — the 3.2 location that binds the whole query string. + # It may declare neither style nor schema, so it is the one location whose + # serialization is stated entirely by its media type. + - name: querystringWhole + in: querystring + content: + application/x-www-form-urlencoded: + schema: {type: object} + responses: + "200": {description: ok} From ff7909bc0ec1fdf97f7f612afdc6399777d914b8 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Mon, 10 Aug 2026 19:21:00 +0300 Subject: [PATCH 2/5] test(compilers/openapi): make the matrix contract catch what it claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The corpus-to-matrix checks added on this branch had several ways to pass while the thing they describe was broken. The parameter-style completeness check counted deduplicated sets, so any parameter shadowed by another resolving the same way could be deleted from both the fixture and the expectation undetected — every location-defaulting row among them, including the cookie row the fixture was written for. With it gone, the regression that motivated the fixture went green again. The expectation is now generated from the legal (in, style) table rather than typed out beside the fixture, so the only way to stop demanding a parameter is to delete the pair OpenAPI allows. The matrix reader ended the table at the first line not starting with a pipe, so a blank line or an indented row dropped that row and every row below it out of every check. It now requires the document to hold no table-shaped line outside the table, matches the header on parsed cells instead of raw text, and treats only a backslash-pipe as an escape. Six specs named capability rows their goldens do not show: a spec whose subject is keywords with no IR home claimed constraints, one whose format keyword lowers to a primitive claimed encoding hints, and so on. All six rows were witnessed elsewhere, so nothing went red. The pagination row had only a false witness and is now listed as uncovered. Also: a golden pins each row key to the capability it labels, so a column shift or a rename of a row no spec names is a diff; the legend guard covers all nine format columns rather than one; an excuse for a row OpenAPI cannot express is now rejected instead of living forever; the parameter map is keyed on (name, location) as OpenAPI keys it; and the querystring parameter moves to its own path item, since 3.2 forbids it sharing one with a query parameter. Two matrixRowsUncovered reasons pointed at an IR that could not hold the capability yet; ir.LongRunning and ir.Idempotency both model theirs, and the reasons now name the real blocker. --- CLAUDE.md | 6 +- compilers/openapi/conformance_matrix_test.go | 225 ++++++-- compilers/openapi/conformance_test.go | 272 +++++---- .../openapi/conformance_unmodeled_test.go | 3 +- .../openapi/internal/openapitest/result.go | 13 + .../internal/openapitest/result_test.go | 18 + .../openapi/internal/operation/params.go | 8 +- .../openapi/testdata/matrix-rows.golden.txt | 54 ++ docs/architecture.md | 10 +- docs/ir-spec-matrix.md | 11 +- .../inline-hoist-positions.golden.json | 2 +- .../openapi/inline-hoist-positions.yaml | 13 +- .../openapi/param-style-matrix.golden.json | 514 ++++++++++++++---- .../openapi/param-style-matrix.yaml | 66 ++- 14 files changed, 917 insertions(+), 298 deletions(-) create mode 100644 compilers/openapi/testdata/matrix-rows.golden.txt diff --git a/CLAUDE.md b/CLAUDE.md index f23bd474..31ed4670 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,7 +150,11 @@ These all exist already — extend them rather than building a parallel mechanis Corpus under `testdata/golden/`. - **Capability conformance corpus** (`testdata/conformance/`): one minimal spec per `ir-spec-matrix.md` row per format that can express it, asserting lossless capture. This is what - keeps "lossless by default" honest. + keeps "lossless by default" honest. The row↔spec mapping is machine-read, not prose: matrix rows + carry stable keys, each case names the keys it witnesses, and + `compilers/openapi/conformance_matrix_test.go` requires every expressible row to be witnessed or + listed with a reason. What it cannot check is whether a spec that *names* a row exercises that + capability — that claim is read by a reviewer, so weigh it like any other. - **Oracles**: `internal/harness` drives a spec through no-panic → no error diagnostic → `irverify` invariants → JSON round-trip → determinism → order-invariance, stopping at the first one that fires. `harness.Check` is the list — read it there rather than trusting this sentence; diff --git a/compilers/openapi/conformance_matrix_test.go b/compilers/openapi/conformance_matrix_test.go index 2d109884..befc0a71 100644 --- a/compilers/openapi/conformance_matrix_test.go +++ b/compilers/openapi/conformance_matrix_test.go @@ -1,12 +1,21 @@ // This file is a package-level suite, not a per-source-file test: it reads // docs/ir-spec-matrix.md and measures the whole committed corpus against it, so // it pairs with no single source file. +// +// The table reader below is format-agnostic and lives in one compiler's test +// package, which is the wrong altitude for it the moment a second compiler needs +// a second column. It is here rather than in internal/testspec because +// compilers/openapi is not allowed to reach outside the pipeline — archtest's +// allowlist for it is ir, compilers, compilers/compile and its own internal/* +// — and archtest skips _test.go files, so importing testspec from here would +// pass by way of a blind spot rather than by the rule. Whoever adds the Swagger +// column moves it to a home both compilers can reach, and will have two callers +// to shape the API with. package openapi_test // external test package — exercises only the public API import ( "os" "regexp" - "slices" "strings" "testing" @@ -17,8 +26,15 @@ import ( // matrixPath is the capability matrix, addressed relative to this test file. const matrixPath = "../../docs/ir-spec-matrix.md" -// matrixHeaderPrefix opens the one keyed capability table in the matrix. -const matrixHeaderPrefix = "| Key |" +// matrixRowsGolden snapshots each row key beside the capability it labels. +const matrixRowsGolden = "testdata/matrix-rows.golden.txt" + +// matrixKeyColumn and matrixCapabilityColumn are the two fixed columns every +// keyed capability table opens with; the rest are one format each. +const ( + matrixKeyColumn = "Key" + matrixCapabilityColumn = "Capability" +) // matrixOpenAPIColumn is the header of the column this compiler answers to. const matrixOpenAPIColumn = "OpenAPI 3.x" @@ -28,23 +44,44 @@ const matrixOpenAPIColumn = "OpenAPI 3.x" // makes two spellings of one row possible. var matrixKeyPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) +// matrixCell is one format's answer for a capability row. +type matrixCell struct { + column string + cell string +} + // matrixRow is one capability row: its stable key, the capability it names, and -// the cell saying how OpenAPI expresses it. +// every format's cell in document order. type matrixRow struct { key string capability string - openAPI string + formats []matrixCell +} + +// openAPI returns the cell of the column this compiler answers to. +func (r matrixRow) openAPI() string { + for _, f := range r.formats { + if f.column == matrixOpenAPIColumn { + return f.cell + } + } + return "" } // TestMatrix_RowsCarryUniqueSlugKeys pins the half of the contract that lives in -// the document: every row has a key, the keys are unique, and every OpenAPI cell +// the document: every row has a key, the keys are unique, and every format cell // opens with one of the legend's three markers. // -// The marker check is what makes the coverage test below trustworthy. It reads -// an unmarked cell as neither expressible nor absent, so a row whose marker was +// The marker check is what makes the coverage test below trustworthy. An +// unmarked cell is neither expressible nor absent, so a row whose marker was // lost in an edit would drop out of the corpus contract without failing // anything — the silent direction of the failure, which is why it is rejected // here rather than defaulted. +// +// It sweeps every format column, not only the one this compiler answers to. The +// legend is the document's, the next compiler reads the next column, and a +// contract enforced for one column of nine says nothing about the eight a +// reviewer would assume it covered. func TestMatrix_RowsCarryUniqueSlugKeys(t *testing.T) { t.Parallel() rows := readMatrixRows(t) @@ -54,9 +91,35 @@ func TestMatrix_RowsCarryUniqueSlugKeys(t *testing.T) { assert.NotContains(t, seen, row.key, "rows %q and %q share the key %q", seen[row.key], row.capability, row.key) seen[row.key] = row.capability - openAPIExpressible(t, row) + for _, f := range row.formats { + if _, known := legendMarker(f.cell); !known { + t.Errorf("matrix row %q: %s cell %q opens with none of the legend markers ✅ ⚠ —", + row.key, f.column, f.cell) + } + } + } +} + +// TestMatrix_KeysStayPinnedToTheirCapabilities snapshots every row as +// "keycapability". Regenerate with +// `go test ./compilers/openapi -run TestMatrix -update`. +// +// Nothing else binds a key to the row it labels. Every other test here asks only +// whether a key is *declared*, so an insert or delete that shifts the Key column +// against the Capability column by one leaves them all green while each corpus +// spec silently witnesses its neighbour's capability. +// +// It is also what makes the document's own promise — keys are append-only, and +// renaming one fails a test — true for rows no Go file names. Those are exactly +// the rows the next compiler will be first to bind to, and without a snapshot +// every one of them is freely renameable today. +func TestMatrix_KeysStayPinnedToTheirCapabilities(t *testing.T) { + t.Parallel() + var b strings.Builder + for _, row := range readMatrixRows(t) { + b.WriteString(row.key + "\t" + row.capability + "\n") } - assert.Len(t, seen, len(rows), "every row contributes a distinct key") + compareTextGolden(t, matrixRowsGolden, b.String()) } // TestConformance_EveryExpressibleMatrixRowIsWitnessed is the corpus contract @@ -69,19 +132,30 @@ func TestMatrix_RowsCarryUniqueSlugKeys(t *testing.T) { // added without a table row and a row cannot be added to the matrix without // either a witnessing spec or a written reason there is none yet. // -// A row OpenAPI cannot express must have no witness either. That direction -// catches the matrix and the corpus disagreeing the other way round: a spec -// naming such a row means one of the two is wrong about the source format, and -// leaving it unchecked would let the corpus quietly redefine the matrix. +// What it cannot check is that a spec naming a row exercises that capability; +// that claim is read by a reviewer, and docs/architecture.md says so rather than +// promising otherwise. +// +// A row OpenAPI cannot express must have neither a witness nor an excuse. Both +// directions catch the matrix and the corpus disagreeing: a witness means one of +// the two is wrong about the source format, and an excuse describes a gap that +// cannot exist, which nothing else would ever retire. func TestConformance_EveryExpressibleMatrixRowIsWitnessed(t *testing.T) { t.Parallel() witnesses := matrixRowWitnesses(t) uncovered := matrixRowsUncovered() for _, row := range readMatrixRows(t) { - if !openAPIExpressible(t, row) { + expressible, known := legendMarker(row.openAPI()) + if !known { + continue // TestMatrix_RowsCarryUniqueSlugKeys reports the unmarked cell. + } + if !expressible { assert.NotContains(t, witnesses, row.key, "matrix row %q is marked absent for OpenAPI, yet %v witness it", row.key, witnesses[row.key]) + assert.NotContains(t, uncovered, row.key, + "matrix row %q is marked absent for OpenAPI, so matrixRowsUncovered's line for it "+ + "describes a gap no spec could ever close; delete it", row.key) continue } if reason, excused := uncovered[row.key]; excused { @@ -124,17 +198,28 @@ func TestConformance_MatrixRowNamesResolve(t *testing.T) { // not witness, each with the reason it has none. Closing a gap means deleting // its line here and naming the row from a case: a row that is both listed and // witnessed fails, so the list cannot outlive the gap it describes. +// +// Each reason names the blocker as it stands today. Two of them used to point at +// an IR that could not hold the capability yet, which had stopped being true — +// ir.LongRunning and ir.Idempotency both model theirs — and nothing here can +// tell a stale reason from a live one, so a reader chasing one is sent to wait +// on a change that already landed. func matrixRowsUncovered() map[string]string { return map[string]string{ "open-enums": "OpenAPI has no open-enum keyword; the matrix's ⚠ is the " + "anyOf: [{enum: [...]}, {type: string}] idiom, which lowers as an ordinary union " + "and needs a spec pinning that the enum branch survives beside the open one", - "long-running-operations": "OpenAPI states it only through vendor extensions, so a spec " + - "for this row would assert what extensions-x already asserts — that x-* survives — " + - "until the IR models polling as something a spec can read back", - "idempotency": "OpenAPI conveys it through HTTP verb semantics alone, and the method " + - "string http-binding pins is the whole of what a spec could read; there is no " + - "idempotency declaration to capture until the IR infers one as policy", + "pagination": "OpenAPI states it only through links and x-*, and this compiler keeps both " + + "verbatim rather than reading either into ir.Pagination — response-links pins that they " + + "survive, so a spec for this row would assert nothing further until a policy pass infers " + + "pagination, which invariant 6 puts outside the compiler", + "long-running-operations": "OpenAPI states it only through vendor extensions, so a spec for " + + "this row would assert what extensions-x already asserts — that x-* survives. ir.LongRunning " + + "models the capability; what is missing is a policy pass reading those extensions into it, " + + "which invariant 6 puts outside the compiler", + "idempotency": "OpenAPI conveys it through HTTP verb semantics alone, and the method string " + + "http-binding pins is the whole of what a spec could read; ir.Idempotency models the " + + "capability, but nothing in the document declares it for a compiler to capture", } } @@ -153,19 +238,22 @@ func matrixRowWitnesses(t *testing.T) map[string][]string { return witnesses } -// openAPIExpressible reports whether the OpenAPI cell claims the capability is -// expressible, failing the test on a cell that opens with no legend marker. -func openAPIExpressible(t *testing.T, row matrixRow) bool { - t.Helper() +// legendMarker classifies one matrix cell by the legend marker it opens with: +// expressible says whether the format can state the capability, known whether a +// marker was found at all. +// +// The two are separate answers because an unmarked cell is not an absent one. +// Folding them into one bool made a lost marker report "marked absent for +// OpenAPI, yet these specs witness it" — a confident, false claim about the +// document, printed over the real cause. +func legendMarker(cell string) (expressible, known bool) { switch { - case strings.HasPrefix(row.openAPI, "✅"), strings.HasPrefix(row.openAPI, "⚠"): - return true - case strings.HasPrefix(row.openAPI, "—"): - return false + case strings.HasPrefix(cell, "✅"), strings.HasPrefix(cell, "⚠"): + return true, true + case strings.HasPrefix(cell, "—"): + return false, true default: - t.Errorf("matrix row %q: OpenAPI cell %q opens with none of the legend markers ✅ ⚠ —", - row.key, row.openAPI) - return false + return false, false } } @@ -174,18 +262,22 @@ func readMatrixRows(t *testing.T) []matrixRow { t.Helper() lines := matrixTableLines(t) header := splitMatrixCells(lines[0]) - require.Equal(t, "Key", header[0], "the capability table's first column is the row key") - openAPIAt := slices.Index(header, matrixOpenAPIColumn) - require.Positive(t, openAPIAt, "the capability table needs a %q column", matrixOpenAPIColumn) + require.Equal(t, matrixKeyColumn, header[0], "the capability table's first column is the row key") + require.Equal(t, matrixCapabilityColumn, header[1], "and its second names the capability") + require.Contains(t, header, matrixOpenAPIColumn, "the capability table needs a %q column", matrixOpenAPIColumn) rows := make([]matrixRow, 0, len(lines)) for _, line := range lines[2:] { cells := splitMatrixCells(line) require.Len(t, cells, len(header), "row %q has a different cell count than the header", line) + formats := make([]matrixCell, 0, len(header)-2) + for i, column := range header[2:] { + formats = append(formats, matrixCell{column: column, cell: cells[i+2]}) + } rows = append(rows, matrixRow{ key: strings.Trim(cells[0], "`"), capability: cells[1], - openAPI: cells[openAPIAt], + formats: formats, }) } require.NotEmpty(t, rows, "the capability table must hold data rows") @@ -195,53 +287,82 @@ func readMatrixRows(t *testing.T) []matrixRow { // matrixTableLines returns the keyed capability table: its header, its separator // and every contiguous row under it. // -// It requires the document to hold exactly one such table. The alternative — take -// the first — would let a second keyed table be added and read by nothing, which -// is the same class of silence this whole file exists to remove. +// It requires the document to hold exactly one such table, and to hold no +// table-shaped line outside it. That second requirement is what makes the first +// mean anything: the walk ends the table at the first line that is not a row, so +// a blank line, an HTML comment or a GFM-legal indent between two rows would +// otherwise drop that row and every row below it out of every check in this +// file — silently, since the rows at the end of the table are the ones no spec +// names. func matrixTableLines(t *testing.T) []string { t.Helper() data, err := os.ReadFile(matrixPath) require.NoError(t, err) var table []string + tableShaped := 0 inTable := false - for _, line := range strings.Split(string(data), "\n") { + for _, raw := range strings.Split(string(data), "\n") { + line := strings.TrimSpace(raw) + isRow := strings.HasPrefix(line, "|") + if isRow { + tableShaped++ + } switch { - case strings.HasPrefix(line, matrixHeaderPrefix): + case isRow && isMatrixHeader(line): require.Empty(t, table, "ir-spec-matrix.md holds more than one keyed capability table") inTable = true - case !strings.HasPrefix(line, "|"): + case !isRow: inTable = false } if inTable { table = append(table, line) } } + require.Greater(t, len(table), 2, "the keyed table needs a header, a separator and rows") - require.True(t, strings.HasPrefix(table[1], "|---"), "the header is followed by its separator") + require.True(t, strings.HasPrefix(strings.ReplaceAll(table[1], " ", ""), "|---"), + "the header is followed by its separator") + require.Len(t, table, tableShaped, + "ir-spec-matrix.md holds %d table-shaped lines and the keyed capability table reaches %d of "+ + "them: a blank line, a comment or an indented row between two rows ends the table early, "+ + "dropping every row below it from this file's checks", tableShaped, len(table)) return table } +// isMatrixHeader reports whether a table line opens the keyed capability table, +// deciding on parsed cells rather than raw text. +// +// A header padded for column alignment and one written without padding are the +// same header to every markdown reader. Matching raw text recognizes only one of +// the two: it lets a second keyed table evade the "exactly one" guard by leaving +// the padding out, and it loses the real table the moment a formatter adds it. +func isMatrixHeader(line string) bool { + cells := splitMatrixCells(line) + return len(cells) > 2 && cells[0] == matrixKeyColumn && cells[1] == matrixCapabilityColumn +} + // splitMatrixCells splits one markdown table row into trimmed cells, honouring // the \| escape a cell uses for a literal pipe — the Erlang union spelling has // one, and splitting naively would give that row an extra cell. +// +// Only \| is an escape. Consuming every backslash would silently rewrite any +// cell holding one for its own sake, which is a thing prose does. func splitMatrixCells(line string) []string { body := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(line), "|"), "|") + runes := []rune(body) var cells []string var cell strings.Builder - escaped := false - for _, r := range body { + for i := 0; i < len(runes); i++ { switch { - case escaped: - cell.WriteRune(r) - escaped = false - case r == '\\': - escaped = true - case r == '|': + case runes[i] == '\\' && i+1 < len(runes) && runes[i+1] == '|': + cell.WriteRune('|') + i++ + case runes[i] == '|': cells = append(cells, strings.TrimSpace(cell.String())) cell.Reset() default: - cell.WriteRune(r) + cell.WriteRune(runes[i]) } } return append(cells, strings.TrimSpace(cell.String())) diff --git a/compilers/openapi/conformance_test.go b/compilers/openapi/conformance_test.go index 39e1106f..fd644e38 100644 --- a/compilers/openapi/conformance_test.go +++ b/compilers/openapi/conformance_test.go @@ -137,6 +137,15 @@ func corpusSpecNames(t *testing.T) []string { // direction the contract runs in is row → spec, checked in // conformance_matrix_test.go; the reverse direction is already covered, by // TestConformance_TableNamesEveryCorpusSpec. +// +// Naming a row claims the spec's *golden* shows that capability captured, and +// no test can check that much — it is read by a reviewer. Reaching for a +// construct on the way to a different subject is not witnessing it: this table +// claimed constraints from a spec whose whole subject is keywords with no IR +// home, and encoding hints from one whose format keyword lowers to a primitive +// type rather than to ir.Encoding. Both rows were witnessed elsewhere, so +// nothing went red; had they not been, the corpus would have reported coverage +// it did not have. type conformanceCase struct { file string assert func(*testing.T, *ir.Document, []ir.Diagnostic) @@ -164,13 +173,13 @@ func conformanceCases() []conformanceCase { {"discriminator-inheritance", assertDiscriminatorInheritance, []string{"tagged-unions", "inheritance"}}, {"discriminator-default-mapping", assertDiscriminatorDefaultMapping, []string{"tagged-unions"}}, {"discriminator-transitive", assertDiscriminatorTransitive, []string{"tagged-unions", "inheritance"}}, - {"unhomed-keywords", assertUnhomedKeywords, []string{"constraints"}}, + {"unhomed-keywords", assertUnhomedKeywords, nil}, {"codeclared-keywords", assertCoDeclaredKeywords, []string{"intersection", "literal-types", "enums-string"}}, {"anyof-untagged", assertAnyOfUntagged, []string{"untagged-unions"}}, {"negation-not", assertNegationNot, []string{"negation"}}, - {"dependent-required", assertDependentRequired, []string{"constraints"}}, + {"dependent-required", assertDependentRequired, nil}, {"dialect-keywords", assertDialectKeywords, nil}, - {"dynamic-ref", assertDynamicRef, []string{"recursive-types"}}, + {"dynamic-ref", assertDynamicRef, nil}, {"enum-string", assertEnumString, []string{"enums-string"}}, {"enum-numeric", assertEnumNumeric, []string{"enums-numeric"}}, {"scalar-format", assertScalarFormat, []string{"custom-scalars"}}, @@ -197,13 +206,13 @@ func conformanceCases() []conformanceCase { {"param-style-matrix", assertParamStyleMatrix, []string{"param-styles"}}, {"param-xml-residue", assertParamXMLResidue, nil}, {"param-ref-inheritance", assertParamRefInheritance, []string{"defaults", "deprecation", "docs-summary-description"}}, - {"header-content-schema", assertHeaderContentSchema, []string{"multi-content"}}, + {"header-content-schema", assertHeaderContentSchema, nil}, {"multi-content", assertMultiContent, []string{"multi-content"}}, {"multipart-encoding", assertMultipartEncoding, []string{"multipart-encoding"}}, - {"file-body", assertFileBody, []string{"encoding-hints"}}, + {"file-body", assertFileBody, nil}, {"sequential-media", assertSequentialMedia, []string{"streaming-server"}}, {"per-status-errors", assertPerStatusErrors, []string{"per-status-errors"}}, - {"response-links", assertResponseLinks, []string{"pagination"}}, + {"response-links", assertResponseLinks, nil}, {"webhooks", assertWebhooks, []string{"events-channels", "server-initiated-messages"}}, {"callbacks", assertCallbacks, []string{"callbacks"}}, {"inline-hoist-positions", assertInlineHoistPositions, []string{"inline-anonymous"}}, @@ -538,17 +547,18 @@ func assertInlineTypes(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { // derives. // // inline-types.yaml pins one position, a schema property; the inlinePositions -// table in compilers/openapi/internal/schema pins the nine that package reaches +// table in compilers/openapi/internal/schema pins the ones that package reaches // on its own. Neither reaches a parameter, a response header, a webhook or a // callback, which are lowered a layer up — and the callback operation body had // no anonymous node anywhere in the corpus, so a hoist that mis-derived its ID // changed no golden at all. // -// Both directions are asserted. The referring site must point at the derived ID, -// which is what a moved position changes; and the six targets must be six -// distinct nodes, which is what a hoist collapsing identical bodies onto one -// node changes. The fixture writes the same body at all six so that second -// failure is reachable at all. +// Two things are asserted, and the second is not spare. The referring site must +// point at the derived ID, which a moved position changes. Then the six targets +// must be six distinct nodes — the check that survives a *synchronized* edit, +// where a hoist collapsing identical bodies onto one node and an expectation +// updated to match would agree with each other and leave the diff green. The +// fixture writes the same body at all six so a collapse is reachable at all. func assertInlineHoistPositions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { got := inlineHoistPositionRefs(t, doc) if diff := cmp.Diff(inlineHoistPositionIDs(), got); diff != "" { @@ -605,21 +615,13 @@ func inlineHoistPositionRefs(t *testing.T, doc *ir.Document) map[string]ir.TypeI return map[string]ir.TypeID{ "parameter schema": audit.Type.Target, "response-header schema": order.Responses[0].Headers[0].Type.Target, - "request-body property": inlinePropTarget(t, doc, bodyTarget(t, order.Request), "shipping"), - "response-body property": inlinePropTarget(t, doc, bodyTarget(t, order.Responses[0].Payload), "receipt"), - "webhook body": bodyTarget(t, webhook.Request), - "callback body": bodyTarget(t, callback.Request), + "request-body property": inlinePropTarget(t, doc, openapitest.BodyTarget(t, order.Request), "shipping"), + "response-body property": inlinePropTarget(t, doc, openapitest.BodyTarget(t, order.Responses[0].Payload), "receipt"), + "webhook body": openapitest.BodyTarget(t, webhook.Request), + "callback body": openapitest.BodyTarget(t, callback.Request), } } -// bodyTarget returns the type a single-media-type payload refers to. -func bodyTarget(t *testing.T, payload *ir.Payload) ir.TypeID { - t.Helper() - require.NotNil(t, payload, "the operation declares a body") - require.Len(t, payload.Contents, 1, "the body declares one media type") - return payload.Contents[0].Type.Target -} - // inlinePropTarget returns the type the named property of the model at id refers // to — the body-property positions, one level inside a body root. func inlinePropTarget(t *testing.T, doc *ir.Document, id ir.TypeID, wire string) ir.TypeID { @@ -662,13 +664,13 @@ func assertComponentReuse(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { assert.Equal(t, sortID, gadgets.Params[0].Type.Target, "the shared parameter interns once") listedID := ir.TypeID("t/anon/components/responses/Listed/content/application~1json/schema") - assert.Equal(t, listedID, widgets.Responses[0].Payload.Contents[0].Type.Target) - assert.Equal(t, listedID, order.Responses[0].Payload.Contents[0].Type.Target, + assert.Equal(t, listedID, openapitest.BodyTarget(t, widgets.Responses[0].Payload)) + assert.Equal(t, listedID, openapitest.BodyTarget(t, order.Responses[0].Payload), "a response reused across unrelated operations interns once") bodyID := ir.TypeID("t/anon/components/requestBodies/OrderBody/content/application~1json/schema") require.NotNil(t, order.Request) - assert.Equal(t, bodyID, order.Request.Contents[0].Type.Target) + assert.Equal(t, bodyID, openapitest.BodyTarget(t, order.Request)) assert.Equal(t, "OrderBody", doc.Types[bodyID].Common().Name.Hint, "a shared body is named after its component, not the operation that reached it first") @@ -1684,131 +1686,161 @@ func assertAllowEmptyValueKept(t *testing.T, op ir.Operation) { assert.JSONEq(t, `true`, string(entry.Value)) } +// paramID is a parameter's identity. OpenAPI keys the Parameter object on +// (name, in), so two parameters may legally share a name at different +// locations; a map keyed on the name alone silently keeps one of such a pair and +// compares the other against nothing. The compiler itself is keyed on both. +type paramID struct { + Param string + Location ir.HTTPLocation +} + // paramWire is what a parameter's location-dependent serialization resolves to: -// where it binds, the style it serializes with, and whether it explodes. The -// three travel together because OpenAPI settles them together — the location -// picks the style when none is written, and the style picks explode. +// the style it serializes with, and whether it explodes. The two travel together +// because OpenAPI settles them together — the location picks the style when none +// is written, and the style picks explode. type paramWire struct { - Location ir.HTTPLocation - Style string - Explode bool + Style string + Explode bool +} + +// paramStylePair is one legal (in, style) combination of the Parameter object. +type paramStylePair struct { + location ir.HTTPLocation + style string +} + +// param names the fixture parameter declaring this pair with one explode +// spelling: "