From fbef8a856123b936c3e0672aa183f2dab5b704ae Mon Sep 17 00:00:00 2001 From: ShocOne <62835948+ShocOne@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:10:23 +0100 Subject: [PATCH 1/2] fix: a replayed configuration takes back the name it invented A recorded create carries the values the API accepted, and for most properties that is exactly what a configuration wants. A name is the exception: an API that requires one to be unique accepted the document's example once and refuses it for good afterwards, so the value that proves the shape is the one value a configuration cannot reuse. The invented name carries NamePrefix, which is what WithRunSuffix needs to make it unique per run and what the audit's cleanup contract matches a live object by. Without it a replayed fixture emitted the random_string block and referenced it nowhere. Only name-bearing entries are restored; every other property keeps the value the API took. A field whose document declares a format has no invented name to put back, so a URL, an email or a uuid is never rewritten. Co-Authored-By: Claude Opus 5 (1M context) --- .../observe/{bodies.go => request_bodies.go} | 0 ...{bodies_test.go => request_bodies_test.go} | 0 internal/fixtures/fixtures.go | 50 +++++++++++++++++-- internal/fixtures/fixtures_test.go | 48 ++++++++++++++++-- 4 files changed, 92 insertions(+), 6 deletions(-) rename internal/audit/observe/{bodies.go => request_bodies.go} (100%) rename internal/audit/observe/{bodies_test.go => request_bodies_test.go} (100%) diff --git a/internal/audit/observe/bodies.go b/internal/audit/observe/request_bodies.go similarity index 100% rename from internal/audit/observe/bodies.go rename to internal/audit/observe/request_bodies.go diff --git a/internal/audit/observe/bodies_test.go b/internal/audit/observe/request_bodies_test.go similarity index 100% rename from internal/audit/observe/bodies_test.go rename to internal/audit/observe/request_bodies_test.go diff --git a/internal/fixtures/fixtures.go b/internal/fixtures/fixtures.go index 68523d9..25b5d1a 100644 --- a/internal/fixtures/fixtures.go +++ b/internal/fixtures/fixtures.go @@ -638,8 +638,8 @@ func suffixedEntries(values []Entry) []Entry { return out } -// FromAcceptedBody answers the entries a recorded create actually carried, -// with the values it carried them as. +// FromAcceptedRequestBody answers the entries a recorded create actually +// carried, with the values it carried them as. // // This is the difference between a configuration that looks like one the API // would take and one it demonstrably did. A value derived from the document is @@ -649,13 +649,57 @@ func suffixedEntries(values []Entry) []Entry { // reason recorded: terraform compares what it planned against what the // provider answers, so a value the API never echoes reads as the provider // losing it, and no configuration can hold one. -func (s Fixture) FromAcceptedBody(request, response map[string]any, requiredWire map[string]bool) Fixture { +func (s Fixture) FromAcceptedRequestBody(request, response map[string]any, requiredWire map[string]bool) Fixture { out := s out.Entries, out.Omissions = overlayEntries(s.Entries, request, response, requiredWire, nil) out.Omissions = append(out.Omissions, s.Omissions...) + out.Entries = restoredNames(out.Entries) return out } +// restoredNames puts back the invented name of every name-bearing entry a +// declared example displaced. +// +// A replayed body carries the values the API accepted, and for most properties +// that is exactly what a configuration wants. A name is the exception. An API +// that requires one to be unique accepted the document's example once and +// refuses it for good afterwards, so the value that proves the shape is the one +// value the configuration cannot reuse. The invented name carries NamePrefix, +// which is what WithRunSuffix needs to make it unique per run and what the +// audit's cleanup contract matches a live object by. +// +// Only name-bearing entries are restored: every other property keeps the value +// the API took, because nothing about it demands a different one. A field +// whose document declares a format has no invented name to put back — +// scalarFor keeps none — so a URL, an email or a uuid is never rewritten. +func restoredNames(values []Entry) []Entry { + if values == nil { + return nil + } + out := make([]Entry, len(values)) + copy(out, values) + for i := range out { + if out[i].synthesised != "" && nameBearing(out[i].Name) { + out[i].Scalar = out[i].synthesised + } + out[i].Nested = restoredNames(out[i].Nested) + } + return out +} + +// nameBearing reports whether an attribute names its object — the attributes +// whose values must be unique per run and must carry the prefix cleanup +// matches on. +func nameBearing(name string) bool { + lower := strings.ToLower(name) + for _, suffix := range []string{"name", "title", "label"} { + if lower == suffix || strings.HasSuffix(lower, suffix) { + return true + } + } + return false +} + // overlayEntries keeps the entries the body carried, taking their values from // it, and reports the ones it dropped. func overlayEntries(values []Entry, request, response map[string]any, requiredWire map[string]bool, path []string) ([]Entry, []Omission) { diff --git a/internal/fixtures/fixtures_test.go b/internal/fixtures/fixtures_test.go index 99a082a..0dc55de 100644 --- a/internal/fixtures/fixtures_test.go +++ b/internal/fixtures/fixtures_test.go @@ -533,7 +533,7 @@ func TestUnit_Fixturespec_AReplayedBodyCarriesWhatTheAPITook(t *testing.T) { "labels": []any{"first"}, "settings": map[string]any{"retries": int64(3)}, } - got := spec.FromAcceptedBody(request, response, map[string]bool{"name": true}) + got := spec.FromAcceptedRequestBody(request, response, map[string]bool{"name": true}) // The values are the ones the request carried, not the ones derived. if v := valueByName(t, got, "name").Scalar; v != "the-name-it-took" { @@ -565,7 +565,7 @@ func TestUnit_Fixturespec_AReplayDropsWhatTheAPINeverReturns(t *testing.T) { // The API took matchType and answered without it. response := map[string]any{"name": "n", "colour": "#FF0000"} - got := spec.FromAcceptedBody(request, response, map[string]bool{"name": true}) + got := spec.FromAcceptedRequestBody(request, response, map[string]bool{"name": true}) for _, e := range got.Entries { if e.Name == "match_type" { @@ -582,7 +582,7 @@ func TestUnit_Fixturespec_AReplayDropsWhatTheAPINeverReturns(t *testing.T) { t.Errorf("the dropped property was not explained: %#v", got.Omissions) } // A required property stays even unreturned: the create needs it. - requiredUnreturned := spec.FromAcceptedBody( + requiredUnreturned := spec.FromAcceptedRequestBody( map[string]any{"name": "n"}, map[string]any{}, map[string]bool{"name": true}) if len(requiredUnreturned.Entries) != 1 { t.Errorf("a required property was dropped for not being echoed: %#v", requiredUnreturned.Entries) @@ -605,3 +605,45 @@ func TestUnit_Fixturespec_TheRunSuffixOnlyTouchesInventedNames(t *testing.T) { t.Errorf("a document value = %#v, want it left alone", v) } } + +func TestUnit_Fixturespec_AReplayedNameGoesBackToTheInventedOne(t *testing.T) { + spec := Derive(exampleTree()) + // What the audit sent and the API took: the document's own example, which + // an API that requires a unique name accepts once and refuses thereafter. + body := map[string]any{ + "name": "Production metrics stream", + "endpointUrl": "https://api.example.otel-collector", + } + + got := spec.FromAcceptedRequestBody(body, body, map[string]bool{"name": true}) + + if v := valueByName(t, got, "name").Scalar; v != NamePrefix+"name" { + t.Errorf("name = %#v, want the invented name back", v) + } + // The invented name is what the run suffix recognises, so a replayed + // configuration is unique per run rather than a constant that collides. + suffixed := got.WithRunSuffix() + if v := valueByName(t, suffixed, "name").Scalar; v != NamePrefix+"name-"+RunSuffixExpr { + t.Errorf("suffixed name = %#v, want the run suffix appended", v) + } +} + +func TestUnit_Fixturespec_AReplayKeepsEveryValueThatDoesNotNameTheObject(t *testing.T) { + spec := Derive(exampleTree()) + body := map[string]any{ + "name": "Production metrics stream", + "endpointUrl": "https://api.example.otel-collector", + "interval": int64(300), + } + + got := spec.FromAcceptedRequestBody(body, body, map[string]bool{"name": true}) + + // A string the API took that names nothing keeps the value it took: an + // invented name is a string, and this one had to be a URL. + if v := valueByName(t, got, "endpoint_url").Scalar; v != "https://api.example.otel-collector" { + t.Errorf("endpoint_url = %#v, want the value the API took", v) + } + if v := valueByName(t, got, "interval").Scalar; v != int64(300) { + t.Errorf("interval = %#v, want the value the API took", v) + } +} From df72af1c5187052df7303e6c63991bf9656ad250 Mon Sep 17 00:00:00 2001 From: ShocOne <62835948+ShocOne@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:10:23 +0100 Subject: [PATCH 2/2] refactor: the recorded artifact is named for the request bodies it holds The artifact records the create request bodies an API accepted, and calling it "bodies" left it ambiguous with the response bodies a mock answers with and the collection responses the inference reads. Names it for the half a configuration has to reproduce. Renames the type, its constants and its accessors, the committed path audit/request_bodies/.request_bodies.json, and the loader's map of components.requestBodies. ListBodies keeps its name: those are responses. Also clears two lint findings the branch carried: an unused parameter on reduceMaximal and a negated conjunction in the program-order test. Co-Authored-By: Claude Opus 5 (1M context) --- docs/glossary.md | 1 + handoff.md | 14 ++--- internal/audit/infer/evidence.go | 8 +-- internal/audit/infer/infer.go | 4 +- internal/audit/infer/infer_test.go | 38 ++++++------ internal/audit/observe/request_bodies.go | 61 ++++++++++--------- internal/audit/observe/request_bodies_test.go | 28 ++++----- internal/audit/run/adaptive_test.go | 4 +- internal/audit/run/entity.go | 28 ++++----- internal/audit/run/evidence.go | 8 +-- internal/audit/run/run.go | 6 +- internal/audit/run/run_test.go | 2 +- internal/audit/run/steps_create.go | 12 ++-- internal/audit/run/strategize.go | 4 +- internal/audit/strategy/strategy_test.go | 2 +- internal/cli/audit.go | 8 +-- internal/emit/provider_core.go | 4 +- internal/emit/render_fixtures.go | 6 +- internal/providergen/providergen.go | 2 +- internal/specmodel/load.go | 26 ++++---- 20 files changed, 135 insertions(+), 131 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index c67af20..36a4a4e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -11,6 +11,7 @@ a binder that drafts a call or a loader that merges `allOf` is prose. |---|---| | **audit** | The credentialed stage that exercises a live API to learn its true behaviour — minimum and maximum valid configuration, field dependencies, value-conditional rules. `tfpfgen audit run`. The only stage that touches a network. | | **observation** | One recorded finding of an audit: what the live API actually accepted or rejected, with a redacted request/response excerpt as proof. Committed per entity in `audit/observations/.observations.json`, stamped with the spec hash it was observed against. Deliberately not replayable. | +| **request bodies** | The create request bodies a run got the API to accept, committed per entity in `audit/request_bodies/.request_bodies.json` with the status each was answered and the response it was answered with. An observation says something about one property; these say what a whole create looked like when it worked, which is the one thing a generated acceptance test cannot derive — the document describes what should be accepted, and only a run knows what was. Acceptance fixtures replay these values rather than deriving them again. Named for the request half deliberately: the response is carried as evidence of what the API echoed, and it is the request that a configuration has to reproduce. | | **correction** | One committed correction to the imported OpenAPI document: RFC 6902 operations plus a required justification and an optional evidence pointer to an observation. Lives in `spec/corrections/`; proposed ones await a human in `spec/corrections/proposed/`; rejected ones leave a marker in `spec/corrections/rejected/`. Kinds listed in config `audit.auto_accept` skip `proposed/` and land accepted directly, named with an `auto-NNN-` prefix. | | **revise** | To fold observations into proposed corrections and apply accepted ones — `tfpfgen spec revise`. The spec is revised based on audit observations; the output is the revised spec (`spec/revised.yaml`), the single source of truth for all generation. | | **import** | To pin the upstream OpenAPI document by hash — `tfpfgen spec import`. The imported document is immutable evidence of what the vendor published. | diff --git a/handoff.md b/handoff.md index 92bc54b..933303e 100644 --- a/handoff.md +++ b/handoff.md @@ -77,7 +77,7 @@ For `` under `internal/services/resources//v1//`: | File | Purpose | |---|---| | `resource_acceptance_test.go` | the live lifecycle: **step 1** apply minimal, **step 2** import + `ImportStateVerify`, **step 3** apply maximal | -| `tests/terraform/acceptance/resource_{minimal,maximal}.tf` | what the live steps apply — **built from recorded bodies** | +| `tests/terraform/acceptance/resource_{minimal,maximal}.tf` | what the live steps apply — **built from recorded request bodies** | | `tests/terraform/unit/resource_{minimal,maximal}.tf` | what the unit tests apply — **still derived from the document** | | `tests/responses/resource_{minimal,maximal}.json` | wire JSON the mocks answer with | | `mocks/responders.go` | httpmock responders built from those | @@ -94,7 +94,7 @@ Under the provider tree: | Path | What | |---|---| | `audit/observations/.observations.json` | one fact per property | -| `audit/bodies/.bodies.json` | **the accepted create bodies** — request, response, status | +| `audit/request_bodies/.request_bodies.json` | **the accepted create request bodies** — request, response, status | | `audit/inputs.json` | operator-supplied values the probe cannot invent (authored) | | `spec/corrections/*.correction.json` | accepted corrections (authored) | | `spec/corrections/proposed/` | awaiting a human decision | @@ -111,11 +111,11 @@ OpenAPI doc ──> specmodel ──> IR ──> sdkbind ──> emit ──> pr ^ | | v corrections <── revise <── observations <── audit (live probe) - + bodies + + request bodies ``` Presence (`required` / `optional` / `computed`) is corrected into the document -and re-derived. **Values are not.** Values come from `audit/bodies/` and are +and re-derived. **Values are not.** Values come from `audit/request_bodies/` and are replayed directly into acceptance fixtures. That split is the point: deriving values again from the document is what produced a year of one-field-at-a-time failures (`icon`, `match_type`, `filters` were all the same bug). @@ -127,8 +127,8 @@ failures (`icon`, `match_type`, `filters` were all the same bug). | Additive minimal search (add a field until 2xx) | `internal/audit/run/steps_create.go: searchMinimal` | | Subtractive maximal reduction (drop until 2xx) | `internal/audit/run/steps_create.go: reduceMaximal` | | Refusal grammar (what a 4xx names) | `internal/audit/run/adjust.go: classifyRefusal` | -| Recorded bodies artifact | `internal/audit/observe/bodies.go` | -| Replaying a body into a fixture | `internal/fixtures/fixtures.go: FromAcceptedBody` | +| Recorded request bodies artifact | `internal/audit/observe/request_bodies.go` | +| Replaying a request body into a fixture | `internal/fixtures/fixtures.go: FromAcceptedRequestBody` | | Per-run unique names | `internal/fixtures/fixtures.go: WithRunSuffix`, `RunSuffixBlock` | | Acceptance vs unit split | `internal/emit/render_fixtures.go: resourceFixtures` | | Probe step ordering | `internal/audit/strategy/program.go: buildProgram` | @@ -182,7 +182,7 @@ probe**. Everything else is downstream of that. (`tests_ftp_server`), a dashboard id (`dashboard_snapshot`), a discriminator `type` for `connectors_generic` / `operations_webhook`. 3. **The four inconsistent-result failures.** A value sent and echoed back - differently. `FromAcceptedBody` already drops what is never returned; this is + differently. `FromAcceptedRequestBody` already drops what is never returned; this is the narrower case of a value the API rewrites. 4. **`Invalid id` / `request never completed`** — one each, `templates_sharing_setting` (a singleton) and `stream` (`lastSuccess`/`lastFailure` declared `int64`, the diff --git a/internal/audit/infer/evidence.go b/internal/audit/infer/evidence.go index f7da935..9b75cd3 100644 --- a/internal/audit/infer/evidence.go +++ b/internal/audit/infer/evidence.go @@ -96,10 +96,10 @@ type ConditionalValue struct { type Evidence struct { // Entity is the classified entity key the evidence is about. Entity string - // AcceptedBodies is every create body the API accepted, resolved as + // AcceptedRequestBodies is every create body the API accepted, resolved as // sent. Their gate values and field sets are the positive half of // variant diffing. - AcceptedBodies []map[string]any + AcceptedRequestBodies []map[string]any // Adjustments is every body change the executor was forced to make. Adjustments []RequestAdjustment // CombinedRefusals lists field pairs a create was refused for carrying @@ -164,9 +164,9 @@ func gateOf(compiled *strategy.Strategy) gate { // value that body pinned, returning value -> set of field names present // (excluding the gate field itself). A body with no gate value lands under the // empty string. -func acceptedUnder(bodies []map[string]any, gateField string) map[string]map[string]bool { +func acceptedUnder(requestBodies []map[string]any, gateField string) map[string]map[string]bool { out := map[string]map[string]bool{} - for _, body := range bodies { + for _, body := range requestBodies { val := "" if gateField != "" { if raw, ok := body[gateField]; ok { diff --git a/internal/audit/infer/infer.go b/internal/audit/infer/infer.go index ef21e86..fa2f2ef 100644 --- a/internal/audit/infer/infer.go +++ b/internal/audit/infer/infer.go @@ -92,7 +92,7 @@ type model struct { func newModel(ev Evidence, compiled *strategy.Strategy) *model { g := gateOf(compiled) - accepted := acceptedUnder(ev.AcceptedBodies, g.field) + accepted := acceptedUnder(ev.AcceptedRequestBodies, g.field) created := make([]string, 0, len(accepted)) for v := range accepted { if v != "" { @@ -466,7 +466,7 @@ func (m *model) removedValues(f string) []string { // acceptedAlone reports whether field a appeared in an accepted body that did // not also carry field b. func (m *model) acceptedAlone(a, b string) bool { - for _, body := range m.ev.AcceptedBodies { + for _, body := range m.ev.AcceptedRequestBodies { if _, hasA := body[a]; !hasA { continue } diff --git a/internal/audit/infer/infer_test.go b/internal/audit/infer/infer_test.go index 2f1eaf7..8baffc7 100644 --- a/internal/audit/infer/infer_test.go +++ b/internal/audit/infer/infer_test.go @@ -33,7 +33,7 @@ func monitorEvidence() Evidence { } return Evidence{ Entity: "monitor", - AcceptedBodies: []map[string]any{ + AcceptedRequestBodies: []map[string]any{ {"kind": "ping", "interval": 5.0, "target_host": "h", "name": "n"}, {"kind": "web", "interval": 5.0, "web": map[string]any{"url": "u"}, "name": "n"}, {"kind": "dns", "interval": 5.0, "domain": "d", "dnssec": true, "name": "n"}, @@ -176,7 +176,7 @@ func TestUnit_Infer_PlantedFalseEdgeIsInconclusive(t *testing.T) { } ev := Evidence{ Entity: "monitor", - AcceptedBodies: []map[string]any{ + AcceptedRequestBodies: []map[string]any{ {"kind": "ping", "foo": "x"}, {"kind": "dns", "foo": "y"}, // foo accepted under BOTH values: not gated }, @@ -203,8 +203,8 @@ func TestUnit_Infer_LoneAmbiguousRemovalAssertsNothing(t *testing.T) { Variants: []strategy.Variant{{}, {GateField: "kind", GateValue: "ping"}, {GateField: "kind", GateValue: "dns"}}, } ev := Evidence{ - Entity: "monitor", - AcceptedBodies: []map[string]any{{"kind": "ping"}}, + Entity: "monitor", + AcceptedRequestBodies: []map[string]any{{"kind": "ping"}}, Adjustments: []RequestAdjustment{ {Entity: "monitor", Action: AdjustRemove, Field: "bar", GateField: "kind", GateValue: "dns"}, }, @@ -223,8 +223,8 @@ func TestUnit_Infer_ConflictingAcceptanceAndRemovalIsNoEdge(t *testing.T) { Variants: []strategy.Variant{{}, {GateField: "kind", GateValue: "a"}, {GateField: "kind", GateValue: "b"}}, } ev := Evidence{ - Entity: "e", - AcceptedBodies: []map[string]any{{"kind": "a", "foo": 1.0}}, + Entity: "e", + AcceptedRequestBodies: []map[string]any{{"kind": "a", "foo": 1.0}}, Adjustments: []RequestAdjustment{ {Entity: "e", Action: AdjustRemove, Field: "foo", GateField: "kind", GateValue: "a"}, }, @@ -240,7 +240,7 @@ func TestUnit_Infer_MutuallyExclusive(t *testing.T) { t.Parallel() ev := Evidence{ Entity: "widget", - AcceptedBodies: []map[string]any{ + AcceptedRequestBodies: []map[string]any{ {"a": 1.0, "name": "n"}, {"b": 2.0, "name": "n"}, }, @@ -262,9 +262,9 @@ func TestUnit_Infer_MutuallyExclusive(t *testing.T) { func TestUnit_Infer_MutuallyExclusiveNeedsBothAlone(t *testing.T) { t.Parallel() ev := Evidence{ - Entity: "widget", - AcceptedBodies: []map[string]any{{"a": 1.0, "b": 2.0}}, // never a without b - CombinedRefusals: []FieldPair{{A: "a", B: "b"}}, + Entity: "widget", + AcceptedRequestBodies: []map[string]any{{"a": 1.0, "b": 2.0}}, // never a without b + CombinedRefusals: []FieldPair{{A: "a", B: "b"}}, } if o := find(Infer(ev, &strategy.Strategy{Entity: "widget"}), "", observe.KindMutuallyExclusive); o != nil { t.Fatalf("asserted %+v without evidence each is valid alone", o) @@ -276,8 +276,8 @@ func TestUnit_Infer_MutuallyExclusiveNeedsBothAlone(t *testing.T) { func TestUnit_Infer_FlatResourceHasNoVariantEdges(t *testing.T) { t.Parallel() ev := Evidence{ - Entity: "assignment", - AcceptedBodies: []map[string]any{{"name": "n", "agent_id": "agent-1"}}, + Entity: "assignment", + AcceptedRequestBodies: []map[string]any{{"name": "n", "agent_id": "agent-1"}}, Adjustments: []RequestAdjustment{ {Entity: "assignment", Action: AdjustBorrow, Field: "agent_id", GateField: "agent"}, }, @@ -305,7 +305,7 @@ func TestUnit_Infer_ValidConfigurationNeedsADistinguishingField(t *testing.T) { } ev := Evidence{ Entity: "e", - AcceptedBodies: []map[string]any{ + AcceptedRequestBodies: []map[string]any{ {"kind": "a", "name": "n"}, {"kind": "b", "name": "n"}, }, @@ -327,8 +327,8 @@ func TestUnit_Infer_HypothesisProvenanceIsCarried(t *testing.T) { }, } ev := Evidence{ - Entity: "e", - AcceptedBodies: []map[string]any{{"kind": "a", "foo": 1.0}, {"kind": "b"}}, + Entity: "e", + AcceptedRequestBodies: []map[string]any{{"kind": "a", "foo": 1.0}, {"kind": "b"}}, Adjustments: []RequestAdjustment{ {Entity: "e", Action: AdjustRemove, Field: "foo", GateField: "kind", GateValue: "b"}, }, @@ -454,7 +454,7 @@ func TestUnit_Infer_ValueConditionalConfiguration(t *testing.T) { } ev := Evidence{ Entity: "stream", - AcceptedBodies: []map[string]any{ + AcceptedRequestBodies: []map[string]any{ {"format": "avro", "mode": "streaming", "name": "n"}, {"format": "json", "mode": "batch", "name": "n"}, }, @@ -486,7 +486,7 @@ func TestUnit_Infer_ValueConditionalNeedsBothDirections(t *testing.T) { } ev := Evidence{ Entity: "stream", - AcceptedBodies: []map[string]any{ + AcceptedRequestBodies: []map[string]any{ {"format": "avro", "mode": "streaming", "name": "n"}, {"format": "json", "mode": "streaming", "name": "n"}, }, @@ -511,8 +511,8 @@ func TestUnit_Infer_ValueConditionalNeedsTwoCreatedValues(t *testing.T) { Variants: []strategy.Variant{{}, {GateField: "format", GateValue: "avro"}, {GateField: "format", GateValue: "json"}}, } ev := Evidence{ - Entity: "stream", - AcceptedBodies: []map[string]any{{"format": "avro", "mode": "streaming", "name": "n"}}, + Entity: "stream", + AcceptedRequestBodies: []map[string]any{{"format": "avro", "mode": "streaming", "name": "n"}}, ConditionalValues: []ConditionalValue{ {GateField: "format", GateValue: "avro", Field: "mode", Value: "batch", Accepted: false}, {GateField: "format", GateValue: "json", Field: "mode", Value: "batch", Accepted: true}, diff --git a/internal/audit/observe/request_bodies.go b/internal/audit/observe/request_bodies.go index 286b256..6c8415c 100644 --- a/internal/audit/observe/request_bodies.go +++ b/internal/audit/observe/request_bodies.go @@ -16,34 +16,36 @@ import ( "sort" ) -// encodeBodies renders one entity's record deterministically, matching the -// observations beside it: sorted map keys, no HTML escaping, two-space indent. -func encodeBodies(b Bodies) ([]byte, error) { +// encodeRequestBodies renders one entity's record deterministically, +// matching the observations beside it: sorted map keys, no HTML escaping, +// two-space indent. +func encodeRequestBodies(b RequestBodies) ([]byte, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) enc.SetIndent("", " ") if err := enc.Encode(b); err != nil { - return nil, fmt.Errorf("encoding bodies for %s: %w", b.Entity, err) + return nil, fmt.Errorf("encoding request bodies for %s: %w", b.Entity, err) } return buf.Bytes(), nil } -// BodiesSuffix is the committed file naming, one file per entity, matching -// the observations beside it. -const BodiesSuffix = ".bodies.json" +// RequestBodiesSuffix is the committed file naming, one file per entity, +// matching the observations beside it. +const RequestBodiesSuffix = ".request_bodies.json" -// Bodies is what one entity's creates looked like when the API accepted them. -type Bodies struct { +// RequestBodies is what one entity's creates looked like when the API +// accepted them. +type RequestBodies struct { Entity string `json:"entity"` // Minimal is the smallest create the run got accepted, and Maximal the // fullest. Either may be absent when no create of that shape succeeded. - Minimal *AcceptedBody `json:"minimal,omitempty"` - Maximal *AcceptedBody `json:"maximal,omitempty"` + Minimal *AcceptedRequestBody `json:"minimal,omitempty"` + Maximal *AcceptedRequestBody `json:"maximal,omitempty"` } -// AcceptedBody is one create the API answered 2xx to. -type AcceptedBody struct { +// AcceptedRequestBody is one create the API answered 2xx to. +type AcceptedRequestBody struct { // Status is the code the API answered, kept so a reader can see that this // was an acceptance rather than an assumption. Status int `json:"status"` @@ -61,7 +63,7 @@ type AcceptedBody struct { // A field the API accepts and never echoes cannot appear in a generated // configuration: terraform compares what it planned against what the provider // answers, and a value that never comes back reads as the provider losing it. -func (b *AcceptedBody) Echoed(wire string) bool { +func (b *AcceptedRequestBody) Echoed(wire string) bool { if b == nil || b.Response == nil { return false } @@ -69,15 +71,15 @@ func (b *AcceptedBody) Echoed(wire string) bool { return ok } -// WriteBodies commits one .bodies.json per entity under dir. -// Encoding matches the observations: sorted keys, stable bytes, so a re-run -// that learned nothing new rewrites nothing. -func WriteBodies(dir string, bodies []Bodies) error { - if len(bodies) == 0 { +// WriteRequestBodies commits one .request_bodies.json per entity +// under dir. Encoding matches the observations: sorted keys, stable bytes, +// so a re-run that learned nothing new rewrites nothing. +func WriteRequestBodies(dir string, requestBodies []RequestBodies) error { + if len(requestBodies) == 0 { return nil } - byEntity := map[string]Bodies{} - for _, b := range bodies { + byEntity := map[string]RequestBodies{} + for _, b := range requestBodies { if b.Entity == "" || (b.Minimal == nil && b.Maximal == nil) { continue } @@ -91,7 +93,7 @@ func WriteBodies(dir string, bodies []Bodies) error { encoded := make(map[string][]byte, len(entities)) for _, entity := range entities { - raw, err := encodeBodies(byEntity[entity]) + raw, err := encodeRequestBodies(byEntity[entity]) if err != nil { return err } @@ -101,7 +103,7 @@ func WriteBodies(dir string, bodies []Bodies) error { return fmt.Errorf("creating %s: %w", dir, err) } for _, entity := range entities { - path := filepath.Join(dir, entity+BodiesSuffix) + path := filepath.Join(dir, entity+RequestBodiesSuffix) if err := os.WriteFile(path, encoded[entity], 0o644); err != nil { return fmt.Errorf("writing %s: %w", path, err) } @@ -109,11 +111,12 @@ func WriteBodies(dir string, bodies []Bodies) error { return nil } -// ReadBodies loads every recorded body under dir, keyed by entity. A missing -// directory is not an error: an entity the probe never cleared has none, and -// generation falls back to deriving values from the document. -func ReadBodies(dir string) (map[string]Bodies, error) { - out := map[string]Bodies{} +// ReadRequestBodies loads every recorded request body under dir, keyed by +// entity. A missing directory is not an error: an entity the probe never +// cleared has none, and generation falls back to deriving values from the +// document. +func ReadRequestBodies(dir string) (map[string]RequestBodies, error) { + out := map[string]RequestBodies{} entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { @@ -129,7 +132,7 @@ func ReadBodies(dir string) (map[string]Bodies, error) { if err != nil { return nil, fmt.Errorf("reading %s: %w", e.Name(), err) } - var b Bodies + var b RequestBodies if err := json.Unmarshal(raw, &b); err != nil { return nil, fmt.Errorf("reading %s: %w", e.Name(), err) } diff --git a/internal/audit/observe/request_bodies_test.go b/internal/audit/observe/request_bodies_test.go index b6397a4..ea76981 100644 --- a/internal/audit/observe/request_bodies_test.go +++ b/internal/audit/observe/request_bodies_test.go @@ -5,16 +5,16 @@ import ( "testing" ) -// TestUnit_Observe_RecordedBodiesRoundTrip proves a recorded body survives the +// TestUnit_Observe_RecordedRequestBodiesRoundTrip proves a recorded body survives the // trip to disk unchanged. A generated configuration is built from these, so a // value that shifts in the file is a configuration that no longer matches the // request the API accepted. -func TestUnit_Observe_RecordedBodiesRoundTrip(t *testing.T) { +func TestUnit_Observe_RecordedRequestBodiesRoundTrip(t *testing.T) { dir := t.TempDir() - in := []Bodies{ + in := []RequestBodies{ { Entity: "tag", - Minimal: &AcceptedBody{ + Minimal: &AcceptedRequestBody{ Status: 201, Request: map[string]any{"key": "branch", "value": "sfo"}, Response: map[string]any{"key": "branch", "value": "sfo", "id": "7"}, @@ -22,18 +22,18 @@ func TestUnit_Observe_RecordedBodiesRoundTrip(t *testing.T) { }, { Entity: "role", - Maximal: &AcceptedBody{Status: 200, Request: map[string]any{"name": "n"}}, + Maximal: &AcceptedRequestBody{Status: 200, Request: map[string]any{"name": "n"}}, }, // Neither shape accepted: nothing to record, and nothing written. {Entity: "user"}, } - if err := WriteBodies(dir, in); err != nil { - t.Fatalf("WriteBodies: %v", err) + if err := WriteRequestBodies(dir, in); err != nil { + t.Fatalf("WriteRequestBodies: %v", err) } - out, err := ReadBodies(dir) + out, err := ReadRequestBodies(dir) if err != nil { - t.Fatalf("ReadBodies: %v", err) + t.Fatalf("ReadRequestBodies: %v", err) } if len(out) != 2 { t.Fatalf("read %d entities, want the two that had an accepted create", len(out)) @@ -57,7 +57,7 @@ func TestUnit_Observe_RecordedBodiesRoundTrip(t *testing.T) { // depends on: a property the API took and never returned cannot be held in // terraform state. func TestUnit_Observe_EchoedReadsTheResponse(t *testing.T) { - b := &AcceptedBody{ + b := &AcceptedRequestBody{ Request: map[string]any{"name": "n", "matchType": "and"}, Response: map[string]any{"name": "n"}, } @@ -68,17 +68,17 @@ func TestUnit_Observe_EchoedReadsTheResponse(t *testing.T) { t.Error("a property the response omitted reads as echoed") } // No response recorded says nothing about any property. - var none *AcceptedBody + var none *AcceptedRequestBody if none.Echoed("name") { t.Error("an absent record claimed an echo") } } -// TestUnit_Observe_ReadBodiesToleratesNoRun proves a tree the probe has never +// TestUnit_Observe_ReadRequestBodiesToleratesNoRun proves a tree the probe has never // run against reads as empty rather than as an error: generation falls back to // deriving values from the document. -func TestUnit_Observe_ReadBodiesToleratesNoRun(t *testing.T) { - out, err := ReadBodies(filepath.Join(t.TempDir(), "never-written")) +func TestUnit_Observe_ReadRequestBodiesToleratesNoRun(t *testing.T) { + out, err := ReadRequestBodies(filepath.Join(t.TempDir(), "never-written")) if err != nil { t.Fatalf("a missing directory is a normal state: %v", err) } diff --git a/internal/audit/run/adaptive_test.go b/internal/audit/run/adaptive_test.go index e812784..57246a0 100644 --- a/internal/audit/run/adaptive_test.go +++ b/internal/audit/run/adaptive_test.go @@ -528,8 +528,8 @@ func TestUnit_Adaptive_TheReductionKeepsWhatTheCreateNeeded(t *testing.T) { _, sum := mustRun(t, testOptions(t, s, p, testEnv(), nil)) - var recorded *observe.AcceptedBody - for _, b := range sum.Bodies { + var recorded *observe.AcceptedRequestBody + for _, b := range sum.RequestBodies { if b.Entity == "thing" { recorded = b.Maximal } diff --git a/internal/audit/run/entity.go b/internal/audit/run/entity.go index dd1eef1..f263b85 100644 --- a/internal/audit/run/entity.go +++ b/internal/audit/run/entity.go @@ -65,35 +65,35 @@ func (r *runner) runEntity(ctx context.Context, ep *plan.EntityPlan) { r.finalizeEvidence(ent) r.evidence[ep.Entity] = &infer.Evidence{ - Entity: ep.Entity, - AcceptedBodies: ent.ev.acceptedBodies, - ListBodies: ent.ev.listBodies, - CombinedRefusals: ent.ev.combinedRefusals, - ConditionalValues: ent.ev.conditionalValues, - IdentifierProperty: ent.ev.idField, - } - r.summary.Bodies = append(r.summary.Bodies, recordedBodies(ep.Entity, ent)) + Entity: ep.Entity, + AcceptedRequestBodies: ent.ev.acceptedRequestBodies, + ListBodies: ent.ev.listBodies, + CombinedRefusals: ent.ev.combinedRefusals, + ConditionalValues: ent.ev.conditionalValues, + IdentifierProperty: ent.ev.idField, + } + r.summary.RequestBodies = append(r.summary.RequestBodies, recordedRequestBodies(ep.Entity, ent)) r.summary.Entities = append(r.summary.Entities, EntityResult{ Entity: ep.Entity, Status: ent.status, Reason: ent.reason, }) r.log.Info().Str("entity", ep.Entity).Str("status", ent.status).Str("reason", ent.reason).Int("requests", ent.requests).Msg("entity finished") } -// recordedBodies is what this entity's accepted creates looked like, for the -// generated acceptance tests to be built from. +// recordedRequestBodies is what this entity's accepted creates looked like, +// for the generated acceptance tests to be built from. // // A create the API refused is not here: the point of the record is that these // are requests it took, so a configuration replaying one is a configuration // known to apply. -func recordedBodies(entity string, ent *entityState) observe.Bodies { - out := observe.Bodies{Entity: entity} +func recordedRequestBodies(entity string, ent *entityState) observe.RequestBodies { + out := observe.RequestBodies{Entity: entity} if ent.ev.sent != nil { - out.Minimal = &observe.AcceptedBody{ + out.Minimal = &observe.AcceptedRequestBody{ Status: ent.ev.sentStatus, Request: ent.ev.sent, Response: ent.ev.got, } } if ent.ev.maximalSent != nil { - out.Maximal = &observe.AcceptedBody{ + out.Maximal = &observe.AcceptedRequestBody{ Status: ent.ev.maximalStatus, Request: ent.ev.maximalSent, Response: ent.ev.maximalGot, } } diff --git a/internal/audit/run/evidence.go b/internal/audit/run/evidence.go index 7972734..ef7d440 100644 --- a/internal/audit/run/evidence.go +++ b/internal/audit/run/evidence.go @@ -62,15 +62,15 @@ type evidence struct { readProof *observe.Excerpt // The raw material the triangulating inference reads, gathered across - // every step. acceptedBodies is every create body the API accepted, + // every step. acceptedRequestBodies is every create body the API accepted, // resolved as sent — the positive half of variant diffing. listBodies is // the collection responses the pre-flight captured, for the list-shape // finding. combinedRefusals holds any field pairs a create was refused // for carrying together — the mutual-exclusion signal, empty unless the // refusal grammar names one. - acceptedBodies []map[string]any - listBodies [][]byte - combinedRefusals []infer.FieldPair + acceptedRequestBodies []map[string]any + listBodies [][]byte + combinedRefusals []infer.FieldPair // conditionalValues records the value-cycling outcomes the executor // gathered healing free-form conditional refusals — each (discriminator // value, sibling field, sibling value) the API accepted or refused — the diff --git a/internal/audit/run/run.go b/internal/audit/run/run.go index 234abc0..eabefee 100644 --- a/internal/audit/run/run.go +++ b/internal/audit/run/run.go @@ -155,10 +155,10 @@ type Summary struct { Blocked int `json:"blocked"` TimedOut int `json:"timedOut"` Skipped int `json:"skipped"` - // Bodies is what each entity's accepted creates looked like. A generated + // RequestBodies is what each entity's accepted creates looked like. A generated // acceptance test is built from these rather than from values derived // again from the document, because only these were actually accepted. - Bodies []observe.Bodies `json:"-"` + RequestBodies []observe.RequestBodies `json:"-"` // SkippedEntities is every entity the plan left out, with its reason. // Carried because the count alone cannot be acted on: it does not // distinguish a run that covered the API from one that skipped most of it. @@ -303,7 +303,7 @@ type runner struct { // non-strategy run, which is what makes such a run skip inference. strategies map[string]*strategy.Strategy // evidence accumulates, per entity, the raw record the inference reads — - // accepted bodies, forced adjustments, list responses. + // accepted request bodies, forced adjustments, list responses. evidence map[string]*infer.Evidence // borrowed caches one real id per collection the run has borrowed a // reference from, so a second create needing it costs no extra request. diff --git a/internal/audit/run/run_test.go b/internal/audit/run/run_test.go index 05affc8..77c1564 100644 --- a/internal/audit/run/run_test.go +++ b/internal/audit/run/run_test.go @@ -115,7 +115,7 @@ func TestUnit_Run_HappyPathDerivesTheExpectedObservations(t *testing.T) { if d, err := time.ParseDuration(ra.Value.(string)); err != nil || d > time.Second { t.Errorf("readAfterWrite = %v, want a small duration", ra.Value) } - // PUT preserved what the update bodies omitted. + // PUT preserved what the update request bodies omitted. if o := wantConfirmed(t, obs, "thing", "", observe.KindUpdateStyle); o.Value != "patch-merge" { t.Errorf("updateStyle = %v, want patch-merge", o.Value) } diff --git a/internal/audit/run/steps_create.go b/internal/audit/run/steps_create.go index 98ca180..8999573 100644 --- a/internal/audit/run/steps_create.go +++ b/internal/audit/run/steps_create.go @@ -50,7 +50,7 @@ func (r *runner) runCreateMinimal(ctx context.Context, ent *entityState, step *p ent.ev.sent = sent ent.ev.sentStatus = rr.res.status ent.ev.createProof = &rr.res.excerpt - ent.ev.acceptedBodies = append(ent.ev.acceptedBodies, cloneAnyMap(rr.body)) + ent.ev.acceptedRequestBodies = append(ent.ev.acceptedRequestBodies, cloneAnyMap(rr.body)) return nil } if _, exists := r.registry[ent.plan.Entity]; exists { @@ -92,7 +92,7 @@ func (r *runner) runCreateMaximal(ctx context.Context, ent *entityState, step *p ent.ev.maximalSent = sent ent.ev.maximalGot = rr.res.object() ent.ev.maximalStatus = rr.res.status - ent.ev.acceptedBodies = append(ent.ev.acceptedBodies, cloneAnyMap(rr.body)) + ent.ev.acceptedRequestBodies = append(ent.ev.acceptedRequestBodies, cloneAnyMap(rr.body)) _, _ = r.deleteObject(ctx, ent, ent.recipe, rr.obj) return nil } @@ -104,7 +104,7 @@ func (r *runner) runCreateMaximal(ctx context.Context, ent *entityState, step *p if err := r.bisectMaximal(ctx, ent, step, rr.res); err != nil { return err } - return r.reduceMaximal(ctx, ent, step, rr.body, rr.res) + return r.reduceMaximal(ctx, ent, rr.body, rr.res) } // bisectMaximal narrows a refused maximal create to the optional field @@ -271,7 +271,7 @@ func (r *runner) runCreatePerEnumValue(ctx context.Context, ent *entityState, st } accepted := obj != nil if accepted { - ent.ev.acceptedBodies = append(ent.ev.acceptedBodies, cloneAnyMap(rr.body)) + ent.ev.acceptedRequestBodies = append(ent.ev.acceptedRequestBodies, cloneAnyMap(rr.body)) _, _ = r.deleteObject(ctx, ent, ent.recipe, obj) } else if !res.refused() { return nil @@ -436,7 +436,7 @@ func (r *runner) searchCandidates(ent *entityState, body map[string]any, refusal // one removes. What survives is the fullest create this run could get taken, // which is what a generated maximal configuration has to be — every field in // it is one the API demonstrably tolerates alongside the others. -func (r *runner) reduceMaximal(ctx context.Context, ent *entityState, step *plan.Step, body map[string]any, refusal *httpResult) error { +func (r *runner) reduceMaximal(ctx context.Context, ent *entityState, body map[string]any, refusal *httpResult) error { minimal := ent.recipe.minimalBody allowance := searchAllowance(len(body)) last := refusal @@ -462,7 +462,7 @@ func (r *runner) reduceMaximal(ctx context.Context, ent *entityState, step *plan ent.ev.maximalSent = sent ent.ev.maximalGot = res.object() ent.ev.maximalStatus = res.status - ent.ev.acceptedBodies = append(ent.ev.acceptedBodies, cloneAnyMap(body)) + ent.ev.acceptedRequestBodies = append(ent.ev.acceptedRequestBodies, cloneAnyMap(body)) _, _ = r.deleteObject(ctx, ent, ent.recipe, obj) return nil } diff --git a/internal/audit/run/strategize.go b/internal/audit/run/strategize.go index 7a3e43f..0e0d9cf 100644 --- a/internal/audit/run/strategize.go +++ b/internal/audit/run/strategize.go @@ -7,7 +7,7 @@ package run // shaped by that strategy: creates and reads repeated per variant, the widest // body per variant, the negatives and per-value creates the gates imply, all // under a complexity-scaled budget. The addressing is reused verbatim; only -// the program, the bodies and the per-entity request budget change. +// the program, the request bodies and the per-entity request budget change. // // Field values are synthesised here, at run start, from the strategy's // per-field SynthHints — never baked into the compiled strategy, which stays a @@ -144,7 +144,7 @@ func addressingOf(ep *plan.EntityPlan) addressing { } // translateProgram turns a strategy's ordered, value-free program into -// executable steps: addressing from addr, bodies synthesised from the variant +// executable steps: addressing from addr, request bodies synthesised from the variant // skeletons and per-field hints. func translateProgram(compiled *strategy.Strategy, addr addressing, entity, prefix string) []plan.Step { baseMinimal := map[string]any{} diff --git a/internal/audit/strategy/strategy_test.go b/internal/audit/strategy/strategy_test.go index 52ee43b..cafd435 100644 --- a/internal/audit/strategy/strategy_test.go +++ b/internal/audit/strategy/strategy_test.go @@ -775,7 +775,7 @@ func TestUnit_Strategy_TheMaximalCreateFollowsTheDelete(t *testing.T) { maximal := posOf("createMaximal") cleanup := posOf("cleanupDelete") - if !(create < del && del < maximal && maximal < cleanup) { + if create >= del || del >= maximal || maximal >= cleanup { t.Errorf("program order is createMinimal=%d delete=%d createMaximal=%d cleanup=%d; "+ "the maximal create must sit between the delete and the cleanup", create, del, maximal, cleanup) diff --git a/internal/cli/audit.go b/internal/cli/audit.go index ce38544..ef8cb21 100644 --- a/internal/cli/audit.go +++ b/internal/cli/audit.go @@ -100,12 +100,12 @@ func newAuditRunCommand() *cobra.Command { return writeErr } } - // The accepted bodies sit beside the observations: an observation + // The accepted request bodies sit beside the observations: an observation // says something about a property, these say what a whole create // looked like when the API took it. - if len(sum.Bodies) > 0 { - bodiesDir := filepath.Join(filepath.Dir(out), "bodies") - if writeErr := observe.WriteBodies(bodiesDir, sum.Bodies); writeErr != nil { + if len(sum.RequestBodies) > 0 { + requestBodiesDir := filepath.Join(filepath.Dir(out), "request_bodies") + if writeErr := observe.WriteRequestBodies(requestBodiesDir, sum.RequestBodies); writeErr != nil { if runErr != nil { return fmt.Errorf("%v; additionally %w", runErr, writeErr) } diff --git a/internal/emit/provider_core.go b/internal/emit/provider_core.go index e25f7be..8c2a27c 100644 --- a/internal/emit/provider_core.go +++ b/internal/emit/provider_core.go @@ -27,12 +27,12 @@ const DefaultGoVersion = "1.25" // template assembles a value — and the backend and auth booleans exist so // templates branch on presence, never on meaning. type ProviderCore struct { - // AcceptedBodies is what the probe recorded of each entity's accepted + // AcceptedRequestBodies is what the probe recorded of each entity's accepted // creates, keyed by entity. An acceptance fixture is built from these // rather than from values derived again from the document: the document // says what should be accepted, these are what was. Empty for an entity // no run has cleared, which falls back to the derivation. - AcceptedBodies map[string]observe.Bodies + AcceptedRequestBodies map[string]observe.RequestBodies // Module is the provider repo's Go module path, e.g. // "github.com/exampleco/terraform-provider-petstore". diff --git a/internal/emit/render_fixtures.go b/internal/emit/render_fixtures.go index 17dd3c1..0ae71b3 100644 --- a/internal/emit/render_fixtures.go +++ b/internal/emit/render_fixtures.go @@ -81,15 +81,15 @@ func (e *serviceRenderer) resourceFixtures(r *ir.Resource, spec fixtures.Fixture // because its mock is built from the same derived values. accMinimal, accMaximal := spec, spec replayed := false - if rec, ok := e.pc.AcceptedBodies[r.Names.Key]; ok { + if rec, ok := e.pc.AcceptedRequestBodies[r.Names.Key]; ok { required := requiredWireNames(spec) if rec.Minimal != nil { - accMinimal = spec.FromAcceptedBody(rec.Minimal.Request, rec.Minimal.Response, required) + accMinimal = spec.FromAcceptedRequestBody(rec.Minimal.Request, rec.Minimal.Response, required) replayed = true } switch { case rec.Maximal != nil: - accMaximal = spec.FromAcceptedBody(rec.Maximal.Request, rec.Maximal.Response, required) + accMaximal = spec.FromAcceptedRequestBody(rec.Maximal.Request, rec.Maximal.Response, required) replayed = true case rec.Minimal != nil: // No larger create was ever accepted, so the fullest known diff --git a/internal/providergen/providergen.go b/internal/providergen/providergen.go index 1c40971..24be8fb 100644 --- a/internal/providergen/providergen.go +++ b/internal/providergen/providergen.go @@ -255,7 +255,7 @@ func generate(opts Options) (*generation, error) { // What a probe run recorded of the API's accepted creates. Absent before // any run, which leaves every acceptance fixture derived from the // document as it was. - pc.AcceptedBodies, err = observe.ReadBodies(filepath.Join(opts.Root, "audit", "bodies")) + pc.AcceptedRequestBodies, err = observe.ReadRequestBodies(filepath.Join(opts.Root, "audit", "request_bodies")) if err != nil { return nil, err } diff --git a/internal/specmodel/load.go b/internal/specmodel/load.go index e8d1c73..35c9972 100644 --- a/internal/specmodel/load.go +++ b/internal/specmodel/load.go @@ -34,10 +34,10 @@ func Load(data []byte) (*Document, error) { } l := &loader{ - doc: &Document{Schemas: map[string]*Schema{}}, - parameters: map[string]Parameter{}, - bodies: map[string]*Schema{}, - responses: map[string]*Schema{}, + doc: &Document{Schemas: map[string]*Schema{}}, + parameters: map[string]Parameter{}, + requestBodies: map[string]*Schema{}, + responses: map[string]*Schema{}, } if err := l.document(top); err != nil { return nil, err @@ -52,11 +52,11 @@ func Load(data []byte) (*Document, error) { // that exist only to be referenced, and the reference fixups the resolution // pass completes once every named schema exists. type loader struct { - doc *Document - parameters map[string]Parameter - bodies map[string]*Schema - responses map[string]*Schema - refs []pendingRef + doc *Document + parameters map[string]Parameter + requestBodies map[string]*Schema + responses map[string]*Schema + refs []pendingRef } func (l *loader) document(top *yaml.Node) error { @@ -128,13 +128,13 @@ func (l *loader) components(node *yaml.Node) error { l.parameters[name] = p } } - if bodies := lookup(node, "requestBodies"); bodies != nil { - for name, bn := range pairs(bodies) { + if requestBodies := lookup(node, "requestBodies"); requestBodies != nil { + for name, bn := range pairs(requestBodies) { s, err := l.contentSchema(deref(bn), "components.requestBodies."+name) if err != nil { return err } - l.bodies[name] = s + l.requestBodies[name] = s } } if responses := lookup(node, "responses"); responses != nil { @@ -238,7 +238,7 @@ func (l *loader) bodySchema(node *yaml.Node, at string) (*Schema, error) { if err != nil { return nil, err } - s, ok := l.bodies[name] + s, ok := l.requestBodies[name] if !ok { return nil, fmt.Errorf("%s: references #/components/requestBodies/%s, which the document does not declare", at, name) }