Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
416 changes: 416 additions & 0 deletions compilers/openapi/conformance_matrix_test.go

Large diffs are not rendered by default.

489 changes: 411 additions & 78 deletions compilers/openapi/conformance_test.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion compilers/openapi/conformance_unmodeled_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/dexpace/morphic/compilers/openapi/internal/openapitest"
"github.com/dexpace/morphic/ir"
)

Expand Down Expand Up @@ -352,7 +353,7 @@ func assertDynamicRef(t *testing.T, doc *ir.Document, diags []ir.Diagnostic) {
func assertInlineResidue(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) {
op, ok := opByName(doc, "getThing")
require.True(t, ok)
bodyID := op.Responses[0].Payload.Contents[0].Type.Target
bodyID := openapitest.BodyTarget(t, op.Responses[0].Payload)
body, ok := doc.Types[bodyID]
require.True(t, ok, "the response body owns a node")
assertResidue(t, body.Common().Unmodeled, map[string]string{
Expand Down
13 changes: 13 additions & 0 deletions compilers/openapi/internal/openapitest/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ func PropsByWire(props []ir.Property) map[string]ir.Property {
return IndexBy(props, func(p ir.Property) string { return p.WireName })
}

// BodyTarget returns the type a single-media-type payload refers to.
//
// The two requires are the reason this is a function rather than the indexing
// expression it wraps: written inline, a payload that is nil or that grew a
// second media type panics on a line that says nothing about which of the two
// happened.
func BodyTarget(t TB, 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
}

// RequireNoErrorDiags fails the test if any diagnostic has error severity,
// reporting the first offending diagnostic.
func RequireNoErrorDiags(t TB, diags []ir.Diagnostic) {
Expand Down
20 changes: 20 additions & 0 deletions compilers/openapi/internal/openapitest/result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ func TestIndexBy_KeysEveryItem(t *testing.T) {
assert.Len(t, byWire, 2)
}

// TestBodyTarget_RequiresExactlyOneMediaType pins that a second media type is a
// failure rather than a silent pick of the first, which is what the indexing
// expression this helper replaced did at every site.
//
// The recorder can drive this case and not the two that would leave the helper
// with nothing to index — a nil payload, or a payload with no media type at all
// — because its FailNow returns where a real one aborts, so the helper runs on
// past a guard that has already fired and panics. Two media types is the one
// failure that survives the round trip, so it is the one asserted here; the
// other two are covered by the passing case executing both guards.
func TestBodyTarget_RequiresExactlyOneMediaType(t *testing.T) {
t.Parallel()
one := &ir.Payload{Contents: []ir.Content{{Type: ir.TypeRef{Target: "t/prim/string"}}}}
assert.Equal(t, ir.TypeID("t/prim/string"), openapitest.BodyTarget(t, one))

r := &recorder{}
openapitest.BodyTarget(r, &ir.Payload{Contents: []ir.Content{{}, {}}})
assert.True(t, r.failed(), "a second media type must fail the test")
}

// TestRequireNoErrorDiags_FailsOnlyOnAnErrorSeverity pins that warnings pass
// and an error does not, which is the whole contract the suites lean on.
func TestRequireNoErrorDiags_FailsOnlyOnAnErrorSeverity(t *testing.T) {
Expand Down
8 changes: 6 additions & 2 deletions compilers/openapi/internal/operation/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,12 @@ func preserveAllowEmptyValue(c lowering.Ctx, param *ir.Parameter, p *soa.Paramet

// resolveStyleExplode materializes a parameter's resolved serialization style
// and explode flag: an explicit value wins, else the OpenAPI per-location
// default (query/cookie → form/true, path/header → simple/false). The result is
// declared facts, not policy.
// default (query/cookie → form/true, path/header → simple/false).
//
// For those four locations the result is declared facts, not policy. The fifth,
// querystring, is neither: the specification gives it no style at all, and it
// falls through the query arm here and comes out carrying form/true — a style
// that location may not have (GitHub #334).
func resolveStyleExplode(p *soa.Parameter, in soa.ParameterIn) (string, *bool) {
style := defaultParamStyle(in)
if p.Style != nil {
Expand Down
54 changes: 54 additions & 0 deletions compilers/openapi/testdata/matrix-rows.golden.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
named-objects Named object types
inline-anonymous Inline/anonymous types
inheritance Inheritance / base types
mixins Mixins / spread
tagged-unions Tagged unions
untagged-unions Untagged unions
intersection Intersection
negation Negation
enums-string Enums (string)
enums-numeric Enums (numeric, valued)
open-enums Open enums (unknown values allowed)
custom-scalars Custom scalars
encoding-hints Wire encoding hints (@encode / format)
field-wire-ids Field wire IDs (numeric tags)
wire-name-distinct Wire name ≠ model name
optionality-vs-nullability Optionality vs nullability distinct
defaults Defaults
constraints Constraints (min/max/pattern…)
visibility readOnly/writeOnly / visibility
recursive-types Recursive types
maps Maps / additionalProperties
tuples Tuples
literal-types Literal types
operation-grouping Operations grouped by service/interface
resource-hierarchy Resource hierarchy (CRUDL)
http-binding HTTP binding (method/path/status)
param-styles Param styles (explode, matrix…)
multi-content Multiple content types per body
multipart-encoding Multipart/form encoding
per-status-errors Per-status error types
streaming-server Streaming: server (SSE/chunk)
streaming-client Streaming: client / bidi
events-channels Events / pub-sub channels
callbacks Callbacks / request-reply
pagination Pagination (first-class)
long-running-operations Long-running operations
idempotency Idempotency
auth-schemes Auth schemes
per-op-auth Per-op auth override (AND/OR)
servers Servers / endpoints
protocol-bindings Protocol bindings (kafka/amqp/…)
versioning Versioning (added/removed)
deprecation Deprecation w/ message
examples Examples
docs-summary-description Docs: summary + description
vendor-extensions Vendor extensions / traits / directives
one-way-operations One-way (fire-and-forget) operations
positional-encoding Positional wire encoding (records as tuples)
symbol-literals Symbol/atom literal values
server-initiated-messages Unsolicited server-initiated messages
multi-format-payloads Multi-format payload schemas
field-extension-ranges Third-party field extensions / extension ranges
field-arguments Field arguments (parameterized fields)
selection-sets Client-selectable response shape
7 changes: 6 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,12 @@ 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. Row and spec are tied to each other
rather than left to prose: every matrix row carries a stable key, each corpus spec names the keys
it witnesses, and a row the OpenAPI column marks expressible must be witnessed by a spec or
listed as not-yet-covered with a reason (`compilers/openapi/conformance_matrix_test.go`). What
stays a reviewer's job is the claim inside that link — a spec naming a row has to *exercise* that
capability, and no test can read a golden and tell you whether it does.
- **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
Expand Down
Loading
Loading