From 3fb76778d87fba51032f7475749ecfd7f1748b4f Mon Sep 17 00:00:00 2001 From: ShocOne <62835948+ShocOne@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:00:09 +0100 Subject: [PATCH] refactor: the intermediate representation's exported names say what they hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every exported identifier of the intermediate representation that the owner ruled on is renamed, and every consumer follows: Attribute.Kind -> Type; Attribute.Nested -> NestedAttributes; Resource/Datasource/ListResource.Schema -> Attributes, ListResource.AddressingSchema -> AddressingAttributes, Action.RequestSchema -> RequestAttributes; Parameter -> URLPathParameter; Authority -> SchemaAttributeTypeDetermination (and its four routes); Resource.EventualConsistency -> ReadAfterWriteDelay; ConfigVariant -> ValidConfigurationVariant; Equals -> WhenPropertyEquals; Valid -> AttributesValidWhenEqual; Attribute.Filter -> IsDatasourceFilterArgument; SilentlyIgnoredOnUpdate -> IgnoredOnUpdate; UnsupportedEntity.Kind -> TerraformBlockType; OperationInvoke -> OperationAction. "excluded" is the one word for the toolkit setting an entity or attribute aside: refuse() -> exclude(), refuseReservedRootNames() -> excludeReservedRootNames(), refusedAttributes() -> excludedAttributes(), and the docs say the same. The audit's "refusal" — an API answering a request with a 4xx — is a different fact and keeps its word. The toolkit's own JSON is camelCase throughout, as the OpenAPI document's keys are: the intermediate representation's and sdkbind's tags migrate, and unsupported.json moves to format version 3 — formatVersion, terraformBlockType in place of kind, and the cause objectWithoutPropertiesOrAdditionalProperties in place of shapelessObject. The resource template reads HasReadAfterWriteDelay / ReadAfterWriteDelay and emits the constant readAfterWriteDelay. The glossary records every new term and retires authority, refuse (as the code verb) and shapelessObject; the naming standard's R6 row names camelCase as the JSON convention and points at the acronym table as the record of that set. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XPLePCqzyYncF7TedagFkg --- CLAUDE.md | 11 +- docs/comment-style.md | 4 +- docs/config.md | 2 +- docs/contract.md | 14 +- docs/emittance_tracker.md | 22 +- docs/glossary.md | 43 ++-- docs/mapping.md | 2 +- docs/naming-standard.md | 6 +- docs/rehearsal.md | 4 +- docs/releasing.md | 4 +- internal/config/config.go | 2 +- internal/config/reference.go | 2 +- internal/config/validate.go | 2 +- internal/emit/emittance_test.go | 10 +- internal/emit/explain.go | 2 +- internal/emit/explain_test.go | 2 +- internal/emit/map_nested_test.go | 10 +- internal/emit/map_scalar_action_test.go | 10 +- internal/emit/render_action.go | 12 +- internal/emit/render_constraints.go | 8 +- internal/emit/render_constraints_test.go | 50 ++-- internal/emit/render_datasource.go | 32 +-- internal/emit/render_datasource_test.go | 8 +- internal/emit/render_fixtures.go | 10 +- internal/emit/render_fixtures_parent_test.go | 18 +- internal/emit/render_identity.go | 10 +- internal/emit/render_identity_test.go | 18 +- internal/emit/render_listresource.go | 20 +- internal/emit/render_listresource_test.go | 20 +- internal/emit/render_mapping.go | 20 +- internal/emit/render_mapping_test.go | 4 +- internal/emit/render_nested.go | 8 +- internal/emit/render_resource.go | 28 +-- internal/emit/render_schema.go | 30 +-- internal/emit/render_schema_test.go | 16 +- internal/emit/render_validators.go | 18 +- internal/emit/render_validators_test.go | 8 +- internal/emit/schema_type.go | 12 +- internal/emit/schema_type_test.go | 8 +- internal/emit/services.go | 24 +- internal/emit/services_errors_test.go | 94 +++---- internal/emit/services_fixture_test.go | 166 ++++++------- .../emit/services_openapigenerator_test.go | 110 ++++----- internal/emit/services_test.go | 2 +- internal/emit/unsupported.go | 98 ++++---- internal/emit/unsupported_test.go | 76 +++--- internal/fixtures/fixtures.go | 28 +-- internal/fixtures/fixtures_test.go | 114 ++++----- internal/fixtures/map_scalar_test.go | 8 +- internal/fixtures/map_test.go | 8 +- internal/fixtures/replay_test.go | 6 +- .../attribute_addressing.go | 30 +-- .../attribute_rules_test.go | 24 +- .../attribute_types.go | 86 +++---- .../intermediate_representation/attributes.go | 40 +-- .../attributes_test.go | 78 +++--- .../authority_test.go | 32 +-- .../constraints_test.go | 44 ++-- .../intermediate_representation/derive.go | 52 ++-- .../derive_list_resource_test.go | 10 +- .../derive_test.go | 64 ++--- .../intermediate_representation/map_test.go | 12 +- internal/intermediate_representation/model.go | 232 +++++++++--------- .../intermediate_representation/naming.go | 8 +- .../unions_test.go | 16 +- .../vendor_openapi_specs_test.go | 2 +- internal/providergen/curated_test.go | 2 +- internal/providergen/providergen.go | 2 +- internal/sdkbind/binder.go | 24 +- internal/sdkbind/binder_kiota.go | 6 +- internal/sdkbind/binder_kiota_test.go | 28 +-- internal/sdkbind/binder_openapigenerator.go | 6 +- .../sdkbind/binder_openapigenerator_test.go | 8 +- internal/sdkbind/fixtures_test.go | 40 +-- internal/sdkbind/helpers_test.go | 8 +- internal/sdkbind/model.go | 76 +++--- internal/sdkbind/prune_edge_test.go | 60 ++--- internal/sdkbind/prune_fields.go | 20 +- internal/sdkbind/prune_test.go | 2 +- internal/sdkbind/reconcile_test.go | 4 +- .../templates/services/resource/crud.go.tmpl | 10 +- 81 files changed, 1140 insertions(+), 1130 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0fbe4e3..e5d2ac5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,9 +73,10 @@ A test is named `TestUnit__` or pilot vendor name in non-test source, and no tracked file over 1 MiB. - **Never commit generated pilot output or binaries.** Generated provider trees live in provider repos; this repo holds only the machinery and its fixtures. -- **Exit codes are 0, 1 and 2** — success, a verb that ran and refused, and an - invocation that was misspelt. The table in `docs/contract.md` is the - contract, and a new code is appended rather than renumbering one. +- **Exit codes are 0, 1 and 2** — success, a verb that ran and stopped on an + exclusion, and an invocation that was misspelt. The table in + `docs/contract.md` is the contract, and a new code is appended rather than + renumbering one. - **Vendor OpenAPI specs are committed and embedded.** The third-party documents the tests parse live in `internal/vendor_openapi_specs`, taken as the vendor published them. They are test input and nothing else: never @@ -103,7 +104,7 @@ A test is named `TestUnit__` or // all for the provider. // Yes — what, then why. - // Refuses one entity rather than the run: an unrenderable shape is a + // Excludes one entity rather than the run: an unrenderable shape is a // fact about that entity, and the rest still generate. ``` @@ -124,7 +125,7 @@ change: description with no key both fail that test, so the schema and its reference cannot drift apart. - `docs/emittance_tracker.md` is the only place counts of what the toolkit - emits and refuses may live. A count is a fact about one toolkit commit + emits and excludes may live. A count is a fact about one toolkit commit against one pinned document; stating one in a readme, a comment or another doc puts it somewhere it cannot be re-measured, where it goes stale invisibly. diff --git a/docs/comment-style.md b/docs/comment-style.md index 65a3bfa..ba7d600 100644 --- a/docs/comment-style.md +++ b/docs/comment-style.md @@ -43,7 +43,7 @@ These are facts, and they are the point of the comment. that scope a claim: "`list/schema` attribute types declare no `Sensitive` field", "measured against terraform-plugin-framework v1.19.0". - **Raw evidence quoted inline** — an SDK accessor spelling - (`GetPasswordEscaped`), a refusal reason as the report prints it, an HTTP + (`GetPasswordEscaped`), an exclusion reason as the report prints it, an HTTP status, a framework type name. They are what makes the surrounding claim checkable. - **Indented tabular blocks.** They render as godoc code blocks and usually carry @@ -60,7 +60,7 @@ These are facts, and they are the point of the comment. is evidence and stays. Drop a bare URL only when the sentence still stands without it. - **Contracts** — locking requirements, `nil, nil` returns, ordering guarantees, - what a caller must do first, which stage owns a refusal. + what a caller must do first, which stage owns an exclusion. - **Operator-visible risk in credentialed tests** — why a test is opt-in, what it does to somebody's tenant. State it as a standing risk, not as an anecdote. diff --git a/docs/config.md b/docs/config.md index 09b9d3d..14bef83 100644 --- a/docs/config.md +++ b/docs/config.md @@ -22,7 +22,7 @@ The schema is owned by `internal/config`; this page is generated from it, so eve | Key | Type | Default | Description | |---|---|---|---| -| `generator.version` | string | — | Exact toolkit release tag (vX.Y.Z) the pipeline installs; branch names are refused. | +| `generator.version` | string | — | Exact toolkit release tag (vX.Y.Z) the pipeline installs; branch names are not accepted. | ## `spec` diff --git a/docs/contract.md b/docs/contract.md index 7c1bdb6..9dd97fd 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -26,8 +26,8 @@ succeeded jobs' artifacts persist within the run. ## The decision gate -Job [6] refuses to write while any correction awaits a decision. That -refusal is the point, but a refusal alone destroys what it refuses: proposals +Job [6] stops without writing while any correction awaits a decision. That +stop is the point, but a stop alone destroys what it set aside: proposals written into the runner's workspace by a job that then exits 1 leave with the runner, and the operator has nothing to review. Jobs [4] and [5] and the `20-corrections.yml` workflow close that loop. @@ -252,7 +252,7 @@ only `open-correction-prs` and `open-pr` keep write. `secrets: inherit` hands the repo's secrets across by name — the `TFPFGEN_AUTH_*` roles, read only by the audit job, and `TFPFGEN_APP_ID` / `TFPFGEN_APP_PRIVATE_KEY` if the repo has them. Every secret is declared `required: false`; validation, -not the workflow, is what refuses a missing role. +not the workflow, is what stops on a missing role. `20-corrections.yml` is the one caller whose trigger is not a dispatch, because it answers a decision rather than starting work: @@ -295,7 +295,7 @@ follow. | Code | Meaning | |---|---| | 0 | Success. | -| 1 | Failure: the operation ran and refused, or broke. | +| 1 | Failure: the operation ran and stopped on an exclusion, or broke. | | 2 | Usage: the invocation itself was misspelt. | New codes are appended, never renumbered. @@ -331,7 +331,7 @@ The App must be installed on the provider repo with **contents: write**, ## Versioning - Provider repos pin `generator.version` to an exact release tag; branches - are refused by validation. + are not accepted by validation. - Caller workflows reference `deploymenttheory/terraform-plugin-framework-codegen/.github/workflows/.yml` at the moving major tag — `@v0` until the 1.0.0 contract freeze, `@v1` after it. Compatible releases fast-forward the tag; breaking changes cut @@ -344,6 +344,6 @@ The App must be installed on the provider repo with **contents: write**, Everything in a provider repo is derived — regenerated wholesale, digest- tracked in `manifest.json` — except the authored data files: `tfpfgen.yaml`, -`spec/corrections/**`, `audit/inputs.json`. Generation refuses to write an -authored path; CI refuses a hand edit to a derived one. There are no +`spec/corrections/**`, `audit/inputs.json`. Generation never writes an +authored path; CI fails on a hand edit to a derived one. There are no hand-owned code files. diff --git a/docs/emittance_tracker.md b/docs/emittance_tracker.md index 1296afa..c87d2f2 100644 --- a/docs/emittance_tracker.md +++ b/docs/emittance_tracker.md @@ -1,7 +1,7 @@ # Emittance tracker What the toolkit currently emits from each pilot document, and what it -refuses. One measurement per row, taken by running the generator rather than +excludes. One measurement per row, taken by running the generator rather than estimated. A count here is a fact about a particular toolkit commit against a particular @@ -18,8 +18,8 @@ were measured against, so a stale row is visible as a stale row. - **Resources, data sources, list resources and actions** are the entities that survived derivation, binding and emission — what the generated provider actually registers, not what the document might have supported. -- **Refusals** is the `unsupported.json` total, split by the stage that refused: - `derivation`, `binding`, `emission`. A refusal is one entity or one attribute +- **Exclusions** is the `unsupported.json` total, split by the stage that excluded: + `derivation`, `binding`, `emission`. An exclusion is one entity or one attribute the toolkit declined to emit, each carrying the reason. - **Builds** means the generate verb's postcheck passed in that tree: `go mod tidy`, `go build`, `go vet`. @@ -36,7 +36,7 @@ under `tfpfgen provider verify`. | ThousandEyes | 1912 | 35 | 95 | 30 | 51 | yes | | Total | 10269 | 170 | 629 | 98 | 229 | | -Refusals, by the stage that refused: +Exclusions, by the stage that excluded: | Document | Total | Derivation | Binding | Emission | |---|---|---|---|---| @@ -45,23 +45,23 @@ Refusals, by the stage that refused: | ThousandEyes | 343 | 88 | 251 | 4 | | Total | 1507 | 460 | 1011 | 36 | -A refusal total is not a score, and this one rose while the toolkit refused +An exclusion total is not a score, and this one rose while the toolkit excluded less. Unions now derive as one attribute per variant, which brings each -variant's own fields into the tree: GitHub's derivation refusals fall by 60 as -the unions clear, and its binding refusals rise by 130 as the fields those +variant's own fields into the tree: GitHub's derivation exclusions fall by 60 as +the unions clear, and its binding exclusions rise by 130 as the fields those variants carry are each resolved against the SDK and some deleted. More schema is served, so more of it is individually accounted for. -Union refusals, which is the number this measures against, fall from 90 to 13 +Union exclusions, which is the number this measures against, fall from 90 to 13 — all 13 in GitHub, 11 for a branch referencing no component and 2 for a union in a writable position. -Binding refuses most of what is refused, and that is the expected shape: it is +Binding excludes most of what is excluded, and that is the expected shape: it is the only stage that resolves a drafted mapping against the SDK that was actually generated, so it is where a document's ambition meets what the backend could carry. -Eleven of the emission refusals are one shape: a list element whose key the +Eleven of the emission exclusions are one shape: a list element whose key the document spells its own way, where no rule derives that spelling from the path — `/roles/{id}` beside an element carrying `roleId`, `/users/{id}` beside `uid`. Each names its candidates in its reason. They need the field named as @@ -95,7 +95,7 @@ In a provider repo with a committed revised spec and a generated SDK: tfpfgen provider generate ``` -The verb prints the entity counts and the refusal split as its last two lines, +The verb prints the entity counts and the exclusion split as its last two lines, and runs the postcheck. `tfpfgen provider verify` regenerates into a temporary tree and byte-compares, which is what proves a committed tree still matches the toolkit that claims to produce it. diff --git a/docs/glossary.md b/docs/glossary.md index eaf388e..c90568e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -4,7 +4,7 @@ Every domain term in this toolkit was individually approved by the repository owner. `docs/naming-standard.md` is the rule set a name has to pass before it is proposed for this table. A term not in this table may not be introduced — present options to the owner first, record the decision here, then use it. The -retired terms may not reappear: *cassette*, *blueprint*, *doctor*, *corpus*, +retired terms may not reappear: *cassette*, *blueprint*, *doctor*, *corpus*, *authority* (the computed-optional route; now **SchemaAttributeTypeDetermination**), *refuse* as the code verb for setting an entity or attribute aside (now **exclude**), *shapelessObject* (now `objectWithoutPropertiesOrAdditionalProperties`), *refinement*, `Op`/`Ops`, `Param`/`Params`/`PathParams`, `TypeKind`, `ElementKind`, `APIVersionDir`, `Presence*`, `optional-computed`, `TF__*`, and the correction-branch spelling @@ -20,7 +20,7 @@ single recorded exception. |---|---| | **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. | -| **audit summary** | How far a run got with each entity: its status, the reason it stopped, and the redacted refusal behind that reason, committed at `audit/summary.json` beside the observations and the request bodies. An observation says what a run learned about one property and a request body what a create looked like when it worked; neither says why an entity produced nothing, and an entity blocked by a refusal nobody watched for otherwise leaves no trace to act on. Written whatever the run did, so a run where every entity blocked still records why. The aggregate counts it also carries are a fact about one run against one document — `docs/emittance_tracker.md` remains the only place a count of what the toolkit emits or refuses may live. | +| **audit summary** | How far a run got with each entity: its status, the reason it stopped, and the redacted refusal behind that reason, committed at `audit/summary.json` beside the observations and the request bodies. An observation says what a run learned about one property and a request body what a create looked like when it worked; neither says why an entity produced nothing, and an entity blocked by a refusal nobody watched for otherwise leaves no trace to act on. Written whatever the run did, so a run where every entity blocked still records why. The aggregate counts it also carries are a fact about one run against one document — `docs/emittance_tracker.md` remains the only place a count of what the toolkit emits or excludes may live. | | **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. A correction is **proposed**, then **accepted** (merged), **rejected** (closed, leaving a marker that suppresses re-proposal permanently) or **withdrawn** (closed by a later run that no longer proposes it, recording nothing, so the observation can be proposed again). | | **revise** | To fold observations into proposed corrections and apply accepted ones — `tfpfgen spec revise`. Its two halves are `Propose` and `WriteRevision`. 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. | @@ -41,14 +41,25 @@ single recorded exception. | **intermediate representation** | The ephemeral, never-committed derivation (`internal/intermediate_representation`) recomputed from the revised spec and config on every generation run; its model vocabulary (Model, Resource, Datasource, ListResource, Action, AttributeTree, ComputedOptionalRequired, Operation, Names) is approved. Every identifier in the package is fully worded — no abbreviated type, field, function, parameter or local. `Operation`/`Operations` replace the earlier `Op`/`Ops`, `AttributeType` replaces `TypeKind`, `Parameter`/`PathParameters` replace `Param`/`PathParams`, and `APIVersionDirectory` replaces `APIVersionDir`. `OneOf` is the one deliberate exception: it is named for the `stringvalidator.OneOf` it generates, and generated things are spelt the way HashiCorp spells them. `Datasource` likewise stays one word here, per the fixed spelling below. | | **ComputedOptionalRequired** | How an attribute participates in a plan, and the four values it takes: `required`, `optional`, `computed`, `computed_optional`. The name and the values are [terraform-plugin-codegen-spec](https://github.com/hashicorp/terraform-plugin-codegen-spec)'s (`schema.ComputedOptionalRequired`), adopted so the toolkit and the specification that could describe its output call the same fact by the same name. It replaces the earlier `Presence` / `PresenceRequired` / `PresenceOptional` / `PresenceComputed` / `PresenceOptionalComputed`, and the value `optional-computed`; those spellings are retired. | | **ElementType** | The type within a list — the spec's `schema.ElementType`. Replaces `ElementKind`, which is retired. Distinct from `sdkbind`'s own `ElementType`, which is a finished Go type expression for an SDK collection element rather than a terraform attribute type. | +| **NestedAttributes** | The child attribute tree an object attribute carries, or the object at the bottom of a list or map of objects: `Attribute.NestedAttributes`. Replaces the bare adjective `Nested`, which is retired as a field name. | +| **Attributes** | The `AttributeTree` a resource, datasource, list resource or action carries: `Resource.Attributes`, `Datasource.Attributes`, `ListResource.Attributes` and `.AddressingAttributes`, `Action.RequestAttributes`. The earlier field name `Schema` is retired for these: `schema` means the OpenAPI schema (`specmodel.Schema`) and nothing else. | +| **Type** | The terraform attribute type of an attribute or a path parameter: `Attribute.Type`, `URLPathParameter.Type`, `QueryParameter.Type`, all `AttributeType`. `Attribute.Kind` is retired; `Kind` is left to `OperationKind` (an operation's lifecycle role) and to `specmodel`'s entity kinds. | +| **URLPathParameter** | One parameter of a URL path template, with its name and terraform type: `Operation.PathParameters []URLPathParameter`. Replaces the bare `Parameter`, which said neither which parameter nor of what. | +| **SchemaAttributeTypeDetermination** | Which declaration decided that an attribute is computed-optional, recorded so an operator can find the route and correct it: `serverDefault` (the audit measured it), `responseRequired` (the response schema requires it), `requestDefault` (the request property declares a default), `responseProperty` (the response schema merely describes it). `Attribute.SchemaAttributeTypeDetermination`; the constants are `SchemaAttributeTypeDetermination`. Replaces the unrecorded root word `Authority`, which is retired. | +| **ReadAfterWriteDelay** | The longest read-after-write lag the document declares across an entity's lifecycle, as a duration: `Resource.ReadAfterWriteDelay`, the template field `HasReadAfterWriteDelay`, and the emitted constant `readAfterWriteDelay`. One spelling for the fact the observation kind `readAfterWrite` records; the earlier `EventualConsistency` / `HasEC` / `ECDuration` are retired. | +| **TerraformBlockType** | Which kind of terraform block an entity was, or would have become — `resource`, `datasource`, `list_resource` or `action` — on an `UnsupportedEntity` and in `unsupported.json` as `terraformBlockType`. Replaces the bare `Kind` there. | +| **exclude** | What the toolkit does when it sets an entity or an attribute aside rather than generating it, at any stage — configuration, classification, derivation, binding or emission. The code verb is `exclude` (`exclude()`, `excludeReservedRootNames()`, `excludedAttributes()`), the model fields are `ExcludedBy*`, the cause is `excludedByConfiguration`, and the committed record is `unsupported.json`. `refuse` is retired for this sense; it remains the audit's word for an API answering a request with a 4xx (`refusal`, `uncorrectableRefusal`, the refusal **grammar**), which is a different fact. | +| **IsDatasourceFilterArgument** | The bool marking a datasource argument that selects which listed objects come back rather than describing one (`Attribute.IsDatasourceFilterArgument`); it binds to no SDK field. Replaces the bare `Filter`. | +| **ValidConfigurationVariant** | One discriminator value of a **validConfiguration** with the attribute names valid under it (`Value`, `AttributesValidWhenEqual`). Replaces the abbreviated `ConfigVariant`. | +| **WhenPropertyEquals** / **AttributesValidWhenEqual** | On a `ConditionalRequirement` or `ConditionalValidity`: the value the gate property must equal, and the attribute names that are required or valid when it does. Replace the bare `Equals` and `Valid`. | | **code.Import** | One source-code import an emitted expression needs: `{Alias, Path}`, the spec's `code.Import` mirrored in `internal/code` rather than depended upon, because a mirrored struct costs nothing and a module costs a dependency. | | **SchemaDefinition** | A finished code expression, carried together with the `code.Import`s it needs — the spec's spelling for the same thing `CLAUDE.md` already demands of every value a template consumes. `code.CustomValidator` and `code.CustomPlanModifier` are the two carriers: an expression declares the packages it references on the value it returns, so a validator or plan modifier can never be rendered into a file whose import block forgot it. | | **binding** | The dialect-neutral mapping from one intermediate-representation entity onto the generated SDK's surface (`internal/sdkbind`): finished call expressions, accessor names, model types. Drafted by a per-backend binder, resolved against the real SDK with go/types. Its vocabulary (Bindings, Call, FieldAccess, Segment) is approved. | | **prune** | To resolve drafted bindings against the generated SDK and delete whatever the SDK cannot carry, recording the SDK's reason for each deletion. A spelling is repaired only where the SDK admits exactly one answer — never invented, never widened. | | **provider-core** | The shared plumbing the toolkit emits into every generated provider: the client, crud retry, error semantics, the conversion catalog, schema helpers, and the test harness. An emitted copy, not a shared library — every file is manifest-covered and regenerated wholesale. Templates live in `internal/templates/provider-core`. | | **emit** | The render-and-write layer (`internal/emit`): it turns the provider-core and service templates plus finished context values into provider files and reports what it wrote as manifest entries. Its emission vocabulary is approved: `RenderServices` renders every entity's service files and answers a `ServiceFiles` (the files plus a `Registry` of registration lines); `Register` writes one slot's `Registrations` into a registry file at its sentinels; `RegistrySlots` is the fixed slot order. | -| **emittance tracker** | `docs/emittance_tracker.md`, the one place counts of what the toolkit emits are kept: provider tree files, entities by kind, and refusals by the stage that refused, per pilot document. A count is a fact about one toolkit commit against one pinned document, so every row records both. Stating such a count anywhere else — a readme, a comment, a doc — is what this file exists to stop: elsewhere it cannot be re-measured and goes stale invisibly. | -| **fixtures** | The single derivation of one entity's test fixture values (`internal/fixtures`), rendered twice — HCL and wire JSON — from one result so the two can never disagree. Its vocabulary is approved: a `Fixture` carries `Entries` (one `Entry` per supported attribute) and `Omissions` (one `Omission` per refused attribute, with its reason); a `Form` (`ConfigMinimal`, `ConfigMaximal`, `ResponseMinimal`, `ResponseMaximal`) selects which entries a rendering carries; `NamePrefix` (`tfpfgen-test-`) marks every synthesised name-bearing string. | +| **emittance tracker** | `docs/emittance_tracker.md`, the one place counts of what the toolkit emits are kept: provider tree files, entities by kind, and exclusions by the stage that excluded, per pilot document. A count is a fact about one toolkit commit against one pinned document, so every row records both. Stating such a count anywhere else — a readme, a comment, a doc — is what this file exists to stop: elsewhere it cannot be re-measured and goes stale invisibly. | +| **fixtures** | The single derivation of one entity's test fixture values (`internal/fixtures`), rendered twice — HCL and wire JSON — from one result so the two can never disagree. Its vocabulary is approved: a `Fixture` carries `Entries` (one `Entry` per supported attribute) and `Omissions` (one `Omission` per excluded attribute, with its reason); a `Form` (`ConfigMinimal`, `ConfigMaximal`, `ResponseMinimal`, `ResponseMaximal`) selects which entries a rendering carries; `NamePrefix` (`tfpfgen-test-`) marks every synthesised name-bearing string. | | **step kind** | One audit derivation rule's output, named for what the step does. The set is closed, twelve strong, spelled identically in Go (`Step*`) and in plan JSON: `createMinimal`, `readWithRetry`, `readConsecutive`, `updateField`, `deleteWithConfirmation`, `createMaximal`, `omitRequired`, `undocumentedEnumValue`, `undeclaredSpecField`, `createPerEnumValue`, `read`, `cleanupDelete`. | | **outcome** | How far the audit got with one claim. The set is closed, four strong, spelled identically in Go (`Outcome*`) and in observation JSON: `confirmed`, `inconclusive`, `blocked`, `timeoutExhausted`. Despite its name's emphasis on time, `timeoutExhausted` covers every exhausted run budget alike — request, live-object and time. | | **claim** | One thing the audit believes about an API before the live run settles it: read off the document's structure or mined from a field description, carried with its **provenance**. `strategy.Claim`; its kinds are spelled exactly as the **observation** kinds they become, so a claim and the finding it turns into share a name. A confirmed claim is an observation; an unconfirmed one is `inconclusive`. | @@ -59,7 +70,7 @@ single recorded exception. | **gate** | The field whose value decides which other fields an API accepts — a required enum, an optional enum or a boolean, ranked in that order. Also, separately, the English verb: a check that refuses passage (the drift gate, the coverage gate). Both senses are approved; they are different parts of speech and no reader confuses them. | | **edge** | One conditional rule between fields, of the kinds `validWhen`, `validConfiguration`, `dependsOn` and `mutuallyExclusive`. Counted on the audit summary as `edgesConfirmed` and `edgesInconclusive`. | | **the prefix pass** | The half of **cleanup** that sweeps every reachable collection deleting objects whose names carry the run's prefix, as opposed to the half that deletes by id from an **activity ledger**. Counted separately on the cleanup summary because something getting past a ledger is worth distinguishing. | -| **preflight** | A cheap check run before an expensive thing, which refuses early. One idea at three stages: `config validate` before anything credentialed runs, the audit's tenant check before its first create, and the release job before publishing. | +| **preflight** | A cheap check run before an expensive thing, which stops early. One idea at three stages: `config validate` before anything credentialed runs, the audit's tenant check before its first create, and the release job before publishing. | | **grammar** | The fixed sentence form a refusal takes, so a parser can pull the offending field out of it with one expression. One contract described from both ends: the **stub** API emits it, the executor parses it. | | **stub** | A hand-written stand-in for a real component, used as test input. Covers the **test API server**, `sdkgen`'s fake generator binaries, and the curated fixture's hand-written SDKs. | | **prenormalise** | The five rewrites every SDK generation applies to a copy of the revised document before the backend runs: strip schema defaults, collapse single-member anonymous `allOf`, widen byte-array collections, reduce unions, drop unacceptable error content. Each answers a standing generator behaviour rather than one document's mistake, so they are built in rather than committed as corrections. Output at `spec/revised.prenormalised.yaml`, never committed. | @@ -146,7 +157,7 @@ single recorded exception. now. Terraform matches the two by type name and refuses to load a provider whose list resource names no resource, so one is derived exactly where an entity is both a resource and enumerable, and a - resource the bindings or emission refuse takes its list resource with + resource the bindings or emission exclude takes its list resource with it. The earlier meaning — a list-only entity, enumerable but not addressable — is retired: no resource can ever match such an entity, so it could not be a list resource at all. Those entities are datasources. @@ -200,7 +211,7 @@ single recorded exception. per branch. A variant takes its name from the component the branch references and keeps that component's spelling as its wire name, which is what makes the drafted accessor land on the one the SDK generated. A branch - referencing no component refuses the whole union rather than half of it. + referencing no component excludes the whole union rather than half of it. Variants are computed wherever nothing writes the union; a writable union is not served yet, because `Optional + Computed` cannot express mutual exclusion — see `docs/mapping.md` row 13. @@ -312,30 +323,30 @@ single recorded exception. other generated file: manifest-covered, byte-compared by `provider verify`, and carrying no timestamp and no count of anything outside the run, because either would fail that gate on every run. It is a view of the - same records `unsupported.json` holds, never a second place a refusal is + same records `unsupported.json` holds, never a second place an exclusion is recorded. - **unsupported.json** — the committed record of everything generation - refused, at the provider repo root: `{format_version, unsupported: [{kind, + excluded, at the provider repo root: `{formatVersion, unsupported: [{terraformBlockType, entity, attribute, service, tag, stage, reason}]}`. The subject is fields - rather than one rendered sentence, so a reader grouping refusals never + rather than one rendered sentence, so a reader grouping exclusions never parses prose to do it. `entity` is the entity key and `attribute` its - dotted path beneath it, empty when the whole entity was refused. `kind` is + dotted path beneath it, empty when the whole entity was excluded. `terraformBlockType` is `resource | datasource | list_resource | action`, empty for an entity - refused before it became any of them. `service` and `tag` are where it + excluded before it became any of them. `service` and `tag` are where it belongs — the service area derived from its path, and the group the document places it in — carried so an entity that became nothing can still be grouped with the ones that did. `cause` is the fact behind - the refusal — a code from a closed set per stage, and the subject it is + the exclusion — a code from a closed set per stage, and the subject it is about, such as the SDK type that carries none of an entity's fields. Two - refusals belong to one cause when both match, so consequences of one fact + exclusions belong to one cause when both match, so consequences of one fact group by an exact comparison rather than by a guess at which prose reasons mean the same thing. `stage` is the closed set `configuration | classification | derivation | binding | emission`, naming which decision - refused it. The first three are separate because their remedies are: an + excluded it. The first three are separate because their remedies are: an entry the operator wrote, a shape the document does not offer, and an attribute the derivation will not guess at. Derived content like any other: manifest-covered and byte-compared by `provider verify`. Generation never fails on it — one entity must not take the whole - provider with it — the point is that a refusal appearing or disappearing + provider with it — the point is that an exclusion appearing or disappearing is a line in a generation pull request rather than a line in a CI log nobody reads. diff --git a/docs/mapping.md b/docs/mapping.md index e36f8b1..5d0c800 100644 --- a/docs/mapping.md +++ b/docs/mapping.md @@ -39,7 +39,7 @@ sticky — and a union is where it does real damage. A variant is named for the component its branch references, which is also what the SDK names its accessor after, so the two agree without either being told about the other. A branch that references no component names nothing on either -side, and one of those refuses the whole union: half a union is a schema that +side, and one of those excludes the whole union: half a union is a schema that cannot hold what the API returns. ## Entity operation sets diff --git a/docs/naming-standard.md b/docs/naming-standard.md index 23ce3c1..d5f73b1 100644 --- a/docs/naming-standard.md +++ b/docs/naming-standard.md @@ -92,7 +92,7 @@ alike. The glossary already required this inside Three exceptions, and no others: 1. **Acronyms in the acronym table** (`internal/intermediate_representation/naming.go`) - — `API`, `HTTP`, `SDK`, `ID`, `URL`, `JSON`, `YAML`, `RPS`, `SHA`. Cased + — the table is the record of the set, and this document does not repeat it. Cased Go-idiomatically: uppercase whole in Pascal (`APIKey`), lowered whole when leading in camel (`apiKey`, `id`). Additions to the table go to the owner. 2. **Names an external contract fixes** — kiota's `GetPasswordEscaped`, cobra's @@ -146,7 +146,7 @@ nothing else. | Surface | Convention | |---|---| | Go identifier | `PascalCase` exported, `camelCase` unexported | -| JSON field, observation kind, step kind, outcome | `camelCase` | +| JSON field, observation kind, step kind, outcome | `camelCase`, matching the OpenAPI document's own keys | | `x-tfpfgen-*` key, config key, file name, directory | `kebab-case` for keys and directories, `snake_case` for config keys | | Terraform attribute | `snake_case` | @@ -251,7 +251,7 @@ not a proposal. | `envelope` (both senses) | R3 | `x-tfpfgen-list-wrapper` + `x-tfpfgen-list-pagination`; `ErrorBodyShape` | | `orphan` (three senses) | R3, R5 | `UndeletedObjects`, `UnproducedFilesOf`, `widget` | | `shape` as an identifier | R1, R5 | `handle*`, `has*Operations`, `listResponseFormat` split in two | -| `json:"element_kind"` | retired | `json:"element_type"` | +| `json:"element_kind"` | retired | `json:"elementType"` | | `x-tfpfgen-read-after-write` | R6 | `x-tfpfgen-read-after-write` — the kind is `readAfterWrite` | | `oag*` | R4 | `openAPIGenerator*` | | `MapRemoteStateToTerraform` | asymmetric | `MapRemoteStateToResource`, pairing with `MapRemoteStateToDatasource` | diff --git a/docs/rehearsal.md b/docs/rehearsal.md index e065f66..42c1b08 100644 --- a/docs/rehearsal.md +++ b/docs/rehearsal.md @@ -50,7 +50,7 @@ provider repo (a `git init` repo, so Go's `-buildvcs` stamping succeeds). | # | Verb | Exit | What happened | |---|---|---|---| -| 1 | `config validate --secrets` (secret unset) | 1 | Refused, naming `TFPFGEN_AUTH_TOKEN` — the preflight dies in milliseconds. | +| 1 | `config validate --secrets` (secret unset) | 1 | Stopped, naming `TFPFGEN_AUTH_TOKEN` — the preflight dies in milliseconds. | | 2 | `config validate --secrets` | 0 | `tfpfgen.yaml is valid: provider orbit, backend kiota@1.34.1, auth bearer_token`. | | 3 | `spec import ./monitor.openapi.yaml` | 0 | Pinned `spec/imported.yaml`, sha256 `d47ccb0bcc54`, openapi 3.0.3. | | 4 | `spec revise` (no observations) | 0 | `spec/revised.yaml` = the upstream document, 0 corrections. | @@ -130,7 +130,7 @@ realizing every edge as a stock-idiom config validator (the `#42` form): `kindValidConfigurationValidator`, each a `resource.ConfigValidator`, plus `stringvalidator.OneOf("ping","web","dns")` on the `kind` attribute. These are the value-conditional equivalent of `AlsoRequires` / `ConflictsWith`: -the config is refused when a variant's required field is missing or when a +the config is not accepted when a variant's required field is missing or when a field is set under the wrong `kind`. The generated resource unit tests apply variant-consistent configurations and pass against them. diff --git a/docs/releasing.md b/docs/releasing.md index 465b721..81d728d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -22,7 +22,7 @@ leaves every `v1` caller untouched. - **Provider repos** pin an exact release tag in config (`generator.version: v1.2.3`). The `setup-tfpfgen` action downloads that release's binary for the runner, verifies it against the release's - checksums file, and refuses a binary that does not report the requested + checksums file, and does not accept a binary that does not report the requested version. Branches are never accepted. - **Caller workflows** reference this repo's reusable workflows and actions at the moving major tag, e.g. `@v1`. Compatible releases reach them when @@ -31,4 +31,4 @@ leaves every `v1` caller untouched. Note the stamped version has no leading `v`: `tfpfgen version` for tag `v0.1.0` prints `0.1.0`. A source build prints `dev`, which a pinned -pipeline treats as a refusal. +pipeline treats as a failure. diff --git a/internal/config/config.go b/internal/config/config.go index b431d48..0e0aa76 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -79,7 +79,7 @@ type Services struct { Exclude []string `mapstructure:"exclude"` } -// SDK backends. The set is closed; config validation refuses others. +// SDK backends. The set is closed; config validation does not accept others. const ( BackendKiota = "kiota" BackendOpenAPIGenerator = "openapi-generator" diff --git a/internal/config/reference.go b/internal/config/reference.go index a33c204..73dea86 100644 --- a/internal/config/reference.go +++ b/internal/config/reference.go @@ -22,7 +22,7 @@ var descriptions = map[string]string{ "version": "Schema version of this file; must equal the schema version the installed toolkit understands.", "provider.name": "Provider name as published to the registry; a lowercase DNS label.", "provider.registry_namespace": "Registry namespace the provider is published under; a lowercase DNS label.", - "generator.version": "Exact toolkit release tag (vX.Y.Z) the pipeline installs; branch names are refused.", + "generator.version": "Exact toolkit release tag (vX.Y.Z) the pipeline installs; branch names are not accepted.", "spec.document_url": "http(s) URL of the upstream OpenAPI document that `tfpfgen spec import` pins by hash.", "sdk.backend": "SDK generator behind the common interface; exactly one per provider repo.", "sdk.backend_version": "Exact version pin of the backend tool to install.", diff --git a/internal/config/validate.go b/internal/config/validate.go index 50e06e6..162a208 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -47,7 +47,7 @@ func (c *Config) problems() []string { case c.Generator.Version == "": report("generator.version: required") case !releaseTag.MatchString(c.Generator.Version): - report("generator.version: %q is not an exact release tag (vX.Y.Z); branches are refused", c.Generator.Version) + report("generator.version: %q is not an exact release tag (vX.Y.Z); branches are not accepted", c.Generator.Version) } switch c.Spec.DocumentURL { diff --git a/internal/emit/emittance_test.go b/internal/emit/emittance_test.go index c9ee857..34c063c 100644 --- a/internal/emit/emittance_test.go +++ b/internal/emit/emittance_test.go @@ -33,9 +33,9 @@ func emittanceModel() *ir.Model { func TestUnit_RenderEmittance_ConsequencesOfOneFactAreOneEntry(t *testing.T) { shared := &ir.Cause{Code: "noAccessor", Subject: "models.Envelopeable"} refusals := []Unsupported{ - {Kind: "datasource", Entity: "preload", Attribute: "asset_tag", Tag: "Inventory Preload", + {TerraformBlockType: "datasource", Entity: "preload", Attribute: "asset_tag", Tag: "Inventory Preload", Cause: shared, Stage: StageBinding, Reason: "models.Envelopeable carries no GetAssetTag"}, - {Kind: "datasource", Entity: "preload", Attribute: "serial_number", Tag: "Inventory Preload", + {TerraformBlockType: "datasource", Entity: "preload", Attribute: "serial_number", Tag: "Inventory Preload", Cause: shared, Stage: StageBinding, Reason: "models.Envelopeable carries no GetSerialNumber"}, } @@ -94,10 +94,10 @@ func TestUnit_RenderEmittance_AnEntityThatBecameNothingIsStillUnderItsTag(t *tes t.Fatal("the tag is missing from the report") } -// TestUnit_RenderEmittance_ARefusalNamingNoDerivedEntityIsNotFiled keeps the +// TestUnit_RenderEmittance_AnExclusionNamingNoDerivedEntityIsNotFiled keeps the // page honest. Inventing a tag for a refusal whose entity the run never // derived would file it somewhere a reader cannot check. -func TestUnit_RenderEmittance_ARefusalNamingNoDerivedEntityIsNotFiled(t *testing.T) { +func TestUnit_RenderEmittance_AnExclusionNamingNoDerivedEntityIsNotFiled(t *testing.T) { refusals := []Unsupported{{ Entity: "never_derived", Stage: StageBinding, Cause: &ir.Cause{Code: "noAccessor"}, Reason: "the SDK carries no such thing", @@ -121,7 +121,7 @@ func TestUnit_RenderEmittance_ARefusalNamingNoDerivedEntityIsNotFiled(t *testing func TestUnit_RenderEmittance_IsAFunctionOfItsInput(t *testing.T) { e := Emittance{Provider: "acme", Document: EmittanceDocument{Source: "https://example.invalid/api.json"}} refusals := []Unsupported{{ - Kind: "datasource", Entity: "preload", Attribute: "asset_tag", Tag: "Inventory Preload", + TerraformBlockType: "datasource", Entity: "preload", Attribute: "asset_tag", Tag: "Inventory Preload", Cause: &ir.Cause{Code: "noAccessor", Subject: "models.Envelopeable"}, Stage: StageBinding, Reason: "no getter", }} diff --git a/internal/emit/explain.go b/internal/emit/explain.go index 2c88319..ba22a0c 100644 --- a/internal/emit/explain.go +++ b/internal/emit/explain.go @@ -148,7 +148,7 @@ var explanations = map[string]Explanation{ Means: "The OpenAPI 3 specification says this object may carry any properties, without saying what their values look like. There is nothing to give the Terraform attribute a type.", Fix: "A correction can declare the value type, or the API vendor can add it to the specification.", }, - ir.CauseShapelessObject: { + ir.CauseObjectWithoutPropertiesOrAdditionalProperties: { Title: "An object with no fields described", Means: "The OpenAPI 3 specification says this is an object and then describes nothing inside it, so there is no attribute to generate.", Fix: "A correction can describe the object's fields, or the API vendor can add them to the specification.", diff --git a/internal/emit/explain_test.go b/internal/emit/explain_test.go index 4aa62a1..c107e6e 100644 --- a/internal/emit/explain_test.go +++ b/internal/emit/explain_test.go @@ -23,7 +23,7 @@ var allCauses = []string{ specmodel.CausePartialLifecycle, ir.CauseUndeclaredType, ir.CauseUnsupportedType, ir.CauseWritableUnion, ir.CauseUnnamedUnionBranch, ir.CauseEmptyUnion, ir.CauseUntypedAdditionalProperties, - ir.CauseShapelessObject, ir.CauseMapOfObjects, ir.CauseMapOfMaps, ir.CauseUnsupportedMapValue, + ir.CauseObjectWithoutPropertiesOrAdditionalProperties, ir.CauseMapOfObjects, ir.CauseMapOfMaps, ir.CauseUnsupportedMapValue, ir.CauseItemlessArray, ir.CauseFreeFormArrayElement, ir.CauseUnsupportedArrayElement, ir.CauseReservedRootName, sdkbind.CauseNoAccessor, sdkbind.CauseNoSetter, sdkbind.CauseNotAnAccessor, diff --git a/internal/emit/map_nested_test.go b/internal/emit/map_nested_test.go index 576e76d..1dfc054 100644 --- a/internal/emit/map_nested_test.go +++ b/internal/emit/map_nested_test.go @@ -12,14 +12,14 @@ import ( // one: the value type is the map's, not the field's. func portsNode() node { child := node{ - attribute: ir.Attribute{Name: "bore", WireName: "bore", Kind: ir.TypeInt64}, + attribute: ir.Attribute{Name: "bore", WireName: "bore", Type: ir.TypeInt64}, fb: &sdkbind.FieldBinding{Attr: "bore", Wire: "bore", Access: sdkbind.FieldAccess{Get: "GetBore", Set: "SetBore", SDKType: "*int64", ConvertGet: "FromPtrInt64", ConvertSet: "ToPtrInt64"}}, } return node{ - attribute: ir.Attribute{Name: "ports", WireName: "ports", Kind: ir.TypeMap, - ElementType: ir.TypeObject, Nested: &ir.AttributeTree{}}, + attribute: ir.Attribute{Name: "ports", WireName: "ports", Type: ir.TypeMap, + ElementType: ir.TypeObject, NestedAttributes: &ir.AttributeTree{}}, fb: &sdkbind.FieldBinding{Attr: "ports", Wire: "ports", NestedModel: "models.Portable", NestedWriteModel: "models.Port", NestedConstructor: "models.NewPort()", @@ -90,7 +90,7 @@ func TestUnit_StateNested_ReadsAMapIntoAKeyedValue(t *testing.T) { func TestUnit_SchemaTypeOf_AMapOfObjectsIsAMapNestedAttribute(t *testing.T) { t.Parallel() resolved := schemaTypeOf(node{attribute: ir.Attribute{ - Kind: ir.TypeMap, ElementType: ir.TypeObject, Nested: &ir.AttributeTree{}, + Type: ir.TypeMap, ElementType: ir.TypeObject, NestedAttributes: &ir.AttributeTree{}, }}) if resolved.SchemaAttribute != "MapNestedAttribute" { t.Errorf("schema attribute = %q, want MapNestedAttribute", resolved.SchemaAttribute) @@ -104,7 +104,7 @@ func TestUnit_SchemaTypeOf_AMapOfObjectsIsAMapNestedAttribute(t *testing.T) { // carry unknown, which a Go map of structs cannot. func TestUnit_FieldType_AMapOfObjectsIsHeldAsAMap(t *testing.T) { t.Parallel() - n := node{attribute: ir.Attribute{Kind: ir.TypeMap, ElementType: ir.TypeObject, Nested: &ir.AttributeTree{}}} + n := node{attribute: ir.Attribute{Type: ir.TypeMap, ElementType: ir.TypeObject, NestedAttributes: &ir.AttributeTree{}}} if got := fieldType(n); got != "types.Map" { t.Errorf("field type = %q, want types.Map", got) } diff --git a/internal/emit/map_scalar_action_test.go b/internal/emit/map_scalar_action_test.go index f8ee640..729f579 100644 --- a/internal/emit/map_scalar_action_test.go +++ b/internal/emit/map_scalar_action_test.go @@ -18,10 +18,10 @@ import ( // so parsing is not the check. The literal has to be a literal. func TestUnit_Emit_AMapValueRendersAGoLiteralOfItsElementKind(t *testing.T) { tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "limits", WireName: "limits", Kind: ir.TypeMap, ElementType: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, - {Name: "weights", WireName: "weights", Kind: ir.TypeMap, ElementType: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, - {Name: "flags", WireName: "flags", Kind: ir.TypeMap, ElementType: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, - {Name: "labels", WireName: "labels", Kind: ir.TypeMap, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "limits", WireName: "limits", Type: ir.TypeMap, ElementType: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, + {Name: "weights", WireName: "weights", Type: ir.TypeMap, ElementType: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, + {Name: "flags", WireName: "flags", Type: ir.TypeMap, ElementType: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "labels", WireName: "labels", Type: ir.TypeMap, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, }} for _, v := range fixtures.Derive(tree).Entries { @@ -48,7 +48,7 @@ func TestUnit_Emit_AMapValueRendersAGoLiteralOfItsElementKind(t *testing.T) { // name the same kind rather than merely parsing. func TestUnit_Emit_AMapLiteralMatchesItsDeclaredTftype(t *testing.T) { tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "limits", WireName: "limits", Kind: ir.TypeMap, ElementType: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, + {Name: "limits", WireName: "limits", Type: ir.TypeMap, ElementType: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, }} got := tftypeNewValue(fixtures.Derive(tree).Entries[0]) diff --git a/internal/emit/render_action.go b/internal/emit/render_action.go index cf56e93..48c9638 100644 --- a/internal/emit/render_action.go +++ b/internal/emit/render_action.go @@ -59,14 +59,14 @@ func (e *serviceRenderer) action(a *ir.Action, ab *sdkbind.ActionBinding) ([]Fil Type: a.Names.PascalCase + "Action", TerraformType: a.Names.TerraformType, ClientType: "*sdk." + e.bindings.SDK.ClientTypeName, - HasBody: a.RequestSchema != nil && ab.WriteModel != "", + HasBody: a.RequestAttributes != nil && ab.WriteModel != "", AuthGitHubApp: e.pc.AuthGitHubApp, } // The schema is the invocation's arguments: one attribute per path // parameter, then the request body's tree. paramNodes := actionParameterNodes(&a.InvokeOperation) - bodyNodes := e.joinTree(bindingKindAction, a.Names.Key, a.RequestSchema, ab.Fields) + bodyNodes := e.joinTree(bindingKindAction, a.Names.Key, a.RequestAttributes, ab.Fields) nodes := append(append([]node{}, paramNodes...), invocable(bodyNodes)...) imports := newImportSet(e.pc.Module) @@ -77,7 +77,7 @@ func (e *serviceRenderer) action(a *ir.Action, ab *sdkbind.ActionBinding) ([]Fil imports.add("sdk", e.bindings.SDK.ImportPath) sb := &schemaBuilder{kind: schemaAction, imports: imports} d.SchemaAttributes = sb.attributeDeclarations(nodes, 3) - description := entityDescription(a.RequestSchema, "Invokes the "+a.Names.Key+" operation.") + description := entityDescription(a.RequestAttributes, "Invokes the "+a.Names.Key+" operation.") if a.CoManagementNote != "" { description += " " + a.CoManagementNote } @@ -129,7 +129,7 @@ func (e *serviceRenderer) action(a *ir.Action, ab *sdkbind.ActionBinding) ([]Fil d.InvokeImports = invokeImports.render() // Test wiring. - spec := deriveFixtures(actionTree(paramNodes, a.RequestSchema), nodes) + spec := deriveFixtures(actionTree(paramNodes, a.RequestAttributes), nodes) spec.PinNumeric(integerParsedParameters(ab.Invoke, nodes)) d.InvokeMethod = a.InvokeOperation.Method d.InvokeStatus = successStatus(&a.InvokeOperation, 204) @@ -199,7 +199,7 @@ func actionParameterNodes(operation *ir.Operation) []node { out = append(out, node{attribute: ir.Attribute{ Name: ir.TerraformName(p.Name), WireName: p.Name, - Kind: kind, + Type: kind, ComputedOptionalRequired: ir.Required, }}) } @@ -344,7 +344,7 @@ func invocable(nodes []node) []node { if n.attribute.ComputedOptionalRequired == ir.Computed { continue } - if n.attribute.Nested != nil { + if n.attribute.NestedAttributes != nil { n.children = invocable(n.children) } kept = append(kept, n) diff --git a/internal/emit/render_constraints.go b/internal/emit/render_constraints.go index 7a28071..5fbe531 100644 --- a/internal/emit/render_constraints.go +++ b/internal/emit/render_constraints.go @@ -25,13 +25,13 @@ const validatorPackageRoot = "github.com/hashicorp/terraform-plugin-framework-va // Nothing here registers an import; each expression carries the packages it // references, and validatorLines is the single place those are honoured. func constraintValidators(n node) []code.CustomValidator { - kind := n.attribute.Kind + kind := n.attribute.Type switch { case kind == ir.TypeList || kind == ir.TypeMap: // A size bound applies to the collection whether its elements are // scalars or objects, so this is deliberately not gated on Nested. return sizeValidators(n) - case n.attribute.Nested != nil: + case n.attribute.NestedAttributes != nil: // An object declaring a length or a range is declaring it about // something this attribute does not hold. return nil @@ -76,7 +76,7 @@ func numericValidators(n node) []code.CustomValidator { format := func(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) } minimum, maximum := n.attribute.Minimum, n.attribute.Maximum - if n.attribute.Kind == ir.TypeInt64 { + if n.attribute.Type == ir.TypeInt64 { goPackage = "int64validator" format = func(v float64) string { return strconv.FormatInt(int64(v), 10) } // A fractional bound on an integer attribute describes a value the @@ -95,7 +95,7 @@ func numericValidators(n node) []code.CustomValidator { // sizeValidators is the declared member-count range of a list or a map. func sizeValidators(n node) []code.CustomValidator { goPackage := "listvalidator" - if n.attribute.Kind == ir.TypeMap { + if n.attribute.Type == ir.TypeMap { goPackage = "mapvalidator" } if call, ok := boundCall("Size", "Size", n.attribute.MinItems, n.attribute.MaxItems); ok { diff --git a/internal/emit/render_constraints_test.go b/internal/emit/render_constraints_test.go index f6ee545..acb4282 100644 --- a/internal/emit/render_constraints_test.go +++ b/internal/emit/render_constraints_test.go @@ -27,47 +27,47 @@ func TestUnit_ConstraintValidators_RenderTheDeclaredBounds(t *testing.T) { }{ { "a string with both length bounds", - ir.Attribute{Name: "n", Kind: ir.TypeString, MinLength: i64(8), MaxLength: i64(64)}, + ir.Attribute{Name: "n", Type: ir.TypeString, MinLength: i64(8), MaxLength: i64(64)}, "stringvalidator.UTF8LengthBetween(8, 64)", }, { "a string with only a minimum length", - ir.Attribute{Name: "n", Kind: ir.TypeString, MinLength: i64(8)}, + ir.Attribute{Name: "n", Type: ir.TypeString, MinLength: i64(8)}, "stringvalidator.UTF8LengthAtLeast(8)", }, { "a string with only a maximum length", - ir.Attribute{Name: "n", Kind: ir.TypeString, MaxLength: i64(64)}, + ir.Attribute{Name: "n", Type: ir.TypeString, MaxLength: i64(64)}, "stringvalidator.UTF8LengthAtMost(64)", }, { "an integer range", - ir.Attribute{Name: "n", Kind: ir.TypeInt64, Minimum: f64(1), Maximum: f64(10)}, + ir.Attribute{Name: "n", Type: ir.TypeInt64, Minimum: f64(1), Maximum: f64(10)}, "int64validator.Between(1, 10)", }, { "an integer with only a maximum", - ir.Attribute{Name: "n", Kind: ir.TypeInt64, Maximum: f64(10)}, + ir.Attribute{Name: "n", Type: ir.TypeInt64, Maximum: f64(10)}, "int64validator.AtMost(10)", }, { "a float range keeps its fraction", - ir.Attribute{Name: "n", Kind: ir.TypeFloat64, Minimum: f64(0.5), Maximum: f64(99.5)}, + ir.Attribute{Name: "n", Type: ir.TypeFloat64, Minimum: f64(0.5), Maximum: f64(99.5)}, "float64validator.Between(0.5, 99.5)", }, { "a list size range", - ir.Attribute{Name: "n", Kind: ir.TypeList, ElementType: ir.TypeString, MinItems: i64(1), MaxItems: i64(5)}, + ir.Attribute{Name: "n", Type: ir.TypeList, ElementType: ir.TypeString, MinItems: i64(1), MaxItems: i64(5)}, "listvalidator.SizeBetween(1, 5)", }, { "a map size floor", - ir.Attribute{Name: "n", Kind: ir.TypeMap, ElementType: ir.TypeString, MinItems: i64(1)}, + ir.Attribute{Name: "n", Type: ir.TypeMap, ElementType: ir.TypeString, MinItems: i64(1)}, "mapvalidator.SizeAtLeast(1)", }, { "a pattern becomes a compiled match", - ir.Attribute{Name: "n", Kind: ir.TypeString, Pattern: `^[a-z]+$`}, + ir.Attribute{Name: "n", Type: ir.TypeString, Pattern: `^[a-z]+$`}, "stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z]+$`), \"must match ^[a-z]+$\")", }, } { @@ -83,7 +83,7 @@ func TestUnit_ConstraintValidators_RenderTheDeclaredBounds(t *testing.T) { // package initialisation, where go build cannot see it. func TestUnit_ConstraintValidators_SkipAPatternRE2CannotCompile(t *testing.T) { declaration := constraintDeclaration(ir.Attribute{ - Name: "n", Kind: ir.TypeString, Pattern: `^(?=.*[A-Z]).{8,}$`, + Name: "n", Type: ir.TypeString, Pattern: `^(?=.*[A-Z]).{8,}$`, }) if strings.Contains(declaration, "RegexMatches") { t.Errorf("a pattern RE2 cannot compile was emitted anyway:\n%s", declaration) @@ -92,7 +92,7 @@ func TestUnit_ConstraintValidators_SkipAPatternRE2CannotCompile(t *testing.T) { // The rest of the attribute still stands: one unrenderable pattern is a // fact about the pattern, not about the length bound beside it. both := constraintDeclaration(ir.Attribute{ - Name: "n", Kind: ir.TypeString, Pattern: `^(?=.*[A-Z]).{8,}$`, MaxLength: i64(64), + Name: "n", Type: ir.TypeString, Pattern: `^(?=.*[A-Z]).{8,}$`, MaxLength: i64(64), }) if !strings.Contains(both, "stringvalidator.UTF8LengthAtMost(64)") { t.Errorf("an unrenderable pattern took the length bound with it:\n%s", both) @@ -104,7 +104,7 @@ func TestUnit_ConstraintValidators_SkipAPatternRE2CannotCompile(t *testing.T) { // which would silently move the boundary. func TestUnit_ConstraintValidators_DropAFractionalBoundOnAnInteger(t *testing.T) { declaration := constraintDeclaration(ir.Attribute{ - Name: "n", Kind: ir.TypeInt64, Minimum: f64(1.5), Maximum: f64(10), + Name: "n", Type: ir.TypeInt64, Minimum: f64(1.5), Maximum: f64(10), }) if strings.Contains(declaration, "1.5") || strings.Contains(declaration, "Between") { t.Errorf("a fractional minimum survived onto an integer attribute:\n%s", declaration) @@ -120,18 +120,18 @@ func TestUnit_ConstraintValidators_DropAFractionalBoundOnAnInteger(t *testing.T) // applies, because the list is the thing being sized. func TestUnit_ConstraintValidators_LeaveANestedObjectAlone(t *testing.T) { nested := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "inner", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "inner", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, }} object := constraintDeclaration(ir.Attribute{ - Name: "n", Kind: ir.TypeObject, Nested: nested, MaxLength: i64(64), Maximum: f64(10), + Name: "n", Type: ir.TypeObject, NestedAttributes: nested, MaxLength: i64(64), Maximum: f64(10), }) if strings.Contains(object, "Validators:") { t.Errorf("a bound was applied to an object:\n%s", object) } list := constraintDeclaration(ir.Attribute{ - Name: "n", Kind: ir.TypeList, Nested: nested, MaxItems: i64(5), + Name: "n", Type: ir.TypeList, NestedAttributes: nested, MaxItems: i64(5), }) if !strings.Contains(list, "listvalidator.SizeAtMost(5)") { t.Errorf("a nested list was not sized:\n%s", list) @@ -144,7 +144,7 @@ func TestUnit_ConstraintValidators_LeaveANestedObjectAlone(t *testing.T) { func TestUnit_ConstraintValidators_DeclareTheirOwnImports(t *testing.T) { sb := &schemaBuilder{kind: schemaResource, imports: newImportSet("example.com/m")} sb.attributeDeclaration(node{attribute: ir.Attribute{ - Name: "n", Kind: ir.TypeString, MaxLength: i64(64), Pattern: `^[a-z]+$`, + Name: "n", Type: ir.TypeString, MaxLength: i64(64), Pattern: `^[a-z]+$`, }}, 0) rendered := sb.imports.render() @@ -161,7 +161,7 @@ func TestUnit_ConstraintValidators_DeclareTheirOwnImports(t *testing.T) { func TestUnit_Validators_AListOfEnumeratedStringsValidatesEachMember(t *testing.T) { sb := &schemaBuilder{kind: schemaResource} - n := node{attribute: ir.Attribute{Name: "modules", Kind: ir.TypeList, ElementType: ir.TypeString, OneOf: []string{"default", "extended"}}} + n := node{attribute: ir.Attribute{Name: "modules", Type: ir.TypeList, ElementType: ir.TypeString, OneOf: []string{"default", "extended"}}} got := sb.validators(n, 1) if len(got) != 1 || got[0].SchemaDefinition != `listvalidator.ValueStringsAre(stringvalidator.OneOf("default", "extended"))` { t.Fatalf("validators = %+v, want the member validator", got) @@ -195,10 +195,10 @@ func TestUnit_CheckLines_AddressMapsByKeyAndReferencesByPresence(t *testing.T) { func TestUnit_StateLines_ANormalisedStringKeepsTheConfiguredSpelling(t *testing.T) { nodes := []node{ - {attribute: ir.Attribute{Name: "server", Kind: ir.TypeString, Normalisation: "extended"}, - fb: &sdkbind.FieldBinding{Attr: "server", Wire: "server", Kind: ir.TypeString, Access: kiotaAccess("Server", "*string", "FromPtrString", "ToPtrString", "")}}, - {attribute: ir.Attribute{Name: "name", Kind: ir.TypeString}, - fb: &sdkbind.FieldBinding{Attr: "name", Wire: "name", Kind: ir.TypeString, Access: kiotaAccess("Name", "*string", "FromPtrString", "ToPtrString", "")}}, + {attribute: ir.Attribute{Name: "server", Type: ir.TypeString, Normalisation: "extended"}, + fb: &sdkbind.FieldBinding{Attr: "server", Wire: "server", Type: ir.TypeString, Access: kiotaAccess("Server", "*string", "FromPtrString", "ToPtrString", "")}}, + {attribute: ir.Attribute{Name: "name", Type: ir.TypeString}, + fb: &sdkbind.FieldBinding{Attr: "name", Wire: "name", Type: ir.TypeString, Access: kiotaAccess("Name", "*string", "FromPtrString", "ToPtrString", "")}}, } got, err := stateLines(nodes, "Thing", "data") if err != nil { @@ -215,8 +215,8 @@ func TestUnit_StateLines_ANormalisedStringKeepsTheConfiguredSpelling(t *testing. func TestUnit_ParameterNode_TheFirstAttributeCarryingTheWireNameAnswers(t *testing.T) { // A parent spelled id sits ahead of the resource's own id. nodes := []node{ - {attribute: ir.Attribute{Name: "template_id", WireName: "id", Kind: ir.TypeString}}, - {attribute: ir.Attribute{Name: "id", WireName: "id", Kind: ir.TypeString}}, + {attribute: ir.Attribute{Name: "template_id", WireName: "id", Type: ir.TypeString}}, + {attribute: ir.Attribute{Name: "id", WireName: "id", Type: ir.TypeString}}, } got, err := parameterNode(sdkbind.CallParameter{Wire: "id"}, nodes, true) if err != nil || got.attribute.Name != "template_id" { @@ -225,8 +225,8 @@ func TestUnit_ParameterNode_TheFirstAttributeCarryingTheWireNameAnswers(t *testi // An object's key the document also declares as a property is answered // by the id, which the create and the import fill. nodes = []node{ - {attribute: ir.Attribute{Name: "id", WireName: "testId", Kind: ir.TypeString}}, - {attribute: ir.Attribute{Name: "test_id", WireName: "testId", Kind: ir.TypeString}}, + {attribute: ir.Attribute{Name: "id", WireName: "testId", Type: ir.TypeString}}, + {attribute: ir.Attribute{Name: "test_id", WireName: "testId", Type: ir.TypeString}}, } got, err = parameterNode(sdkbind.CallParameter{Wire: "testId"}, nodes, true) if err != nil || got.attribute.Name != "id" { diff --git a/internal/emit/render_datasource.go b/internal/emit/render_datasource.go index 230118f..6295cb3 100644 --- a/internal/emit/render_datasource.go +++ b/internal/emit/render_datasource.go @@ -143,14 +143,14 @@ func (e *serviceRenderer) lookupDatasource(d *datasourceData, ds *ir.Datasource, return fixtures.Fixture{}, unrenderable(CauseDatasourceNoReadCall, "a lookup-by-key datasource needs a bound read call") } - nodes := e.joinTree(bindingKindDatasource, ds.Names.Key, ds.Schema, db.Fields, addressingNames(ds.Schema, ds.Operations.Read, ds.Operations.List)) + nodes := e.joinTree(bindingKindDatasource, ds.Names.Key, ds.Attributes, db.Fields, addressingNames(ds.Attributes, ds.Operations.Read, ds.Operations.List)) key := keyAttrName(ds) if !hasNode(nodes, key) { // The SDK model does not carry the key parameter as a field; the // schema still must, so the caller has somewhere to put it. nodes = append([]node{{attribute: ir.Attribute{ Name: key, WireName: ds.KeyParameter, - Kind: ir.TypeString, ComputedOptionalRequired: ir.Required, + Type: ir.TypeString, ComputedOptionalRequired: ir.Required, }}}, nodes...) } @@ -164,7 +164,7 @@ func (e *serviceRenderer) lookupDatasource(d *datasourceData, ds *ir.Datasource, imports.add("commonschema", e.pc.Module+"/internal/services/common/schema") sb := &schemaBuilder{kind: schemaDatasource, imports: imports} d.SchemaAttributes = sb.attributeDeclarations(nodes, 3) - description := entityDescription(ds.Schema, "Reads one "+ds.Names.Key+" by its "+key+".") + description := entityDescription(ds.Attributes, "Reads one "+ds.Names.Key+" by its "+key+".") if ds.CoManagementNote != "" { description += " " + ds.CoManagementNote } @@ -228,7 +228,7 @@ func (e *serviceRenderer) lookupDatasource(d *datasourceData, ds *ir.Datasource, e.addSDKImports(stateImports, stateBody, d.ReadModel) d.StateImports = stateImports.render() - spec := deriveFixtures(ds.Schema, nodes) + spec := deriveFixtures(ds.Attributes, nodes) spec.PinNumeric(integerParsedParameters(db.Read, nodes)) e.datasourceMocks(d, ds, spec) e.datasourceChecks(d, ds.Names.Key, spec) @@ -252,7 +252,7 @@ func (e *serviceRenderer) companionDatasource(d *datasourceData, ds *ir.Datasour // where that field does. Binding deletes what the SDK cannot carry, and // the item model is built from what is left: a filter over a deleted // field would compare against a model field that does not exist. - filters := make([]ir.Attribute, 0, len(ds.Schema.Attributes)) + filters := make([]ir.Attribute, 0, len(ds.Attributes.Attributes)) for _, f := range companionFilters(ds) { if hasNode(itemNodes, f.Name) { filters = append(filters, f) @@ -321,7 +321,7 @@ func (e *serviceRenderer) companionDatasource(d *datasourceData, ds *ir.Datasour // from config and which no item model can hold. root := "type " + d.Type + "Model struct {\n" for _, a := range append(companionAddressing(ds), filters...) { - root += "\t" + ir.GoName(a.Name) + " " + scalarSchemaType(a.Kind).ValueType + + root += "\t" + ir.GoName(a.Name) + " " + scalarSchemaType(a.Type).ValueType + " `tfsdk:\"" + a.Name + "\"`\n" } root += "\tItems []" + d.ItemModel + " `tfsdk:\"items\"`\n" + @@ -422,12 +422,12 @@ func itemPayloadExpr(db *sdkbind.DatasourceBinding) string { // companionItemTree finds the items attribute's element tree. func companionItemTree(ds *ir.Datasource) *ir.AttributeTree { - if ds.Schema == nil { + if ds.Attributes == nil { return nil } - for _, a := range ds.Schema.Attributes { + for _, a := range ds.Attributes.Attributes { if a.Name == "items" { - return a.Nested + return a.NestedAttributes } } return nil @@ -588,15 +588,15 @@ func reindentJSON(block, prefix string) string { // neither of the filter inputs nor the item list: the path parameters // derivation added so a parent-scoped collection can be reached. func companionAddressing(ds *ir.Datasource) []ir.Attribute { - if ds == nil || ds.Schema == nil { + if ds == nil || ds.Attributes == nil { return nil } var out []ir.Attribute - for _, a := range ds.Schema.Attributes { - if a.Name == "items" || a.Filter { + for _, a := range ds.Attributes.Attributes { + if a.Name == "items" || a.IsDatasourceFilterArgument { continue } - if a.Nested == nil { + if a.NestedAttributes == nil { out = append(out, a) } } @@ -634,12 +634,12 @@ func filterChecks(filters []ir.Attribute) string { // companionFilters is the arguments that select which listed objects come // back: one per scalar field at the root of a listed object. func companionFilters(ds *ir.Datasource) []ir.Attribute { - if ds == nil || ds.Schema == nil { + if ds == nil || ds.Attributes == nil { return nil } var out []ir.Attribute - for _, a := range ds.Schema.Attributes { - if a.Filter { + for _, a := range ds.Attributes.Attributes { + if a.IsDatasourceFilterArgument { out = append(out, a) } } diff --git a/internal/emit/render_datasource_test.go b/internal/emit/render_datasource_test.go index 0f08fb2..f61c0f0 100644 --- a/internal/emit/render_datasource_test.go +++ b/internal/emit/render_datasource_test.go @@ -23,13 +23,13 @@ func scopedCompanion(t *testing.T) *ServiceFiles { ds.Operations.List = &ir.Operation{ Kind: ir.OperationList, Method: "GET", PathTemplate: "/v7/tenants/{tenantId}/http-servers", - PathParameters: []ir.Parameter{{Name: "tenantId", Type: ir.TypeString}}, + PathParameters: []ir.URLPathParameter{{Name: "tenantId", Type: ir.TypeString}}, SuccessCode: 200, } - ds.Schema.Attributes = append([]ir.Attribute{{ - Name: "tenant_id", WireName: "tenantId", Kind: ir.TypeString, + ds.Attributes.Attributes = append([]ir.Attribute{{ + Name: "tenant_id", WireName: "tenantId", Type: ir.TypeString, ComputedOptionalRequired: ir.Required, - }}, ds.Schema.Attributes...) + }}, ds.Attributes.Attributes...) b.Datasources["http_server"].List.Parameters = []sdkbind.CallParameter{ {Local: "tenantId", GoType: "string", Wire: "tenantId"}} diff --git a/internal/emit/render_fixtures.go b/internal/emit/render_fixtures.go index 37b08af..b236fec 100644 --- a/internal/emit/render_fixtures.go +++ b/internal/emit/render_fixtures.go @@ -153,9 +153,9 @@ func (e *serviceRenderer) dependencyBlock(dependency *ir.Resource, depth int, li return "", false } e.dependencyTypes = append(e.dependencyTypes, dependency.Names.TerraformType) - nodes := e.joinTree(bindingKindResource, dependency.Names.Key, dependency.Schema, pb.Fields, addressingNames(dependency.Schema, + nodes := e.joinTree(bindingKindResource, dependency.Names.Key, dependency.Attributes, pb.Fields, addressingNames(dependency.Attributes, dependency.Operations.Read, dependency.Operations.Create, dependency.Operations.Update, dependency.Operations.Delete)) - spec := deriveFixtures(dependency.Schema, nodes) + spec := deriveFixtures(dependency.Attributes, nodes) // A replayed body is already the smallest accepted create and renders // whole; a derived fixture still selects the required attributes. minimal, form := spec, fixtures.ConfigMinimal @@ -183,7 +183,7 @@ func (e *serviceRenderer) dependencyBlock(dependency *ir.Resource, depth int, li // the immediate parent: the last parameter above the item key, or the last // parameter of a singleton, whose path names no item of its own. func parentAttribute(r *ir.Resource) string { - if r.Operations.Read == nil || r.Schema == nil { + if r.Operations.Read == nil || r.Attributes == nil { return "" } parameters := r.Operations.Read.PathParameters @@ -197,8 +197,8 @@ func parentAttribute(r *ir.Resource) string { return "" } last := parameters[len(parameters)-1] - for _, a := range r.Schema.Attributes { - if a.Nested != nil || a.Name == idAttributeName { + for _, a := range r.Attributes.Attributes { + if a.NestedAttributes != nil || a.Name == idAttributeName { continue } if a.WireName == last.Name || a.Name == ir.TerraformName(last.Name) { diff --git a/internal/emit/render_fixtures_parent_test.go b/internal/emit/render_fixtures_parent_test.go index 7774d0d..8c7a288 100644 --- a/internal/emit/render_fixtures_parent_test.go +++ b/internal/emit/render_fixtures_parent_test.go @@ -18,12 +18,12 @@ func settingUnder(parentParam string) ir.Resource { ParentEntity: "http_server", Operations: ir.Operations{ Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/http-servers/{" + parentParam + "}/setting", - PathParameters: []ir.Parameter{{Name: parentParam, Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: parentParam, Type: ir.TypeString}}, SuccessCode: 200}, }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "http_server_id", WireName: parentParam, Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "scope", WireName: "scope", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "http_server_id", WireName: parentParam, Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "scope", WireName: "scope", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }}, } } @@ -79,7 +79,7 @@ func TestUnit_DependencyBlocks_CarryABorrowedObjectsBlock(t *testing.T) { e := &serviceRenderer{pc: pc, bindings: b, resources: map[string]*ir.Resource{"http_server": &m.Resources[0]}} rule := ir.Resource{Names: names("alert_rule", "AlertRule", "alerts"), Operations: ir.Operations{ Create: &ir.Operation{Kind: ir.OperationCreate, Method: "POST", PathTemplate: "/v7/alert-rules", SuccessCode: 201}, - Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/alert-rules/{ruleId}", PathParameters: []ir.Parameter{{Name: "ruleId"}}}, + Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/alert-rules/{ruleId}", PathParameters: []ir.URLPathParameter{{Name: "ruleId"}}}, }} fixture := fixtures.Fixture{Entries: []fixtures.Entry{ {Name: "name", Wire: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required, Scalar: "r"}, @@ -114,16 +114,16 @@ func TestUnit_ParentAttribute_NamesTheImmediateParentsAttribute(t *testing.T) { } keyed := settingUnder("httpServerId") keyed.Singleton = false - keyed.Operations.Read.PathParameters = []ir.Parameter{{Name: "httpServerId"}, {Name: "settingId"}} + keyed.Operations.Read.PathParameters = []ir.URLPathParameter{{Name: "httpServerId"}, {Name: "settingId"}} if got := parentAttribute(&keyed); got != "http_server_id" { t.Errorf("a keyed child's parent attribute = %q, want the parameter above the key", got) } - keyed.Operations.Read.PathParameters = []ir.Parameter{{Name: "settingId"}} + keyed.Operations.Read.PathParameters = []ir.URLPathParameter{{Name: "settingId"}} if got := parentAttribute(&keyed); got != "" { t.Errorf("a top-level resource has a parent attribute: %q", got) } unanswered := settingUnder("ownerId") - unanswered.Schema.Attributes = unanswered.Schema.Attributes[1:] + unanswered.Attributes.Attributes = unanswered.Attributes.Attributes[1:] if got := parentAttribute(&unanswered); got != "" { t.Errorf("a parameter no attribute answers = %q, want none", got) } diff --git a/internal/emit/render_identity.go b/internal/emit/render_identity.go index bfe8ffb..a6c502a 100644 --- a/internal/emit/render_identity.go +++ b/internal/emit/render_identity.go @@ -44,25 +44,25 @@ var identitySchemaTypes = map[ir.AttributeType]string{ // Empty when the resource carries no id, which is the one attribute an // identity cannot do without. func resourceIdentity(r *ir.Resource) []identityAttribute { - if r.Schema == nil { + if r.Attributes == nil { return nil } addressing := identityAddressing(r.Operations.Read, r.Operations.Create, r.Operations.Delete) var out []identityAttribute var carriesID bool - for _, attribute := range r.Schema.Attributes { - if attribute.Nested != nil || attribute.Unsupported { + for _, attribute := range r.Attributes.Attributes { + if attribute.NestedAttributes != nil || attribute.Unsupported { continue } if attribute.Name != idAttributeName && !addressing[attribute.Name] { continue } - schemaType, ok := identitySchemaTypes[attribute.Kind] + schemaType, ok := identitySchemaTypes[attribute.Type] if !ok { continue } - kind := attribute.Kind + kind := attribute.Type if attribute.Name == idAttributeName { carriesID = true // The id is a string in the identity whatever the API keys its diff --git a/internal/emit/render_identity_test.go b/internal/emit/render_identity_test.go index 77397c5..916f927 100644 --- a/internal/emit/render_identity_test.go +++ b/internal/emit/render_identity_test.go @@ -32,10 +32,10 @@ func TestUnit_ResourceIdentity_TheItemKeyIsTheIDAndNotAlsoAddressing(t *testing. name: "the body declares the item key as a property", read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/alerts/rules/{ruleId}", - PathParameters: []ir.Parameter{{Name: "ruleId", Type: ir.TypeString}}}, + PathParameters: []ir.URLPathParameter{{Name: "ruleId", Type: ir.TypeString}}}, tree: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "ruleId", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "rule_id", WireName: "ruleId", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "id", WireName: "ruleId", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "rule_id", WireName: "ruleId", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, }}, want: []string{"id"}, }, @@ -43,21 +43,21 @@ func TestUnit_ResourceIdentity_TheItemKeyIsTheIDAndNotAlsoAddressing(t *testing. name: "a parent scopes the object", read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/repos/{owner}/hooks/{hookId}", - PathParameters: []ir.Parameter{ + PathParameters: []ir.URLPathParameter{ {Name: "owner", Type: ir.TypeString}, {Name: "hookId", Type: ir.TypeString}, }}, tree: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "owner", WireName: "owner", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "id", WireName: "hookId", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "hook_id", WireName: "hookId", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "owner", WireName: "owner", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "id", WireName: "hookId", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "hook_id", WireName: "hookId", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, }}, want: []string{"owner", "id"}, }, } { t.Run(testCase.name, func(t *testing.T) { got := identityNames(resourceIdentity(&ir.Resource{ - Schema: testCase.tree, + Attributes: testCase.tree, Operations: ir.Operations{Read: testCase.read}, })) if !reflect.DeepEqual(got, testCase.want) { @@ -73,7 +73,7 @@ func TestUnit_ResourceIdentity_ACollectionPathKeepsEveryParameter(t *testing.T) got := identityAddressing(&ir.Operation{ Kind: ir.OperationCreate, Method: "POST", PathTemplate: "/orgs/{org}/teams", - PathParameters: []ir.Parameter{ + PathParameters: []ir.URLPathParameter{ {Name: "org", Type: ir.TypeString}, }}) if !got["org"] { diff --git a/internal/emit/render_listresource.go b/internal/emit/render_listresource.go index ec6856b..9444c46 100644 --- a/internal/emit/render_listresource.go +++ b/internal/emit/render_listresource.go @@ -69,7 +69,7 @@ func (e *serviceRenderer) listResource(lr *ir.ListResource, lb *sdkbind.ListReso if lb.List == nil { return nil, unrenderable(sdkbind.CauseListResourceNoListCall, "a list resource needs a bound list call") } - nodes := e.joinTree(bindingKindListResource, lr.Names.Key, lr.Schema, lb.Fields, addressingNames(lr.Schema, &lr.ListOperation)) + nodes := e.joinTree(bindingKindListResource, lr.Names.Key, lr.Attributes, lb.Fields, addressingNames(lr.Attributes, &lr.ListOperation)) d := &listResourceData{ Package: lr.Names.Package, @@ -84,7 +84,7 @@ func (e *serviceRenderer) listResource(lr *ir.ListResource, lb *sdkbind.ListReso ProviderName: e.pc.ProviderName, } - description := entityDescription(lr.Schema, "Lists "+lr.Names.Key+" objects. The entity is enumerable but not addressable, so listing is the whole surface.") + description := entityDescription(lr.Attributes, "Lists "+lr.Names.Key+" objects. The entity is enumerable but not addressable, so listing is the whole surface.") if lr.CoManagementNote != "" { description += " " + lr.CoManagementNote } @@ -94,7 +94,7 @@ func (e *serviceRenderer) listResource(lr *ir.ListResource, lb *sdkbind.ListReso // configuration, not from an element: a list resource has no object to // address, so the practitioner supplies the scope the collection path // names. - configNodes := joinTree(lr.AddressingSchema, nil, addressingNames(lr.AddressingSchema, &lr.ListOperation)) + configNodes := joinTree(lr.AddressingAttributes, nil, addressingNames(lr.AddressingAttributes, &lr.ListOperation)) listOp := lr.ListOperation plan, err := buildCallPlan(lb.List, "result", configNodes, "config", streamDiagnostics()) @@ -149,8 +149,8 @@ func (e *serviceRenderer) listResource(lr *ir.ListResource, lb *sdkbind.ListReso addPlanImports(listImports, plan) d.ListImports = listImports.render() - spec := deriveFixtures(lr.Schema, nodes) - configSpec := deriveFixtures(lr.AddressingSchema, configNodes) + spec := deriveFixtures(lr.Attributes, nodes) + configSpec := deriveFixtures(lr.AddressingAttributes, configNodes) configSpec.PinNumeric(integerParsedParameters(lb.List, configNodes)) // A parameterised collection path is requested with the addressing // substituted in, so the mock matches the shape rather than the template, @@ -287,10 +287,10 @@ func listResultLines(nodes []node, identity []identityAttribute, config []node) func identityCandidates(nodes []node) string { var found []string for _, n := range nodes { - if n.attribute.Nested != nil || n.fb == nil || n.fb.Access.Get == "" { + if n.attribute.NestedAttributes != nil || n.fb == nil || n.fb.Access.Get == "" { continue } - switch n.attribute.Kind { + switch n.attribute.Type { case ir.TypeString, ir.TypeInt64, ir.TypeFloat64: default: continue @@ -309,7 +309,7 @@ func identityCandidates(nodes []node) string { // findStringNode finds a plain string attribute by name. func findStringNode(nodes []node, name string) (node, bool) { for _, n := range nodes { - if n.attribute.Name == name && n.attribute.Kind == ir.TypeString && n.attribute.Nested == nil && n.fb != nil && n.fb.Access.Get != "" { + if n.attribute.Name == name && n.attribute.Type == ir.TypeString && n.attribute.NestedAttributes == nil && n.fb != nil && n.fb.Access.Get != "" { return n, true } } @@ -323,10 +323,10 @@ func findStringNode(nodes []node, name string) (node, bool) { // repositories, issues and organizations among them. func findIdentityNode(nodes []node) (node, bool) { for _, n := range nodes { - if n.attribute.Name != idAttributeName || n.attribute.Nested != nil || n.fb == nil || n.fb.Access.Get == "" { + if n.attribute.Name != idAttributeName || n.attribute.NestedAttributes != nil || n.fb == nil || n.fb.Access.Get == "" { continue } - switch n.attribute.Kind { + switch n.attribute.Type { case ir.TypeString, ir.TypeInt64, ir.TypeFloat64, ir.TypeBool: return n, true } diff --git a/internal/emit/render_listresource_test.go b/internal/emit/render_listresource_test.go index c5159ea..00cab1f 100644 --- a/internal/emit/render_listresource_test.go +++ b/internal/emit/render_listresource_test.go @@ -15,9 +15,9 @@ func scopedListResource(t *testing.T) *ServiceFiles { t.Helper() m, b := fictionalModel(), fictionalBindings() m.ListResources[0].ListOperation.PathTemplate = "/v7/tenants/{tenantId}/audit-events" - m.ListResources[0].ListOperation.PathParameters = []ir.Parameter{{Name: "tenantId", Type: ir.TypeString}} - m.ListResources[0].AddressingSchema = &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "tenant_id", WireName: "tenantId", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + m.ListResources[0].ListOperation.PathParameters = []ir.URLPathParameter{{Name: "tenantId", Type: ir.TypeString}} + m.ListResources[0].AddressingAttributes = &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "tenant_id", WireName: "tenantId", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }} b.ListResources["http_server"].List.Parameters = []sdkbind.CallParameter{ {Local: "tenantId", GoType: "string", Wire: "tenantId"}} @@ -169,9 +169,9 @@ func TestUnit_ListResource_PublishesAnIdentityKeyedTheAPIsWay(t *testing.T) { t.Fatalf("the fictional list resource moved: %q", lr.Names.Key) } // The element carries the key as the API words it, not as "id". - for i := range lr.Schema.Attributes { - if lr.Schema.Attributes[i].Name == "id" { - lr.Schema.Attributes[i].WireName = "httpServerId" + for i := range lr.Attributes.Attributes { + if lr.Attributes.Attributes[i].Name == "id" { + lr.Attributes.Attributes[i].WireName = "httpServerId" } } lb := b.ListResources["http_server"] @@ -201,18 +201,18 @@ func TestUnit_ListResource_PublishesAnIdentityKeyedTheAPIsWay(t *testing.T) { // The refusal an element with no key at all still earns has to say what it // looked for and what the element does carry, or the only way to write the // correction is to read the toolkit. -func TestUnit_ListResource_RefusalNamesWhatTheElementCarries(t *testing.T) { +func TestUnit_ListResource_ExclusionNamesWhatTheElementCarries(t *testing.T) { m, b := fictionalModel(), fictionalBindings() lr := &m.ListResources[0] - kept := make([]ir.Attribute, 0, len(lr.Schema.Attributes)) - for _, a := range lr.Schema.Attributes { + kept := make([]ir.Attribute, 0, len(lr.Attributes.Attributes)) + for _, a := range lr.Attributes.Attributes { if a.Name == "id" { a.Name, a.WireName = "server_uid", "serverUid" } kept = append(kept, a) } - lr.Schema.Attributes = kept + lr.Attributes.Attributes = kept lb := b.ListResources["http_server"] fields := make([]sdkbind.FieldBinding, 0, len(lb.Fields)) for _, f := range lb.Fields { diff --git a/internal/emit/render_mapping.go b/internal/emit/render_mapping.go index c30bfd0..fdfaa6f 100644 --- a/internal/emit/render_mapping.go +++ b/internal/emit/render_mapping.go @@ -60,7 +60,7 @@ func constructLines(namer *modelNamer, path string, nodes []node, source, destin var lines string var err error var nestedUsesFmt bool - if n.attribute.Nested != nil { + if n.attribute.NestedAttributes != nil { lines, nestedUsesFmt, err = constructNested(namer, childPath(path, n), n, source, destination, attrPath, depth) usesFmt = usesFmt || nestedUsesFmt } else if strings.HasSuffix(n.fb.Access.ConvertSet, "MapAdditionalData") { @@ -74,7 +74,7 @@ func constructLines(namer *modelNamer, path string, nodes []node, source, destin return "", false, err } - if gateUpdates && n.attribute.SilentlyIgnoredOnUpdate { + if gateUpdates && n.attribute.IgnoredOnUpdate { guarded := indent + "if isCreate {\n" + reindent(lines, "\t") + indent + "}\n" b.WriteString(guarded) continue @@ -148,7 +148,7 @@ func stateLinesWith(namer *modelNamer, path string, nodes []node, source, destin if n.fb == nil || n.fb.Access.Get == "" { continue } - if n.attribute.Nested != nil { + if n.attribute.NestedAttributes != nil { lines, err := stateNested(namer, childPath(path, n), n, source, destination, depth) if err != nil { return "", err @@ -163,7 +163,7 @@ func stateLinesWith(namer *modelNamer, path string, nodes []node, source, destin // A root string the API stores in a spelling of its own reads back // as the configured value where the answer is that spelling: the // model being filled still holds the planned or prior value. - if path == "" && n.attribute.Normalisation != "" && n.attribute.Kind == ir.TypeString { + if path == "" && n.attribute.Normalisation != "" && n.attribute.Type == ir.TypeString { fmt.Fprintf(&b, "%s%s.%s = convert.Normalised(%s.%s, convert.%s(%s.%s()), %q)\n", indent, destination, ir.GoName(n.attribute.Name), destination, ir.GoName(n.attribute.Name), fn, source, n.fb.Access.Get, n.attribute.Normalisation) continue @@ -266,7 +266,7 @@ func buildCallPlan(call *sdkbind.Call, payloadName string, nodes []node, modelVa if err != nil { return finalisedAPIRequest{}, err } - declaration, needs, err := parameterDeclaration(p, modelVar, ir.GoName(n.attribute.Name), n.attribute.Kind, n.attribute.Name, fail) + declaration, needs, err := parameterDeclaration(p, modelVar, ir.GoName(n.attribute.Name), n.attribute.Type, n.attribute.Name, fail) if err != nil { return finalisedAPIRequest{}, err } @@ -335,19 +335,19 @@ func parameterNode(p sdkbind.CallParameter, nodes []node, idFallback bool) (node // an object's own key, which the document may declare as a property // too, is answered by the id, which the create and the import fill. for _, n := range nodes { - if n.attribute.WireName == p.Wire && n.attribute.Nested == nil { + if n.attribute.WireName == p.Wire && n.attribute.NestedAttributes == nil { return n, nil } } snake := ir.TerraformName(p.Wire) for _, n := range nodes { - if n.attribute.Name == snake && n.attribute.Nested == nil { + if n.attribute.Name == snake && n.attribute.NestedAttributes == nil { return n, nil } } if idFallback { for _, n := range nodes { - if n.attribute.Name == idAttributeName && n.attribute.Nested == nil { + if n.attribute.Name == idAttributeName && n.attribute.NestedAttributes == nil { return n, nil } } @@ -365,7 +365,7 @@ func parameterNode(p sdkbind.CallParameter, nodes []node, idFallback bool) (node func namesAnAttribute(p sdkbind.CallParameter, nodes []node) bool { snake := ir.TerraformName(p.Wire) for _, n := range nodes { - if n.attribute.Nested != nil { + if n.attribute.NestedAttributes != nil { continue } if n.attribute.WireName == p.Wire || n.attribute.Name == snake { @@ -537,7 +537,7 @@ func integerParsedParameters(call *sdkbind.Call, nodes []node) map[string]bool { if err != nil { continue } - if n.attribute.Kind == ir.TypeString && isIntegerType(p.GoType) { + if n.attribute.Type == ir.TypeString && isIntegerType(p.GoType) { out[n.attribute.Name] = true } } diff --git a/internal/emit/render_mapping_test.go b/internal/emit/render_mapping_test.go index 518c8ae..e097efa 100644 --- a/internal/emit/render_mapping_test.go +++ b/internal/emit/render_mapping_test.go @@ -109,10 +109,10 @@ func TestUnit_ParamDeclaration_IntegerParsesWithADiagnostic(t *testing.T) { } } -// TestUnit_ParamDeclaration_RefusesATruncatingConversion proves a numeric +// TestUnit_ParamDeclaration_ExcludesATruncatingConversion proves a numeric // field into a narrower integer parameter is still refused: the conversion // loses information silently, and no parse reports that. -func TestUnit_ParamDeclaration_RefusesATruncatingConversion(t *testing.T) { +func TestUnit_ParamDeclaration_ExcludesATruncatingConversion(t *testing.T) { p := sdkbind.CallParameter{Local: "groupId", Wire: "runner_group_id", GoType: "int32"} if _, _, err := parameterDeclaration(p, "data", "GroupID", ir.TypeFloat64, "runner_group_id", respDiagnostics()); err == nil { diff --git a/internal/emit/render_nested.go b/internal/emit/render_nested.go index c54e61c..682351a 100644 --- a/internal/emit/render_nested.go +++ b/internal/emit/render_nested.go @@ -37,7 +37,7 @@ func constructNested(namer *modelNamer, path string, n node, source, destination deref = "*" } - if n.attribute.Kind == ir.TypeList { + if n.attribute.Type == ir.TypeList { listVar := lowerCamel(n.attribute.Name) + "List" + depthSuffix(depth) indexVar := "index" + depthSuffix(depth) elemVar := lowerCamel(n.attribute.Name) + "Element" + depthSuffix(depth) @@ -73,7 +73,7 @@ func constructNested(namer *modelNamer, path string, n node, source, destination return b.String(), true, nil } - if n.attribute.Kind == ir.TypeMap { + if n.attribute.Type == ir.TypeMap { mapVar := lowerCamel(n.attribute.Name) + "Map" + depthSuffix(depth) keyVar := "key" + depthSuffix(depth) elemVar := lowerCamel(n.attribute.Name) + "Element" + depthSuffix(depth) @@ -146,7 +146,7 @@ func stateNested(namer *modelNamer, path string, n node, source, destination str field := destination + "." + ir.GoName(n.attribute.Name) modelType := namer.name(path) - if n.attribute.Kind == ir.TypeList { + if n.attribute.Type == ir.TypeList { elementsVar := "elements" + depthSuffix(depth) indexVar := "index" + depthSuffix(depth) listVar := lowerCamel(n.attribute.Name) + "List" + depthSuffix(depth) @@ -177,7 +177,7 @@ func stateNested(namer *modelNamer, path string, n node, source, destination str return b.String(), nil } - if n.attribute.Kind == ir.TypeMap { + if n.attribute.Type == ir.TypeMap { entriesVar := "entries" + depthSuffix(depth) keyVar := "key" + depthSuffix(depth) mapVar := lowerCamel(n.attribute.Name) + "Map" + depthSuffix(depth) diff --git a/internal/emit/render_resource.go b/internal/emit/render_resource.go index de60fc1..77e9901 100644 --- a/internal/emit/render_resource.go +++ b/internal/emit/render_resource.go @@ -103,8 +103,8 @@ type resourceData struct { UpdateParamCopies string MissingUpdate bool - HasEC bool - ECDuration string + HasReadAfterWriteDelay bool + ReadAfterWriteDelay string HasValidators bool ConfigValidatorExprs string @@ -143,7 +143,7 @@ func (e *serviceRenderer) resource(r *ir.Resource, rb *sdkbind.ResourceBinding) return nil, unrenderable(CauseResourceNoLifecycleCall, "a resource needs bound create, read and delete calls") } - nodes := e.joinTree(bindingKindResource, r.Names.Key, r.Schema, rb.Fields, addressingNames(r.Schema, + nodes := e.joinTree(bindingKindResource, r.Names.Key, r.Attributes, rb.Fields, addressingNames(r.Attributes, r.Operations.Read, r.Operations.Create, r.Operations.Update, r.Operations.Delete)) d := &resourceData{ Package: r.Names.Package, @@ -168,9 +168,9 @@ func (e *serviceRenderer) resource(r *ir.Resource, rb *sdkbind.ResourceBinding) // a constant rather than anything the API answers with. d.SingletonID = r.Names.TerraformType } - if r.EventualConsistency > 0 { - d.HasEC = true - d.ECDuration = goDuration(int64(r.EventualConsistency)) + if r.ReadAfterWriteDelay > 0 { + d.HasReadAfterWriteDelay = true + d.ReadAfterWriteDelay = goDuration(int64(r.ReadAfterWriteDelay)) } if err := e.resourceCode(d, r, rb, nodes); err != nil { @@ -179,7 +179,7 @@ func (e *serviceRenderer) resource(r *ir.Resource, rb *sdkbind.ResourceBinding) if err := e.resourceCRUD(d, rb, nodes); err != nil { return nil, err } - spec := deriveFixtures(r.Schema, nodes) + spec := deriveFixtures(r.Attributes, nodes) spec.PinNumeric(integerParsedParameters(rb.Read, nodes)) if !d.Singleton { if err := e.resourceMocks(d, r, rb, spec); err != nil { @@ -260,7 +260,7 @@ func (e *serviceRenderer) resourceCode(d *resourceData, r *ir.Resource, rb *sdkb for _, n := range nodes { byName[n.attribute.Name] = n } - deps, err := dependencyMap(r.Schema, byName) + deps, err := dependencyMap(r.Attributes, byName) if err != nil { return err } @@ -282,7 +282,7 @@ func (e *serviceRenderer) resourceCode(d *resourceData, r *ir.Resource, rb *sdkb } } - description := entityDescription(r.Schema, "Manages the "+r.Names.Key+" entity.") + description := entityDescription(r.Attributes, "Manages the "+r.Names.Key+" entity.") if r.CoManagementNote != "" { description += " " + r.CoManagementNote } @@ -324,7 +324,7 @@ func (e *serviceRenderer) resourceCode(d *resourceData, r *ir.Resource, rb *sdkb updateBody := "" updateUsesFmt := false if rb.UpdateWriteModel != "" { - updateNodes := e.joinTree(bindingKindResource, r.Names.Key, r.Schema, rb.UpdateFields, addressingNames(r.Schema, + updateNodes := e.joinTree(bindingKindResource, r.Names.Key, r.Attributes, rb.UpdateFields, addressingNames(r.Attributes, r.Operations.Read, r.Operations.Create, r.Operations.Update, r.Operations.Delete)) updateBody, updateUsesFmt, err = constructLinesFor(updateNodes, d.Pascal, "data", "body", "", 1, false) if err != nil { @@ -373,8 +373,8 @@ func (e *serviceRenderer) resourceCode(d *resourceData, r *ir.Resource, rb *sdkb // AlsoRequires in the schema above; this file carries the resource's // ConfigValidators method — the stock Conflicting for mutually-exclusive // groups and the named custom validators for the value-conditional edges. - if treeHasConfigValidators(r.Schema) { - exprs, declarations, err := configValidators(d.Type, r.Schema, nodes) + if treeHasConfigValidators(r.Attributes) { + exprs, declarations, err := configValidators(d.Type, r.Attributes, nodes) if err != nil { return err } @@ -385,7 +385,7 @@ func (e *serviceRenderer) resourceCode(d *resourceData, r *ir.Resource, rb *sdkb validatorImports.add("", "context") validatorImports.add("", "github.com/hashicorp/terraform-plugin-framework/path") validatorImports.add("", "github.com/hashicorp/terraform-plugin-framework/resource") - if len(r.Schema.MutuallyExclusiveGroups) > 0 { + if len(r.Attributes.MutuallyExclusiveGroups) > 0 { validatorImports.add("", "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator") } d.ValidatorImports = validatorImports.render() @@ -466,7 +466,7 @@ func (e *serviceRenderer) resourceCRUD(d *resourceData, rb *sdkbind.ResourceBind imports.add("", "github.com/hashicorp/terraform-plugin-framework/resource") imports.add("", e.pc.Module+"/internal/services/common/crud") imports.add("", e.pc.Module+"/internal/services/common/errors") - if d.HasEC { + if d.HasReadAfterWriteDelay { imports.add("", "time") imports.add("", "github.com/hashicorp/terraform-plugin-framework/tfsdk") } diff --git a/internal/emit/render_schema.go b/internal/emit/render_schema.go index 06d9085..7ad0dd9 100644 --- a/internal/emit/render_schema.go +++ b/internal/emit/render_schema.go @@ -108,7 +108,7 @@ func (sb *schemaBuilder) attributeDeclaration(n node, depth int) string { fmt.Fprintf(&b, "%s\tDeprecationMessage: %s,\n", indent, strconv.Quote(deprecationMessage)) } - if (n.attribute.Kind == ir.TypeList || n.attribute.Kind == ir.TypeMap) && n.attribute.Nested == nil { + if (n.attribute.Type == ir.TypeList || n.attribute.Type == ir.TypeMap) && n.attribute.NestedAttributes == nil { sb.imports.add("", "github.com/hashicorp/terraform-plugin-framework/types") fmt.Fprintf(&b, "%s\tElementType: %s,\n", indent, schemaTypeOf(n).ElementType) } @@ -117,11 +117,11 @@ func (sb *schemaBuilder) attributeDeclaration(n node, depth int) string { b.WriteString(sb.planModifierLines(n, indent+"\t")) - if n.attribute.Nested != nil { + if n.attribute.NestedAttributes != nil { // A list and a map both wrap their attributes in a // NestedAttributeObject; only a single nested object carries them // directly. - if n.attribute.Kind == ir.TypeList || n.attribute.Kind == ir.TypeMap { + if n.attribute.Type == ir.TypeList || n.attribute.Type == ir.TypeMap { fmt.Fprintf(&b, "%s\tNestedObject: %s.NestedAttributeObject{\n", indent, sb.goPackage()) fmt.Fprintf(&b, "%s\t\tAttributes: map[string]%s.Attribute{\n", indent, sb.goPackage()) b.WriteString(sb.attributeDeclarations(n.children, depth+3)) @@ -150,7 +150,7 @@ func (sb *schemaBuilder) attributeDeclaration(n node, depth int) string { func (sb *schemaBuilder) validators(n node, depth int) []code.CustomValidator { var validators []code.CustomValidator - if len(n.attribute.OneOf) > 0 && n.attribute.Kind == ir.TypeString && n.attribute.Nested == nil { + if len(n.attribute.OneOf) > 0 && n.attribute.Type == ir.TypeString && n.attribute.NestedAttributes == nil { quoted := make([]string, len(n.attribute.OneOf)) for i, v := range n.attribute.OneOf { quoted[i] = strconv.Quote(v) @@ -163,7 +163,7 @@ func (sb *schemaBuilder) validators(n node, depth int) []code.CustomValidator { }) } - if len(n.attribute.OneOf) > 0 && n.attribute.Kind == ir.TypeList && n.attribute.ElementType == ir.TypeString { + if len(n.attribute.OneOf) > 0 && n.attribute.Type == ir.TypeList && n.attribute.ElementType == ir.TypeString { quoted := make([]string, len(n.attribute.OneOf)) for i, v := range n.attribute.OneOf { quoted[i] = strconv.Quote(v) @@ -272,7 +272,7 @@ func (sb *schemaBuilder) planModifiers(n node) []code.CustomPlanModifier { }) } isID := n.attribute.Name == "id" && n.attribute.ComputedOptionalRequired == ir.Computed && - n.attribute.Kind == ir.TypeString && n.attribute.Nested == nil + n.attribute.Type == ir.TypeString && n.attribute.NestedAttributes == nil // A computed-optional attribute is one the practitioner may set and the // server fills when they do not. Unpinned it re-plans as unknown on every // run, which is a permanent diff on a resource nothing has changed. @@ -335,7 +335,7 @@ func attributeDescription(a ir.Attribute, keptFromPlan bool) string { if a.RequiresReplace { parts = append(parts, "Changing this attribute forces replacement.") } - if a.SilentlyIgnoredOnUpdate { + if a.IgnoredOnUpdate { parts = append(parts, "The API ignores this attribute on update.") } return strings.Join(parts, " ") @@ -387,7 +387,7 @@ func newModelNamer(typePrefix string, nodes []node) *modelNamer { var survey func(parent string, nodes []node) survey = func(parent string, nodes []node) { for _, n := range nodes { - if n.attribute.Nested == nil { + if n.attribute.NestedAttributes == nil { continue } path := n.attribute.Name @@ -476,7 +476,7 @@ func buildModels(rootName, typePrefix string, nodes []node, extraFields []string } for _, n := range nodes { - if n.attribute.Nested == nil { + if n.attribute.NestedAttributes == nil { continue } nested := childPath(path, n) @@ -498,11 +498,11 @@ func buildModels(rootName, typePrefix string, nodes []node, extraFields []string // back into, through the AttrTypes function beside it. func fieldType(n node) string { switch { - case n.attribute.Nested != nil && n.attribute.Kind == ir.TypeList: + case n.attribute.NestedAttributes != nil && n.attribute.Type == ir.TypeList: return "types.List" - case n.attribute.Nested != nil && n.attribute.Kind == ir.TypeMap: + case n.attribute.NestedAttributes != nil && n.attribute.Type == ir.TypeMap: return "types.Map" - case n.attribute.Nested != nil: + case n.attribute.NestedAttributes != nil: return "types.Object" default: return schemaTypeOf(n).ValueType @@ -513,11 +513,11 @@ func fieldType(n node) string { // object or list value must be given to be built or nulled. func attrTypeExpr(namer *modelNamer, path string, n node) string { switch { - case n.attribute.Nested != nil && n.attribute.Kind == ir.TypeList: + case n.attribute.NestedAttributes != nil && n.attribute.Type == ir.TypeList: return "types.ListType{ElemType: " + nestedObjectType(namer, path) + "}" - case n.attribute.Nested != nil && n.attribute.Kind == ir.TypeMap: + case n.attribute.NestedAttributes != nil && n.attribute.Type == ir.TypeMap: return "types.MapType{ElemType: " + nestedObjectType(namer, path) + "}" - case n.attribute.Nested != nil: + case n.attribute.NestedAttributes != nil: return nestedObjectType(namer, path) default: resolved := schemaTypeOf(n) diff --git a/internal/emit/render_schema_test.go b/internal/emit/render_schema_test.go index c591c21..247e3b4 100644 --- a/internal/emit/render_schema_test.go +++ b/internal/emit/render_schema_test.go @@ -20,7 +20,7 @@ func declarationOf(kind schemaKind, attribute ir.Attribute) string { func TestUnit_AttributeDecl_MarksASecretSensitive(t *testing.T) { for _, kind := range []schemaKind{schemaResource, schemaDatasource} { declaration := declarationOf(kind, ir.Attribute{ - Name: "password", Kind: ir.TypeString, + Name: "password", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, Sensitive: true, }) if !strings.Contains(declaration, "Sensitive: true,") { @@ -29,7 +29,7 @@ func TestUnit_AttributeDecl_MarksASecretSensitive(t *testing.T) { } plain := declarationOf(schemaResource, ir.Attribute{ - Name: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + Name: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, }) if strings.Contains(plain, "Sensitive") { t.Errorf("an ordinary attribute is marked sensitive:\n%s", plain) @@ -42,7 +42,7 @@ func TestUnit_AttributeDecl_MarksASecretSensitive(t *testing.T) { func TestUnit_AttributeDecl_OmitsSensitiveWhereThePackageLacksIt(t *testing.T) { for _, kind := range []schemaKind{schemaAction, schemaListResource} { declaration := declarationOf(kind, ir.Attribute{ - Name: "password", Kind: ir.TypeString, + Name: "password", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, Sensitive: true, }) if strings.Contains(declaration, "Sensitive") { @@ -57,7 +57,7 @@ func TestUnit_AttributeDecl_OmitsSensitiveWhereThePackageLacksIt(t *testing.T) { func TestUnit_AttributeDecl_WarnsOnADeprecatedAttribute(t *testing.T) { for _, kind := range []schemaKind{schemaResource, schemaDatasource, schemaAction, schemaListResource} { declaration := declarationOf(kind, ir.Attribute{ - Name: "legacy", Kind: ir.TypeString, + Name: "legacy", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, Deprecated: true, }) if !strings.Contains(declaration, `DeprecationMessage: "This attribute is deprecated and may be removed in a future API version.",`) { @@ -66,7 +66,7 @@ func TestUnit_AttributeDecl_WarnsOnADeprecatedAttribute(t *testing.T) { } current := declarationOf(schemaResource, ir.Attribute{ - Name: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + Name: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, }) if strings.Contains(current, "DeprecationMessage") { t.Errorf("an undeprecated attribute carries a warning:\n%s", current) @@ -78,7 +78,7 @@ func TestUnit_AttributeDecl_WarnsOnADeprecatedAttribute(t *testing.T) { // rather than re-planning that value as unknown for ever. func TestUnit_AttributeDecl_PinsAComputedOptionalValue(t *testing.T) { pinned := declarationOf(schemaResource, ir.Attribute{ - Name: "timezone", Kind: ir.TypeString, + Name: "timezone", Type: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional, }) if !strings.Contains(pinned, "UseStateForUnknown()") { @@ -89,7 +89,7 @@ func TestUnit_AttributeDecl_PinsAComputedOptionalValue(t *testing.T) { // because it is computed, and pinning it makes terraform insist on a value // the next read contradicts. unpinned := declarationOf(schemaResource, ir.Attribute{ - Name: "modified_at", Kind: ir.TypeString, + Name: "modified_at", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed, }) if strings.Contains(unpinned, "UseStateForUnknown()") { @@ -98,7 +98,7 @@ func TestUnit_AttributeDecl_PinsAComputedOptionalValue(t *testing.T) { // A datasource has no plan to modify at all. ds := declarationOf(schemaDatasource, ir.Attribute{ - Name: "timezone", Kind: ir.TypeString, + Name: "timezone", Type: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional, }) if strings.Contains(ds, "PlanModifiers") { diff --git a/internal/emit/render_validators.go b/internal/emit/render_validators.go index 9208c52..4b4e393 100644 --- a/internal/emit/render_validators.go +++ b/internal/emit/render_validators.go @@ -67,7 +67,7 @@ func configValidators(typeName string, t *ir.AttributeTree, nodes []node) (expre } name := namer.name(lowerFirst(ir.GoName(request.Property)) + "RequiredWhen") description := fmt.Sprintf("%s must be set when %s is %q.", - strings.Join(request.Required, ", "), request.Property, request.Equals) + strings.Join(request.Required, ", "), request.Property, request.WhenPropertyEquals) declarationLines.WriteString(renderCustomValidator(name, typeName, description, body.String())) fmt.Fprintf(&expressionLines, "\t\t%s{},\n", name) } @@ -78,7 +78,7 @@ func configValidators(typeName string, t *ir.AttributeTree, nodes []node) (expre } name := namer.name(lowerFirst(ir.GoName(v.Property)) + "ValidWhen") description := fmt.Sprintf("%s may be set only when %s is %q.", - strings.Join(v.Valid, ", "), v.Property, v.Equals) + strings.Join(v.AttributesValidWhenEqual, ", "), v.Property, v.WhenPropertyEquals) declarationLines.WriteString(renderCustomValidator(name, typeName, description, body.String())) fmt.Fprintf(&expressionLines, "\t\t%s{},\n", name) } @@ -185,7 +185,7 @@ func emitRequiredWhen(b *strings.Builder, byName map[string]node, request ir.Con if err := stringGate(byName, request.Property, "conditional requirement"); err != nil { return err } - fmt.Fprintf(b, "\tif data.%s.ValueString() == %s {\n", ir.GoName(request.Property), strconv.Quote(request.Equals)) + fmt.Fprintf(b, "\tif data.%s.ValueString() == %s {\n", ir.GoName(request.Property), strconv.Quote(request.WhenPropertyEquals)) for _, required := range request.Required { target, ok := byName[required] if !ok { @@ -193,7 +193,7 @@ func emitRequiredWhen(b *strings.Builder, byName map[string]node, request ir.Con } fmt.Fprintf(b, "\t\tif %s {\n", nullCheck(target)) writeError(b, "\t\t\t", required, "Missing required attribute", - fmt.Sprintf("%s must be set when %s is %q.", required, request.Property, request.Equals)) + fmt.Sprintf("%s must be set when %s is %q.", required, request.Property, request.WhenPropertyEquals)) b.WriteString("\t\t}\n") } b.WriteString("\t}\n") @@ -206,15 +206,15 @@ func emitValidWhen(b *strings.Builder, byName map[string]node, v ir.ConditionalV if err := stringGate(byName, v.Property, "conditional validity"); err != nil { return err } - fmt.Fprintf(b, "\tif data.%s.ValueString() != %s {\n", ir.GoName(v.Property), strconv.Quote(v.Equals)) - for _, name := range v.Valid { + fmt.Fprintf(b, "\tif data.%s.ValueString() != %s {\n", ir.GoName(v.Property), strconv.Quote(v.WhenPropertyEquals)) + for _, name := range v.AttributesValidWhenEqual { target, ok := byName[name] if !ok { return fmt.Errorf("conditional validity allows %q, which is not an attribute", name) } fmt.Fprintf(b, "\t\tif %s {\n", notNull(target)) writeError(b, "\t\t\t", name, "Invalid attribute combination", - fmt.Sprintf("%s is valid only when %s is %q.", name, v.Property, v.Equals)) + fmt.Sprintf("%s is valid only when %s is %q.", name, v.Property, v.WhenPropertyEquals)) b.WriteString("\t\t}\n") } b.WriteString("\t}\n") @@ -233,7 +233,7 @@ func emitValidConfiguration(b *strings.Builder, byName map[string]node, vc ir.Va allowed := map[string][]string{} var fields []string for _, variant := range vc.Variants { - for _, f := range variant.Valid { + for _, f := range variant.AttributesValidWhenEqual { if _, seen := allowed[f]; !seen { fields = append(fields, f) } @@ -269,7 +269,7 @@ func stringGate(byName map[string]node, name, label string) error { if !ok { return unrenderable(CauseUnvalidatableAttribute, "%s names %q, which is not an attribute", label, name) } - if gate.attribute.Kind != ir.TypeString || gate.attribute.Nested != nil { + if gate.attribute.Type != ir.TypeString || gate.attribute.NestedAttributes != nil { return unrenderable(CauseUnvalidatableAttribute, "%s on %q needs a string attribute", label, name) } return nil diff --git a/internal/emit/render_validators_test.go b/internal/emit/render_validators_test.go index 2fd9c4d..341dbc4 100644 --- a/internal/emit/render_validators_test.go +++ b/internal/emit/render_validators_test.go @@ -14,12 +14,12 @@ import ( // anyway declares an index nothing reads, which does not compile. func TestUnit_ConstructNested_SkipsAnUnwritableBlock(t *testing.T) { readOnlyChild := node{ - attribute: ir.Attribute{Name: "label", WireName: "label", Kind: ir.TypeString}, + attribute: ir.Attribute{Name: "label", WireName: "label", Type: ir.TypeString}, fb: &sdkbind.FieldBinding{Attr: "label", Wire: "label", Access: sdkbind.FieldAccess{Get: "GetLabel"}}, } for _, kind := range []ir.AttributeType{ir.TypeList, ir.TypeObject} { n := node{ - attribute: ir.Attribute{Name: "roles", WireName: "roles", Kind: kind, Nested: &ir.AttributeTree{}}, + attribute: ir.Attribute{Name: "roles", WireName: "roles", Type: kind, NestedAttributes: &ir.AttributeTree{}}, fb: &sdkbind.FieldBinding{Attr: "roles", Wire: "roles", NestedWriteModel: "models.Role", NestedConstructor: "models.NewRole()", Access: sdkbind.FieldAccess{Get: "GetRoles", Set: "SetRoles", SDKType: "[]models.Roleable"}}, @@ -40,12 +40,12 @@ func TestUnit_ConstructNested_SkipsAnUnwritableBlock(t *testing.T) { // type the getter answers. func TestUnit_ConstructNested_BuildsTheWriteType(t *testing.T) { child := node{ - attribute: ir.Attribute{Name: "name", WireName: "name", Kind: ir.TypeString}, + attribute: ir.Attribute{Name: "name", WireName: "name", Type: ir.TypeString}, fb: &sdkbind.FieldBinding{Attr: "name", Wire: "name", Access: sdkbind.FieldAccess{Get: "GetName", Set: "SetName", ConvertSet: "ToPtrString"}}, } n := node{ - attribute: ir.Attribute{Name: "services", WireName: "services", Kind: ir.TypeList, Nested: &ir.AttributeTree{}}, + attribute: ir.Attribute{Name: "services", WireName: "services", Type: ir.TypeList, NestedAttributes: &ir.AttributeTree{}}, fb: &sdkbind.FieldBinding{Attr: "services", Wire: "services", NestedWriteModel: "models.ServiceCreate", NestedConstructor: "models.NewServiceCreate()", Access: sdkbind.FieldAccess{Get: "GetServices", Set: "SetServices", diff --git a/internal/emit/schema_type.go b/internal/emit/schema_type.go index 531c2b9..8895b41 100644 --- a/internal/emit/schema_type.go +++ b/internal/emit/schema_type.go @@ -141,21 +141,21 @@ func frameworkElementType(kind ir.AttributeType) string { // element kind says. func schemaTypeOf(n node) schemaType { switch { - case n.attribute.Nested != nil && n.attribute.Kind == ir.TypeList: + case n.attribute.NestedAttributes != nil && n.attribute.Type == ir.TypeList: return newSchemaType("ListNestedAttribute", "", "List") - case n.attribute.Nested != nil && n.attribute.Kind == ir.TypeMap: + case n.attribute.NestedAttributes != nil && n.attribute.Type == ir.TypeMap: return newSchemaType("MapNestedAttribute", "", "Map") - case n.attribute.Nested != nil: + case n.attribute.NestedAttributes != nil: return newSchemaType("SingleNestedAttribute", "", "Object") - case n.attribute.Kind == ir.TypeList: + case n.attribute.Type == ir.TypeList: resolved := newSchemaType("ListAttribute", "types.List", "List") resolved.ElementType = frameworkElementType(n.attribute.ElementType) return resolved - case n.attribute.Kind == ir.TypeMap: + case n.attribute.Type == ir.TypeMap: resolved := newSchemaType("MapAttribute", "types.Map", "Map") resolved.ElementType = frameworkElementType(n.attribute.ElementType) return resolved default: - return scalarSchemaType(n.attribute.Kind) + return scalarSchemaType(n.attribute.Type) } } diff --git a/internal/emit/schema_type_test.go b/internal/emit/schema_type_test.go index ae48755..cc5f29d 100644 --- a/internal/emit/schema_type_test.go +++ b/internal/emit/schema_type_test.go @@ -9,15 +9,15 @@ import ( // listNode and scalarNode build the two shapes schemaTypeOf branches on // without dragging a whole entity in. func scalarNode(kind ir.AttributeType) node { - return node{attribute: ir.Attribute{Name: "x", WireName: "x", Kind: kind}} + return node{attribute: ir.Attribute{Name: "x", WireName: "x", Type: kind}} } func listNode(element ir.AttributeType) node { - return node{attribute: ir.Attribute{Name: "x", WireName: "x", Kind: ir.TypeList, ElementType: element}} + return node{attribute: ir.Attribute{Name: "x", WireName: "x", Type: ir.TypeList, ElementType: element}} } func nestedNode(kind ir.AttributeType) node { - return node{attribute: ir.Attribute{Name: "x", WireName: "x", Kind: kind, Nested: &ir.AttributeTree{}}} + return node{attribute: ir.Attribute{Name: "x", WireName: "x", Type: kind, NestedAttributes: &ir.AttributeTree{}}} } // TestUnit_SchemaTypeOf_EverySpelling pins the whole table. Generated code @@ -127,7 +127,7 @@ func TestUnit_SchemaType_ImportPathsAreWellFormed(t *testing.T) { // MapAttribute with its element type, and picks up the map plan-modifier // and validator packages from the same base word as every other type. func TestUnit_SchemaTypeOf_Map(t *testing.T) { - n := node{attribute: ir.Attribute{Name: "x", WireName: "x", Kind: ir.TypeMap, ElementType: ir.TypeString}} + n := node{attribute: ir.Attribute{Name: "x", WireName: "x", Type: ir.TypeMap, ElementType: ir.TypeString}} resolved := schemaTypeOf(n) for _, check := range []struct{ got, want, what string }{ diff --git a/internal/emit/services.go b/internal/emit/services.go index 7d263d3..43da19f 100644 --- a/internal/emit/services.go +++ b/internal/emit/services.go @@ -276,12 +276,12 @@ const ( // does not say reads as an entity that became nothing at all. func excludedEntity(kind string, names ir.Names, cause ir.Cause, reason string) ir.UnsupportedEntity { return ir.UnsupportedEntity{ - Key: names.Key, - Kind: kind, - Service: names.Service, - Tag: names.Tag, - Cause: cause, - Reason: reason, + Key: names.Key, + TerraformBlockType: kind, + Service: names.Service, + Tag: names.Tag, + Cause: cause, + Reason: reason, } } @@ -470,7 +470,7 @@ func joinAttributes(tree *ir.AttributeTree, fbs []sdkbind.FieldBinding, addressi for _, a := range tree.Attributes { fb, ok := byAttr[a.Name] if !ok { - if !root || !addressing[a.Name] || a.Nested != nil { + if !root || !addressing[a.Name] || a.NestedAttributes != nil { continue } out = append(out, node{attribute: a}) @@ -478,8 +478,8 @@ func joinAttributes(tree *ir.AttributeTree, fbs []sdkbind.FieldBinding, addressi continue } n := node{attribute: a, fb: fb, held: held || fb.KeptFromPlan} - if a.Nested != nil { - n.children = joinAttributes(a.Nested, fb.Nested, addressing, false, n.held, kept) + if a.NestedAttributes != nil { + n.children = joinAttributes(a.NestedAttributes, fb.Nested, addressing, false, n.held, kept) } out = append(out, n) } @@ -514,7 +514,7 @@ func addressingNames(tree *ir.AttributeTree, operations ...*ir.Operation) map[st continue } for _, attribute := range tree.Attributes { - if attribute.WireName == parameter.Name && attribute.Nested == nil { + if attribute.WireName == parameter.Name && attribute.NestedAttributes == nil { names[attribute.Name] = true } } @@ -556,8 +556,8 @@ func supportedTree(tree *ir.AttributeTree, nodes []node) *ir.AttributeTree { continue } copied := a - if a.Nested != nil { - copied.Nested = supportedTree(a.Nested, n.children) + if a.NestedAttributes != nil { + copied.NestedAttributes = supportedTree(a.NestedAttributes, n.children) } out.Attributes = append(out.Attributes, copied) } diff --git a/internal/emit/services_errors_test.go b/internal/emit/services_errors_test.go index bc33521..8098938 100644 --- a/internal/emit/services_errors_test.go +++ b/internal/emit/services_errors_test.go @@ -131,54 +131,54 @@ func TestUnit_RenderServices_NamesTheEntityAndAttributeAtFault(t *testing.T) { // A conditional requirement naming a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ConditionalRequirements = []ir.ConditionalRequirement{ - {Property: "ghost", Equals: "x", Required: []string{"name"}}} + m.Resources[0].Attributes.ConditionalRequirements = []ir.ConditionalRequirement{ + {Property: "ghost", WhenPropertyEquals: "x", Required: []string{"name"}}} expectRenderExclusion(t, pc, m, b, "http_server", "ghost") // A conditional requirement on a non-string attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ConditionalRequirements = []ir.ConditionalRequirement{ - {Property: "enabled", Equals: "true", Required: []string{"name"}}} + m.Resources[0].Attributes.ConditionalRequirements = []ir.ConditionalRequirement{ + {Property: "enabled", WhenPropertyEquals: "true", Required: []string{"name"}}} expectRenderExclusion(t, pc, m, b, "http_server", "enabled") // A conditional requirement requiring a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ConditionalRequirements = []ir.ConditionalRequirement{ - {Property: "kind", Equals: "advanced", Required: []string{"ghost"}}} + m.Resources[0].Attributes.ConditionalRequirements = []ir.ConditionalRequirement{ + {Property: "kind", WhenPropertyEquals: "advanced", Required: []string{"ghost"}}} expectRenderError(t, pc, m, b, "http_server", "ghost") // A valid-when gated on a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ConditionalValidities = []ir.ConditionalValidity{ - {Property: "ghost", Equals: "x", Valid: []string{"name"}}} + m.Resources[0].Attributes.ConditionalValidities = []ir.ConditionalValidity{ + {Property: "ghost", WhenPropertyEquals: "x", AttributesValidWhenEqual: []string{"name"}}} expectRenderExclusion(t, pc, m, b, "http_server", "ghost") // A valid-when allowing a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ConditionalValidities = []ir.ConditionalValidity{ - {Property: "kind", Equals: "advanced", Valid: []string{"ghost"}}} + m.Resources[0].Attributes.ConditionalValidities = []ir.ConditionalValidity{ + {Property: "kind", WhenPropertyEquals: "advanced", AttributesValidWhenEqual: []string{"ghost"}}} expectRenderError(t, pc, m, b, "http_server", "ghost") // A dependency whose subject is a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.Dependencies = []ir.Dependency{{Attribute: "ghost", Requires: []string{"name"}}} + m.Resources[0].Attributes.Dependencies = []ir.Dependency{{Attribute: "ghost", Requires: []string{"name"}}} expectRenderError(t, pc, m, b, "http_server", "ghost") // A dependency requiring a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.Dependencies = []ir.Dependency{{Attribute: "ratio", Requires: []string{"ghost"}}} + m.Resources[0].Attributes.Dependencies = []ir.Dependency{{Attribute: "ratio", Requires: []string{"ghost"}}} expectRenderError(t, pc, m, b, "http_server", "ghost") // A valid configuration on a missing discriminator. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ValidConfigurations = []ir.ValidConfiguration{ - {Discriminator: "ghost", Variants: []ir.ConfigVariant{{Value: "x", Valid: []string{"name"}}}}} + m.Resources[0].Attributes.ValidConfigurations = []ir.ValidConfiguration{ + {Discriminator: "ghost", Variants: []ir.ValidConfigurationVariant{{Value: "x", AttributesValidWhenEqual: []string{"name"}}}}} expectRenderExclusion(t, pc, m, b, "http_server", "ghost") // A valid configuration admitting a missing attribute. m, b = fictionalModel(), fictionalBindings() - m.Resources[0].Schema.ValidConfigurations = []ir.ValidConfiguration{ - {Discriminator: "kind", Variants: []ir.ConfigVariant{{Value: "basic", Valid: []string{"ghost"}}}}} + m.Resources[0].Attributes.ValidConfigurations = []ir.ValidConfiguration{ + {Discriminator: "kind", Variants: []ir.ValidConfigurationVariant{{Value: "basic", AttributesValidWhenEqual: []string{"ghost"}}}}} expectRenderError(t, pc, m, b, "http_server", "ghost") // A path parameter no attribute can feed. @@ -190,30 +190,30 @@ func TestUnit_RenderServices_NamesTheEntityAndAttributeAtFault(t *testing.T) { // Two nested objects claiming one model struct name. m, b = fictionalModel(), fictionalBindings() inner := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "inner", WireName: "inner", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "x", WireName: "x", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}}}}, + {Name: "inner", WireName: "inner", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "x", WireName: "x", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}}}}, }} - m.Resources[0].Schema.Attributes = append(m.Resources[0].Schema.Attributes, - ir.Attribute{Name: "one", WireName: "one", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, Nested: inner}, - ir.Attribute{Name: "two", WireName: "two", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, Nested: inner}, + m.Resources[0].Attributes.Attributes = append(m.Resources[0].Attributes.Attributes, + ir.Attribute{Name: "one", WireName: "one", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, NestedAttributes: inner}, + ir.Attribute{Name: "two", WireName: "two", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, NestedAttributes: inner}, ) innerBinding := func() []sdkbind.FieldBinding { - return []sdkbind.FieldBinding{{Attr: "inner", Wire: "inner", Kind: ir.TypeObject, + return []sdkbind.FieldBinding{{Attr: "inner", Wire: "inner", Type: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetInner", Set: "SetInner", SDKType: "models.Innerable"}, NestedModel: "models.Inner", NestedWriteModel: "models.Inner", NestedConstructor: "models.NewInner()", - Nested: []sdkbind.FieldBinding{{Attr: "x", Wire: "x", Kind: ir.TypeString, + Nested: []sdkbind.FieldBinding{{Attr: "x", Wire: "x", Type: ir.TypeString, Access: kiotaAccess("X", "*string", "FromPtrString", "ToPtrString", "")}}, }} } b.Resources["http_server"].Fields = append(b.Resources["http_server"].Fields, - sdkbind.FieldBinding{Attr: "one", Wire: "one", Kind: ir.TypeObject, + sdkbind.FieldBinding{Attr: "one", Wire: "one", Type: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetOne", Set: "SetOne", SDKType: "models.Oneable"}, NestedModel: "models.One", NestedWriteModel: "models.One", NestedConstructor: "models.NewOne()", Nested: innerBinding()}, - sdkbind.FieldBinding{Attr: "two", Wire: "two", Kind: ir.TypeObject, + sdkbind.FieldBinding{Attr: "two", Wire: "two", Type: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetTwo", Set: "SetTwo", SDKType: "models.Twoable"}, NestedModel: "models.Two", NestedWriteModel: "models.Two", NestedConstructor: "models.NewTwo()", Nested: innerBinding()}, @@ -222,7 +222,7 @@ func TestUnit_RenderServices_NamesTheEntityAndAttributeAtFault(t *testing.T) { // A list resource whose element carries no id. m, b = fictionalModel(), fictionalBindings() - m.ListResources[0].Schema.Attributes = m.ListResources[0].Schema.Attributes[1:] + m.ListResources[0].Attributes.Attributes = m.ListResources[0].Attributes.Attributes[1:] b.ListResources["http_server"].Fields = b.ListResources["http_server"].Fields[1:] expectRenderExclusion(t, pc, m, b, "http_server", "id") @@ -413,11 +413,11 @@ func TestUnit_JoinTree_KeepsAddressingAttributesTheSDKCannotCarry(t *testing.T) // request or response body declares it and no SDK model carries it. It // must survive the join all the same, or nothing can fill the call. tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "owner", WireName: "owner", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "owner", WireName: "owner", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, }} - bound := []sdkbind.FieldBinding{{Attr: "name", Wire: "name", Kind: ir.TypeString, + bound := []sdkbind.FieldBinding{{Attr: "name", Wire: "name", Type: ir.TypeString, Access: kiotaAccess("Name", "*string", "FromPtrString", "ToPtrString", "")}} nodes := joinTree(tree, bound, map[string]bool{"owner": true}) @@ -435,7 +435,7 @@ func TestUnit_JoinTree_KeepsAddressingAttributesTheSDKCannotCarry(t *testing.T) func TestUnit_JoinTree_DropsAnUnboundAttributeThatAddressesNothing(t *testing.T) { tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "ghost", WireName: "ghost", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "ghost", WireName: "ghost", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, }} if nodes := joinTree(tree, nil); len(nodes) != 0 { t.Fatalf("an ordinary unbound attribute must be dropped, got %d node(s)", len(nodes)) @@ -443,12 +443,12 @@ func TestUnit_JoinTree_DropsAnUnboundAttributeThatAddressesNothing(t *testing.T) } func TestUnit_AddressingNames_TakesEveryPathParameterInTerraformSpelling(t *testing.T) { - read := &ir.Operation{PathParameters: []ir.Parameter{ + read := &ir.Operation{PathParameters: []ir.URLPathParameter{ {Name: "owner", Type: ir.TypeString}, {Name: "repo", Type: ir.TypeString}, {Name: "ruleset_id", Type: ir.TypeInt64}, }} - list := &ir.Operation{PathParameters: []ir.Parameter{{Name: "org", Type: ir.TypeString}}} + list := &ir.Operation{PathParameters: []ir.URLPathParameter{{Name: "org", Type: ir.TypeString}}} names := addressingNames(nil, read, nil, list) for _, want := range []string{"owner", "repo", "ruleset_id", "org"} { @@ -461,13 +461,13 @@ func TestUnit_AddressingNames_TakesEveryPathParameterInTerraformSpelling(t *test } } -func TestUnit_ParamNode_RefusesAnObjectOfTheSameName(t *testing.T) { +func TestUnit_ParamNode_ExcludesAnObjectOfTheSameName(t *testing.T) { // A path parameter is a scalar in the URL. An attribute of the same name // that is an object is a different thing the document spells the same // way, and reading a value out of it does not compile. nodes := []node{ - {attribute: ir.Attribute{Name: "owner", WireName: "owner", Kind: ir.TypeObject, - Nested: &ir.AttributeTree{}}}, + {attribute: ir.Attribute{Name: "owner", WireName: "owner", Type: ir.TypeObject, + NestedAttributes: &ir.AttributeTree{}}}, } if _, err := parameterNode(sdkbind.CallParameter{Local: "owner", Wire: "owner", GoType: "string"}, nodes, false); err == nil { t.Fatal("an object must not answer a path parameter") @@ -478,7 +478,7 @@ func TestUnit_ParamNode_TakesTheLastParameterAsTheID(t *testing.T) { // An item path with a parent takes two parameters, and its key is the // last one — whatever the response happens to call its own id. nodes := []node{ - {attribute: ir.Attribute{Name: "id", WireName: "id", Kind: ir.TypeString}}, + {attribute: ir.Attribute{Name: "id", WireName: "id", Type: ir.TypeString}}, } got, err := parameterNode(sdkbind.CallParameter{Local: "cfg", Wire: "configuration_id", GoType: "string"}, nodes, true) if err != nil { @@ -498,7 +498,7 @@ func TestUnit_Invocable_DropsWhatAnActionCannotTake(t *testing.T) { nodes := []node{ {attribute: ir.Attribute{Name: "kept", ComputedOptionalRequired: ir.Required}}, {attribute: ir.Attribute{Name: "dropped", ComputedOptionalRequired: ir.Computed}}, - {attribute: ir.Attribute{Name: "block", ComputedOptionalRequired: ir.Optional, Nested: &ir.AttributeTree{}}, + {attribute: ir.Attribute{Name: "block", ComputedOptionalRequired: ir.Optional, NestedAttributes: &ir.AttributeTree{}}, children: []node{ {attribute: ir.Attribute{Name: "inner_kept", ComputedOptionalRequired: ir.Optional}}, {attribute: ir.Attribute{Name: "inner_dropped", ComputedOptionalRequired: ir.Computed}}, @@ -528,7 +528,7 @@ func TestUnit_ComputedOptionalRequiredLines_NeverRendersComputedInAnActionSchema } } -func TestUnit_RenderServices_RefusesALookupWhoseReadAnswersACollection(t *testing.T) { +func TestUnit_RenderServices_ExcludesALookupWhoseReadAnswersACollection(t *testing.T) { // A read that answers with a collection is not a lookup: the state // mapper reads fields off one object, and which element it should map is // not something the document says. @@ -545,8 +545,8 @@ func TestUnit_FindIdentityNode_AcceptsAnIdentityTheAPIDidNotSpellAsAString(t *te // with a numeric id. for _, kind := range []ir.AttributeType{ir.TypeString, ir.TypeInt64, ir.TypeFloat64, ir.TypeBool} { nodes := []node{{ - attribute: ir.Attribute{Name: "id", WireName: "id", Kind: kind}, - fb: &sdkbind.FieldBinding{Attr: "id", Wire: "id", Kind: kind, Access: sdkbind.FieldAccess{Get: "GetId"}}, + attribute: ir.Attribute{Name: "id", WireName: "id", Type: kind}, + fb: &sdkbind.FieldBinding{Attr: "id", Wire: "id", Type: kind, Access: sdkbind.FieldAccess{Get: "GetId"}}, }} if _, ok := findIdentityNode(nodes); !ok { t.Fatalf("a %s id must serve as the list identity", kind) @@ -555,20 +555,20 @@ func TestUnit_FindIdentityNode_AcceptsAnIdentityTheAPIDidNotSpellAsAString(t *te // An object of that name is not an identity, and neither is an unbound one. nested := []node{{ - attribute: ir.Attribute{Name: "id", Kind: ir.TypeObject, Nested: &ir.AttributeTree{}}, + attribute: ir.Attribute{Name: "id", Type: ir.TypeObject, NestedAttributes: &ir.AttributeTree{}}, fb: &sdkbind.FieldBinding{Attr: "id", Access: sdkbind.FieldAccess{Get: "GetId"}}, }} if _, ok := findIdentityNode(nested); ok { t.Fatal("an object must not serve as the list identity") } - if _, ok := findIdentityNode([]node{{attribute: ir.Attribute{Name: "id", Kind: ir.TypeString}}}); ok { + if _, ok := findIdentityNode([]node{{attribute: ir.Attribute{Name: "id", Type: ir.TypeString}}}); ok { t.Fatal("an unbound attribute cannot be read for the identity") } } func TestUnit_ReadStringLocal_RendersANonStringIdentityThroughFmt(t *testing.T) { numeric := node{ - attribute: ir.Attribute{Name: "id", Kind: ir.TypeInt64}, + attribute: ir.Attribute{Name: "id", Type: ir.TypeInt64}, fb: &sdkbind.FieldBinding{Access: sdkbind.FieldAccess{Get: "GetId", SDKType: "*int64"}}, } got := readStringLocal("id", numeric) @@ -577,7 +577,7 @@ func TestUnit_ReadStringLocal_RendersANonStringIdentityThroughFmt(t *testing.T) } text := node{ - attribute: ir.Attribute{Name: "id", Kind: ir.TypeString}, + attribute: ir.Attribute{Name: "id", Type: ir.TypeString}, fb: &sdkbind.FieldBinding{Access: sdkbind.FieldAccess{Get: "GetId", SDKType: "*string"}}, } if got := readStringLocal("id", text); strings.Contains(got, "fmt.Sprintf") { @@ -630,7 +630,7 @@ func TestUnit_RenderServices_EmitsASingletonWithoutCreateOrDelete(t *testing.T) } } -func TestUnit_RenderServices_RefusesASingletonWithoutAnUpdate(t *testing.T) { +func TestUnit_RenderServices_ExcludesASingletonWithoutAnUpdate(t *testing.T) { pc := fictionalProviderCore() m, b := fictionalModel(), fictionalBindings() diff --git a/internal/emit/services_fixture_test.go b/internal/emit/services_fixture_test.go index 7aa7ae6..4609c25 100644 --- a/internal/emit/services_fixture_test.go +++ b/internal/emit/services_fixture_test.go @@ -31,36 +31,36 @@ func names(key, pascal, service string) ir.Names { func fictionalModel() *ir.Model { httpServerSchema := &ir.AttributeTree{ Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "enabled", WireName: "enabled", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, - {Name: "port", WireName: "port", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, RequiresReplace: true}, - {Name: "ratio", WireName: "ratio", Kind: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, - {Name: "kind", WireName: "kind", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "enabled", WireName: "enabled", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "port", WireName: "port", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, RequiresReplace: true}, + {Name: "ratio", WireName: "ratio", Type: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, + {Name: "kind", WireName: "kind", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, OneOf: []string{"basic", "advanced"}}, - {Name: "tags", WireName: "tags", Kind: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "protocols", WireName: "protocols", Kind: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional, + {Name: "tags", WireName: "tags", Type: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "protocols", WireName: "protocols", Type: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional, OneOf: []string{"http", "https"}}, - {Name: "seed", WireName: "seed", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, SilentlyIgnoredOnUpdate: true}, - {Name: "settings", WireName: "settings", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "retries", WireName: "retries", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, - {Name: "trace", WireName: "trace", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "seed", WireName: "seed", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, IgnoredOnUpdate: true}, + {Name: "settings", WireName: "settings", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "retries", WireName: "retries", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, + {Name: "trace", WireName: "trace", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, }}}, - {Name: "rules", WireName: "rules", Kind: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "pattern", WireName: "pattern", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "rules", WireName: "rules", Type: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "pattern", WireName: "pattern", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }}}, {Name: "blob", WireName: "blob", ComputedOptionalRequired: ir.Optional, Unsupported: true, UnsupportedReason: "free-form object declares no properties"}, - {Name: "description", WireName: "description", Kind: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional}, - {Name: "owner_id", WireName: "ownerId", Kind: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional}, + {Name: "description", WireName: "description", Type: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional}, + {Name: "owner_id", WireName: "ownerId", Type: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional}, }, ConditionalRequirements: []ir.ConditionalRequirement{ - {Property: "kind", Equals: "advanced", Required: []string{"settings"}}, + {Property: "kind", WhenPropertyEquals: "advanced", Required: []string{"settings"}}, }, ConditionalValidities: []ir.ConditionalValidity{ - {Property: "kind", Equals: "advanced", Valid: []string{"rules"}}, + {Property: "kind", WhenPropertyEquals: "advanced", AttributesValidWhenEqual: []string{"rules"}}, }, Dependencies: []ir.Dependency{ {Attribute: "ratio", Requires: []string{"port"}}, @@ -68,17 +68,17 @@ func fictionalModel() *ir.Model { MutuallyExclusiveGroups: [][]string{{"protocols", "tags"}}, ValidConfigurations: []ir.ValidConfiguration{{ Discriminator: "kind", - Variants: []ir.ConfigVariant{ - {Value: "advanced", Valid: []string{"settings"}}, - {Value: "basic", Valid: []string{"port"}}, + Variants: []ir.ValidConfigurationVariant{ + {Value: "advanced", AttributesValidWhenEqual: []string{"settings"}}, + {Value: "basic", AttributesValidWhenEqual: []string{"port"}}, }, }}, } itemTree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "enabled", WireName: "enabled", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Computed}, + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "enabled", WireName: "enabled", Type: ir.TypeBool, ComputedOptionalRequired: ir.Computed}, }} return &ir.Model{ @@ -89,16 +89,16 @@ func fictionalModel() *ir.Model { Operations: ir.Operations{ Create: &ir.Operation{Kind: ir.OperationCreate, Method: "POST", PathTemplate: "/v7/http-servers", SuccessCode: 201}, Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/http-servers/{httpServerId}", - PathParameters: []ir.Parameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 200}, Update: &ir.Operation{Kind: ir.OperationUpdate, Method: "PATCH", PathTemplate: "/v7/http-servers/{httpServerId}", - PathParameters: []ir.Parameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 200}, Delete: &ir.Operation{Kind: ir.OperationDelete, Method: "DELETE", PathTemplate: "/v7/http-servers/{httpServerId}", - PathParameters: []ir.Parameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 204}, + PathParameters: []ir.URLPathParameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 204}, List: &ir.Operation{Kind: ir.OperationList, Method: "GET", PathTemplate: "/v7/http-servers", SuccessCode: 200}, }, - Schema: httpServerSchema, + Attributes: httpServerSchema, UpdateStyle: ir.UpdateStylePatchMerge, - EventualConsistency: 30 * time.Second, + ReadAfterWriteDelay: 30 * time.Second, DeleteNotFoundOK: true, ListWrapperKey: "value", Timeouts: ir.Timeouts{ @@ -112,14 +112,14 @@ func fictionalModel() *ir.Model { Operations: ir.Operations{ Create: &ir.Operation{Kind: ir.OperationCreate, Method: "POST", PathTemplate: "/v7/alert-rules", SuccessCode: 201}, Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/alert-rules/{alertRuleId}", - PathParameters: []ir.Parameter{{Name: "alertRuleId", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "alertRuleId", Type: ir.TypeString}}, SuccessCode: 200}, Delete: &ir.Operation{Kind: ir.OperationDelete, Method: "DELETE", PathTemplate: "/v7/alert-rules/{alertRuleId}", - PathParameters: []ir.Parameter{{Name: "alertRuleId", Type: ir.TypeString}}, SuccessCode: 204}, + PathParameters: []ir.URLPathParameter{{Name: "alertRuleId", Type: ir.TypeString}}, SuccessCode: 204}, }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required, RequiresReplace: true}, - {Name: "severity", WireName: "severity", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, RequiresReplace: true}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required, RequiresReplace: true}, + {Name: "severity", WireName: "severity", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, RequiresReplace: true}, }}, MissingUpdate: true, Timeouts: ir.Timeouts{ @@ -133,15 +133,15 @@ func fictionalModel() *ir.Model { Names: names("http_server", "HTTPServer", "servers"), Operations: ir.Operations{ Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/http-servers/{httpServerId}", - PathParameters: []ir.Parameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 200}, List: &ir.Operation{Kind: ir.OperationList, Method: "GET", PathTemplate: "/v7/http-servers", SuccessCode: 200}, }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, Filter: true}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, Filter: true}, - {Name: "enabled", WireName: "enabled", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional, Filter: true}, - {Name: "items", WireName: "items", Kind: ir.TypeList, ElementType: ir.TypeObject, - ComputedOptionalRequired: ir.Computed, Nested: itemTree}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, IsDatasourceFilterArgument: true}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, IsDatasourceFilterArgument: true}, + {Name: "enabled", WireName: "enabled", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional, IsDatasourceFilterArgument: true}, + {Name: "items", WireName: "items", Type: ir.TypeList, ElementType: ir.TypeObject, + ComputedOptionalRequired: ir.Computed, NestedAttributes: itemTree}, }}, ListWrapperKey: "http_servers", }, @@ -149,12 +149,12 @@ func fictionalModel() *ir.Model { Names: names("license", "License", "licenses"), Operations: ir.Operations{ Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/v7/licenses/{licenseKey}", - PathParameters: []ir.Parameter{{Name: "licenseKey", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "licenseKey", Type: ir.TypeString}}, SuccessCode: 200}, }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "license_key", WireName: "licenseKey", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "seats", WireName: "seats", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Computed}, - {Name: "expires", WireName: "expires", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "license_key", WireName: "licenseKey", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "seats", WireName: "seats", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Computed}, + {Name: "expires", WireName: "expires", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, }}, LookupByKey: true, KeyParameter: "licenseKey", @@ -164,9 +164,9 @@ func fictionalModel() *ir.Model { { Names: names("http_server", "HTTPServer", "servers"), ListOperation: ir.Operation{Kind: ir.OperationList, Method: "GET", PathTemplate: "/v7/http-servers", SuccessCode: 200}, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, }}, ListWrapperKey: "value", }, @@ -174,11 +174,11 @@ func fictionalModel() *ir.Model { Actions: []ir.Action{ { Names: names("http_server_restart", "HTTPServerRestart", "servers"), - InvokeOperation: ir.Operation{Kind: ir.OperationInvoke, Method: "POST", + InvokeOperation: ir.Operation{Kind: ir.OperationAction, Method: "POST", PathTemplate: "/v7/http-servers/{httpServerId}/restart", - PathParameters: []ir.Parameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 204}, - RequestSchema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "mode", WireName: "mode", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + PathParameters: []ir.URLPathParameter{{Name: "httpServerId", Type: ir.TypeString}}, SuccessCode: 204}, + RequestAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "mode", WireName: "mode", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }}, ParentEntity: "http_server", }, @@ -202,49 +202,49 @@ func readOnly(a sdkbind.FieldAccess) sdkbind.FieldAccess { func httpServerFields() []sdkbind.FieldBinding { return []sdkbind.FieldBinding{ - {Attr: "id", Wire: "id", Kind: ir.TypeString, + {Attr: "id", Wire: "id", Type: ir.TypeString, Access: readOnly(kiotaAccess("Id", "*string", "FromPtrString", "ToPtrString", ""))}, - {Attr: "name", Wire: "name", Kind: ir.TypeString, + {Attr: "name", Wire: "name", Type: ir.TypeString, Access: kiotaAccess("Name", "*string", "FromPtrString", "ToPtrString", "")}, - {Attr: "enabled", Wire: "enabled", Kind: ir.TypeBool, + {Attr: "enabled", Wire: "enabled", Type: ir.TypeBool, Access: kiotaAccess("Enabled", "*bool", "FromPtrBool", "ToPtrBool", "")}, - {Attr: "port", Wire: "port", Kind: ir.TypeInt64, + {Attr: "port", Wire: "port", Type: ir.TypeInt64, Access: kiotaAccess("Port", "*int32", "FromPtrInt32", "ToPtrInt32", "")}, - {Attr: "ratio", Wire: "ratio", Kind: ir.TypeFloat64, + {Attr: "ratio", Wire: "ratio", Type: ir.TypeFloat64, Access: kiotaAccess("Ratio", "*float64", "FromPtrFloat64", "ToPtrFloat64", "")}, - {Attr: "kind", Wire: "kind", Kind: ir.TypeString, + {Attr: "kind", Wire: "kind", Type: ir.TypeString, Access: kiotaAccess("Kind", "*models.HttpServerKind", "FromPtrEnum", "ToPtrEnum", "models.ParseHttpServerKind")}, - {Attr: "tags", Wire: "tags", Kind: ir.TypeList, ElementType: ir.TypeString, + {Attr: "tags", Wire: "tags", Type: ir.TypeList, ElementType: ir.TypeString, Access: kiotaAccess("Tags", "[]string", "FromStringSlice", "ToStringSlice", "")}, - {Attr: "protocols", Wire: "protocols", Kind: ir.TypeList, ElementType: ir.TypeString, + {Attr: "protocols", Wire: "protocols", Type: ir.TypeList, ElementType: ir.TypeString, Access: kiotaAccess("Protocols", "[]models.HttpServerProtocol", "FromEnumSlice", "ToEnumSlice", "models.ParseHttpServerProtocol")}, - {Attr: "seed", Wire: "seed", Kind: ir.TypeString, + {Attr: "seed", Wire: "seed", Type: ir.TypeString, Access: kiotaAccess("Seed", "*string", "FromPtrString", "ToPtrString", "")}, - {Attr: "settings", Wire: "settings", Kind: ir.TypeObject, + {Attr: "settings", Wire: "settings", Type: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetSettings", Set: "SetSettings", SDKType: "models.HttpServerSettingsable"}, NestedModel: "models.HttpServerSettings", NestedWriteModel: "models.HttpServerSettings", NestedConstructor: "models.NewHttpServerSettings()", Nested: []sdkbind.FieldBinding{ - {Attr: "retries", Wire: "retries", Kind: ir.TypeInt64, + {Attr: "retries", Wire: "retries", Type: ir.TypeInt64, Access: kiotaAccess("Retries", "*int64", "FromPtrInt64", "ToPtrInt64", "")}, - {Attr: "trace", Wire: "trace", Kind: ir.TypeBool, + {Attr: "trace", Wire: "trace", Type: ir.TypeBool, Access: kiotaAccess("Trace", "*bool", "FromPtrBool", "ToPtrBool", "")}, }}, - {Attr: "rules", Wire: "rules", Kind: ir.TypeList, ElementType: ir.TypeObject, + {Attr: "rules", Wire: "rules", Type: ir.TypeList, ElementType: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetRules", Set: "SetRules", SDKType: "[]models.HttpServerRuleable"}, NestedModel: "models.HttpServerRule", NestedWriteModel: "models.HttpServerRule", NestedConstructor: "models.NewHttpServerRule()", Nested: []sdkbind.FieldBinding{ - {Attr: "pattern", Wire: "pattern", Kind: ir.TypeString, + {Attr: "pattern", Wire: "pattern", Type: ir.TypeString, Access: kiotaAccess("Pattern", "*string", "FromPtrString", "ToPtrString", "")}, }}, - {Attr: "description", Wire: "description", Kind: ir.TypeString, + {Attr: "description", Wire: "description", Type: ir.TypeString, Access: kiotaAccess("Description", "*string", "FromPtrString", "ToPtrString", "")}, // Carried by the request and answered by no response: state keeps // the planned value. - {Attr: "owner_id", Wire: "ownerId", Kind: ir.TypeString, KeptFromPlan: true, + {Attr: "owner_id", Wire: "ownerId", Type: ir.TypeString, KeptFromPlan: true, Access: sdkbind.FieldAccess{Set: "SetOwnerId", SDKType: "*string", ConvertSet: "ToPtrString"}}, } } @@ -299,11 +299,11 @@ func fictionalBindings() *sdkbind.Bindings { WriteModel: "models.AlertRule", WriteConstructor: "models.NewAlertRule()", Fields: []sdkbind.FieldBinding{ - {Attr: "id", Wire: "id", Kind: ir.TypeString, + {Attr: "id", Wire: "id", Type: ir.TypeString, Access: readOnly(kiotaAccess("Id", "*string", "FromPtrString", "ToPtrString", ""))}, - {Attr: "name", Wire: "name", Kind: ir.TypeString, + {Attr: "name", Wire: "name", Type: ir.TypeString, Access: kiotaAccess("Name", "*string", "FromPtrString", "ToPtrString", "")}, - {Attr: "severity", Wire: "severity", Kind: ir.TypeInt64, + {Attr: "severity", Wire: "severity", Type: ir.TypeInt64, Access: kiotaAccess("Severity", "*int64", "FromPtrInt64", "ToPtrInt64", "")}, }, }, @@ -318,11 +318,11 @@ func fictionalBindings() *sdkbind.Bindings { CollectionAccess: "GetHttpServers()", ListWrapperKey: "http_servers", Fields: []sdkbind.FieldBinding{ - {Attr: "id", Wire: "id", Kind: ir.TypeString, + {Attr: "id", Wire: "id", Type: ir.TypeString, Access: readOnly(kiotaAccess("Id", "*string", "FromPtrString", "", ""))}, - {Attr: "name", Wire: "name", Kind: ir.TypeString, + {Attr: "name", Wire: "name", Type: ir.TypeString, Access: readOnly(kiotaAccess("Name", "*string", "FromPtrString", "", ""))}, - {Attr: "enabled", Wire: "enabled", Kind: ir.TypeBool, + {Attr: "enabled", Wire: "enabled", Type: ir.TypeBool, Access: readOnly(kiotaAccess("Enabled", "*bool", "FromPtrBool", "", ""))}, }, }, @@ -333,9 +333,9 @@ func fictionalBindings() *sdkbind.Bindings { "models.Licenseable", "models.Licenseable", "error"), ReadModel: "models.Licenseable", Fields: []sdkbind.FieldBinding{ - {Attr: "seats", Wire: "seats", Kind: ir.TypeInt64, + {Attr: "seats", Wire: "seats", Type: ir.TypeInt64, Access: readOnly(kiotaAccess("Seats", "*int64", "FromPtrInt64", "", ""))}, - {Attr: "expires", Wire: "expires", Kind: ir.TypeString, + {Attr: "expires", Wire: "expires", Type: ir.TypeString, Access: readOnly(kiotaAccess("Expires", "*time.Time", "FromPtrTime", "", ""))}, }, }, @@ -347,9 +347,9 @@ func fictionalBindings() *sdkbind.Bindings { ElementType: "models.AuditEventable", CollectionAccess: "GetValue()", Fields: []sdkbind.FieldBinding{ - {Attr: "id", Wire: "id", Kind: ir.TypeString, + {Attr: "id", Wire: "id", Type: ir.TypeString, Access: readOnly(kiotaAccess("Id", "*string", "FromPtrString", "", ""))}, - {Attr: "name", Wire: "name", Kind: ir.TypeString, + {Attr: "name", Wire: "name", Type: ir.TypeString, Access: readOnly(kiotaAccess("Name", "*string", "FromPtrString", "", ""))}, }, }, @@ -362,7 +362,7 @@ func fictionalBindings() *sdkbind.Bindings { WriteModel: "models.HttpServerRestartRequest", WriteConstructor: "models.NewHttpServerRestartRequest()", Fields: []sdkbind.FieldBinding{ - {Attr: "mode", Wire: "mode", Kind: ir.TypeString, + {Attr: "mode", Wire: "mode", Type: ir.TypeString, Access: sdkbind.FieldAccess{Set: "SetMode", SDKType: "*string", ConvertSet: "ToPtrString"}}, }, }, diff --git a/internal/emit/services_openapigenerator_test.go b/internal/emit/services_openapigenerator_test.go index a6296a5..cde7e52 100644 --- a/internal/emit/services_openapigenerator_test.go +++ b/internal/emit/services_openapigenerator_test.go @@ -23,10 +23,10 @@ func openAPIGeneratorAccess(base, sdkType, convGet, convSet, parse string) sdkbi func openAPIGeneratorModel() *ir.Model { opt := func(name string, kind ir.AttributeType) ir.Attribute { - return ir.Attribute{Name: name, WireName: name, Kind: kind, ComputedOptionalRequired: ir.Optional} + return ir.Attribute{Name: name, WireName: name, Type: kind, ComputedOptionalRequired: ir.Optional} } list := func(name string, element ir.AttributeType) ir.Attribute { - return ir.Attribute{Name: name, WireName: name, Kind: ir.TypeList, ElementType: element, ComputedOptionalRequired: ir.Optional} + return ir.Attribute{Name: name, WireName: name, Type: ir.TypeList, ElementType: element, ComputedOptionalRequired: ir.Optional} } return &ir.Model{ @@ -36,15 +36,15 @@ func openAPIGeneratorModel() *ir.Model { Operations: ir.Operations{ Create: &ir.Operation{Kind: ir.OperationCreate, Method: "POST", PathTemplate: "/tags", SuccessCode: 201}, Read: &ir.Operation{Kind: ir.OperationRead, Method: "GET", PathTemplate: "/tags/{tagId}", - PathParameters: []ir.Parameter{{Name: "tagId", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "tagId", Type: ir.TypeString}}, SuccessCode: 200}, Update: &ir.Operation{Kind: ir.OperationUpdate, Method: "PUT", PathTemplate: "/tags/{tagId}", - PathParameters: []ir.Parameter{{Name: "tagId", Type: ir.TypeString}}, SuccessCode: 200}, + PathParameters: []ir.URLPathParameter{{Name: "tagId", Type: ir.TypeString}}, SuccessCode: 200}, Delete: &ir.Operation{Kind: ir.OperationDelete, Method: "DELETE", PathTemplate: "/tags/{tagId}", - PathParameters: []ir.Parameter{{Name: "tagId", Type: ir.TypeString}}, SuccessCode: 204}, + PathParameters: []ir.URLPathParameter{{Name: "tagId", Type: ir.TypeString}}, SuccessCode: 204}, }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, opt("enabled", ir.TypeBool), opt("count", ir.TypeInt64), opt("big", ir.TypeInt64), @@ -58,10 +58,10 @@ func openAPIGeneratorModel() *ir.Model { list("ratios", ir.TypeFloat64), list("sizes", ir.TypeInt64), list("modes", ir.TypeString), - {Name: "meta", WireName: "meta", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{opt("note", ir.TypeString)}}}, - {Name: "rules", WireName: "rules", Kind: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{opt("pattern", ir.TypeString)}}}, + {Name: "meta", WireName: "meta", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{opt("note", ir.TypeString)}}}, + {Name: "rules", WireName: "rules", Type: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{opt("pattern", ir.TypeString)}}}, }}, UpdateStyle: ir.UpdateStylePutFull, Timeouts: ir.Timeouts{ @@ -74,13 +74,13 @@ func openAPIGeneratorModel() *ir.Model { Operations: ir.Operations{ List: &ir.Operation{Kind: ir.OperationList, Method: "GET", PathTemplate: "/tags", SuccessCode: 200}, }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, Filter: true}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, Filter: true}, - {Name: "items", WireName: "items", Kind: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Computed, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, IsDatasourceFilterArgument: true}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, IsDatasourceFilterArgument: true}, + {Name: "items", WireName: "items", Type: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Computed, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, }}}, }}, // A wrapped list whose openapi-generator envelope is a struct @@ -89,16 +89,16 @@ func openAPIGeneratorModel() *ir.Model { }}, Actions: []ir.Action{{ Names: names("tag_rotate", "TagRotate", "tags"), - InvokeOperation: ir.Operation{Kind: ir.OperationInvoke, Method: "POST", PathTemplate: "/tags/{tagId}/rotate", - PathParameters: []ir.Parameter{{Name: "tagId", Type: ir.TypeInt64}}, SuccessCode: 200}, - RequestSchema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "force", WireName: "force", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Required}, + InvokeOperation: ir.Operation{Kind: ir.OperationAction, Method: "POST", PathTemplate: "/tags/{tagId}/rotate", + PathParameters: []ir.URLPathParameter{{Name: "tagId", Type: ir.TypeInt64}}, SuccessCode: 200}, + RequestAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "force", WireName: "force", Type: ir.TypeBool, ComputedOptionalRequired: ir.Required}, opt("factor", ir.TypeFloat64), list("labels", ir.TypeString), - {Name: "window", WireName: "window", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{opt("hours", ir.TypeInt64)}}}, - {Name: "steps", WireName: "steps", Kind: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{opt("order", ir.TypeInt64)}}}, + {Name: "window", WireName: "window", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{opt("hours", ir.TypeInt64)}}}, + {Name: "steps", WireName: "steps", Type: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{opt("order", ir.TypeInt64)}}}, }}, }}, } @@ -129,52 +129,52 @@ func openAPIGeneratorBindings() *sdkbind.Bindings { WriteModel: "sdk.Tag", WriteConstructor: "sdk.NewTagWithDefaults()", Fields: []sdkbind.FieldBinding{ - {Attr: "id", Wire: "id", Kind: ir.TypeString, + {Attr: "id", Wire: "id", Type: ir.TypeString, Access: readOnly(openAPIGeneratorAccess("Id", "string", "FromString", "ToString", ""))}, - {Attr: "name", Wire: "name", Kind: ir.TypeString, + {Attr: "name", Wire: "name", Type: ir.TypeString, Access: openAPIGeneratorAccess("Name", "string", "FromString", "ToString", "")}, - {Attr: "enabled", Wire: "enabled", Kind: ir.TypeBool, + {Attr: "enabled", Wire: "enabled", Type: ir.TypeBool, Access: openAPIGeneratorAccess("Enabled", "bool", "FromBool", "ToBool", "")}, - {Attr: "count", Wire: "count", Kind: ir.TypeInt64, + {Attr: "count", Wire: "count", Type: ir.TypeInt64, Access: openAPIGeneratorAccess("Count", "int32", "FromInt32", "ToInt32", "")}, - {Attr: "big", Wire: "big", Kind: ir.TypeInt64, + {Attr: "big", Wire: "big", Type: ir.TypeInt64, Access: openAPIGeneratorAccess("Big", "int64", "FromInt64", "ToInt64", "")}, - {Attr: "score", Wire: "score", Kind: ir.TypeFloat64, + {Attr: "score", Wire: "score", Type: ir.TypeFloat64, Access: openAPIGeneratorAccess("Score", "float32", "FromFloat32", "ToFloat32", "")}, - {Attr: "weight", Wire: "weight", Kind: ir.TypeFloat64, + {Attr: "weight", Wire: "weight", Type: ir.TypeFloat64, Access: openAPIGeneratorAccess("Weight", "float64", "FromFloat64", "ToFloat64", "")}, - {Attr: "status", Wire: "status", Kind: ir.TypeString, + {Attr: "status", Wire: "status", Type: ir.TypeString, Access: openAPIGeneratorAccess("Status", "sdk.TagStatus", "FromEnum", "ToEnum", "sdk.NewTagStatusFromValue")}, - {Attr: "phase", Wire: "phase", Kind: ir.TypeString, + {Attr: "phase", Wire: "phase", Type: ir.TypeString, Access: openAPIGeneratorAccess("Phase", "*sdk.TagPhase", "FromPtrEnum", "ToPtrEnum", "sdk.NewTagPhaseFromValue")}, - {Attr: "when", Wire: "when", Kind: ir.TypeString, + {Attr: "when", Wire: "when", Type: ir.TypeString, Access: openAPIGeneratorAccess("When", "time.Time", "FromTime", "ToTime", "")}, - {Attr: "levels", Wire: "levels", Kind: ir.TypeList, ElementType: ir.TypeInt64, + {Attr: "levels", Wire: "levels", Type: ir.TypeList, ElementType: ir.TypeInt64, Access: openAPIGeneratorAccess("Levels", "[]int64", "FromInt64Slice", "ToInt64Slice", "")}, - {Attr: "flags", Wire: "flags", Kind: ir.TypeList, ElementType: ir.TypeBool, + {Attr: "flags", Wire: "flags", Type: ir.TypeList, ElementType: ir.TypeBool, Access: openAPIGeneratorAccess("Flags", "[]bool", "FromBoolSlice", "ToBoolSlice", "")}, - {Attr: "ratios", Wire: "ratios", Kind: ir.TypeList, ElementType: ir.TypeFloat64, + {Attr: "ratios", Wire: "ratios", Type: ir.TypeList, ElementType: ir.TypeFloat64, Access: openAPIGeneratorAccess("Ratios", "[]float64", "FromFloat64Slice", "ToFloat64Slice", "")}, - {Attr: "sizes", Wire: "sizes", Kind: ir.TypeList, ElementType: ir.TypeInt64, + {Attr: "sizes", Wire: "sizes", Type: ir.TypeList, ElementType: ir.TypeInt64, Access: openAPIGeneratorAccess("Sizes", "[]int32", "FromInt32Slice", "ToInt32Slice", "")}, - {Attr: "modes", Wire: "modes", Kind: ir.TypeList, ElementType: ir.TypeString, + {Attr: "modes", Wire: "modes", Type: ir.TypeList, ElementType: ir.TypeString, Access: openAPIGeneratorAccess("Modes", "[]sdk.Mode", "FromEnumSlice", "ToEnumSlice", "sdk.NewModeFromValue")}, - {Attr: "meta", Wire: "meta", Kind: ir.TypeObject, + {Attr: "meta", Wire: "meta", Type: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetMeta", Set: "SetMeta", SDKType: "sdk.TagMeta"}, NestedModel: "sdk.TagMeta", NestedWriteModel: "sdk.TagMeta", NestedConstructor: "sdk.NewTagMetaWithDefaults()", Nested: []sdkbind.FieldBinding{ - {Attr: "note", Wire: "note", Kind: ir.TypeString, + {Attr: "note", Wire: "note", Type: ir.TypeString, Access: openAPIGeneratorAccess("Note", "string", "FromString", "ToString", "")}, }}, - {Attr: "rules", Wire: "rules", Kind: ir.TypeList, ElementType: ir.TypeObject, + {Attr: "rules", Wire: "rules", Type: ir.TypeList, ElementType: ir.TypeObject, Access: sdkbind.FieldAccess{Get: "GetRules", Set: "SetRules", SDKType: "[]sdk.TagRule"}, NestedModel: "sdk.TagRule", NestedWriteModel: "sdk.TagRule", NestedConstructor: "sdk.NewTagRuleWithDefaults()", Nested: []sdkbind.FieldBinding{ - {Attr: "pattern", Wire: "pattern", Kind: ir.TypeString, + {Attr: "pattern", Wire: "pattern", Type: ir.TypeString, Access: openAPIGeneratorAccess("Pattern", "string", "FromString", "ToString", "")}, }}, }, @@ -188,9 +188,9 @@ func openAPIGeneratorBindings() *sdkbind.Bindings { CollectionAccess: "Tags", ListWrapperKey: "tags", Fields: []sdkbind.FieldBinding{ - {Attr: "id", Wire: "id", Kind: ir.TypeString, + {Attr: "id", Wire: "id", Type: ir.TypeString, Access: readOnly(openAPIGeneratorAccess("Id", "string", "FromString", "", ""))}, - {Attr: "name", Wire: "name", Kind: ir.TypeString, + {Attr: "name", Wire: "name", Type: ir.TypeString, Access: readOnly(openAPIGeneratorAccess("Name", "string", "FromString", "", ""))}, }, }, @@ -204,28 +204,28 @@ func openAPIGeneratorBindings() *sdkbind.Bindings { WriteModel: "sdk.RotateRequest", WriteConstructor: "sdk.NewRotateRequestWithDefaults()", Fields: []sdkbind.FieldBinding{ - {Attr: "force", Wire: "force", Kind: ir.TypeBool, + {Attr: "force", Wire: "force", Type: ir.TypeBool, Access: sdkbind.FieldAccess{Set: "SetForce", SDKType: "bool", ConvertSet: "ToBool"}}, - {Attr: "factor", Wire: "factor", Kind: ir.TypeFloat64, + {Attr: "factor", Wire: "factor", Type: ir.TypeFloat64, Access: sdkbind.FieldAccess{Set: "SetFactor", SDKType: "float64", ConvertSet: "ToFloat64"}}, - {Attr: "labels", Wire: "labels", Kind: ir.TypeList, ElementType: ir.TypeString, + {Attr: "labels", Wire: "labels", Type: ir.TypeList, ElementType: ir.TypeString, Access: sdkbind.FieldAccess{Set: "SetLabels", SDKType: "[]string", ConvertSet: "ToStringSlice"}}, - {Attr: "window", Wire: "window", Kind: ir.TypeObject, + {Attr: "window", Wire: "window", Type: ir.TypeObject, Access: sdkbind.FieldAccess{Set: "SetWindow", SDKType: "sdk.RotateWindow"}, NestedModel: "sdk.RotateWindow", NestedWriteModel: "sdk.RotateWindow", NestedConstructor: "sdk.NewRotateWindowWithDefaults()", Nested: []sdkbind.FieldBinding{ - {Attr: "hours", Wire: "hours", Kind: ir.TypeInt64, + {Attr: "hours", Wire: "hours", Type: ir.TypeInt64, Access: sdkbind.FieldAccess{Set: "SetHours", SDKType: "int64", ConvertSet: "ToInt64"}}, }}, - {Attr: "steps", Wire: "steps", Kind: ir.TypeList, ElementType: ir.TypeObject, + {Attr: "steps", Wire: "steps", Type: ir.TypeList, ElementType: ir.TypeObject, Access: sdkbind.FieldAccess{Set: "SetSteps", SDKType: "[]sdk.RotateStep"}, NestedModel: "sdk.RotateStep", NestedWriteModel: "sdk.RotateStep", NestedConstructor: "sdk.NewRotateStepWithDefaults()", Nested: []sdkbind.FieldBinding{ - {Attr: "order", Wire: "order", Kind: ir.TypeInt64, + {Attr: "order", Wire: "order", Type: ir.TypeInt64, Access: sdkbind.FieldAccess{Set: "SetOrder", SDKType: "int64", ConvertSet: "ToInt64"}}, }}, }, diff --git a/internal/emit/services_test.go b/internal/emit/services_test.go index 12b2e5c..b1a0037 100644 --- a/internal/emit/services_test.go +++ b/internal/emit/services_test.go @@ -199,7 +199,7 @@ func TestUnit_RenderServices_TheRenderedCodeCarriesTheDecisions(t *testing.T) { "Delete(ctx, &abstractions.RequestConfiguration[sdk.HttpServerItemRequestBuilderDeleteQueryParameters]{QueryParameters: &sdk.HttpServerItemRequestBuilderDeleteQueryParameters{Confirm: convert.PointerTo(true)}})", `"github.com/microsoft/kiota-abstractions-go"`, `"example.test/provider/internal/services/common/convert"`, - "const eventualConsistency = 30 * time.Second", + "const readAfterWriteDelay = 30 * time.Second", "opts.ConsistencyPredicate = consistencyPredicate", "crud.DeleteWithRetry", "errors.HandleCreateError", diff --git a/internal/emit/unsupported.go b/internal/emit/unsupported.go index 36199f2..773e254 100644 --- a/internal/emit/unsupported.go +++ b/internal/emit/unsupported.go @@ -15,7 +15,7 @@ const UnsupportedName = "unsupported.json" // unsupportedFormatVersion is the report's own schema version, bumped when // the shape changes so a reader can tell. -const unsupportedFormatVersion = 2 +const unsupportedFormatVersion = 3 // The stages that can refuse something. Each names one place in the // pipeline, so a reader can tell a document the toolkit could not model @@ -49,10 +49,10 @@ const ( // otherwise have to parse prose to do it, and prose that must be parsed is // a format with no version. type Unsupported struct { - // Kind is what was refused: "resource", "datasource", "list_resource" + // TerraformBlockType is what was refused: "resource", "datasource", "list_resource" // or "action". Empty for an entity refused before it became any of // them, which is the honest answer rather than a default. - Kind string `json:"kind,omitempty"` + TerraformBlockType string `json:"terraformBlockType,omitempty"` // Entity is the entity key. Entity string `json:"entity"` // Attribute is the dotted attribute path, empty when the whole entity @@ -80,7 +80,7 @@ type Unsupported struct { // UnsupportedReport is the whole committed report. type UnsupportedReport struct { - FormatVersion int `json:"format_version"` + FormatVersion int `json:"formatVersion"` Unsupported []Unsupported `json:"unsupported"` } @@ -161,7 +161,7 @@ func namesByKey(m *ir.Model) map[string]ir.Names { // change that breaks fifteen entities becomes fifteen added lines in a // generation pull request instead of fifteen deleted directories among // thousands of regenerated ones. -func RenderUnsupported(m *ir.Model, removals []sdkbind.Removal, dropped []sdkbind.Dropped, emissionRefusals []ir.UnsupportedEntity, keptUnbound map[string]bool) (File, []Unsupported, error) { +func RenderUnsupported(m *ir.Model, removals []sdkbind.Removal, dropped []sdkbind.Dropped, emissionExclusions []ir.UnsupportedEntity, keptUnbound map[string]bool) (File, []Unsupported, error) { report := UnsupportedReport{FormatVersion: unsupportedFormatVersion} if m != nil { @@ -174,50 +174,50 @@ func RenderUnsupported(m *ir.Model, removals []sdkbind.Removal, dropped []sdkbin } { for _, exclusion := range excluded.entries { report.Unsupported = append(report.Unsupported, Unsupported{ - Kind: exclusion.Kind, - Entity: exclusion.Key, - Service: exclusion.Service, - Tag: exclusion.Tag, - Cause: causeOf(exclusion.Cause), - Stage: excluded.stage, - Reason: exclusion.Reason, + TerraformBlockType: exclusion.TerraformBlockType, + Entity: exclusion.Key, + Service: exclusion.Service, + Tag: exclusion.Tag, + Cause: causeOf(exclusion.Cause), + Stage: excluded.stage, + Reason: exclusion.Reason, }) } } - report.Unsupported = append(report.Unsupported, refusedAttributes(m)...) + report.Unsupported = append(report.Unsupported, excludedAttributes(m)...) } refused, _ := SeparateKept(removals, keptUnbound) for _, removal := range refused { report.Unsupported = append(report.Unsupported, located(m, Unsupported{ - Kind: removal.Kind, - Entity: removal.Key, - Attribute: removal.Attribute, - Cause: causeOf(removal.Cause), - Stage: StageBinding, - Reason: removal.Reason, + TerraformBlockType: removal.Kind, + Entity: removal.Key, + Attribute: removal.Attribute, + Cause: causeOf(removal.Cause), + Stage: StageBinding, + Reason: removal.Reason, })) } for _, drop := range dropped { report.Unsupported = append(report.Unsupported, located(m, Unsupported{ - Kind: drop.Kind, - Entity: drop.Key, - Cause: causeOf(ir.Cause{Code: sdkbind.CauseUnbuildableEntity}), - Stage: StageBinding, - Reason: drop.Reason, + TerraformBlockType: drop.Kind, + Entity: drop.Key, + Cause: causeOf(ir.Cause{Code: sdkbind.CauseUnbuildableEntity}), + Stage: StageBinding, + Reason: drop.Reason, })) } - for _, refusal := range emissionRefusals { + for _, exclusion := range emissionExclusions { report.Unsupported = append(report.Unsupported, Unsupported{ - Kind: refusal.Kind, - Entity: refusal.Key, - Service: refusal.Service, - Tag: refusal.Tag, - Cause: causeOf(refusal.Cause), - Stage: StageEmission, - Reason: refusal.Reason, + TerraformBlockType: exclusion.TerraformBlockType, + Entity: exclusion.Key, + Service: exclusion.Service, + Tag: exclusion.Tag, + Cause: causeOf(exclusion.Cause), + Stage: StageEmission, + Reason: exclusion.Reason, }) } @@ -245,8 +245,8 @@ func sortUnsupported(entries []Unsupported) { switch { case a.Entity != z.Entity: return a.Entity < z.Entity - case a.Kind != z.Kind: - return a.Kind < z.Kind + case a.TerraformBlockType != z.TerraformBlockType: + return a.TerraformBlockType < z.TerraformBlockType case a.Attribute != z.Attribute: return a.Attribute < z.Attribute case a.Stage != z.Stage: @@ -257,9 +257,9 @@ func sortUnsupported(entries []Unsupported) { }) } -// refusedAttributes walks every entity's tree for the attributes +// excludedAttributes walks every entity's tree for the attributes // deriveType marked unsupported, at any depth. -func refusedAttributes(m *ir.Model) []Unsupported { +func excludedAttributes(m *ir.Model) []Unsupported { var out []Unsupported walk := func(kind string, names ir.Names, tree *ir.AttributeTree) { @@ -276,34 +276,34 @@ func refusedAttributes(m *ir.Model) []Unsupported { } if attribute.Unsupported { out = append(out, Unsupported{ - Kind: kind, - Entity: key, - Attribute: name, - Cause: causeOf(attribute.UnsupportedCause), - Service: service, - Tag: tag, - Stage: StageDerivation, - Reason: attribute.UnsupportedReason, + TerraformBlockType: kind, + Entity: key, + Attribute: name, + Cause: causeOf(attribute.UnsupportedCause), + Service: service, + Tag: tag, + Stage: StageDerivation, + Reason: attribute.UnsupportedReason, }) continue } - descend(name, attribute.Nested) + descend(name, attribute.NestedAttributes) } } descend("", tree) } for i := range m.Resources { - walk(bindingKindResource, m.Resources[i].Names, m.Resources[i].Schema) + walk(bindingKindResource, m.Resources[i].Names, m.Resources[i].Attributes) } for i := range m.Datasources { - walk(bindingKindDatasource, m.Datasources[i].Names, m.Datasources[i].Schema) + walk(bindingKindDatasource, m.Datasources[i].Names, m.Datasources[i].Attributes) } for i := range m.ListResources { - walk(bindingKindListResource, m.ListResources[i].Names, m.ListResources[i].Schema) + walk(bindingKindListResource, m.ListResources[i].Names, m.ListResources[i].Attributes) } for i := range m.Actions { - walk(bindingKindAction, m.Actions[i].Names, m.Actions[i].RequestSchema) + walk(bindingKindAction, m.Actions[i].Names, m.Actions[i].RequestAttributes) } return out } diff --git a/internal/emit/unsupported_test.go b/internal/emit/unsupported_test.go index 2f7476b..6a6c464 100644 --- a/internal/emit/unsupported_test.go +++ b/internal/emit/unsupported_test.go @@ -20,31 +20,31 @@ func unsupportedModel() *ir.Model { }, Resources: []ir.Resource{{ Names: ir.Names{Key: "tag"}, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "metadata", Unsupported: true, UnsupportedCause: ir.Cause{Code: ir.CauseShapelessObject}, UnsupportedReason: "free-form object: map support is out of scope"}, - {Name: "settings", Kind: ir.TypeObject, Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "retries", Kind: ir.TypeInt64}, + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "metadata", Unsupported: true, UnsupportedCause: ir.Cause{Code: ir.CauseObjectWithoutPropertiesOrAdditionalProperties}, UnsupportedReason: "free-form object: map support is out of scope"}, + {Name: "settings", Type: ir.TypeObject, NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "retries", Type: ir.TypeInt64}, {Name: "extra", Unsupported: true, UnsupportedCause: ir.Cause{Code: ir.CauseUndeclaredType}, UnsupportedReason: "no type declared"}, }}}, - {Name: "rules", Kind: ir.TypeList, Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "rules", Type: ir.TypeList, NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ {Name: "shape", Unsupported: true, UnsupportedCause: ir.Cause{Code: ir.CauseWritableUnion}, UnsupportedReason: "oneOf/anyOf union: no single attribute type describes it"}, }}}, }}, }}, Actions: []ir.Action{{ Names: ir.Names{Key: "deploy"}, - RequestSchema: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "payload", Unsupported: true, UnsupportedCause: ir.Cause{Code: ir.CauseShapelessObject}, UnsupportedReason: "free-form object: map support is out of scope"}, + RequestAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "payload", Unsupported: true, UnsupportedCause: ir.Cause{Code: ir.CauseObjectWithoutPropertiesOrAdditionalProperties}, UnsupportedReason: "free-form object: map support is out of scope"}, }}, }}, } } -// TestUnit_RenderUnsupported_FindsEveryRefusalAtEveryDepth proves the walk +// TestUnit_RenderUnsupported_FindsEveryExclusionAtEveryDepth proves the walk // reaches nested objects and list elements, and addresses each by its // dotted path — the half of the report that surfaces nowhere else. -func TestUnit_RenderUnsupported_FindsEveryRefusalAtEveryDepth(t *testing.T) { +func TestUnit_RenderUnsupported_FindsEveryExclusionAtEveryDepth(t *testing.T) { _, entries, err := RenderUnsupported(unsupportedModel(), nil, nil, nil, nil) if err != nil { t.Fatalf("RenderUnsupported: %v", err) @@ -171,7 +171,7 @@ func TestUnit_RenderUnsupported_IsAStableDiff(t *testing.T) { t.Fatalf("the report is not valid JSON: %v", err) } if report.FormatVersion != unsupportedFormatVersion { - t.Errorf("format_version = %d, want %d", report.FormatVersion, unsupportedFormatVersion) + t.Errorf("formatVersion = %d, want %d", report.FormatVersion, unsupportedFormatVersion) } for i := 1; i < len(report.Unsupported); i++ { if report.Unsupported[i-1].Entity > report.Unsupported[i].Entity { @@ -201,7 +201,7 @@ func TestUnit_RenderUnsupported_EmptyModelStillRendersTheReport(t *testing.T) { t.Fatalf("the report is not valid JSON: %v", err) } if report.FormatVersion != unsupportedFormatVersion { - t.Errorf("format_version = %d, want %d", report.FormatVersion, unsupportedFormatVersion) + t.Errorf("formatVersion = %d, want %d", report.FormatVersion, unsupportedFormatVersion) } } @@ -214,8 +214,8 @@ func TestUnit_UnsupportedSummary_CountsByStageAndStaysSilentWhenClean(t *testing } got := UnsupportedSummary([]Unsupported{ - {Kind: "resource", Entity: "a", Stage: StageBinding}, - {Kind: "resource", Entity: "b", Stage: StageBinding}, + {TerraformBlockType: "resource", Entity: "a", Stage: StageBinding}, + {TerraformBlockType: "resource", Entity: "b", Stage: StageBinding}, {Entity: "c", Stage: StageDerivation}, }) for _, want := range []string{UnsupportedName, "3 refusals", "2 in binding", "1 in derivation"} { @@ -224,7 +224,7 @@ func TestUnit_UnsupportedSummary_CountsByStageAndStaysSilentWhenClean(t *testing } } - if one := UnsupportedSummary([]Unsupported{{Kind: "resource", Entity: "a", Stage: StageBinding}}); !strings.Contains(one, "1 refusal (") { + if one := UnsupportedSummary([]Unsupported{{TerraformBlockType: "resource", Entity: "a", Stage: StageBinding}}); !strings.Contains(one, "1 refusal (") { t.Errorf("a single refusal summarised as %q, want the singular noun", one) } } @@ -309,10 +309,10 @@ func TestUnit_RenderUnsupported_KeptIsMatchedPerEntity(t *testing.T) { // that was dropped. func TestUnit_JoinTreeKeeping_ReportsOnlyWhatItKeptUnbound(t *testing.T) { tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "id", Kind: ir.TypeString}, - {Name: "org", Kind: ir.TypeString}, - {Name: "name", Kind: ir.TypeString}, - {Name: "colour", Kind: ir.TypeString}, + {Name: "id", Type: ir.TypeString}, + {Name: "org", Type: ir.TypeString}, + {Name: "name", Type: ir.TypeString}, + {Name: "colour", Type: ir.TypeString}, }} // Only "name" has a binding; "colour" has none and is not addressing. fbs := []sdkbind.FieldBinding{{Attr: "name"}} @@ -351,7 +351,7 @@ func TestUnit_JoinTreeKeeping_ReportsOnlyWhatItKeptUnbound(t *testing.T) { // The record carries fields; a test asserting on one entity's refusal still // reads better as a sentence than as five comparisons. func subject(e Unsupported) string { - kind := e.Kind + kind := e.TerraformBlockType if kind == "" { kind = "entity" } @@ -361,10 +361,10 @@ func subject(e Unsupported) string { return fmt.Sprintf("%s %q attribute %q", kind, e.Entity, e.Attribute) } -// TestUnit_RenderUnsupported_ARefusalCarriesItsSubjectAsFields holds the +// TestUnit_RenderUnsupported_AnExclusionCarriesItsSubjectAsFields holds the // subject to fields rather than to a rendered sentence. A reader grouping // refusals by entity or by kind should not have to parse prose to do it. -func TestUnit_RenderUnsupported_ARefusalCarriesItsSubjectAsFields(t *testing.T) { +func TestUnit_RenderUnsupported_AnExclusionCarriesItsSubjectAsFields(t *testing.T) { model := unsupportedModel() model.Resources[0].Names = ir.Names{Key: "tag", Service: "tags", Tag: "Tags"} @@ -379,8 +379,8 @@ func TestUnit_RenderUnsupported_ARefusalCarriesItsSubjectAsFields(t *testing.T) continue } found = true - if e.Kind != bindingKindResource { - t.Errorf("Kind = %q, want %q", e.Kind, bindingKindResource) + if e.TerraformBlockType != bindingKindResource { + t.Errorf("Kind = %q, want %q", e.TerraformBlockType, bindingKindResource) } if e.Service != "tags" || e.Tag != "Tags" { t.Errorf("Service/Tag = %q/%q, want tags/Tags", e.Service, e.Tag) @@ -394,11 +394,11 @@ func TestUnit_RenderUnsupported_ARefusalCarriesItsSubjectAsFields(t *testing.T) } } -// TestUnit_RenderUnsupported_AnEntityRefusedBeforeItBecameAnythingIsStillPlaced +// TestUnit_RenderUnsupported_AnEntityExcludedBeforeItBecameAnythingIsStillPlaced // is the case the fields exist for. An entity refused at classification has // no kind, and until it carried its own location there was nothing to group // it by — which is most of what a large document refuses. -func TestUnit_RenderUnsupported_AnEntityRefusedBeforeItBecameAnythingIsStillPlaced(t *testing.T) { +func TestUnit_RenderUnsupported_AnEntityExcludedBeforeItBecameAnythingIsStillPlaced(t *testing.T) { model := unsupportedModel() model.ExcludedByClassification[0] = ir.UnsupportedEntity{ Key: "orphan", @@ -417,8 +417,8 @@ func TestUnit_RenderUnsupported_AnEntityRefusedBeforeItBecameAnythingIsStillPlac if e.Entity != "orphan" { continue } - if e.Kind != "" { - t.Errorf("Kind = %q, want empty: it became no kind, which is why it was refused", e.Kind) + if e.TerraformBlockType != "" { + t.Errorf("Kind = %q, want empty: it became no kind, which is why it was refused", e.TerraformBlockType) } if e.Service != "orphans" || e.Tag != "Orphans" { t.Errorf("Service/Tag = %q/%q, want orphans/Orphans", e.Service, e.Tag) @@ -445,8 +445,8 @@ func TestUnit_RenderUnsupported_ADroppedEntityKeepsItsKind(t *testing.T) { if e.Stage != StageBinding || e.Entity != "tag" || e.Attribute != "" { continue } - if e.Kind != bindingKindResource { - t.Errorf("Kind = %q, want %q", e.Kind, bindingKindResource) + if e.TerraformBlockType != bindingKindResource { + t.Errorf("Kind = %q, want %q", e.TerraformBlockType, bindingKindResource) } if e.Service != "tags" { t.Errorf("Service = %q, want the location read back from the model", e.Service) @@ -477,11 +477,11 @@ func TestUnit_SeparateKept_DividesWhatCostsTheOperatorSomething(t *testing.T) { } } -// TestUnit_RenderUnsupported_AKeptRemovalIsNotARefusal keeps the record to +// TestUnit_RenderUnsupported_AKeptRemovalIsNotAnExclusion keeps the record to // what the provider will not carry. The attribute reaches the schema, so // recording it as unsupported would overstate the loss in the one file a // reviewer reads to see what a spec change cost. -func TestUnit_RenderUnsupported_AKeptRemovalIsNotARefusal(t *testing.T) { +func TestUnit_RenderUnsupported_AKeptRemovalIsNotAnExclusion(t *testing.T) { model := unsupportedModel() removals := []sdkbind.Removal{ {Kind: "resource", Key: "tag", Attribute: "id", Reason: "models.Tagable carries no GetIdEscaped"}, @@ -529,12 +529,12 @@ func TestUnit_RenderUnsupported_ConfigurationAndClassificationAreSeparateStages( } } -// TestUnit_RenderUnsupported_EveryRefusalCarriesACause is the invariant the +// TestUnit_RenderUnsupported_EveryExclusionCarriesACause is the invariant the // report rests on. A refusal with no cause cannot be grouped with the ones // that share its fact, so it reads as its own finding — which for a model // carrying none of an entity's fields means one finding per attribute // instead of one fact with a list of casualties. -func TestUnit_RenderUnsupported_EveryRefusalCarriesACause(t *testing.T) { +func TestUnit_RenderUnsupported_EveryExclusionCarriesACause(t *testing.T) { model := unsupportedModel() model.ExcludedByConfiguration = []ir.UnsupportedEntity{ {Key: "unwanted", Cause: ir.Cause{Code: ir.CauseExcludedByConfiguration}, Reason: "excluded by configuration"}, @@ -548,7 +548,7 @@ func TestUnit_RenderUnsupported_EveryRefusalCarriesACause(t *testing.T) { }} dropped := []sdkbind.Dropped{{Key: "ruleset", Kind: "resource", Reason: "the SDK does not carry this resource's binding"}} emission := []ir.UnsupportedEntity{{ - Key: "nested_path", Kind: "datasource", + Key: "nested_path", TerraformBlockType: "datasource", Cause: ir.Cause{Code: CauseUnmatchedPathArgument}, Reason: "a path parameter no attribute answers", }} @@ -563,16 +563,16 @@ func TestUnit_RenderUnsupported_EveryRefusalCarriesACause(t *testing.T) { for _, e := range entries { if e.Cause == nil || e.Cause.Code == "" { t.Errorf("%s %q attribute %q (%s) carries no cause: %s", - e.Kind, e.Entity, e.Attribute, e.Stage, e.Reason) + e.TerraformBlockType, e.Entity, e.Attribute, e.Stage, e.Reason) } } } -// TestUnit_RenderUnsupported_RefusalsSharingAFactShareACause proves the +// TestUnit_RenderUnsupported_ExclusionsSharingAFactShareACause proves the // grouping is an exact comparison of fields rather than a guess at which // prose reasons mean the same thing. The two reasons below differ — they // name different accessors — and the fact behind them does not. -func TestUnit_RenderUnsupported_RefusalsSharingAFactShareACause(t *testing.T) { +func TestUnit_RenderUnsupported_ExclusionsSharingAFactShareACause(t *testing.T) { shared := ir.Cause{Code: sdkbind.CauseNoAccessor, Subject: "models.Envelopeable"} removals := []sdkbind.Removal{ {Kind: "datasource", Key: "preload", Attribute: "asset_tag", Cause: shared, diff --git a/internal/fixtures/fixtures.go b/internal/fixtures/fixtures.go index 962e8f6..7ec5cc8 100644 --- a/internal/fixtures/fixtures.go +++ b/internal/fixtures/fixtures.go @@ -282,7 +282,7 @@ func buildVariantModel(tree *ir.AttributeTree) variantModel { } for _, variant := range vc.Variants { m.values[variant.Value] = true - for _, f := range variant.Valid { + for _, f := range variant.AttributesValidWhenEqual { own(f, variant.Value) } } @@ -294,9 +294,9 @@ func buildVariantModel(tree *ir.AttributeTree) variantModel { if cv.Property != m.discriminator { continue } - m.values[cv.Equals] = true - for _, f := range cv.Valid { - own(f, cv.Equals) + m.values[cv.WhenPropertyEquals] = true + for _, f := range cv.AttributesValidWhenEqual { + own(f, cv.WhenPropertyEquals) } } for _, cr := range tree.ConditionalRequirements { @@ -306,13 +306,13 @@ func buildVariantModel(tree *ir.AttributeTree) variantModel { if cr.Property != m.discriminator { continue } - m.values[cr.Equals] = true - if m.requiredFor[cr.Equals] == nil { - m.requiredFor[cr.Equals] = map[string]bool{} + m.values[cr.WhenPropertyEquals] = true + if m.requiredFor[cr.WhenPropertyEquals] == nil { + m.requiredFor[cr.WhenPropertyEquals] = map[string]bool{} } for _, f := range cr.Required { - own(f, cr.Equals) - m.requiredFor[cr.Equals][f] = true + own(f, cr.WhenPropertyEquals) + m.requiredFor[cr.WhenPropertyEquals][f] = true } } return m @@ -382,7 +382,7 @@ func deriveTree(tree *ir.AttributeTree, path []string) ([]Entry, []Omission) { v := Entry{ Name: a.Name, Wire: a.WireName, - Kind: a.Kind, + Kind: a.Type, ElementType: a.ElementType, ComputedOptionalRequired: a.ComputedOptionalRequired, Normalised: a.Normalisation != "", @@ -390,17 +390,17 @@ func deriveTree(tree *ir.AttributeTree, path []string) ([]Entry, []Omission) { Maximum: a.Maximum, } switch { - case a.Nested != nil: - nested, nestedSkips := deriveTree(a.Nested, at) + case a.NestedAttributes != nil: + nested, nestedSkips := deriveTree(a.NestedAttributes, at) v.Nested = nested skips = append(skips, nestedSkips...) - case a.Kind == ir.TypeList, a.Kind == ir.TypeMap: + case a.Type == ir.TypeList, a.Type == ir.TypeMap: // A collection's fixture value is one member's, so the member // kind decides it. Taking the collection's own kind lands every // member in the string arm, whatever the document declared. v.Scalar, v.synthesised = scalarFor(a.ElementType, a, at) default: - v.Scalar, v.synthesised = scalarFor(a.Kind, a, at) + v.Scalar, v.synthesised = scalarFor(a.Type, a, at) } values = append(values, v) } diff --git a/internal/fixtures/fixtures_test.go b/internal/fixtures/fixtures_test.go index e738702..00de602 100644 --- a/internal/fixtures/fixtures_test.go +++ b/internal/fixtures/fixtures_test.go @@ -19,24 +19,24 @@ import ( func testTree() *ir.AttributeTree { return &ir.AttributeTree{ Attributes: []ir.Attribute{ - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "enabled", WireName: "enabled", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, - {Name: "port", WireName: "port", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, - {Name: "ratio", WireName: "ratio", Kind: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, - {Name: "kind", WireName: "kind", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "enabled", WireName: "enabled", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "port", WireName: "port", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, + {Name: "ratio", WireName: "ratio", Type: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, + {Name: "kind", WireName: "kind", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, OneOf: []string{"basic", "advanced"}}, - {Name: "mode", WireName: "mode", Kind: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional, + {Name: "mode", WireName: "mode", Type: ir.TypeString, ComputedOptionalRequired: ir.ComputedOptional, AdvisoryValues: []string{"auto", "manual"}}, - {Name: "tags", WireName: "tags", Kind: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "settings", WireName: "settings", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "retries", WireName: "retries", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Required}, - {Name: "trace", WireName: "trace", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "tags", WireName: "tags", Type: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "settings", WireName: "settings", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "retries", WireName: "retries", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Required}, + {Name: "trace", WireName: "trace", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, }}}, - {Name: "rules", WireName: "rules", Kind: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "pattern", WireName: "pattern", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "rules", WireName: "rules", Type: ir.TypeList, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "pattern", WireName: "pattern", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }}}, {Name: "blob", WireName: "blob", ComputedOptionalRequired: ir.Optional, Unsupported: true, UnsupportedReason: "free-form object declares no properties"}, @@ -104,8 +104,8 @@ func TestUnit_Fixturespec_SkipsCarryTheirReason(t *testing.T) { } nested := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "outer", WireName: "outer", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "outer", WireName: "outer", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ {Name: "inner", WireName: "inner", ComputedOptionalRequired: ir.Optional, Unsupported: true}, }}}, }} @@ -243,33 +243,33 @@ func TestUnit_Fixturespec_WireJSONKeepsTreeOrderAndParses(t *testing.T) { func variantTree() *ir.AttributeTree { return &ir.AttributeTree{ Attributes: []ir.Attribute{ - {Name: "kind", WireName: "kind", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required, + {Name: "kind", WireName: "kind", Type: ir.TypeString, ComputedOptionalRequired: ir.Required, OneOf: []string{"ping", "web", "dns"}}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "interval", WireName: "interval", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Required}, - {Name: "target_host", WireName: "target_host", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "domain", WireName: "domain", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "dnssec", WireName: "dnssec", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, - {Name: "web", WireName: "web", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "url", WireName: "url", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "interval", WireName: "interval", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Required}, + {Name: "target_host", WireName: "target_host", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "domain", WireName: "domain", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "dnssec", WireName: "dnssec", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "web", WireName: "web", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "url", WireName: "url", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }}}, - {Name: "id", WireName: "id", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "id", WireName: "id", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, }, ConditionalValidities: []ir.ConditionalValidity{ - {Property: "kind", Equals: "dns", Valid: []string{"dnssec", "domain"}}, - {Property: "kind", Equals: "ping", Valid: []string{"target_host"}}, + {Property: "kind", WhenPropertyEquals: "dns", AttributesValidWhenEqual: []string{"dnssec", "domain"}}, + {Property: "kind", WhenPropertyEquals: "ping", AttributesValidWhenEqual: []string{"target_host"}}, }, ConditionalRequirements: []ir.ConditionalRequirement{ - {Property: "kind", Equals: "dns", Required: []string{"domain"}}, - {Property: "kind", Equals: "ping", Required: []string{"target_host"}}, - {Property: "kind", Equals: "web", Required: []string{"web"}}, + {Property: "kind", WhenPropertyEquals: "dns", Required: []string{"domain"}}, + {Property: "kind", WhenPropertyEquals: "ping", Required: []string{"target_host"}}, + {Property: "kind", WhenPropertyEquals: "web", Required: []string{"web"}}, }, ValidConfigurations: []ir.ValidConfiguration{{ Discriminator: "kind", - Variants: []ir.ConfigVariant{ - {Value: "dns", Valid: []string{"dnssec", "domain"}}, - {Value: "ping", Valid: []string{"target_host"}}, + Variants: []ir.ValidConfigurationVariant{ + {Value: "dns", AttributesValidWhenEqual: []string{"dnssec", "domain"}}, + {Value: "ping", AttributesValidWhenEqual: []string{"target_host"}}, }, }}, } @@ -354,13 +354,13 @@ func TestUnit_Fixturespec_SingleVariantEntityIsUngated(t *testing.T) { // refused before any assertion runs. func TestUnit_Fixturespec_AFormatDecidesTheValueShape(t *testing.T) { tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "created_at", WireName: "createdAt", Kind: ir.TypeString, Format: "date-time"}, - {Name: "born_on", WireName: "bornOn", Kind: ir.TypeString, Format: "date"}, - {Name: "agent_id", WireName: "agentId", Kind: ir.TypeString, Format: "uuid"}, - {Name: "owner_email", WireName: "ownerEmail", Kind: ir.TypeString, Format: "email"}, - {Name: "home", WireName: "home", Kind: ir.TypeString, Format: "uri"}, - {Name: "address", WireName: "address", Kind: ir.TypeString, Format: "ipv4"}, - {Name: "label", WireName: "label", Kind: ir.TypeString}, + {Name: "created_at", WireName: "createdAt", Type: ir.TypeString, Format: "date-time"}, + {Name: "born_on", WireName: "bornOn", Type: ir.TypeString, Format: "date"}, + {Name: "agent_id", WireName: "agentId", Type: ir.TypeString, Format: "uuid"}, + {Name: "owner_email", WireName: "ownerEmail", Type: ir.TypeString, Format: "email"}, + {Name: "home", WireName: "home", Type: ir.TypeString, Format: "uri"}, + {Name: "address", WireName: "address", Type: ir.TypeString, Format: "ipv4"}, + {Name: "label", WireName: "label", Type: ir.TypeString}, }} got := map[string]any{} @@ -414,19 +414,19 @@ func exampleTree() *ir.AttributeTree { minimum, maximum := 10.0, 1.0 return &ir.AttributeTree{ Attributes: []ir.Attribute{ - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required, Example: "Production metrics stream"}, - {Name: "endpoint_url", WireName: "endpointUrl", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required, + {Name: "endpoint_url", WireName: "endpointUrl", Type: ir.TypeString, ComputedOptionalRequired: ir.Required, Example: "https://api.example.otel-collector"}, - {Name: "created", WireName: "created", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + {Name: "created", WireName: "created", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, Format: "date-time", Example: "whenever"}, - {Name: "kind", WireName: "kind", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + {Name: "kind", WireName: "kind", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, OneOf: []string{"basic", "advanced"}, Example: "advanced"}, - {Name: "retries", WireName: "retries", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, + {Name: "retries", WireName: "retries", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, Minimum: &minimum}, - {Name: "ratio", WireName: "ratio", Kind: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional, + {Name: "ratio", WireName: "ratio", Type: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional, Maximum: &maximum}, - {Name: "interval", WireName: "interval", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, + {Name: "interval", WireName: "interval", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional, Example: 300}, }, } @@ -491,7 +491,7 @@ func TestUnit_Fixturespec_APrefixedStringSuppressesTheRestore(t *testing.T) { // A string the document says nothing about already carries the prefix, so // nothing needs restoring and every example stands. tree.Attributes = append(tree.Attributes, ir.Attribute{ - Name: "label", WireName: "label", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional, + Name: "label", WireName: "label", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional, }) s := Derive(tree) @@ -508,13 +508,13 @@ func TestUnit_Fixturespec_APrefixedStringSuppressesTheRestore(t *testing.T) { func acceptedTree() *ir.AttributeTree { return &ir.AttributeTree{ Attributes: []ir.Attribute{ - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "match_type", WireName: "matchType", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "colour", WireName: "colour", Kind: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "labels", WireName: "labels", Kind: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "settings", WireName: "settings", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "retries", WireName: "retries", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "match_type", WireName: "matchType", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "colour", WireName: "colour", Type: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "labels", WireName: "labels", Type: ir.TypeList, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "settings", WireName: "settings", Type: ir.TypeObject, ComputedOptionalRequired: ir.Optional, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "retries", WireName: "retries", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, }}}, }, } diff --git a/internal/fixtures/map_scalar_test.go b/internal/fixtures/map_scalar_test.go index c9502ac..8561939 100644 --- a/internal/fixtures/map_scalar_test.go +++ b/internal/fixtures/map_scalar_test.go @@ -14,10 +14,10 @@ import ( func mapScalarTree() *ir.AttributeTree { return &ir.AttributeTree{ Attributes: []ir.Attribute{ - {Name: "labels", WireName: "labels", Kind: ir.TypeMap, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, - {Name: "limits", WireName: "limits", Kind: ir.TypeMap, ElementType: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, - {Name: "weights", WireName: "weights", Kind: ir.TypeMap, ElementType: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, - {Name: "flags", WireName: "flags", Kind: ir.TypeMap, ElementType: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + {Name: "labels", WireName: "labels", Type: ir.TypeMap, ElementType: ir.TypeString, ComputedOptionalRequired: ir.Optional}, + {Name: "limits", WireName: "limits", Type: ir.TypeMap, ElementType: ir.TypeInt64, ComputedOptionalRequired: ir.Optional}, + {Name: "weights", WireName: "weights", Type: ir.TypeMap, ElementType: ir.TypeFloat64, ComputedOptionalRequired: ir.Optional}, + {Name: "flags", WireName: "flags", Type: ir.TypeMap, ElementType: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, }, } } diff --git a/internal/fixtures/map_test.go b/internal/fixtures/map_test.go index 7cb1a26..310912e 100644 --- a/internal/fixtures/map_test.go +++ b/internal/fixtures/map_test.go @@ -12,11 +12,11 @@ import ( // it keys objects by a name the practitioner chooses. func portsTree() *ir.AttributeTree { return &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "ports", WireName: "ports", Kind: ir.TypeMap, ElementType: ir.TypeObject, + {Name: "ports", WireName: "ports", Type: ir.TypeMap, ElementType: ir.TypeObject, ComputedOptionalRequired: ir.Optional, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "bore", WireName: "bore", Kind: ir.TypeInt64, ComputedOptionalRequired: ir.Required}, - {Name: "sealed", WireName: "sealed", Kind: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "bore", WireName: "bore", Type: ir.TypeInt64, ComputedOptionalRequired: ir.Required}, + {Name: "sealed", WireName: "sealed", Type: ir.TypeBool, ComputedOptionalRequired: ir.Optional}, }}}, }} } diff --git a/internal/fixtures/replay_test.go b/internal/fixtures/replay_test.go index 1caa68e..505d0fd 100644 --- a/internal/fixtures/replay_test.go +++ b/internal/fixtures/replay_test.go @@ -55,15 +55,15 @@ func TestUnit_Fixturespec_AnExpressionRendersUnquoted(t *testing.T) { } func TestUnit_Fixturespec_AListTakesItsEnumOrItsExamplesFirstMember(t *testing.T) { - enumerated := ir.Attribute{Name: "modules", Kind: ir.TypeList, ElementType: ir.TypeString, OneOf: []string{"default", "extended"}} + enumerated := ir.Attribute{Name: "modules", Type: ir.TypeList, ElementType: ir.TypeString, OneOf: []string{"default", "extended"}} if v, _ := scalarFor(enumerated.ElementType, enumerated, []string{"modules"}); v != "default" { t.Errorf("an enumerated list element = %#v, want the first member", v) } - exampled := ir.Attribute{Name: "modules", Kind: ir.TypeList, ElementType: ir.TypeString, Example: []any{"default"}} + exampled := ir.Attribute{Name: "modules", Type: ir.TypeList, ElementType: ir.TypeString, Example: []any{"default"}} if v, invented := scalarFor(exampled.ElementType, exampled, []string{"modules"}); v != "default" || invented == "" { t.Errorf("an exampled list element = %#v (invented %q), want the example's first member with the name kept", v, invented) } - bare := ir.Attribute{Name: "modules", Kind: ir.TypeList, ElementType: ir.TypeString, Example: []any{7}} + bare := ir.Attribute{Name: "modules", Type: ir.TypeList, ElementType: ir.TypeString, Example: []any{7}} if v, _ := scalarFor(bare.ElementType, bare, []string{"modules"}); v != NamePrefix+"modules" { t.Errorf("a list whose example is not strings = %#v, want the invented name", v) } diff --git a/internal/intermediate_representation/attribute_addressing.go b/internal/intermediate_representation/attribute_addressing.go index 5cb13f9..06e9c13 100644 --- a/internal/intermediate_representation/attribute_addressing.go +++ b/internal/intermediate_representation/attribute_addressing.go @@ -27,7 +27,7 @@ func ensureID(tree *AttributeTree, keyParameter string, keyAttributeType Attribu tree.Attributes = append([]Attribute{{ Name: "id", WireName: wireName, - Kind: kind, + Type: kind, ComputedOptionalRequired: Computed, }}, tree.Attributes...) } @@ -51,7 +51,7 @@ func ensureID(tree *AttributeTree, keyParameter string, keyAttributeType Attribu // — `template_id` for the template whose settings these are — and keeps // the parameter's spelling as its wire name, which is how a call finds it. // With no parent entity to name it after, it is left to emission to refuse. -func ensureParentParameters(tree *AttributeTree, parents []Parameter, parentEntity string) { +func ensureParentParameters(tree *AttributeTree, parents []URLPathParameter, parentEntity string) { if tree == nil || len(parents) == 0 { return } @@ -84,7 +84,7 @@ func ensureParentParameters(tree *AttributeTree, parents []Parameter, parentEnti addedAttributes = append(addedAttributes, Attribute{ Name: name, WireName: parent.Name, - Kind: kind, + Type: kind, ComputedOptionalRequired: Required, // Addressing is not editable: an object does not move to another // parent in place, and every API that admits the move spells it @@ -121,28 +121,28 @@ func ensureFilterAttributes(tree, item *AttributeTree) { addedAttributes := make([]Attribute, 0, len(item.Attributes)) for _, attribute := range item.Attributes { - if attribute.Nested != nil || declaredNames[attribute.Name] { + if attribute.NestedAttributes != nil || declaredNames[attribute.Name] { continue } - switch attribute.Kind { + switch attribute.Type { case TypeString, TypeBool, TypeInt64, TypeFloat64: default: continue } declaredNames[attribute.Name] = true addedAttributes = append(addedAttributes, Attribute{ - Name: attribute.Name, - WireName: attribute.WireName, - Description: attribute.Description, - Kind: attribute.Kind, - ComputedOptionalRequired: Optional, - Filter: true, + Name: attribute.Name, + WireName: attribute.WireName, + Description: attribute.Description, + Type: attribute.Type, + ComputedOptionalRequired: Optional, + IsDatasourceFilterArgument: true, }) } tree.Attributes = append(tree.Attributes, addedAttributes...) } -// addressingSchema is a collection path's addressing attributes as a tree of +// addressingAttributes is a collection path's addressing attributes as a tree of // their own, for a list resource to declare as the configuration of its list // block. Nil when the path takes no parameters. // @@ -150,7 +150,7 @@ func ensureFilterAttributes(tree, item *AttributeTree) { // there is no id to absorb the last one. None carries RequiresReplace — a // list block declares a query, and a query has no plan for a modifier to act // on. -func addressingSchema(parameters []Parameter) *AttributeTree { +func addressingAttributes(parameters []URLPathParameter) *AttributeTree { if len(parameters) == 0 { return nil } @@ -164,7 +164,7 @@ func addressingSchema(parameters []Parameter) *AttributeTree { // parentParameters is an operation's path parameters above the item key: all // of them but the last, which addresses the object itself and becomes the id. -func parentParameters(parameters []Parameter) []Parameter { +func parentParameters(parameters []URLPathParameter) []URLPathParameter { if len(parameters) < 2 { return nil } @@ -189,7 +189,7 @@ func requireKey(tree *AttributeTree, keyParameter string, keyAttributeType Attri tree.Attributes = append([]Attribute{{ Name: name, WireName: keyParameter, - Kind: kind, + Type: kind, ComputedOptionalRequired: Required, }}, tree.Attributes...) } diff --git a/internal/intermediate_representation/attribute_rules_test.go b/internal/intermediate_representation/attribute_rules_test.go index 65d68a3..671849f 100644 --- a/internal/intermediate_representation/attribute_rules_test.go +++ b/internal/intermediate_representation/attribute_rules_test.go @@ -9,10 +9,10 @@ import ( func TestUnit_EnsureParentParameters_NamesAParentSpelledIdAfterItsEntity(t *testing.T) { tree := &AttributeTree{Attributes: []Attribute{ - {Name: "id", WireName: "id", Kind: TypeString, ComputedOptionalRequired: Computed}, - {Name: "scope", WireName: "scope", Kind: TypeString, ComputedOptionalRequired: Required}, + {Name: "id", WireName: "id", Type: TypeString, ComputedOptionalRequired: Computed}, + {Name: "scope", WireName: "scope", Type: TypeString, ComputedOptionalRequired: Required}, }} - ensureParentParameters(tree, []Parameter{{Name: "id", Type: TypeString}}, "template") + ensureParentParameters(tree, []URLPathParameter{{Name: "id", Type: TypeString}}, "template") if len(tree.Attributes) != 3 || tree.Attributes[0].Name != "template_id" { t.Fatalf("want template_id prepended, got %+v", tree.Attributes) @@ -23,8 +23,8 @@ func TestUnit_EnsureParentParameters_NamesAParentSpelledIdAfterItsEntity(t *test // With no parent entity to name it after, the parameter is not added: // the resource's own id already holds the name. - bare := &AttributeTree{Attributes: []Attribute{{Name: "id", WireName: "id", Kind: TypeString, ComputedOptionalRequired: Computed}}} - ensureParentParameters(bare, []Parameter{{Name: "id", Type: TypeString}}, "") + bare := &AttributeTree{Attributes: []Attribute{{Name: "id", WireName: "id", Type: TypeString, ComputedOptionalRequired: Computed}}} + ensureParentParameters(bare, []URLPathParameter{{Name: "id", Type: TypeString}}, "") if len(bare.Attributes) != 1 { t.Fatalf("an unnameable parent was added: %+v", bare.Attributes) } @@ -33,15 +33,15 @@ func TestUnit_EnsureParentParameters_NamesAParentSpelledIdAfterItsEntity(t *test func TestUnit_BuildTree_MembersOfAServerFilledAttributeAreComputed(t *testing.T) { attributes := []Attribute{ {Name: "name", ComputedOptionalRequired: Required}, - {Name: "groupings", ComputedOptionalRequired: ComputedOptional, Nested: &AttributeTree{Attributes: []Attribute{ + {Name: "groupings", ComputedOptionalRequired: ComputedOptional, NestedAttributes: &AttributeTree{Attributes: []Attribute{ {Name: "title", ComputedOptionalRequired: Required}, {Name: "type", ComputedOptionalRequired: Optional}, {Name: "count", ComputedOptionalRequired: Computed}, - {Name: "inner", ComputedOptionalRequired: Optional, Nested: &AttributeTree{Attributes: []Attribute{ + {Name: "inner", ComputedOptionalRequired: Optional, NestedAttributes: &AttributeTree{Attributes: []Attribute{ {Name: "leaf", ComputedOptionalRequired: Required}, }}}, }}}, - {Name: "settings", ComputedOptionalRequired: Optional, Nested: &AttributeTree{Attributes: []Attribute{ + {Name: "settings", ComputedOptionalRequired: Optional, NestedAttributes: &AttributeTree{Attributes: []Attribute{ {Name: "mode", ComputedOptionalRequired: Required}, }}}, } @@ -50,7 +50,7 @@ func TestUnit_BuildTree_MembersOfAServerFilledAttributeAreComputed(t *testing.T) if attributes[0].ComputedOptionalRequired != Required { t.Errorf("a top-level required attribute changed: %s", attributes[0].ComputedOptionalRequired) } - members := attributes[1].Nested.Attributes + members := attributes[1].NestedAttributes.Attributes for _, want := range []struct { name string cor ComputedOptionalRequired @@ -61,12 +61,12 @@ func TestUnit_BuildTree_MembersOfAServerFilledAttributeAreComputed(t *testing.T) } } } - if leaf := members[3].Nested.Attributes[0]; leaf.ComputedOptionalRequired != ComputedOptional { + if leaf := members[3].NestedAttributes.Attributes[0]; leaf.ComputedOptionalRequired != ComputedOptional { t.Errorf("a member two levels down = %s, want computed-optional", leaf.ComputedOptionalRequired) } // A plain optional parent is the practitioner's whole object: its // members keep what the document declares. - if mode := attributes[2].Nested.Attributes[0]; mode.ComputedOptionalRequired != Required { + if mode := attributes[2].NestedAttributes.Attributes[0]; mode.ComputedOptionalRequired != Required { t.Errorf("a member of an optional attribute = %s, want required", mode.ComputedOptionalRequired) } } @@ -75,7 +75,7 @@ func TestUnit_DeriveListType_CarriesTheElementEnum(t *testing.T) { attribute := Attribute{Name: "modules"} items := &specmodel.Schema{Type: "string", Enum: []any{"default", "extended"}} deriveListType(&attribute, &specmodel.Schema{Type: "array", Items: items}, nil, nil) - if attribute.Kind != TypeList || attribute.ElementType != TypeString { + if attribute.Type != TypeList || attribute.ElementType != TypeString { t.Fatalf("attribute = %+v, want a list of strings", attribute) } if !reflect.DeepEqual(attribute.OneOf, []string{"default", "extended"}) { diff --git a/internal/intermediate_representation/attribute_types.go b/internal/intermediate_representation/attribute_types.go index 976b766..9b7d5f8 100644 --- a/internal/intermediate_representation/attribute_types.go +++ b/internal/intermediate_representation/attribute_types.go @@ -15,13 +15,13 @@ import ( func deriveType(attribute *Attribute, governingSchema resolvedSchema, create, read, update *specmodel.Schema) { switch { case governingSchema.declaredType == "string": - attribute.Kind = TypeString + attribute.Type = TypeString case governingSchema.declaredType == "boolean": - attribute.Kind = TypeBool + attribute.Type = TypeBool case governingSchema.declaredType == "integer": - attribute.Kind = TypeInt64 + attribute.Type = TypeInt64 case governingSchema.declaredType == "number": - attribute.Kind = TypeFloat64 + attribute.Type = TypeFloat64 case governingSchema.declaredType == "array": deriveListType(attribute, create, read, update) case governingSchema.declaredType == "object" || (governingSchema.declaredType == "" && len(governingSchema.properties) > 0): @@ -29,14 +29,14 @@ func deriveType(attribute *Attribute, governingSchema resolvedSchema, create, re deriveMapType(attribute, governingSchema, create, read, update) return } - attribute.Kind = TypeObject - attribute.Nested = buildAttributeTree(create, read, update, false) + attribute.Type = TypeObject + attribute.NestedAttributes = buildAttributeTree(create, read, update, false) case governingSchema.declaredType == "" && governingSchema.hasUnion: deriveUnionType(attribute, governingSchema, create, read) case governingSchema.declaredType == "": - refuse(attribute, Cause{Code: CauseUndeclaredType}, "no type declared") + exclude(attribute, Cause{Code: CauseUndeclaredType}, "no type declared") default: - refuse(attribute, Cause{Code: CauseUnsupportedType, Subject: governingSchema.declaredType}, + exclude(attribute, Cause{Code: CauseUnsupportedType, Subject: governingSchema.declaredType}, fmt.Sprintf("type %q is not supported", governingSchema.declaredType)) } } @@ -58,14 +58,14 @@ func deriveType(attribute *Attribute, governingSchema resolvedSchema, create, re // that. func deriveUnionType(attribute *Attribute, governingSchema resolvedSchema, create, read *specmodel.Schema) { if create != nil { - refuse(attribute, Cause{Code: CauseWritableUnion}, "oneOf/anyOf union in a writable position: its variants would have to be mutually exclusive in configuration, which the schema alone does not express") + exclude(attribute, Cause{Code: CauseWritableUnion}, "oneOf/anyOf union in a writable position: its variants would have to be mutually exclusive in configuration, which the schema alone does not express") return } variants := make([]Attribute, 0, len(governingSchema.unionBranches)) for _, branch := range governingSchema.unionBranches { if branch == nil || branch.Ref == "" { - refuse(attribute, Cause{Code: CauseUnnamedUnionBranch}, fmt.Sprintf( + exclude(attribute, Cause{Code: CauseUnnamedUnionBranch}, fmt.Sprintf( "oneOf/anyOf union with %d branches, of which %d name no component: a variant the document does not name has no attribute to become", len(governingSchema.unionBranches), anonymousBranches(governingSchema.unionBranches))) return @@ -73,21 +73,21 @@ func deriveUnionType(attribute *Attribute, governingSchema resolvedSchema, creat variants = append(variants, Attribute{ Name: snakeCase(branch.Ref), WireName: branch.Ref, - Kind: TypeObject, + Type: TypeObject, // Read-only throughout: the API answers with one branch and the // practitioner chooses none of them. ComputedOptionalRequired: Computed, Description: resolveSchema(branch).description, - Nested: buildAttributeTree(nil, branch, nil, false), + NestedAttributes: buildAttributeTree(nil, branch, nil, false), }) } if len(variants) == 0 { - refuse(attribute, Cause{Code: CauseEmptyUnion}, "oneOf/anyOf union declaring no branches") + exclude(attribute, Cause{Code: CauseEmptyUnion}, "oneOf/anyOf union declaring no branches") return } - attribute.Kind = TypeObject - attribute.Nested = &AttributeTree{Attributes: variants, Description: resolveSchema(read).description} + attribute.Type = TypeObject + attribute.NestedAttributes = &AttributeTree{Attributes: variants, Description: resolveSchema(read).description} } // anonymousBranches counts the branches of a union that reference no @@ -116,23 +116,23 @@ func deriveMapType(attribute *Attribute, governingSchema resolvedSchema, create, valueSchema := governingSchema.additionalProperties if valueSchema == nil { if governingSchema.additionalPropertiesDeclared { - refuse(attribute, Cause{Code: CauseUntypedAdditionalProperties}, "object whose additionalProperties is a bare boolean: it declares no value type to map") + exclude(attribute, Cause{Code: CauseUntypedAdditionalProperties}, "object whose additionalProperties is a bare boolean: it declares no value type to map") return } - refuse(attribute, Cause{Code: CauseShapelessObject}, "object declaring neither properties nor additionalProperties: it has no declared shape") + exclude(attribute, Cause{Code: CauseObjectWithoutPropertiesOrAdditionalProperties}, "object declaring neither properties nor additionalProperties: it has no declared shape") return } resolvedValue := resolveSchema(valueSchema) switch { case resolvedValue.declaredType == "string": - attribute.Kind, attribute.ElementType = TypeMap, TypeString + attribute.Type, attribute.ElementType = TypeMap, TypeString case resolvedValue.declaredType == "boolean": - attribute.Kind, attribute.ElementType = TypeMap, TypeBool + attribute.Type, attribute.ElementType = TypeMap, TypeBool case resolvedValue.declaredType == "integer": - attribute.Kind, attribute.ElementType = TypeMap, TypeInt64 + attribute.Type, attribute.ElementType = TypeMap, TypeInt64 case resolvedValue.declaredType == "number": - attribute.Kind, attribute.ElementType = TypeMap, TypeFloat64 + attribute.Type, attribute.ElementType = TypeMap, TypeFloat64 case resolvedValue.declaredType == "object" || (resolvedValue.declaredType == "" && len(resolvedValue.properties) > 0): if len(resolvedValue.properties) == 0 { // A value that is itself a map has no properties and is not @@ -142,22 +142,22 @@ func deriveMapType(attribute *Attribute, governingSchema resolvedSchema, create, // yet describe an element that is itself a collection, so the // refusal names that rather than blaming the document. if resolvedValue.additionalProperties != nil { - refuse(attribute, Cause{Code: CauseMapOfMaps}, + exclude(attribute, Cause{Code: CauseMapOfMaps}, "map whose values are themselves maps: the derivation carries one element kind and cannot yet nest one") return } - refuse(attribute, Cause{Code: CauseMapOfObjects}, + exclude(attribute, Cause{Code: CauseMapOfObjects}, "map of objects the specification gives no properties: there are no attributes to map") return } - attribute.Kind, attribute.ElementType = TypeMap, TypeObject - attribute.Nested = buildAttributeTree( + attribute.Type, attribute.ElementType = TypeMap, TypeObject + attribute.NestedAttributes = buildAttributeTree( resolveSchema(create).additionalProperties, resolveSchema(read).additionalProperties, resolveSchema(update).additionalProperties, false) default: - refuse(attribute, Cause{Code: CauseUnsupportedMapValue, Subject: resolvedValue.declaredType}, + exclude(attribute, Cause{Code: CauseUnsupportedMapValue, Subject: resolvedValue.declaredType}, fmt.Sprintf("map of %q values is not supported", resolvedValue.declaredType)) } } @@ -173,9 +173,9 @@ func deriveListType(attribute *Attribute, create, read, update *specmodel.Schema resolvedItems := resolveSchema(governingItems) switch { case resolvedItems.empty: - refuse(attribute, Cause{Code: CauseItemlessArray}, "array declares no items schema") + exclude(attribute, Cause{Code: CauseItemlessArray}, "array declares no items schema") case resolvedItems.declaredType == "string": - attribute.Kind, attribute.ElementType = TypeList, TypeString + attribute.Type, attribute.ElementType = TypeList, TypeString // The element's closed set is the list's: each member the // practitioner writes is validated against it, and a fixture takes // its member from it. @@ -183,27 +183,27 @@ func deriveListType(attribute *Attribute, create, read, update *specmodel.Schema attribute.OneOf = renderEnum(resolvedItems.enum) } case resolvedItems.declaredType == "boolean": - attribute.Kind, attribute.ElementType = TypeList, TypeBool + attribute.Type, attribute.ElementType = TypeList, TypeBool case resolvedItems.declaredType == "integer": - attribute.Kind, attribute.ElementType = TypeList, TypeInt64 + attribute.Type, attribute.ElementType = TypeList, TypeInt64 case resolvedItems.declaredType == "number": - attribute.Kind, attribute.ElementType = TypeList, TypeFloat64 + attribute.Type, attribute.ElementType = TypeList, TypeFloat64 case resolvedItems.declaredType == "object" || (resolvedItems.declaredType == "" && len(resolvedItems.properties) > 0): if len(resolvedItems.properties) == 0 { - refuse(attribute, Cause{Code: CauseFreeFormArrayElement}, "array of free-form objects: map support is out of scope") + exclude(attribute, Cause{Code: CauseFreeFormArrayElement}, "array of free-form objects: map support is out of scope") return } - attribute.Kind, attribute.ElementType = TypeList, TypeObject - attribute.Nested = buildAttributeTree(createItems, readItems, updateItems, false) + attribute.Type, attribute.ElementType = TypeList, TypeObject + attribute.NestedAttributes = buildAttributeTree(createItems, readItems, updateItems, false) default: - refuse(attribute, Cause{Code: CauseUnsupportedArrayElement, Subject: resolvedItems.declaredType}, + exclude(attribute, Cause{Code: CauseUnsupportedArrayElement, Subject: resolvedItems.declaredType}, fmt.Sprintf("array of %q elements is not supported", resolvedItems.declaredType)) } } -// refuse marks an attribute unsupported with the reason a person reads. -func refuse(attribute *Attribute, cause Cause, reason string) { - attribute.Kind = "" +// exclude marks an attribute unsupported with the reason a person reads. +func exclude(attribute *Attribute, cause Cause, reason string) { + attribute.Type = "" attribute.Unsupported = true attribute.UnsupportedCause = cause attribute.UnsupportedReason = reason @@ -218,18 +218,18 @@ var reservedRootNames = map[string]bool{ "lifecycle": true, "provider": true, "provisioner": true, } -// refuseReservedRootNames refuses a root attribute terraform will not accept +// excludeReservedRootNames excludes a root attribute terraform will not accept // the name of. // -// Refused rather than renamed: the name is what a practitioner writes, and +// Excluded rather than renamed: the name is what a practitioner writes, and // choosing another belongs in a correction to the document rather than in a // rule here. The cost of declaring one is the whole provider — terraform -// rejects the schema and loads none of it — so this is not a refusal that can +// rejects the schema and loads none of it — so this is not an exclusion that can // be deferred to the operator's judgement. // // Root only, matching the framework: the same name nested inside an object is // an ordinary field and needs no special syntax. -func refuseReservedRootNames(tree *AttributeTree) { +func excludeReservedRootNames(tree *AttributeTree) { if tree == nil { return } @@ -238,7 +238,7 @@ func refuseReservedRootNames(tree *AttributeTree) { if !reservedRootNames[attribute.Name] { continue } - refuse(attribute, Cause{Code: CauseReservedRootName, Subject: attribute.Name}, fmt.Sprintf( + exclude(attribute, Cause{Code: CauseReservedRootName, Subject: attribute.Name}, fmt.Sprintf( "terraform reserves %q at the root of a schema, and refuses to load a provider that declares it; rename the property in a correction", attribute.Name)) } diff --git a/internal/intermediate_representation/attributes.go b/internal/intermediate_representation/attributes.go index fcffd8c..d3e1319 100644 --- a/internal/intermediate_representation/attributes.go +++ b/internal/intermediate_representation/attributes.go @@ -331,8 +331,8 @@ func serverFilledMembersComputed(attributes []Attribute, beneathServerFilledPare if beneathServerFilledParent && attribute.ComputedOptionalRequired != Computed { attribute.ComputedOptionalRequired = ComputedOptional } - if attribute.Nested != nil { - serverFilledMembersComputed(attribute.Nested.Attributes, beneathServerFilledParent || attribute.ComputedOptionalRequired == ComputedOptional) + if attribute.NestedAttributes != nil { + serverFilledMembersComputed(attribute.NestedAttributes.Attributes, beneathServerFilledParent || attribute.ComputedOptionalRequired == ComputedOptional) } } } @@ -342,7 +342,7 @@ func serverFilledMembersComputed(attributes []Attribute, beneathServerFilledPare func convertValidConfiguration(validConfiguration specmodel.ValidConfiguration) ValidConfiguration { out := ValidConfiguration{Discriminator: snakeCase(validConfiguration.Discriminator)} for _, value := range validConfiguration.Variants { - out.Variants = append(out.Variants, ConfigVariant{Value: value.Value, Valid: sortedUnique(snakeCaseAll(value.Fields))}) + out.Variants = append(out.Variants, ValidConfigurationVariant{Value: value.Value, AttributesValidWhenEqual: sortedUnique(snakeCaseAll(value.Fields))}) } sort.Slice(out.Variants, func(first, second int) bool { return out.Variants[first].Value < out.Variants[second].Value }) return out @@ -357,13 +357,13 @@ func sortedConditionals(gates map[[2]string][]string) []ConditionalRequirement { out := make([]ConditionalRequirement, 0, len(gates)) for gate, names := range gates { sort.Strings(names) - out = append(out, ConditionalRequirement{Property: gate[0], Equals: gate[1], Required: names}) + out = append(out, ConditionalRequirement{Property: gate[0], WhenPropertyEquals: gate[1], Required: names}) } sort.Slice(out, func(first, second int) bool { if out[first].Property != out[second].Property { return out[first].Property < out[second].Property } - return out[first].Equals < out[second].Equals + return out[first].WhenPropertyEquals < out[second].WhenPropertyEquals }) return out } @@ -377,13 +377,13 @@ func sortedValidities(gates map[[2]string][]string) []ConditionalValidity { out := make([]ConditionalValidity, 0, len(gates)) for gate, names := range gates { sort.Strings(names) - out = append(out, ConditionalValidity{Property: gate[0], Equals: gate[1], Valid: names}) + out = append(out, ConditionalValidity{Property: gate[0], WhenPropertyEquals: gate[1], AttributesValidWhenEqual: names}) } sort.Slice(out, func(first, second int) bool { if out[first].Property != out[second].Property { return out[first].Property < out[second].Property } - return out[first].Equals < out[second].Equals + return out[first].WhenPropertyEquals < out[second].WhenPropertyEquals }) return out } @@ -470,7 +470,7 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib volatile, _ := extensions.Volatile() immutable, _ := extensions.Immutable() serverDefault, hasServerDefault := extensions.ServerDefault() - attribute.SilentlyIgnoredOnUpdate, _ = extensions.IgnoredOnUpdate() + attribute.IgnoredOnUpdate, _ = extensions.IgnoredOnUpdate() attribute.Normalisation, _ = extensions.Normalisation() // The document's prose, taken from whichever side declares any. A @@ -550,13 +550,13 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib // removal from configuration is sticky — which is a diff to explain // rather than an apply that cannot succeed. attribute.ComputedOptionalRequired = ComputedOptional - attribute.Authority = computedOptionalAuthority(hasServerDefault, property.requiredRead, resolvedCreate.hasDefault) + attribute.SchemaAttributeTypeDetermination = computedOptionalDetermination(hasServerDefault, property.requiredRead, resolvedCreate.hasDefault) default: // Writable, and the server leaves it absent when the request omits it. // Genuinely rare: most APIs answer with something. attribute.ComputedOptionalRequired = Optional } - // refusedByUpdate is the document's own account of immutability: the + // excludedByUpdate is the document's own account of immutability: the // create body declares this property and the update body does not, so // the API offers no way to change it after create. Free, offline, and // true of the document whether or not an audit has ever run — which is @@ -566,9 +566,9 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib // the update body for the uninteresting reason that it is absent from // every request body, and on hasUpdateBody, because a document that // declares no update schema is silent rather than restrictive. - refusedByUpdate := writable && property.hasUpdateBody && property.update == nil + excludedByUpdate := writable && property.hasUpdateBody && property.update == nil - if attribute.ComputedOptionalRequired != Computed && (immutable || property.everyAttributeRequiresReplace || refusedByUpdate) { + if attribute.ComputedOptionalRequired != Computed && (immutable || property.everyAttributeRequiresReplace || excludedByUpdate) { attribute.RequiresReplace = true } @@ -580,7 +580,7 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib } deriveType(&attribute, governingSchema, childCreate, property.read, property.update) - if len(governingSchema.enum) > 0 && attribute.Kind != TypeList && attribute.Kind != TypeObject && !attribute.Unsupported { + if len(governingSchema.enum) > 0 && attribute.Type != TypeList && attribute.Type != TypeObject && !attribute.Unsupported { values := renderEnum(governingSchema.enum) if open, _ := extensions.Values(); open { attribute.AdvisoryValues = values @@ -590,7 +590,7 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib } // A list's set came from its element; the property's own x-tfpfgen-values // says whether that set is closed. - if open, _ := extensions.Values(); open && attribute.Kind == TypeList && len(attribute.OneOf) > 0 { + if open, _ := extensions.Values(); open && attribute.Type == TypeList && len(attribute.OneOf) > 0 { attribute.AdvisoryValues, attribute.OneOf = attribute.OneOf, nil } @@ -607,7 +607,7 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib return attribute, edges } -// computedOptionalAuthority names the strongest declaration that applies, +// computedOptionalDetermination names the strongest declaration that applies, // in the order the presence decision consults them. Several routes reach // computed_optional at once and the strongest is the one worth recording: // an attribute the audit measured is not made weaker by the document also @@ -615,15 +615,15 @@ func buildAttribute(wireName string, property foldedProperty) (Attribute, attrib // // The fourth route needs no condition. It is the remaining disjunct of the // case that got here, so reaching the default arm is what identifies it. -func computedOptionalAuthority(hasServerDefault, requiredRead, hasDefault bool) Authority { +func computedOptionalDetermination(hasServerDefault, requiredRead, hasDefault bool) SchemaAttributeTypeDetermination { switch { case hasServerDefault: - return AuthorityServerDefault + return SchemaAttributeTypeDeterminationServerDefault case requiredRead: - return AuthorityResponseRequired + return SchemaAttributeTypeDeterminationResponseRequired case hasDefault: - return AuthorityRequestDefault + return SchemaAttributeTypeDeterminationRequestDefault default: - return AuthorityResponseProperty + return SchemaAttributeTypeDeterminationResponseProperty } } diff --git a/internal/intermediate_representation/attributes_test.go b/internal/intermediate_representation/attributes_test.go index d415218..ce0c3af 100644 --- a/internal/intermediate_representation/attributes_test.go +++ b/internal/intermediate_representation/attributes_test.go @@ -12,7 +12,7 @@ import ( // thingTree derives the main fixture's resource tree once per test. func thingTree(t *testing.T) *AttributeTree { t.Helper() - return resourceByKey(t, mustDerive(t, thingSpec, testConfig()), "thing").Schema + return resourceByKey(t, mustDerive(t, thingSpec, testConfig()), "thing").Attributes } func TestUnit_Attributes_TypeMapping(t *testing.T) { @@ -29,42 +29,42 @@ func TestUnit_Attributes_TypeMapping(t *testing.T) { {"rules", TypeList}, {"settings", TypeObject}, } { - if a := attribute(t, tree, testCase.name); a.Kind != testCase.kind { - t.Errorf("%s: kind = %q, want %q", testCase.name, a.Kind, testCase.kind) + if a := attribute(t, tree, testCase.name); a.Type != testCase.kind { + t.Errorf("%s: kind = %q, want %q", testCase.name, a.Type, testCase.kind) } } labels := attribute(t, tree, "labels") - if labels.ElementType != TypeString || labels.Nested != nil { + if labels.ElementType != TypeString || labels.NestedAttributes != nil { t.Errorf("labels = %+v, want a list of strings", labels) } rules := attribute(t, tree, "rules") - if rules.ElementType != TypeObject || rules.Nested == nil { + if rules.ElementType != TypeObject || rules.NestedAttributes == nil { t.Fatalf("rules = %+v, want a list of objects", rules) } // rules is server-filled, so its members are computed as well, the // required one included: terraform core reads a non-computed member // holding a value as one the configuration set, and clears the whole // attribute when the configuration omits it. - if kind := attribute(t, rules.Nested, "kind"); kind.ComputedOptionalRequired != ComputedOptional { + if kind := attribute(t, rules.NestedAttributes, "kind"); kind.ComputedOptionalRequired != ComputedOptional { t.Errorf("kind inside rules = %+v, want computed-optional under a server-filled parent", kind) } // Nested attributes take the same presence rule as top-level ones, and // this response describes the create schema wholesale. - if limit := attribute(t, rules.Nested, "limit"); limit.ComputedOptionalRequired != ComputedOptional || - limit.Kind != TypeInt64 { + if limit := attribute(t, rules.NestedAttributes, "limit"); limit.ComputedOptionalRequired != ComputedOptional || + limit.Type != TypeInt64 { t.Errorf("limit inside rules = %+v", limit) } settings := attribute(t, tree, "settings") - if settings.Nested == nil { + if settings.NestedAttributes == nil { t.Fatalf("settings has no nested tree") } - if retries := attribute(t, settings.Nested, "retries"); retries.Kind != TypeInt64 { + if retries := attribute(t, settings.NestedAttributes, "retries"); retries.Type != TypeInt64 { t.Errorf("retries = %+v", retries) } } -func TestUnit_Attributes_ShapelessObjectIsRefused(t *testing.T) { +func TestUnit_Attributes_AnObjectWithoutPropertiesOrAdditionalPropertiesIsExcluded(t *testing.T) { extras := attribute(t, thingTree(t), "extras") if !extras.Unsupported { t.Fatalf("an object with no declared shape derived a type: %+v", extras) @@ -72,8 +72,8 @@ func TestUnit_Attributes_ShapelessObjectIsRefused(t *testing.T) { if !strings.Contains(extras.UnsupportedReason, "no declared shape") { t.Errorf("reason = %q", extras.UnsupportedReason) } - if extras.Kind != "" { - t.Errorf("an unsupported attribute still claims kind %q", extras.Kind) + if extras.Type != "" { + t.Errorf("an unsupported attribute still claims kind %q", extras.Type) } } @@ -113,7 +113,7 @@ func TestUnit_DeriveMapType_TypesEveryValueShape(t *testing.T) { if testCase.wantReason != "" { if !got.Unsupported { - t.Errorf("%s: derived kind %q, want a refusal", testCase.name, got.Kind) + t.Errorf("%s: derived kind %q, want a refusal", testCase.name, got.Type) continue } if !strings.Contains(got.UnsupportedReason, testCase.wantReason) { @@ -125,9 +125,9 @@ func TestUnit_DeriveMapType_TypesEveryValueShape(t *testing.T) { t.Errorf("%s: refused with %q", testCase.name, got.UnsupportedReason) continue } - if got.Kind != testCase.wantKind || got.ElementType != testCase.wantElement { + if got.Type != testCase.wantKind || got.ElementType != testCase.wantElement { t.Errorf("%s: kind/element = %q/%q, want %q/%q", - testCase.name, got.Kind, got.ElementType, testCase.wantKind, testCase.wantElement) + testCase.name, got.Type, got.ElementType, testCase.wantKind, testCase.wantElement) } } } @@ -195,7 +195,7 @@ func TestUnit_Attributes_Enums(t *testing.T) { func TestUnit_Attributes_ConditionalRequirement(t *testing.T) { tree := thingTree(t) - want := []ConditionalRequirement{{Property: "mode", Equals: "custom", Required: []string{"proxy_host"}}} + want := []ConditionalRequirement{{Property: "mode", WhenPropertyEquals: "custom", Required: []string{"proxy_host"}}} if !reflect.DeepEqual(tree.ConditionalRequirements, want) { t.Errorf("conditional requirements = %+v, want %+v", tree.ConditionalRequirements, want) } @@ -324,7 +324,7 @@ components: func TestUnit_Attributes_ConditionalValidity(t *testing.T) { tree := thingTree(t) - want := []ConditionalValidity{{Property: "mode", Equals: "standard", Valid: []string{"quantity"}}} + want := []ConditionalValidity{{Property: "mode", WhenPropertyEquals: "standard", AttributesValidWhenEqual: []string{"quantity"}}} if !reflect.DeepEqual(tree.ConditionalValidities, want) { t.Errorf("conditional validities = %+v, want %+v", tree.ConditionalValidities, want) } @@ -350,9 +350,9 @@ func TestUnit_Attributes_ValidConfigurations(t *testing.T) { tree := thingTree(t) want := []ValidConfiguration{{ Discriminator: "mode", - Variants: []ConfigVariant{ - {Value: "custom", Valid: []string{"proxy_host"}}, - {Value: "standard", Valid: []string{"quantity"}}, + Variants: []ValidConfigurationVariant{ + {Value: "custom", AttributesValidWhenEqual: []string{"proxy_host"}}, + {Value: "standard", AttributesValidWhenEqual: []string{"quantity"}}, }, }} if !reflect.DeepEqual(tree.ValidConfigurations, want) { @@ -361,7 +361,7 @@ func TestUnit_Attributes_ValidConfigurations(t *testing.T) { } func TestUnit_Attributes_SilentlyIgnoredOnUpdate(t *testing.T) { - if a := attribute(t, thingTree(t), "notes"); !a.SilentlyIgnoredOnUpdate { + if a := attribute(t, thingTree(t), "notes"); !a.IgnoredOnUpdate { t.Errorf("x-tfpfgen-ignored-on-update not carried: %+v", a) } } @@ -428,21 +428,21 @@ components: text: {type: string} ` resource := resourceByKey(t, mustDerive(t, spec, testConfig()), "code") - id := resource.Schema.Attributes[0] + id := resource.Attributes.Attributes[0] if id.Name != "id" || id.ComputedOptionalRequired != Computed { t.Fatalf("first attribute = %+v, want the synthesized computed id", id) } if id.WireName != "codeNumber" { t.Errorf("id maps from %q, want the item path parameter", id.WireName) } - if id.Kind != TypeInt64 { - t.Errorf("id kind = %q, want the parameter's integer", id.Kind) + if id.Type != TypeInt64 { + t.Errorf("id kind = %q, want the parameter's integer", id.Type) } } // Shapes derivation refuses to guess at: unions, missing types, arrays // without a usable element. -func TestUnit_Attributes_RefusedShapes(t *testing.T) { +func TestUnit_Attributes_ExcludedShapes(t *testing.T) { const spec = `openapi: 3.0.3 info: {title: T, version: "1"} paths: @@ -496,9 +496,9 @@ components: "grid": `array of "array"`, "blobs": "free-form", } { - derived := attribute(t, resource.Schema, name) + derived := attribute(t, resource.Attributes, name) if !derived.Unsupported { - t.Errorf("%s: derived %q instead of refusing", name, derived.Kind) + t.Errorf("%s: derived %q instead of refusing", name, derived.Type) continue } if !strings.Contains(derived.UnsupportedReason, wantReason) { @@ -549,20 +549,20 @@ components: extra: {type: integer} ` resource := resourceByKey(t, mustDerive(t, spec, testConfig()), "part") - if a := attribute(t, resource.Schema, "core"); a.ComputedOptionalRequired != Required { + if a := attribute(t, resource.Attributes, "core"); a.ComputedOptionalRequired != Required { t.Errorf("core = %+v, want the branch's required to hold", a) } - if a := attribute(t, resource.Schema, "extra"); a.Kind != TypeInt64 { + if a := attribute(t, resource.Attributes, "extra"); a.Type != TypeInt64 { t.Errorf("extra = %+v", a) } } func TestUnit_EnsureParentParameters_AddsWhatNoBodyDeclares(t *testing.T) { tree := &AttributeTree{Attributes: []Attribute{ - {Name: "id", WireName: "id", Kind: TypeString, ComputedOptionalRequired: Computed}, - {Name: "name", WireName: "name", Kind: TypeString, ComputedOptionalRequired: Required}, + {Name: "id", WireName: "id", Type: TypeString, ComputedOptionalRequired: Computed}, + {Name: "name", WireName: "name", Type: TypeString, ComputedOptionalRequired: Required}, }} - ensureParentParameters(tree, []Parameter{ + ensureParentParameters(tree, []URLPathParameter{ {Name: "owner", Type: TypeString}, {Name: "repo", Type: TypeString}, }, "") @@ -589,9 +589,9 @@ func TestUnit_EnsureParentParameters_AddsWhatNoBodyDeclares(t *testing.T) { func TestUnit_EnsureParentParameters_LeavesWhatTheBodyAlreadyDeclares(t *testing.T) { // The document is a better authority on its own field than the URL is. tree := &AttributeTree{Attributes: []Attribute{ - {Name: "owner", WireName: "owner", Kind: TypeString, ComputedOptionalRequired: Optional}, + {Name: "owner", WireName: "owner", Type: TypeString, ComputedOptionalRequired: Optional}, }} - ensureParentParameters(tree, []Parameter{{Name: "owner", Type: TypeString}}, "") + ensureParentParameters(tree, []URLPathParameter{{Name: "owner", Type: TypeString}}, "") if len(tree.Attributes) != 1 { t.Fatalf("a declared parent must not be added twice, got %d", len(tree.Attributes)) @@ -602,7 +602,7 @@ func TestUnit_EnsureParentParameters_LeavesWhatTheBodyAlreadyDeclares(t *testing } func TestUnit_ParentParameters_DropsTheItemKey(t *testing.T) { - parameters := []Parameter{{Name: "owner"}, {Name: "repo"}, {Name: "ruleset_id"}} + parameters := []URLPathParameter{{Name: "owner"}, {Name: "repo"}, {Name: "ruleset_id"}} got := parentParameters(parameters) if len(got) != 2 || got[0].Name != "owner" || got[1].Name != "repo" { t.Fatalf("want owner and repo, got %+v", got) @@ -759,13 +759,13 @@ func TestUnit_BuildTree_UpdateDifferenceRecursesIntoNestedObjects(t *testing.T) tree := buildAttributeTree(create, create, update, false) server := attribute(t, tree, "server") - if server.Nested == nil { + if server.NestedAttributes == nil { t.Fatal("server must derive a nested tree") } - if a := attribute(t, server.Nested, "tenant_id"); !a.RequiresReplace { + if a := attribute(t, server.NestedAttributes, "tenant_id"); !a.RequiresReplace { t.Error("server.tenant_id is absent from the nested update schema, so it must force replacement") } - if a := attribute(t, server.Nested, "host"); a.RequiresReplace { + if a := attribute(t, server.NestedAttributes, "host"); a.RequiresReplace { t.Error("server.host is declared by the nested update schema, so it must not force replacement") } } diff --git a/internal/intermediate_representation/authority_test.go b/internal/intermediate_representation/authority_test.go index b118d43..daba43f 100644 --- a/internal/intermediate_representation/authority_test.go +++ b/internal/intermediate_representation/authority_test.go @@ -97,13 +97,13 @@ components: // the risk docs/contract.md accepts on requestDefault unmeasurable. func TestUnit_IntermediateRepresentation_AnAttributeRecordsTheAuthorityBehindItsPresence(t *testing.T) { t.Parallel() - tree := resourceByKey(t, mustDerive(t, authoritySpec, testConfig()), "widget").Schema + tree := resourceByKey(t, mustDerive(t, authoritySpec, testConfig()), "widget").Attributes - for name, want := range map[string]Authority{ - "measured": AuthorityServerDefault, - "asserted": AuthorityResponseRequired, - "implied": AuthorityRequestDefault, - "described": AuthorityResponseProperty, + for name, want := range map[string]SchemaAttributeTypeDetermination{ + "measured": SchemaAttributeTypeDeterminationServerDefault, + "asserted": SchemaAttributeTypeDeterminationResponseRequired, + "implied": SchemaAttributeTypeDeterminationRequestDefault, + "described": SchemaAttributeTypeDeterminationResponseProperty, } { got := attribute(t, tree, name) if got.ComputedOptionalRequired != ComputedOptional { @@ -111,8 +111,8 @@ func TestUnit_IntermediateRepresentation_AnAttributeRecordsTheAuthorityBehindIts name, got.ComputedOptionalRequired, ComputedOptional) continue } - if got.Authority != want { - t.Errorf("%s: authority = %q, want %q", name, got.Authority, want) + if got.SchemaAttributeTypeDetermination != want { + t.Errorf("%s: authority = %q, want %q", name, got.SchemaAttributeTypeDetermination, want) } } } @@ -123,12 +123,12 @@ func TestUnit_IntermediateRepresentation_AnAttributeRecordsTheAuthorityBehindIts // them would name a choice that was never made. func TestUnit_IntermediateRepresentation_OnlyAComputedOptionalCarriesAnAuthority(t *testing.T) { t.Parallel() - tree := resourceByKey(t, mustDerive(t, authoritySpec, testConfig()), "widget").Schema + tree := resourceByKey(t, mustDerive(t, authoritySpec, testConfig()), "widget").Attributes if id := attribute(t, tree, "id"); id.ComputedOptionalRequired == ComputedOptional { t.Fatalf("id = %+v, want a presence no declaration competes for", id) - } else if id.Authority != "" { - t.Errorf("id carries authority %q, want none for a %q attribute", id.Authority, id.ComputedOptionalRequired) + } else if id.SchemaAttributeTypeDetermination != "" { + t.Errorf("id carries authority %q, want none for a %q attribute", id.SchemaAttributeTypeDetermination, id.ComputedOptionalRequired) } } @@ -139,17 +139,17 @@ func TestUnit_IntermediateRepresentation_OnlyAComputedOptionalCarriesAnAuthority // member that was never made. func TestUnit_IntermediateRepresentation_AMemberPromotedByItsParentCarriesNoAuthority(t *testing.T) { t.Parallel() - tree := resourceByKey(t, mustDerive(t, authoritySpec, testConfig()), "widget").Schema + tree := resourceByKey(t, mustDerive(t, authoritySpec, testConfig()), "widget").Attributes parent := attribute(t, tree, "nested") - if parent.ComputedOptionalRequired != ComputedOptional || parent.Nested == nil { + if parent.ComputedOptionalRequired != ComputedOptional || parent.NestedAttributes == nil { t.Fatalf("nested = %+v, want a server-filled object", parent) } - member := attribute(t, parent.Nested, "inner") + member := attribute(t, parent.NestedAttributes, "inner") if member.ComputedOptionalRequired != ComputedOptional { t.Fatalf("inner = %+v, want promoted by its parent", member) } - if member.Authority != "" { - t.Errorf("inner carries authority %q; its presence came from its parent, not a declaration about it", member.Authority) + if member.SchemaAttributeTypeDetermination != "" { + t.Errorf("inner carries authority %q; its presence came from its parent, not a declaration about it", member.SchemaAttributeTypeDetermination) } } diff --git a/internal/intermediate_representation/constraints_test.go b/internal/intermediate_representation/constraints_test.go index c620af6..466d538 100644 --- a/internal/intermediate_representation/constraints_test.go +++ b/internal/intermediate_representation/constraints_test.go @@ -78,7 +78,7 @@ components: func TestUnit_Attribute_CarriesTheDeclaredConstraints(t *testing.T) { resource := resourceByKey(t, mustDerive(t, constrainedSpec, testConfig()), "key") - secret := attribute(t, resource.Schema, "secret") + secret := attribute(t, resource.Attributes, "secret") if !secret.WriteOnly { t.Error("WriteOnly not carried") } @@ -91,11 +91,11 @@ func TestUnit_Attribute_CarriesTheDeclaredConstraints(t *testing.T) { assertBound(t, "MinLength", secret.MinLength, 8) assertBound(t, "MaxLength", secret.MaxLength, 64) - if !attribute(t, resource.Schema, "legacy").Deprecated { + if !attribute(t, resource.Attributes, "legacy").Deprecated { t.Error("Deprecated not carried") } - weight := attribute(t, resource.Schema, "weight") + weight := attribute(t, resource.Attributes, "weight") if weight.Minimum == nil || *weight.Minimum != 1 { t.Errorf("Minimum = %v", weight.Minimum) } @@ -103,14 +103,14 @@ func TestUnit_Attribute_CarriesTheDeclaredConstraints(t *testing.T) { t.Errorf("Maximum = %v", weight.Maximum) } - tags := attribute(t, resource.Schema, "tags") + tags := attribute(t, resource.Attributes, "tags") if !tags.UniqueItems { t.Error("UniqueItems not carried") } assertBound(t, "MinItems", tags.MinItems, 1) assertBound(t, "MaxItems", tags.MaxItems, 5) - plain := attribute(t, resource.Schema, "plain") + plain := attribute(t, resource.Attributes, "plain") if plain.WriteOnly || plain.Deprecated || plain.UniqueItems || plain.Format != "" || plain.Pattern != "" || plain.MinLength != nil || plain.MaxLength != nil { @@ -165,11 +165,11 @@ components: ` resource := resourceByKey(t, mustDerive(t, spec, testConfig()), "account") for _, name := range []string{"formatted", "write_only", "both"} { - if !attribute(t, resource.Schema, name).Sensitive { + if !attribute(t, resource.Attributes, name).Sensitive { t.Errorf("%q is a declared secret and is not marked sensitive", name) } } - if attribute(t, resource.Schema, "plain").Sensitive { + if attribute(t, resource.Attributes, "plain").Sensitive { t.Error("an ordinary attribute is marked sensitive") } } @@ -235,27 +235,27 @@ components: // A false or an empty default is a declaration like any other: the test // is that the document states one, not that the value is truthy. for _, name := range []string{"retries", "enabled", "label"} { - if got := attribute(t, resource.Schema, name).ComputedOptionalRequired; got != ComputedOptional { + if got := attribute(t, resource.Attributes, name).ComputedOptionalRequired; got != ComputedOptional { t.Errorf("%q with a declared default = %q, want computed_optional", name, got) } } // note declares no default and the response does not describe it, so // nothing says the server fills it: the one route left to plain Optional. - if got := attribute(t, resource.Schema, "note").ComputedOptionalRequired; got != Optional { + if got := attribute(t, resource.Attributes, "note").ComputedOptionalRequired; got != Optional { t.Errorf("%q declares no default and became %q", "note", got) } // A default on the response side says nothing about what happens when a // request omits the field, and summary is not writable at all. - if got := attribute(t, resource.Schema, "summary").ComputedOptionalRequired; got != Computed { + if got := attribute(t, resource.Attributes, "summary").ComputedOptionalRequired; got != Computed { t.Errorf("a response-only property with a default = %q, want computed", got) } } -// TestUnit_Attribute_RefusesAReservedRootName proves a root attribute +// TestUnit_Attribute_ExcludesAReservedRootName proves a root attribute // terraform reserves is refused rather than emitted. Declaring one costs the // whole provider: terraform rejects the schema and loads none of it, so the // entity beside it goes too. -func TestUnit_Attribute_RefusesAReservedRootName(t *testing.T) { +func TestUnit_Attribute_ExcludesAReservedRootName(t *testing.T) { const spec = `openapi: 3.0.3 info: {title: T, version: "1"} paths: @@ -296,7 +296,7 @@ components: resource := resourceByKey(t, mustDerive(t, spec, testConfig()), "group") for _, name := range []string{"count", "lifecycle"} { - got := attribute(t, resource.Schema, name) + got := attribute(t, resource.Attributes, name) if !got.Unsupported { t.Errorf("%q is reserved at the root and was emitted anyway", name) } @@ -304,17 +304,17 @@ components: t.Errorf("%q refused for %q, which does not say why", name, got.UnsupportedReason) } } - if attribute(t, resource.Schema, "name").Unsupported { + if attribute(t, resource.Attributes, "name").Unsupported { t.Error("an ordinary root attribute was refused") } // The same name nested inside an object is an ordinary field: terraform // reserves it only where a practitioner would write a meta-argument. - nested := attribute(t, resource.Schema, "nested") - if nested.Nested == nil { + nested := attribute(t, resource.Attributes, "nested") + if nested.NestedAttributes == nil { t.Fatalf("nested = %+v", nested) } - if attribute(t, nested.Nested, "count").Unsupported { + if attribute(t, nested.NestedAttributes, "count").Unsupported { t.Error("a reserved name nested inside an object was refused") } } @@ -387,13 +387,13 @@ components: func TestUnit_Attribute_CarriesTheDeclaredExample(t *testing.T) { resource := resourceByKey(t, mustDerive(t, exampleSpec, testConfig()), "stream") - if got := attribute(t, resource.Schema, "endpoint_url").Example; got != "https://api.example.otel-collector" { + if got := attribute(t, resource.Attributes, "endpoint_url").Example; got != "https://api.example.otel-collector" { t.Errorf("Example = %#v, want the one declared on the referenced schema", got) } - if got := attribute(t, resource.Schema, "retries").Example; got != 300 { + if got := attribute(t, resource.Attributes, "retries").Example; got != 300 { t.Errorf("Example = %#v, want the declared number", got) } - if got := attribute(t, resource.Schema, "plain").Example; got != nil { + if got := attribute(t, resource.Attributes, "plain").Example; got != nil { t.Errorf("a property declaring no example carries %#v", got) } } @@ -457,7 +457,7 @@ components: func TestUnit_Attribute_TheIdentifierPropertyNamesTheIdsWire(t *testing.T) { resource := resourceByKey(t, mustDerive(t, identifierPropertySpec, testConfig()), "group") - if id := attribute(t, resource.Schema, "id"); id.WireName != "aid" { + if id := attribute(t, resource.Attributes, "id"); id.WireName != "aid" { t.Errorf("the id attribute reads %q, want the property the response carries", id.WireName) } } @@ -469,7 +469,7 @@ func TestUnit_Attribute_TheIdWireFallsBackToThePathParameter(t *testing.T) { plain := strings.Replace(identifierPropertySpec, " x-tfpfgen-identifier-property: aid\n", "", 1) resource := resourceByKey(t, mustDerive(t, plain, testConfig()), "group") - if id := attribute(t, resource.Schema, "id"); id.WireName != "id" { + if id := attribute(t, resource.Attributes, "id"); id.WireName != "id" { t.Errorf("the id attribute reads %q, want the path parameter's name", id.WireName) } } diff --git a/internal/intermediate_representation/derive.go b/internal/intermediate_representation/derive.go index 40f25d8..de7ee59 100644 --- a/internal/intermediate_representation/derive.go +++ b/internal/intermediate_representation/derive.go @@ -327,7 +327,7 @@ func (indexer *operationIndexer) resource(classification specmodel.Classificatio } } ensureID(tree, keyParameter, keyAttributeType) - refuseReservedRootNames(tree) + excludeReservedRootNames(tree) readOperation := indexer.operation(classification.Read, OperationRead) if readOperation != nil { parents := parentParameters(readOperation.PathParameters) @@ -361,12 +361,12 @@ func (indexer *operationIndexer) resource(classification specmodel.Classificatio Delete: indexer.operation(classification.Delete, OperationDelete), List: indexer.operation(classification.List, OperationList), }, - Schema: tree, + Attributes: tree, MissingUpdate: classification.MissingUpdate, Singleton: classification.Singleton, ParentEntity: parentEntity, UpdateStyle: updateStyle, - EventualConsistency: maxEventualConsistency(resolvedCreate, resolvedRead, resolvedUpdate, resolvedDelete), + ReadAfterWriteDelay: longestReadAfterWriteDelay(resolvedCreate, resolvedRead, resolvedUpdate, resolvedDelete), DeleteNotFoundOK: deleteNotFoundOK, Timeouts: defaultTimeouts(), ListWrapperKey: listWrapperKey(indexer.resolveOperation(classification.List)), @@ -470,7 +470,7 @@ func (indexer *operationIndexer) datasource(classification specmodel.Classificat if classification.LookupByKey { requireKey(itemTree, keyParameter, keyAttributeType) - refuseReservedRootNames(itemTree) + excludeReservedRootNames(itemTree) readOperation := indexer.operation(classification.Read, OperationRead) if readOperation != nil { ensureParentParameters(itemTree, parentParameters(readOperation.PathParameters), "") @@ -478,7 +478,7 @@ func (indexer *operationIndexer) datasource(classification specmodel.Classificat return Datasource{ Names: names, Operations: Operations{Read: readOperation}, - Schema: itemTree, + Attributes: itemTree, LookupByKey: true, KeyParameter: keyParameter, } @@ -499,15 +499,15 @@ func (indexer *operationIndexer) datasource(classification specmodel.Classificat } ensureFilterAttributes(companionTree, itemTree) companionTree.Attributes = append(companionTree.Attributes, Attribute{ - Name: "items", WireName: "items", Kind: TypeList, ElementType: TypeObject, - ComputedOptionalRequired: Computed, Nested: itemTree, + Name: "items", WireName: "items", Type: TypeList, ElementType: TypeObject, + ComputedOptionalRequired: Computed, NestedAttributes: itemTree, }) - refuseReservedRootNames(companionTree) + excludeReservedRootNames(companionTree) return Datasource{ Names: names, Operations: Operations{Read: indexer.operation(classification.Read, OperationRead), List: listOperation}, - Schema: companionTree, + Attributes: companionTree, ListWrapperKey: listWrapperKey(indexer.resolveOperation(classification.List)), } } @@ -519,8 +519,8 @@ func (indexer *operationIndexer) listResource(classification specmodel.Classific resolvedList := indexer.resolveOperation(classification.List) element := listElementSchema(resolvedList) listOperation := *indexer.operation(classification.List, OperationList) - addressing := addressingSchema(listOperation.PathParameters) - refuseReservedRootNames(addressing) + addressing := addressingAttributes(listOperation.PathParameters) + excludeReservedRootNames(addressing) // A list result is an identity, and an identity is the resource's id. The // resource takes its id from the item path key where the object declares @@ -536,11 +536,11 @@ func (indexer *operationIndexer) listResource(classification specmodel.Classific ensureID(tree, keyParameter, keyAttributeType) return ListResource{ - Names: names, - ListOperation: listOperation, - Schema: tree, - AddressingSchema: addressing, - ListWrapperKey: listWrapperKey(resolvedList), + Names: names, + ListOperation: listOperation, + Attributes: tree, + AddressingAttributes: addressing, + ListWrapperKey: listWrapperKey(resolvedList), } } @@ -549,13 +549,13 @@ func (indexer *operationIndexer) action(classification specmodel.Classification, var request *AttributeTree if resolvedCreate != nil && resolvedCreate.RequestBody != nil { request = buildAttributeTree(resolvedCreate.RequestBody, nil, nil, false) - refuseReservedRootNames(request) + excludeReservedRootNames(request) } return Action{ - Names: names, - InvokeOperation: *indexer.operation(classification.Create, OperationInvoke), - RequestSchema: request, - ParentEntity: enclosingEntity(classification.CollectionPath, parentKey), + Names: names, + InvokeOperation: *indexer.operation(classification.Create, OperationAction), + RequestAttributes: request, + ParentEntity: enclosingEntity(classification.CollectionPath, parentKey), } } @@ -581,14 +581,14 @@ var templateParameterPattern = regexp.MustCompile(`\{([^}]+)\}`) // pathParameters lists an operation's path parameters in path-template order — // the order any call expression will take them — typed from the declared // parameter schema, string when the document does not say. -func pathParameters(pathTemplate string, operation *specmodel.Operation) []Parameter { +func pathParameters(pathTemplate string, operation *specmodel.Operation) []URLPathParameter { matches := templateParameterPattern.FindAllStringSubmatch(pathTemplate, -1) if len(matches) == 0 { return nil } - out := make([]Parameter, 0, len(matches)) + out := make([]URLPathParameter, 0, len(matches)) for _, match := range matches { - parameter := Parameter{Name: match[1], Type: TypeString} + parameter := URLPathParameter{Name: match[1], Type: TypeString} if operation != nil { for _, declared := range operation.Parameters { if declared.In == "path" && declared.Name == match[1] { @@ -656,10 +656,10 @@ func successCode(operation *specmodel.Operation) int { return 0 } -// maxEventualConsistency takes the largest declared read-after-write lag +// longestReadAfterWriteDelay takes the largest declared read-after-write lag // across the lifecycle: whichever operation observed the worst lag governs // how long generated code must be prepared to wait. -func maxEventualConsistency(operations ...*specmodel.Operation) time.Duration { +func longestReadAfterWriteDelay(operations ...*specmodel.Operation) time.Duration { var longest time.Duration for _, operation := range operations { if operation == nil { diff --git a/internal/intermediate_representation/derive_list_resource_test.go b/internal/intermediate_representation/derive_list_resource_test.go index 880f24f..8af55f7 100644 --- a/internal/intermediate_representation/derive_list_resource_test.go +++ b/internal/intermediate_representation/derive_list_resource_test.go @@ -56,17 +56,17 @@ func TestUnit_Derive_ListResourceTakesItsIDFromTheItemPathKey(t *testing.T) { model := mustDerive(t, keyedSpec, testConfig()) listResource := listResourceByKey(t, model, "widget") - id := attribute(t, listResource.Schema, "id") + id := attribute(t, listResource.Attributes, "id") if id.WireName != "widgetId" { t.Errorf("the list element's id reads %q, want the item path key %q", id.WireName, "widgetId") } - if id.Kind != TypeString || id.ComputedOptionalRequired != Computed { + if id.Type != TypeString || id.ComputedOptionalRequired != Computed { t.Errorf("id = %+v, want a computed string", id) } // The resource is addressed by the same key, and terraform matches the // two by type name: an identity that named a different field would list // identities the resource cannot be imported by. - if got := attribute(t, resourceByKey(t, model, "widget").Schema, "id"); got.WireName != id.WireName { + if got := attribute(t, resourceByKey(t, model, "widget").Attributes, "id"); got.WireName != id.WireName { t.Errorf("resource id reads %q but the list element's reads %q", got.WireName, id.WireName) } } @@ -76,7 +76,7 @@ func TestUnit_Derive_ListResourceTakesItsIDFromTheItemPathKey(t *testing.T) { func TestUnit_Derive_ListResourceKeepsAnElementsOwnID(t *testing.T) { model := mustDerive(t, thingSpec, testConfig()) - id := attribute(t, listResourceByKey(t, model, "thing").Schema, "id") + id := attribute(t, listResourceByKey(t, model, "thing").Attributes, "id") if id.WireName != "id" { t.Errorf("id reads %q, want the element's own %q", id.WireName, "id") } @@ -99,7 +99,7 @@ func TestUnit_Derive_ListResource(t *testing.T) { listResource.Names.TerraformType, resource.Names.TerraformType) } for _, name := range []string{"name", "id"} { - if a := attribute(t, listResource.Schema, name); a.ComputedOptionalRequired != Computed { + if a := attribute(t, listResource.Attributes, name); a.ComputedOptionalRequired != Computed { t.Errorf("%q = %+v", name, a) } } diff --git a/internal/intermediate_representation/derive_test.go b/internal/intermediate_representation/derive_test.go index 28ebd8c..f725af3 100644 --- a/internal/intermediate_representation/derive_test.go +++ b/internal/intermediate_representation/derive_test.go @@ -75,8 +75,8 @@ func TestUnit_Derive_ResourceLifecycle(t *testing.T) { if resource.UpdateStyle != UpdateStylePutFull { t.Errorf("UpdateStyle = %q, want the declared put-full", resource.UpdateStyle) } - if resource.EventualConsistency != 30*time.Second { - t.Errorf("EventualConsistency = %v, want 30s", resource.EventualConsistency) + if resource.ReadAfterWriteDelay != 30*time.Second { + t.Errorf("EventualConsistency = %v, want 30s", resource.ReadAfterWriteDelay) } if !resource.DeleteNotFoundOK { t.Errorf("DeleteNotFoundOK not carried from the delete operation") @@ -98,7 +98,7 @@ func TestUnit_Derive_ResourceLifecycle(t *testing.T) { if deleteOperation.PathTemplate != "/v7/things/{thingId}" || deleteOperation.SuccessCode != 204 { t.Errorf("delete op = %+v", deleteOperation) } - want := []Parameter{{Name: "thingId", Type: TypeString}} + want := []URLPathParameter{{Name: "thingId", Type: TypeString}} if !reflect.DeepEqual(deleteOperation.PathParameters, want) { t.Errorf("delete path parameters = %+v, want %+v", deleteOperation.PathParameters, want) } @@ -153,8 +153,8 @@ components: if resource.Names.APIVersionDirectory != "v1" { t.Errorf("APIVersionDirectory = %q, want the v1 default", resource.Names.APIVersionDirectory) } - if resource.EventualConsistency != 0 { - t.Errorf("EventualConsistency = %v with none declared", resource.EventualConsistency) + if resource.ReadAfterWriteDelay != 0 { + t.Errorf("EventualConsistency = %v with none declared", resource.ReadAfterWriteDelay) } if resource.DeleteNotFoundOK { t.Errorf("DeleteNotFoundOK true with none declared") @@ -206,12 +206,12 @@ components: t.Errorf("UpdateStyle = %q on a resource with no update", resource.UpdateStyle) } for _, name := range []string{"label", "color"} { - if a := attribute(t, resource.Schema, name); !a.RequiresReplace { + if a := attribute(t, resource.Attributes, name); !a.RequiresReplace { t.Errorf("writable %q lacks RequiresReplace under MissingUpdate", name) } } for _, name := range []string{"serial", "id"} { - if a := attribute(t, resource.Schema, name); a.RequiresReplace { + if a := attribute(t, resource.Attributes, name); a.RequiresReplace { t.Errorf("computed %q carries RequiresReplace", name) } } @@ -231,32 +231,32 @@ func TestUnit_Derive_CompanionDatasource(t *testing.T) { // optional and typed as the field it selects on. A filter is how a // caller names the object it wants instead of counting to it. for _, name := range []string{"name", "quantity", "enabled"} { - derived := attribute(t, datasource.Schema, name) - if !derived.Filter || derived.ComputedOptionalRequired != Optional { + derived := attribute(t, datasource.Attributes, name) + if !derived.IsDatasourceFilterArgument || derived.ComputedOptionalRequired != Optional { t.Errorf("%s = %+v, want an optional filter", name, derived) } } - if got := attribute(t, datasource.Schema, "quantity").Kind; got != TypeInt64 { + if got := attribute(t, datasource.Attributes, "quantity").Type; got != TypeInt64 { t.Errorf("a filter takes the type of the field it selects on, got %s", got) } // Nested fields are not filters: HCL would have to describe the whole // object to match one leaf of it. - for _, candidate := range datasource.Schema.Attributes { - if candidate.Filter && candidate.Nested != nil { + for _, candidate := range datasource.Attributes.Attributes { + if candidate.IsDatasourceFilterArgument && candidate.NestedAttributes != nil { t.Errorf("nested attribute %q offered as a filter", candidate.Name) } } - items := attribute(t, datasource.Schema, "items") - if items.Kind != TypeList || items.ElementType != TypeObject || items.ComputedOptionalRequired != Computed { + items := attribute(t, datasource.Attributes, "items") + if items.Type != TypeList || items.ElementType != TypeObject || items.ComputedOptionalRequired != Computed { t.Errorf("items = %+v", items) } // Inside items everything is computed: the datasource never writes. - for _, candidate := range items.Nested.Attributes { + for _, candidate := range items.NestedAttributes.Attributes { if candidate.ComputedOptionalRequired != Computed { t.Errorf("companion item attribute %q is %s, want computed", candidate.Name, candidate.ComputedOptionalRequired) } } - if candidate := attribute(t, items.Nested, "name"); candidate.ComputedOptionalRequired != Computed { + if candidate := attribute(t, items.NestedAttributes, "name"); candidate.ComputedOptionalRequired != Computed { t.Errorf("name inside items = %+v", candidate) } } @@ -273,14 +273,14 @@ func TestUnit_Derive_LookupByKeyDatasource(t *testing.T) { if datasource.Operations.Read == nil || datasource.Operations.List != nil { t.Errorf("lookup ops = %+v", datasource.Operations) } - key := attribute(t, datasource.Schema, "setting_name") - if key.ComputedOptionalRequired != Required || key.Kind != TypeString || key.WireName != "settingName" { + key := attribute(t, datasource.Attributes, "setting_name") + if key.ComputedOptionalRequired != Required || key.Type != TypeString || key.WireName != "settingName" { t.Errorf("key attribute = %+v", key) } - if a := attribute(t, datasource.Schema, "id"); a.ComputedOptionalRequired != Computed { + if a := attribute(t, datasource.Attributes, "id"); a.ComputedOptionalRequired != Computed { t.Errorf("id = %+v", a) } - if a := attribute(t, datasource.Schema, "value"); a.ComputedOptionalRequired != Computed { + if a := attribute(t, datasource.Attributes, "value"); a.ComputedOptionalRequired != Computed { t.Errorf("value = %+v", a) } } @@ -294,8 +294,8 @@ func TestUnit_Derive_ListReadEntityYieldsDatasource(t *testing.T) { if datasource.Operations.List == nil || datasource.Operations.Read == nil { t.Errorf("stream ops = %+v", datasource.Operations) } - items := attribute(t, datasource.Schema, "items") - if a := attribute(t, items.Nested, "topic"); a.ComputedOptionalRequired != Computed { + items := attribute(t, datasource.Attributes, "items") + if a := attribute(t, items.NestedAttributes, "topic"); a.ComputedOptionalRequired != Computed { t.Errorf("topic = %+v", a) } } @@ -305,11 +305,11 @@ func TestUnit_Derive_ListReadEntityYieldsDatasource(t *testing.T) { // after the thing they identify. // and none carries RequiresReplace because a list block has no plan. func TestUnit_AddressingSchema_TakesEveryPathParameter(t *testing.T) { - if tree := addressingSchema(nil); tree != nil { + if tree := addressingAttributes(nil); tree != nil { t.Errorf("a path with no parameters declares no configuration, got %+v", tree) } - tree := addressingSchema([]Parameter{ + tree := addressingAttributes([]URLPathParameter{ {Name: "tenantId", Type: TypeString}, {Name: "groupId", Type: TypeInt64}, }) @@ -317,11 +317,11 @@ func TestUnit_AddressingSchema_TakesEveryPathParameter(t *testing.T) { t.Fatalf("addressingSchema = %+v", tree) } for index, want := range []Attribute{ - {Name: "tenant_id", WireName: "tenantId", Kind: TypeString, ComputedOptionalRequired: Required}, - {Name: "group_id", WireName: "groupId", Kind: TypeInt64, ComputedOptionalRequired: Required}, + {Name: "tenant_id", WireName: "tenantId", Type: TypeString, ComputedOptionalRequired: Required}, + {Name: "group_id", WireName: "groupId", Type: TypeInt64, ComputedOptionalRequired: Required}, } { got := tree.Attributes[index] - if got.Name != want.Name || got.WireName != want.WireName || got.Kind != want.Kind || + if got.Name != want.Name || got.WireName != want.WireName || got.Type != want.Type || got.ComputedOptionalRequired != want.ComputedOptionalRequired { t.Errorf("attribute %d = %+v, want %+v", index, got, want) } @@ -343,11 +343,11 @@ func TestUnit_Derive_Action(t *testing.T) { if action.ParentEntity != "thing" { t.Errorf("ParentEntity = %q, want the enclosing thing entity", action.ParentEntity) } - if action.InvokeOperation.Kind != OperationInvoke || action.InvokeOperation.SuccessCode != 202 { + if action.InvokeOperation.Kind != OperationAction || action.InvokeOperation.SuccessCode != 202 { t.Errorf("invoke op = %+v", action.InvokeOperation) } - force := attribute(t, action.RequestSchema, "force") - if force.Kind != TypeBool || force.ComputedOptionalRequired != Required { + force := attribute(t, action.RequestAttributes, "force") + if force.Type != TypeBool || force.ComputedOptionalRequired != Required { t.Errorf("force = %+v", force) } } @@ -366,8 +366,8 @@ paths: t.Fatalf("%d actions", len(model.Actions)) } action := model.Actions[0] - if action.RequestSchema != nil { - t.Errorf("a bodiless action carries a request schema: %+v", action.RequestSchema) + if action.RequestAttributes != nil { + t.Errorf("a bodiless action carries a request schema: %+v", action.RequestAttributes) } if action.ParentEntity != "" { t.Errorf("ParentEntity = %q with nothing enclosing", action.ParentEntity) diff --git a/internal/intermediate_representation/map_test.go b/internal/intermediate_representation/map_test.go index e758509..45464aa 100644 --- a/internal/intermediate_representation/map_test.go +++ b/internal/intermediate_representation/map_test.go @@ -25,16 +25,16 @@ func TestUnit_DeriveMapType_AMapOfObjectsCarriesItsNestedTree(t *testing.T) { if got.Unsupported { t.Fatalf("refused with %q", got.UnsupportedReason) } - if got.Kind != TypeMap || got.ElementType != TypeObject { - t.Fatalf("kind/element = %q/%q, want a map of objects", got.Kind, got.ElementType) + if got.Type != TypeMap || got.ElementType != TypeObject { + t.Fatalf("kind/element = %q/%q, want a map of objects", got.Type, got.ElementType) } - if got.Nested == nil { + if got.NestedAttributes == nil { t.Fatal("the map carries no nested tree, so nothing describes one value") } - if len(got.Nested.Attributes) != 2 { - t.Errorf("the nested tree carries %d attributes, want the value schema's two", len(got.Nested.Attributes)) + if len(got.NestedAttributes.Attributes) != 2 { + t.Errorf("the nested tree carries %d attributes, want the value schema's two", len(got.NestedAttributes.Attributes)) } for _, want := range []string{"colour", "size"} { - attribute(t, got.Nested, want) + attribute(t, got.NestedAttributes, want) } } diff --git a/internal/intermediate_representation/model.go b/internal/intermediate_representation/model.go index a8bb454..78ddc94 100644 --- a/internal/intermediate_representation/model.go +++ b/internal/intermediate_representation/model.go @@ -1,5 +1,3 @@ -// Package intermediate_representation derives the representation the -// emitter and the SDK binders consume: every entity the revised document // classifies, reduced to the decisions generation acts on — names, // operations, attribute trees, lifecycle behaviour. // @@ -28,15 +26,15 @@ type Model struct { Provider Provider `json:"provider"` Resources []Resource `json:"resources,omitempty"` Datasources []Datasource `json:"datasources,omitempty"` - ListResources []ListResource `json:"list_resources,omitempty"` + ListResources []ListResource `json:"listResources,omitempty"` Actions []Action `json:"actions,omitempty"` // ExcludedByConfiguration is what services.exclude dropped, and // ExcludedByClassification what fit no kind. Two decisions taken at // different points and remedied differently — one is an entry the // operator wrote, the other a shape the document does not offer — so // one slice holding both would report them under one label. - ExcludedByConfiguration []UnsupportedEntity `json:"excluded_by_configuration,omitempty"` - ExcludedByClassification []UnsupportedEntity `json:"excluded_by_classification,omitempty"` + ExcludedByConfiguration []UnsupportedEntity `json:"excludedByConfiguration,omitempty"` + ExcludedByClassification []UnsupportedEntity `json:"excludedByClassification,omitempty"` } // Provider is the identity the generated provider publishes under. @@ -66,20 +64,20 @@ type Cause struct { // refusal with no code cannot be grouped with the refusals that share its // fact, which is the whole reason a cause is recorded. const ( - CauseUndeclaredType = "undeclaredType" - CauseUnsupportedType = "unsupportedType" - CauseWritableUnion = "writableUnion" - CauseUnnamedUnionBranch = "unnamedUnionBranch" - CauseEmptyUnion = "emptyUnion" - CauseUntypedAdditionalProperties = "untypedAdditionalProperties" - CauseShapelessObject = "shapelessObject" - CauseMapOfObjects = "mapOfObjects" - CauseMapOfMaps = "mapOfMaps" - CauseUnsupportedMapValue = "unsupportedMapValue" - CauseItemlessArray = "itemlessArray" - CauseFreeFormArrayElement = "freeFormArrayElement" - CauseUnsupportedArrayElement = "unsupportedArrayElement" - CauseReservedRootName = "reservedRootName" + CauseUndeclaredType = "undeclaredType" + CauseUnsupportedType = "unsupportedType" + CauseWritableUnion = "writableUnion" + CauseUnnamedUnionBranch = "unnamedUnionBranch" + CauseEmptyUnion = "emptyUnion" + CauseUntypedAdditionalProperties = "untypedAdditionalProperties" + CauseObjectWithoutPropertiesOrAdditionalProperties = "objectWithoutPropertiesOrAdditionalProperties" + CauseMapOfObjects = "mapOfObjects" + CauseMapOfMaps = "mapOfMaps" + CauseUnsupportedMapValue = "unsupportedMapValue" + CauseItemlessArray = "itemlessArray" + CauseFreeFormArrayElement = "freeFormArrayElement" + CauseUnsupportedArrayElement = "unsupportedArrayElement" + CauseReservedRootName = "reservedRootName" // CauseExcludedByConfiguration is the operator's own services.exclude // entry, which is the one refusal they already know about. @@ -92,15 +90,15 @@ const ( type UnsupportedEntity struct { // Key is the entity key it would have had. Key string `json:"key"` - // Kind is what it would have become, empty for an entity that became + // TerraformBlockType is what it would have become, empty for an entity that became // nothing at all — which is the honest answer for one refused before a // kind was decided. - Kind string `json:"kind,omitempty"` + TerraformBlockType string `json:"terraformBlockType,omitempty"` // CollectionPath is the path it was derived from, and Service and Tag // are where that path and the document place it. An entity refused // before it became anything still belongs somewhere, and a refusal // carrying no location is one nothing can group or act on. - CollectionPath string `json:"collection_path,omitempty"` + CollectionPath string `json:"collectionPath,omitempty"` Service string `json:"service,omitempty"` Tag string `json:"tag,omitempty"` // Cause is the fact behind the refusal, shared with every other entity @@ -132,12 +130,12 @@ const ( type Resource struct { Names Names `json:"names"` Operations Operations `json:"operations"` - // Schema is the attribute tree: the create request schema combined + // Attributes is the attribute tree: the create request schema combined // with the read response schema, response-only fields computed. - Schema *AttributeTree `json:"schema"` + Attributes *AttributeTree `json:"attributes"` // MissingUpdate means the API declares no update operation, so every // writable attribute carries RequiresReplace. - MissingUpdate bool `json:"missing_update,omitempty"` + MissingUpdate bool `json:"missingUpdate,omitempty"` // Singleton means the entity is one object at a fixed path rather than a // member of a collection. It has no create and no delete: the generated // create writes through the api update (put / patch) operation, and the generated delete @@ -146,29 +144,29 @@ type Resource struct { Singleton bool `json:"singleton,omitempty"` // UpdateStyle is how update treats omitted fields. patch-merge is the // default whenever an update operation exists; empty when none does. - UpdateStyle string `json:"update_style,omitempty"` - // EventualConsistency is how long a read may lag a write, the largest + UpdateStyle string `json:"updateStyle,omitempty"` + // ReadAfterWriteDelay is how long a read may lag a write, the largest // x-tfpfgen-read-after-write declared on any lifecycle operation. - EventualConsistency time.Duration `json:"eventual_consistency,omitempty"` + ReadAfterWriteDelay time.Duration `json:"readAfterWriteDelay,omitempty"` // DeleteNotFoundOK means a 404 on delete reads as "already gone". - DeleteNotFoundOK bool `json:"delete_not_found_ok,omitempty"` + DeleteNotFoundOK bool `json:"deleteNotFoundOK,omitempty"` // Timeouts are the generated timeout defaults. Timeouts Timeouts `json:"timeouts"` // CoManagementNote is set when sibling entities derive from the same // underlying collection path family: prose every emitter appends to // the generated schema description verbatim. Empty for an entity with // no siblings. - CoManagementNote string `json:"co_management_note,omitempty"` + CoManagementNote string `json:"coManagementNote,omitempty"` // ListWrapperKey is the wire property the list response wraps its item // array under, read from the list operation's response schema; empty when // the response is a bare array. It drives the generated list-mock // envelope, replacing the assumption that every API wraps under "value". - ListWrapperKey string `json:"list_wrapper_key,omitempty"` + ListWrapperKey string `json:"listWrapperKey,omitempty"` // ParentEntity is the key of the entity whose collection path encloses // this one's; empty at the top level. A singleton's path parameters all // address that parent, and the attribute answering a parameter the // document spells `id` is named after it. - ParentEntity string `json:"parent_entity,omitempty"` + ParentEntity string `json:"parentEntity,omitempty"` } // Datasource is one entity readable outside Terraform's ownership. Every @@ -177,23 +175,23 @@ type Resource struct { type Datasource struct { Names Names `json:"names"` Operations Operations `json:"operations"` - // Schema is the datasource's attribute tree. A companion datasource is + // Attributes is the datasource's attribute tree. A companion datasource is // one optional filter per scalar field of a listed object, and a // computed list of the objects the filters selected. A lookup-by-key // one is the entity's object with the key parameter as its single // required argument. - Schema *AttributeTree `json:"schema"` + Attributes *AttributeTree `json:"attributes"` // LookupByKey means there is no list operation: the caller supplies // the item path parameter and reads the object it identifies. - LookupByKey bool `json:"lookup_by_key,omitempty"` + LookupByKey bool `json:"lookupByKey,omitempty"` // KeyParameter is the item path parameter's wire name when // LookupByKey is set, empty otherwise. - KeyParameter string `json:"key_parameter,omitempty"` + KeyParameter string `json:"keyParameter,omitempty"` // CoManagementNote is the sibling-entity prose; see Resource. - CoManagementNote string `json:"co_management_note,omitempty"` + CoManagementNote string `json:"coManagementNote,omitempty"` // ListWrapperKey is the list response's item-array wrapper key; see // Resource. Empty for a bare array or a lookup-by-key datasource. - ListWrapperKey string `json:"list_wrapper_key,omitempty"` + ListWrapperKey string `json:"listWrapperKey,omitempty"` } // ListResource is the list capability of a managed resource: terraform @@ -201,32 +199,32 @@ type Datasource struct { // Names and exists only where the resource does. type ListResource struct { Names Names `json:"names"` - ListOperation Operation `json:"list_operation"` - // Schema is the element's attribute tree, everything computed. - Schema *AttributeTree `json:"schema"` - // AddressingSchema is the addressing attributes the collection path + ListOperation Operation `json:"listOperation"` + // Attributes is the element's attribute tree, everything computed. + Attributes *AttributeTree `json:"attributes"` + // AddressingAttributes is the addressing attributes the collection path // requires, declared as the list block's own configuration. Nil for a // collection path that takes no parameters. - AddressingSchema *AttributeTree `json:"addressing_schema,omitempty"` + AddressingAttributes *AttributeTree `json:"addressingAttributes,omitempty"` // CoManagementNote is the sibling-entity prose; see Resource. - CoManagementNote string `json:"co_management_note,omitempty"` + CoManagementNote string `json:"coManagementNote,omitempty"` // ListWrapperKey is the list response's item-array wrapper key; see // Resource. Empty for a bare array. - ListWrapperKey string `json:"list_wrapper_key,omitempty"` + ListWrapperKey string `json:"listWrapperKey,omitempty"` } // Action is a POST with no lifecycle complement — an invocation. type Action struct { Names Names `json:"names"` - InvokeOperation Operation `json:"invoke_operation"` - // RequestSchema is the invocation's argument tree, nil when the POST + InvokeOperation Operation `json:"invokeOperation"` + // RequestAttributes is the invocation's argument tree, nil when the POST // takes no body. - RequestSchema *AttributeTree `json:"request_schema,omitempty"` + RequestAttributes *AttributeTree `json:"requestAttributes,omitempty"` // ParentEntity is the key of the entity whose collection path is the // longest prefix of the action's, empty when no entity encloses it. - ParentEntity string `json:"parent_entity,omitempty"` + ParentEntity string `json:"parentEntity,omitempty"` // CoManagementNote is the sibling-entity prose; see Resource. - CoManagementNote string `json:"co_management_note,omitempty"` + CoManagementNote string `json:"coManagementNote,omitempty"` } // Operations holds an entity's operations by role. Slots an entity lacks are nil. @@ -248,30 +246,30 @@ const ( OperationUpdate OperationKind = "update" OperationDelete OperationKind = "delete" OperationList OperationKind = "list" - OperationInvoke OperationKind = "invoke" + OperationAction OperationKind = "invoke" ) // Operation is one operation, dialect-neutral: enough for any SDK binder to find // the call it must attach, and nothing backend-shaped. Binders produce a // parallel structure keyed by these fields; no backend type appears here. type Operation struct { - Kind OperationKind `json:"kind"` - Method string `json:"method"` - PathTemplate string `json:"path_template"` - OperationID string `json:"operation_id,omitempty"` - PathParameters []Parameter `json:"path_parameters,omitempty"` + Kind OperationKind `json:"kind"` + Method string `json:"method"` + PathTemplate string `json:"pathTemplate"` + OperationID string `json:"operationID,omitempty"` + PathParameters []URLPathParameter `json:"pathParameters,omitempty"` // QueryParameters are the query parameters the operation requires, // each with the value the document states for it. A generated call // sends them as constants: they belong to the operation, not to the // object, so no attribute carries them. - QueryParameters []QueryParameter `json:"query_parameters,omitempty"` + QueryParameters []QueryParameter `json:"queryParameters,omitempty"` // SuccessCode is the first declared 2xx status, 0 when only a // default response exists. - SuccessCode int `json:"success_code,omitempty"` + SuccessCode int `json:"successCode,omitempty"` } -// Parameter is one path parameter, in path-template order. -type Parameter struct { +// URLPathParameter is one path parameter, in path-template order. +type URLPathParameter struct { Name string `json:"name"` Type AttributeType `json:"type"` } @@ -316,7 +314,7 @@ const ( ComputedOptional ComputedOptionalRequired = "computed_optional" ) -// Authority names which declaration decided a computed_optional presence, +// SchemaAttributeTypeDetermination names which declaration decided a computed_optional presence, // where four could have. It is empty where none did — see Attribute. // The set is closed and ordered by how strongly the // declaration is grounded, which is the order derivation consults them in: @@ -330,23 +328,23 @@ const ( // attributes that were never meant to move together. Recording the route // makes that accepted risk something an operator can find and correct // rather than only be told about. -type Authority string +type SchemaAttributeTypeDetermination string const ( - // AuthorityServerDefault is the audit's own measurement, taken by + // SchemaAttributeTypeDeterminationServerDefault is the audit's own measurement, taken by // omitting the attribute and reading what came back. It is the only // route that does not depend on the document being diligent. - AuthorityServerDefault Authority = "serverDefault" - // AuthorityResponseRequired is the response schema's required list: + SchemaAttributeTypeDeterminationServerDefault SchemaAttributeTypeDetermination = "serverDefault" + // SchemaAttributeTypeDeterminationResponseRequired is the response schema's required list: // the document asserting the server always answers with a value. - AuthorityResponseRequired Authority = "responseRequired" - // AuthorityRequestDefault is a default on the request property: the + SchemaAttributeTypeDeterminationResponseRequired SchemaAttributeTypeDetermination = "responseRequired" + // SchemaAttributeTypeDeterminationRequestDefault is a default on the request property: the // document stating what the server substitutes for an omitted value. - AuthorityRequestDefault Authority = "requestDefault" - // AuthorityResponseProperty is the response schema describing the + SchemaAttributeTypeDeterminationRequestDefault SchemaAttributeTypeDetermination = "requestDefault" + // SchemaAttributeTypeDeterminationResponseProperty is the response schema describing the // property at all — the weakest route, and the one that catches the // documents which declare nothing required in their responses. - AuthorityResponseProperty Authority = "responseProperty" + SchemaAttributeTypeDeterminationResponseProperty SchemaAttributeTypeDetermination = "responseProperty" ) // AttributeTree is one object's attributes plus the cross-attribute rules @@ -362,35 +360,35 @@ type AttributeTree struct { Description string `json:"description,omitempty"` // ConditionalRequirements aggregates x-tfpfgen-required-when: when // Property equals Equals, the Required attributes must be set. - ConditionalRequirements []ConditionalRequirement `json:"conditional_requirements,omitempty"` + ConditionalRequirements []ConditionalRequirement `json:"conditionalRequirements,omitempty"` // ConditionalValidities aggregates x-tfpfgen-valid-when: the Valid // attributes may be set only when Property equals Equals. - ConditionalValidities []ConditionalValidity `json:"conditional_validities,omitempty"` + ConditionalValidities []ConditionalValidity `json:"conditionalValidities,omitempty"` // Dependencies aggregates x-tfpfgen-depends-on and dependentRequired: an // attribute may be set only when every attribute it Requires is set too. Dependencies []Dependency `json:"dependencies,omitempty"` // MutuallyExclusiveGroups aggregates x-tfpfgen-mutually-exclusive: at // most one attribute in each group may be set. - MutuallyExclusiveGroups [][]string `json:"mutually_exclusive_groups,omitempty"` + MutuallyExclusiveGroups [][]string `json:"mutuallyExclusiveGroups,omitempty"` // ValidConfigurations aggregates x-tfpfgen-valid-configuration: a // discriminator attribute whose value selects which attributes are valid. - ValidConfigurations []ValidConfiguration `json:"valid_configurations,omitempty"` + ValidConfigurations []ValidConfiguration `json:"validConfigurations,omitempty"` } // ConditionalRequirement is one value-conditional rule, attribute names in // terraform spelling. type ConditionalRequirement struct { - Property string `json:"property"` - Equals string `json:"equals"` - Required []string `json:"required"` + Property string `json:"property"` + WhenPropertyEquals string `json:"whenPropertyEquals"` + Required []string `json:"required"` } // ConditionalValidity is one value-conditional validity rule: the Valid // attributes are valid only while Property equals Equals. type ConditionalValidity struct { - Property string `json:"property"` - Equals string `json:"equals"` - Valid []string `json:"valid"` + Property string `json:"property"` + WhenPropertyEquals string `json:"whenPropertyEquals"` + AttributesValidWhenEqual []string `json:"attributesValidWhenEqual"` } // Dependency is one co-requirement: Attribute may be set only when every name @@ -403,14 +401,14 @@ type Dependency struct { // ValidConfiguration is a discriminator variant structure: Discriminator's // value selects which attributes each variant admits. type ValidConfiguration struct { - Discriminator string `json:"discriminator"` - Variants []ConfigVariant `json:"variants"` + Discriminator string `json:"discriminator"` + Variants []ValidConfigurationVariant `json:"variants"` } -// ConfigVariant is one discriminator value and the attributes valid under it. -type ConfigVariant struct { - Value string `json:"value"` - Valid []string `json:"valid"` +// ValidConfigurationVariant is one discriminator value and the attributes valid under it. +type ValidConfigurationVariant struct { + Value string `json:"value"` + AttributesValidWhenEqual []string `json:"attributesValidWhenEqual"` } // Attribute is one schema attribute, fully decided. @@ -418,38 +416,38 @@ type Attribute struct { // Name is the terraform attribute name, snake_case. Name string `json:"name"` // WireName is the property name the API speaks. - WireName string `json:"wire_name"` + WireName string `json:"wireName"` // Description is the document's own prose for the property, empty when // it declares none. It is the only human-written text in the whole // derivation — everything else a generated schema says about an // attribute is inferred — so it leads the rendered description and the // inferred facts follow it. Description string `json:"description,omitempty"` - Kind AttributeType `json:"kind,omitempty"` + Type AttributeType `json:"type,omitempty"` // ElementType is the type within a list or map of scalars; a list or // map of objects carries Nested instead. - ElementType AttributeType `json:"element_type,omitempty"` - // Nested is the child tree of an object attribute or a list of + ElementType AttributeType `json:"elementType,omitempty"` + // NestedAttributes is the child tree of an object attribute or a list of // objects. - Nested *AttributeTree `json:"nested,omitempty"` - ComputedOptionalRequired ComputedOptionalRequired `json:"computed_optional_required"` - // Authority names which declaration decided this attribute's + NestedAttributes *AttributeTree `json:"nestedAttributes,omitempty"` + ComputedOptionalRequired ComputedOptionalRequired `json:"computedOptionalRequired"` + // SchemaAttributeTypeDetermination names which declaration decided this attribute's // computed_optional presence. Empty where no declaration about this // attribute did: any other presence, or a member promoted to // computed_optional because its parent is server-filled, which is a // fact about the parent. - Authority Authority `json:"authority,omitempty"` + SchemaAttributeTypeDetermination SchemaAttributeTypeDetermination `json:"schemaAttributeTypeDetermination,omitempty"` // RequiresReplace marks an attribute a change to which forces // re-creation: x-tfpfgen-immutable, or every writable attribute of // a resource with no update operation. - RequiresReplace bool `json:"requires_replace,omitempty"` + RequiresReplace bool `json:"requiresReplace,omitempty"` // Format is the document's declared format, which says what a string // carries beyond being a string: "password", "date-time", "uuid". Format string `json:"format,omitempty"` // ServerDefault is the value a run read back for this property when a // create omitted it, from x-tfpfgen-server-default; nil when no run has // measured one. It is a fact about the API rather than the document. - ServerDefault any `json:"server_default,omitempty"` + ServerDefault any `json:"serverDefault,omitempty"` // Example is the document's declared example value. Fixture derivation // prefers it to an invented value: a document that declares no format // often still states, through an example, that the value has a shape the @@ -463,45 +461,45 @@ type Attribute struct { Normalisation string `json:"normalisation,omitempty"` // WriteOnly marks a property the API accepts on write and never // returns. - WriteOnly bool `json:"write_only,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty"` // Sensitive marks a value terraform must keep out of its output: the // document either declares it write-only or formats it as a password. Sensitive bool `json:"sensitive,omitempty"` // Deprecated marks a property the document declares deprecated. Deprecated bool `json:"deprecated,omitempty"` - // Filter marks a datasource argument that selects which listed objects + // IsDatasourceFilterArgument marks a datasource argument that selects which listed objects // come back, rather than describing one. It carries no wire value and // binds to no SDK field: the match runs over the items the list already // answered with. - Filter bool `json:"filter,omitempty"` + IsDatasourceFilterArgument bool `json:"isDatasourceFilterArgument,omitempty"` // UniqueItems marks a collection whose members are a set, so the order // they are returned in carries no meaning. - UniqueItems bool `json:"unique_items,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` // The constraints the document declares, nil or empty when it declares // none. Each becomes a plan-time validator, so a configuration the API // would refuse or silently clamp fails before it is sent. Pattern string `json:"pattern,omitempty"` Minimum *float64 `json:"minimum,omitempty"` Maximum *float64 `json:"maximum,omitempty"` - MinLength *int64 `json:"min_length,omitempty"` - MaxLength *int64 `json:"max_length,omitempty"` - MinItems *int64 `json:"min_items,omitempty"` - MaxItems *int64 `json:"max_items,omitempty"` + MinLength *int64 `json:"minLength,omitempty"` + MaxLength *int64 `json:"maxLength,omitempty"` + MinItems *int64 `json:"minItems,omitempty"` + MaxItems *int64 `json:"maxItems,omitempty"` // OneOf lists a closed enum's values for a validator. - OneOf []string `json:"one_of,omitempty"` + OneOf []string `json:"oneOf,omitempty"` // AdvisoryValues lists an open enum's known values // (x-tfpfgen-values): documentation only, never validated. - AdvisoryValues []string `json:"advisory_values,omitempty"` - // SilentlyIgnoredOnUpdate marks a property updates accept and + AdvisoryValues []string `json:"advisoryValues,omitempty"` + // IgnoredOnUpdate marks a property updates accept and // discard; construct skips it on update. - SilentlyIgnoredOnUpdate bool `json:"silently_ignored_on_update,omitempty"` + IgnoredOnUpdate bool `json:"ignoredOnUpdate,omitempty"` // Unsupported marks a shape derivation refuses to guess at (free-form // objects, unions, undeclared types); the reason says which. Unsupported bool `json:"unsupported,omitempty"` // UnsupportedCause is the fact behind the refusal, shared with every // other attribute refused for the same fact. - UnsupportedCause Cause `json:"unsupported_cause,omitempty"` - UnsupportedReason string `json:"unsupported_reason,omitempty"` + UnsupportedCause Cause `json:"unsupportedCause,omitempty"` + UnsupportedReason string `json:"unsupportedReason,omitempty"` } // Timeouts are the generated per-operation timeout defaults. @@ -527,12 +525,12 @@ func defaultTimeouts() Timeouts { // refusal can be grouped by the same keys a generated entity is. func unsupportedEntity(names Names, collectionPath, kind string, cause Cause, reason string) UnsupportedEntity { return UnsupportedEntity{ - Key: names.Key, - Kind: kind, - CollectionPath: collectionPath, - Service: names.Service, - Tag: names.Tag, - Cause: cause, - Reason: reason, + Key: names.Key, + TerraformBlockType: kind, + CollectionPath: collectionPath, + Service: names.Service, + Tag: names.Tag, + Cause: cause, + Reason: reason, } } diff --git a/internal/intermediate_representation/naming.go b/internal/intermediate_representation/naming.go index 27ec808..5074c83 100644 --- a/internal/intermediate_representation/naming.go +++ b/internal/intermediate_representation/naming.go @@ -14,11 +14,11 @@ type Names struct { // APIVersionDirectory rather than living in the name. Key string `json:"key"` // PascalCase is the exported Go type spelling, e.g. "HTTPServer". - PascalCase string `json:"pascal"` + PascalCase string `json:"pascalCase"` // CamelCase is the unexported Go spelling, e.g. "httpServer". - CamelCase string `json:"camel"` + CamelCase string `json:"camelCase"` // TerraformType is "_". - TerraformType string `json:"terraform_type"` + TerraformType string `json:"terraformType"` // Package is the Go package name: the key with its underscores // removed, per Go convention. Package string `json:"package"` @@ -27,7 +27,7 @@ type Names struct { Service string `json:"service"` // APIVersionDirectory is the stripped version segment, "v1" when the path // declares none. - APIVersionDirectory string `json:"api_version_directory"` + APIVersionDirectory string `json:"apiVersionDirectory"` // Tag is the group the document places the entity in, empty when it // declares none. Unlike Service, which this package derives from the // path, it is the vendor's own grouping and is carried rather than diff --git a/internal/intermediate_representation/unions_test.go b/internal/intermediate_representation/unions_test.go index 158b0d3..9dfec3d 100644 --- a/internal/intermediate_representation/unions_test.go +++ b/internal/intermediate_representation/unions_test.go @@ -46,7 +46,7 @@ func TestUnit_ResolveUnion_CollapsesScalarBranches(t *testing.T) { if testCase.wantReason != "" { if !got.Unsupported { - t.Errorf("%s: derived %q, want a refusal", testCase.name, got.Kind) + t.Errorf("%s: derived %q, want a refusal", testCase.name, got.Type) } else if !strings.Contains(got.UnsupportedReason, testCase.wantReason) { t.Errorf("%s: reason = %q", testCase.name, got.UnsupportedReason) } @@ -56,8 +56,8 @@ func TestUnit_ResolveUnion_CollapsesScalarBranches(t *testing.T) { t.Errorf("%s: refused with %q", testCase.name, got.UnsupportedReason) continue } - if got.Kind != testCase.wantKind { - t.Errorf("%s: kind = %q, want %q", testCase.name, got.Kind, testCase.wantKind) + if got.Type != testCase.wantKind { + t.Errorf("%s: kind = %q, want %q", testCase.name, got.Type, testCase.wantKind) } } } @@ -108,7 +108,7 @@ func TestUnit_DeriveUnion_ReadOnlyYieldsAnAttributePerVariant(t *testing.T) { if owner.Unsupported { t.Fatalf("a read-only union refused: %s", owner.UnsupportedReason) } - if owner.Kind != TypeObject || owner.Nested == nil { + if owner.Type != TypeObject || owner.NestedAttributes == nil { t.Fatalf("owner = %+v, want an object carrying its variants", owner) } @@ -119,14 +119,14 @@ func TestUnit_DeriveUnion_ReadOnlyYieldsAnAttributePerVariant(t *testing.T) { {"simple_user", "simple-user", "login"}, {"enterprise", "Enterprise", "slug"}, } { - variant := attribute(t, owner.Nested, want.name) + variant := attribute(t, owner.NestedAttributes, want.name) if variant.WireName != want.wire { t.Errorf("%s wire name = %q, want %q", want.name, variant.WireName, want.wire) } - if variant.Kind != TypeObject || variant.ComputedOptionalRequired != Computed { + if variant.Type != TypeObject || variant.ComputedOptionalRequired != Computed { t.Errorf("%s = %+v, want a computed object", want.name, variant) } - if a := attribute(t, variant.Nested, want.field); a.Kind != TypeString { + if a := attribute(t, variant.NestedAttributes, want.field); a.Type != TypeString { t.Errorf("%s does not carry its branch's own %s: %+v", want.name, want.field, a) } } @@ -135,7 +135,7 @@ func TestUnit_DeriveUnion_ReadOnlyYieldsAnAttributePerVariant(t *testing.T) { // A variant the document does not name has no attribute to become, and half // a union is a schema that cannot hold what the API returns. The refusal says // how many branches are anonymous so the document can be corrected. -func TestUnit_DeriveUnion_AnAnonymousBranchRefusesTheWholeUnion(t *testing.T) { +func TestUnit_DeriveUnion_AnAnonymousBranchExcludesTheWholeUnion(t *testing.T) { tree := buildAttributeTree(nil, mustLoad(t, unionSpec).Schemas["thing"], nil, false) payload := attribute(t, tree, "payload") diff --git a/internal/intermediate_representation/vendor_openapi_specs_test.go b/internal/intermediate_representation/vendor_openapi_specs_test.go index 02ec26e..b853917 100644 --- a/internal/intermediate_representation/vendor_openapi_specs_test.go +++ b/internal/intermediate_representation/vendor_openapi_specs_test.go @@ -30,7 +30,7 @@ func TestIntegration_IntermediateRepresentation_DerivesAPinnedVendorDocument(t * // Nothing about a real document may come out anonymous or untyped at // the top level of its naming. for _, resource := range model.Resources { - if resource.Names.Key == "" || resource.Names.TerraformType == "" || resource.Schema == nil { + if resource.Names.Key == "" || resource.Names.TerraformType == "" || resource.Attributes == nil { t.Errorf("anonymous resource: %+v", resource.Names) } // A singleton is one object at a fixed path: read and updated, diff --git a/internal/providergen/curated_test.go b/internal/providergen/curated_test.go index f18f758..d54f399 100644 --- a/internal/providergen/curated_test.go +++ b/internal/providergen/curated_test.go @@ -102,7 +102,7 @@ func TestUnit_Run_CuratedFixtureGeneratesTheCompleteTree(t *testing.T) { for _, u := range res.Unsupported { if u.Cause == nil || u.Cause.Code == "" { t.Errorf("refusal carries no cause: %s %q attribute %q (%s): %s", - u.Kind, u.Entity, u.Attribute, u.Stage, u.Reason) + u.TerraformBlockType, u.Entity, u.Attribute, u.Stage, u.Reason) } } for _, r := range res.Removals { diff --git a/internal/providergen/providergen.go b/internal/providergen/providergen.go index 9061f55..91ad716 100644 --- a/internal/providergen/providergen.go +++ b/internal/providergen/providergen.go @@ -501,7 +501,7 @@ func allExclusions(m *ir.Model, dropped []sdkbind.Dropped, refused []ir.Unsuppor out := make([]ir.UnsupportedEntity, 0, len(derived)+len(dropped)+len(refused)) out = append(out, derived...) for _, d := range dropped { - out = append(out, locate(located, ir.UnsupportedEntity{Key: d.Key, Kind: d.Kind, Reason: d.Reason})) + out = append(out, locate(located, ir.UnsupportedEntity{Key: d.Key, TerraformBlockType: d.Kind, Reason: d.Reason})) } for _, r := range refused { out = append(out, locate(located, r)) diff --git a/internal/sdkbind/binder.go b/internal/sdkbind/binder.go index 94ff099..0192040 100644 --- a/internal/sdkbind/binder.go +++ b/internal/sdkbind/binder.go @@ -46,7 +46,7 @@ func bindModel(m *ir.Model, info SDKInfo, d dialect) *Bindings { ReadModel: read, WriteModel: write, WriteConstructor: constructor, - Fields: fieldBindings(d, r.Schema, accessReadWrite), + Fields: fieldBindings(d, r.Attributes, accessReadWrite), } if r.Operations.Create != nil { rb.Create = d.call(r.Operations.Create, r.Names, true, info) @@ -82,17 +82,17 @@ func bindModel(m *ir.Model, info SDKInfo, d dialect) *Bindings { Key: lr.Names.Key, List: d.call(&listOp, lr.Names, false, info), EnvelopeKey: lr.ListWrapperKey, - Fields: fieldBindings(d, lr.Schema, accessReadOnly), + Fields: fieldBindings(d, lr.Attributes, accessReadOnly), } } for _, a := range m.Actions { invokeOp := a.InvokeOperation - hasBody := a.RequestSchema != nil + hasBody := a.RequestAttributes != nil ab := &ActionBinding{ Key: a.Names.Key, Invoke: d.call(&invokeOp, a.Names, hasBody, info), - Fields: fieldBindings(d, a.RequestSchema, accessWriteOnly), + Fields: fieldBindings(d, a.RequestAttributes, accessWriteOnly), } if hasBody { _, write, constructor := d.models(a.Names, info) @@ -111,17 +111,17 @@ func bindModel(m *ir.Model, info SDKInfo, d dialect) *Bindings { // SDK side, so they take no bindings. func datasourceElementTree(ds ir.Datasource) *ir.AttributeTree { if ds.LookupByKey { - return ds.Schema + return ds.Attributes } - if ds.Schema == nil { + if ds.Attributes == nil { return nil } - for _, a := range ds.Schema.Attributes { + for _, a := range ds.Attributes.Attributes { if a.Name == "items" { - return a.Nested + return a.NestedAttributes } } - return ds.Schema + return ds.Attributes } // fieldBindings drafts the field accesses of one attribute tree, @@ -147,7 +147,7 @@ func boundFields(d dialect, t *ir.AttributeTree, mode accessMode, underWriteOnly } fb := FieldBinding{ Attr: a.Name, Wire: a.WireName, - Kind: a.Kind, ElementType: a.ElementType, + Type: a.Type, ElementType: a.ElementType, Access: d.access(a, mode), } // A property the document declares write-only is never read back, @@ -161,12 +161,12 @@ func boundFields(d dialect, t *ir.AttributeTree, mode accessMode, underWriteOnly fb.KeptFromPlan = true writeOnly = true } - if a.Nested != nil { + if a.NestedAttributes != nil { nestedMode := mode if writeOnly { nestedMode = accessWriteOnly } - fb.Nested = boundFields(d, a.Nested, nestedMode, writeOnly) + fb.Nested = boundFields(d, a.NestedAttributes, nestedMode, writeOnly) } out = append(out, fb) } diff --git a/internal/sdkbind/binder_kiota.go b/internal/sdkbind/binder_kiota.go index a99e8fc..dbad3af 100644 --- a/internal/sdkbind/binder_kiota.go +++ b/internal/sdkbind/binder_kiota.go @@ -114,7 +114,7 @@ func (kiotaBinder) access(a ir.Attribute, mode accessMode) FieldAccess { fa.Set = "Set" + base } - switch a.Kind { + switch a.Type { case ir.TypeString: fa.SDKType, fa.ConvertGet, fa.ConvertSet = "*string", "FromPtrString", "ToPtrString" case ir.TypeBool: @@ -124,13 +124,13 @@ func (kiotaBinder) access(a ir.Attribute, mode accessMode) FieldAccess { case ir.TypeFloat64: fa.SDKType, fa.ConvertGet, fa.ConvertSet = "*float64", "FromPtrFloat64", "ToPtrFloat64" case ir.TypeList: - if a.Nested == nil { + if a.NestedAttributes == nil { fa.SDKType = "[]" + strings.TrimPrefix(goTypeOf(a.ElementType), "*") shape := exportedName(string(a.ElementType)) + "Slice" fa.ConvertGet, fa.ConvertSet = "From"+shape, "To"+shape } case ir.TypeMap: - if a.Nested == nil { + if a.NestedAttributes == nil { fa.SDKType = "map[string]" + strings.TrimPrefix(goTypeOf(a.ElementType), "*") shape := exportedName(string(a.ElementType)) + "Map" fa.ConvertGet, fa.ConvertSet = "From"+shape, "To"+shape diff --git a/internal/sdkbind/binder_kiota_test.go b/internal/sdkbind/binder_kiota_test.go index 3664227..e2aa38c 100644 --- a/internal/sdkbind/binder_kiota_test.go +++ b/internal/sdkbind/binder_kiota_test.go @@ -27,30 +27,30 @@ func TestKiotaChainSpelling(t *testing.T) { }, { name: "read indexes by the path parameter", - operation: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), want: "client.Tags().ByTagId(tagId).Get(ctx, nil)", }, { name: "update keeps the declared verb", - operation: operation(ir.OperationUpdate, "PATCH", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation: operation(ir.OperationUpdate, "PATCH", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), hasBody: true, want: "client.Tags().ByTagId(tagId).Patch(ctx, body, nil)", }, { name: "delete takes no body", - operation: operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation: operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), want: "client.Tags().ByTagId(tagId).Delete(ctx, nil)", }, { name: "kebab and nested segments become builder hops", operation: operation(ir.OperationRead, "GET", "/v7/http-server/{serverId}/status", "", - ir.Parameter{Name: "serverId", Type: ir.TypeString}), + ir.URLPathParameter{Name: "serverId", Type: ir.TypeString}), want: "client.V7().HttpServer().ByServerId(serverId).Status().Get(ctx, nil)", }, { name: "a keyword-named parameter gets a safe local", operation: operation(ir.OperationRead, "GET", "/types/{type}", "", - ir.Parameter{Name: "type", Type: ir.TypeString}), + ir.URLPathParameter{Name: "type", Type: ir.TypeString}), want: "client.Types().ByType(type_).Get(ctx, nil)", }, } @@ -73,7 +73,7 @@ func TestKiotaChainSpelling(t *testing.T) { func TestKiotaCallDrafts(t *testing.T) { n := names("tags", "tags") read := kiotaBinder{}.call( - operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), n, false, kiotaInfo()) if read.ResponseType != "models.Tagsable" { @@ -89,7 +89,7 @@ func TestKiotaCallDrafts(t *testing.T) { } del := kiotaBinder{}.call( - operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), n, false, kiotaInfo()) if del.ResponseType != "" || len(del.Results) != 1 || del.Results[0] != "error" { t.Errorf("delete draft = response %q results %v, want none and [error]", del.ResponseType, del.Results) @@ -210,14 +210,14 @@ func TestKiotaBindWalk(t *testing.T) { func TestUnit_Binder_AWriteOnlyRootLeavesItsComputedMembersOut(t *testing.T) { tree := &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "agents", WireName: "agents", Kind: ir.TypeList, ElementType: ir.TypeObject, WriteOnly: true, - ComputedOptionalRequired: ir.Required, Nested: &ir.AttributeTree{Attributes: []ir.Attribute{ - {Name: "agent_id", WireName: "agentId", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, - {Name: "agent_name", WireName: "agentName", Kind: ir.TypeString, ComputedOptionalRequired: ir.Computed}, - {Name: "coordinates", WireName: "coordinates", Kind: ir.TypeObject, ComputedOptionalRequired: ir.Computed, - Nested: &ir.AttributeTree{Attributes: []ir.Attribute{{Name: "latitude", WireName: "latitude", Kind: ir.TypeFloat64, ComputedOptionalRequired: ir.Computed}}}}, + {Name: "agents", WireName: "agents", Type: ir.TypeList, ElementType: ir.TypeObject, WriteOnly: true, + ComputedOptionalRequired: ir.Required, NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{ + {Name: "agent_id", WireName: "agentId", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "agent_name", WireName: "agentName", Type: ir.TypeString, ComputedOptionalRequired: ir.Computed}, + {Name: "coordinates", WireName: "coordinates", Type: ir.TypeObject, ComputedOptionalRequired: ir.Computed, + NestedAttributes: &ir.AttributeTree{Attributes: []ir.Attribute{{Name: "latitude", WireName: "latitude", Type: ir.TypeFloat64, ComputedOptionalRequired: ir.Computed}}}}, }}}, - {Name: "name", WireName: "name", Kind: ir.TypeString, ComputedOptionalRequired: ir.Required}, + {Name: "name", WireName: "name", Type: ir.TypeString, ComputedOptionalRequired: ir.Required}, }} got := fieldBindings(kiotaBinder{}, tree, accessReadWrite) if len(got) != 2 || got[0].Attr != "agents" || !got[0].KeptFromPlan || got[0].Access.Get != "" || got[0].Access.Set == "" { diff --git a/internal/sdkbind/binder_openapigenerator.go b/internal/sdkbind/binder_openapigenerator.go index 77c2929..27c576f 100644 --- a/internal/sdkbind/binder_openapigenerator.go +++ b/internal/sdkbind/binder_openapigenerator.go @@ -111,7 +111,7 @@ func (openAPIGeneratorBinder) access(a ir.Attribute, mode accessMode) FieldAcces fa.Set = "Set" + base } - switch a.Kind { + switch a.Type { case ir.TypeString: fa.SDKType, fa.ConvertGet, fa.ConvertSet = "string", "FromString", "ToString" case ir.TypeBool: @@ -121,13 +121,13 @@ func (openAPIGeneratorBinder) access(a ir.Attribute, mode accessMode) FieldAcces case ir.TypeFloat64: fa.SDKType, fa.ConvertGet, fa.ConvertSet = "float64", "FromFloat64", "ToFloat64" case ir.TypeList: - if a.Nested == nil { + if a.NestedAttributes == nil { fa.SDKType = "[]" + goTypeOf(a.ElementType) shape := exportedName(string(a.ElementType)) + "Slice" fa.ConvertGet, fa.ConvertSet = "From"+shape, "To"+shape } case ir.TypeMap: - if a.Nested == nil { + if a.NestedAttributes == nil { fa.SDKType = "map[string]" + goTypeOf(a.ElementType) shape := exportedName(string(a.ElementType)) + "Map" fa.ConvertGet, fa.ConvertSet = "From"+shape, "To"+shape diff --git a/internal/sdkbind/binder_openapigenerator_test.go b/internal/sdkbind/binder_openapigenerator_test.go index 0d19dba..4d6a8ae 100644 --- a/internal/sdkbind/binder_openapigenerator_test.go +++ b/internal/sdkbind/binder_openapigenerator_test.go @@ -51,18 +51,18 @@ func TestOAGCallSpelling(t *testing.T) { }, { name: "read passes the path parameter", - operation: operation(ir.OperationRead, "GET", "/tags/{tagId}", "getTag", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation: operation(ir.OperationRead, "GET", "/tags/{tagId}", "getTag", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), want: "client.TagsAPI.GetTag(ctx, tagId).Execute()", }, { name: "update passes parameter then body", - operation: operation(ir.OperationUpdate, "PATCH", "/tags/{tagId}", "updateTag", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation: operation(ir.OperationUpdate, "PATCH", "/tags/{tagId}", "updateTag", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), hasBody: true, want: "client.TagsAPI.UpdateTag(ctx, tagId).Tags(*body).Execute()", }, { name: "delete", - operation: operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "deleteTag", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation: operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "deleteTag", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), want: "client.TagsAPI.DeleteTag(ctx, tagId).Execute()", }, } @@ -94,7 +94,7 @@ func TestOAGCallDrafts(t *testing.T) { } del := openAPIGeneratorBinder{}.call( - operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "deleteTag", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "deleteTag", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), n, false, info) if del.ResponseType != "" || len(del.Results) != 2 { t.Errorf("delete drafts = response %q results %v", del.ResponseType, del.Results) diff --git a/internal/sdkbind/fixtures_test.go b/internal/sdkbind/fixtures_test.go index 090be62..0fcb3bb 100644 --- a/internal/sdkbind/fixtures_test.go +++ b/internal/sdkbind/fixtures_test.go @@ -48,7 +48,7 @@ func names(key, service string) ir.Names { } } -func operation(kind ir.OperationKind, method, path, opID string, parameters ...ir.Parameter) *ir.Operation { +func operation(kind ir.OperationKind, method, path, opID string, parameters ...ir.URLPathParameter) *ir.Operation { return &ir.Operation{Kind: kind, Method: method, PathTemplate: path, OperationID: opID, PathParameters: parameters} } @@ -63,14 +63,14 @@ func deleteWithQuery(operation *ir.Operation) *ir.Operation { } func attribute(name, wire string, kind ir.AttributeType, participation ir.ComputedOptionalRequired) ir.Attribute { - return ir.Attribute{Name: name, WireName: wire, Kind: kind, ComputedOptionalRequired: participation} + return ir.Attribute{Name: name, WireName: wire, Type: kind, ComputedOptionalRequired: participation} } // filterAttr is one companion datasource filter: toolkit vocabulary with no // SDK side, so binding must leave it alone rather than look for a field. func filterAttr(name string) ir.Attribute { - return ir.Attribute{Name: name, WireName: name, Kind: ir.TypeString, - ComputedOptionalRequired: ir.Optional, Filter: true} + return ir.Attribute{Name: name, WireName: name, Type: ir.TypeString, + ComputedOptionalRequired: ir.Optional, IsDatasourceFilterArgument: true} } // tagSchema is the shared attribute tree both fake SDKs carry variants @@ -82,7 +82,7 @@ func tagSchema() *ir.AttributeTree { attribute("weight", "weight", ir.TypeFloat64, ir.Optional), }} nested := attribute("detail", "detail", ir.TypeObject, ir.Optional) - nested.Nested = detail + nested.NestedAttributes = detail labels := attribute("labels", "labels", ir.TypeList, ir.Optional) labels.ElementType = ir.TypeString @@ -125,7 +125,7 @@ func tagSchema() *ir.AttributeTree { // kiotaModel is the intermediate representation the kiota fake SDK // implements — plus one entity ("widgets") the SDK does not carry at all. func kiotaModel() *ir.Model { - tagID := ir.Parameter{Name: "tagId", Type: ir.TypeString} + tagID := ir.URLPathParameter{Name: "tagId", Type: ir.TypeString} itemTree := &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("id", "id", ir.TypeString, ir.Computed), @@ -133,7 +133,7 @@ func kiotaModel() *ir.Model { }} items := attribute("items", "items", ir.TypeList, ir.Computed) items.ElementType = ir.TypeObject - items.Nested = itemTree + items.NestedAttributes = itemTree assignTree := &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Required), @@ -150,15 +150,15 @@ func kiotaModel() *ir.Model { Update: operation(ir.OperationUpdate, "PATCH", "/tags/{tagId}", "", tagID), Delete: deleteWithQuery(operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "", tagID)), }, - Schema: tagSchema(), + Attributes: tagSchema(), }, { Names: names("widgets", "widgets"), Operations: ir.Operations{ Create: operation(ir.OperationCreate, "POST", "/widgets", ""), - Read: operation(ir.OperationRead, "GET", "/widgets/{widgetId}", "", ir.Parameter{Name: "widgetId", Type: ir.TypeString}), + Read: operation(ir.OperationRead, "GET", "/widgets/{widgetId}", "", ir.URLPathParameter{Name: "widgetId", Type: ir.TypeString}), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("id", "id", ir.TypeString, ir.Computed), attribute("name", "name", ir.TypeString, ir.Required), }}, @@ -171,7 +171,7 @@ func kiotaModel() *ir.Model { Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", tagID), List: operation(ir.OperationList, "GET", "/tags", ""), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ filterAttr("id"), filterAttr("name"), items, @@ -180,10 +180,10 @@ func kiotaModel() *ir.Model { }, Actions: []ir.Action{ { - Names: names("tags_assign", "tags"), - InvokeOperation: *operation(ir.OperationInvoke, "POST", "/tags/{tagId}/assign", "", tagID), - RequestSchema: assignTree, - ParentEntity: "tags", + Names: names("tags_assign", "tags"), + InvokeOperation: *operation(ir.OperationAction, "POST", "/tags/{tagId}/assign", "", tagID), + RequestAttributes: assignTree, + ParentEntity: "tags", }, }, } @@ -193,7 +193,7 @@ func kiotaModel() *ir.Model { // SDK implements. The tags service area is deliberately misspelled // ("tag") so pruning has to repair the service field off the client. func openAPIGeneratorModel() *ir.Model { - tagID := ir.Parameter{Name: "tagId", Type: ir.TypeString} + tagID := ir.URLPathParameter{Name: "tagId", Type: ir.TypeString} itemTree := &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("id", "id", ir.TypeString, ir.Computed), @@ -201,7 +201,7 @@ func openAPIGeneratorModel() *ir.Model { }} items := attribute("items", "items", ir.TypeList, ir.Computed) items.ElementType = ir.TypeObject - items.Nested = itemTree + items.NestedAttributes = itemTree schema := tagSchema() // The oag fake carries no keyword-mangled, mispromised or @@ -227,7 +227,7 @@ func openAPIGeneratorModel() *ir.Model { Update: operation(ir.OperationUpdate, "PATCH", "/tags/{tagId}", "updateTag", tagID), Delete: operation(ir.OperationDelete, "DELETE", "/tags/{tagId}", "deleteTag", tagID), }, - Schema: schema, + Attributes: schema, }, }, Datasources: []ir.Datasource{ @@ -237,7 +237,7 @@ func openAPIGeneratorModel() *ir.Model { Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "getTag", tagID), List: operation(ir.OperationList, "GET", "/tags", "listTags"), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ filterAttr("id"), filterAttr("name"), items, @@ -248,7 +248,7 @@ func openAPIGeneratorModel() *ir.Model { { Names: names("groups", "groups"), ListOperation: *operation(ir.OperationList, "GET", "/groups", "listGroups"), - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Computed), }}, }, diff --git a/internal/sdkbind/helpers_test.go b/internal/sdkbind/helpers_test.go index 0059e40..a551e7c 100644 --- a/internal/sdkbind/helpers_test.go +++ b/internal/sdkbind/helpers_test.go @@ -55,14 +55,14 @@ func TestGoTypeOf(t *testing.T) { func TestDatasourceElementTree(t *testing.T) { itemTree := &ir.AttributeTree{Attributes: []ir.Attribute{attribute("name", "name", ir.TypeString, ir.Computed)}} items := attribute("items", "items", ir.TypeList, ir.Computed) - items.Nested = itemTree + items.NestedAttributes = itemTree - lookup := ir.Datasource{LookupByKey: true, Schema: itemTree} + lookup := ir.Datasource{LookupByKey: true, Attributes: itemTree} if got := datasourceElementTree(lookup); got != itemTree { t.Error("lookup datasource should bind its whole schema") } - companion := ir.Datasource{Schema: &ir.AttributeTree{Attributes: []ir.Attribute{items}}} + companion := ir.Datasource{Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{items}}} if got := datasourceElementTree(companion); got != itemTree { t.Error("companion datasource should bind the items element") } @@ -71,7 +71,7 @@ func TestDatasourceElementTree(t *testing.T) { t.Error("nil schema should stay nil") } - odd := ir.Datasource{Schema: itemTree} + odd := ir.Datasource{Attributes: itemTree} if got := datasourceElementTree(odd); got != itemTree { t.Error("a tree without items falls back to itself") } diff --git a/internal/sdkbind/model.go b/internal/sdkbind/model.go index 2b6ae72..c705cf1 100644 --- a/internal/sdkbind/model.go +++ b/internal/sdkbind/model.go @@ -38,13 +38,13 @@ import ( type SDKInfo struct { // ImportPath is the SDK's root package import path — the package the // client type lives in. - ImportPath string `json:"import_path"` + ImportPath string `json:"importPath"` // ModelsImportPath is where model types live: a kiota SDK keeps them // in a models subpackage, an openapi-generator SDK keeps everything // flat in the root package. - ModelsImportPath string `json:"models_import_path"` + ModelsImportPath string `json:"modelsImportPath"` // ClientTypeName is the client struct's type name, e.g. "APIClient". - ClientTypeName string `json:"client_type_name"` + ClientTypeName string `json:"clientTypeName"` } // InfoFor computes the SDKInfo for the configured backend and the import @@ -92,7 +92,7 @@ type Bindings struct { SDK SDKInfo `json:"sdk"` Resources map[string]*ResourceBinding `json:"resources,omitempty"` Datasources map[string]*DatasourceBinding `json:"datasources,omitempty"` - ListResources map[string]*ListResourceBinding `json:"list_resources,omitempty"` + ListResources map[string]*ListResourceBinding `json:"listResources,omitempty"` Actions map[string]*ActionBinding `json:"actions,omitempty"` // Removed records everything Prune deleted, with the SDK's reason. Removed []Removal `json:"removed,omitempty"` @@ -109,7 +109,7 @@ type Bindings struct { // model of an inline request body. The emitter reads it to import what // a rendered expression names; without it the generated file would // reference a package it never imported. - OperationPackages map[string]string `json:"operation_packages,omitempty"` + OperationPackages map[string]string `json:"operationPackages,omitempty"` } // recordPackage notes that a type expression qualified with name resolves @@ -140,15 +140,15 @@ type ResourceBinding struct { // // Without it the id is known only to the response the create discards, // and the settling read addresses the object by an empty string. - CreateIDAccess string `json:"create_id_access,omitempty"` + CreateIDAccess string `json:"createIDAccess,omitempty"` // ReadModel is the finished type expression fields are read from, // e.g. "models.Tagable" or "sdk.Tag". - ReadModel string `json:"read_model,omitempty"` + ReadModel string `json:"readModel,omitempty"` // WriteModel is the concrete type the request body is built as. - WriteModel string `json:"write_model,omitempty"` + WriteModel string `json:"writeModel,omitempty"` // WriteConstructor is the finished expression that yields a new, // settable body value, e.g. "models.NewTag()". - WriteConstructor string `json:"write_constructor,omitempty"` + WriteConstructor string `json:"writeConstructor,omitempty"` // Fields mirrors the schema attribute order. Fields []FieldBinding `json:"fields,omitempty"` // UpdateWriteModel, UpdateWriteConstructor and UpdateFields are the @@ -159,9 +159,9 @@ type ResourceBinding struct { // do — an update that omits what cannot change, or adds a version the // create has no use for. The generated SDK types them separately, so // passing the create body to the update does not compile. - UpdateWriteModel string `json:"update_write_model,omitempty"` - UpdateWriteConstructor string `json:"update_write_constructor,omitempty"` - UpdateFields []FieldBinding `json:"update_fields,omitempty"` + UpdateWriteModel string `json:"updateWriteModel,omitempty"` + UpdateWriteConstructor string `json:"updateWriteConstructor,omitempty"` + UpdateFields []FieldBinding `json:"updateFields,omitempty"` } // DatasourceBinding carries a datasource's read (and, for a companion @@ -172,19 +172,19 @@ type DatasourceBinding struct { List *Call `json:"list,omitempty"` // ReadModel is the by-key read's payload type, empty for a companion // that only lists. - ReadModel string `json:"read_model,omitempty"` + ReadModel string `json:"readModel,omitempty"` // ElementType is one list element's finished type expression. - ElementType string `json:"element_type,omitempty"` + ElementType string `json:"elementType,omitempty"` // CollectionAccess is the rendered access from the list call's result // to its element slice: a method call like "GetValue()" or "GetTags()", // a field name like "Items", or empty when the result is the slice // itself. - CollectionAccess string `json:"collection_access,omitempty"` + CollectionAccess string `json:"collectionAccess,omitempty"` // ListWrapperKey is the observed wire key the list response wraps its item // array under (the IR's ListEnvelopeKey), empty for a bare array. Prune // reads it to pick the wrapper's getter or field by name rather than // guessing among several slices. - ListWrapperKey string `json:"envelope_key,omitempty"` + ListWrapperKey string `json:"listWrapperKey,omitempty"` // Fields are the element's field accesses, read direction only. Fields []FieldBinding `json:"fields,omitempty"` } @@ -193,11 +193,11 @@ type DatasourceBinding struct { type ListResourceBinding struct { Key string `json:"key"` List *Call `json:"list,omitempty"` - ElementType string `json:"element_type,omitempty"` - CollectionAccess string `json:"collection_access,omitempty"` + ElementType string `json:"elementType,omitempty"` + CollectionAccess string `json:"collectionAccess,omitempty"` // EnvelopeKey is the observed list-envelope wire key; see // DatasourceBinding.EnvelopeKey. - EnvelopeKey string `json:"envelope_key,omitempty"` + EnvelopeKey string `json:"envelopeKey,omitempty"` Fields []FieldBinding `json:"fields,omitempty"` } @@ -205,8 +205,8 @@ type ListResourceBinding struct { type ActionBinding struct { Key string `json:"key"` Invoke *Call `json:"invoke,omitempty"` - WriteModel string `json:"write_model,omitempty"` - WriteConstructor string `json:"write_constructor,omitempty"` + WriteModel string `json:"writeModel,omitempty"` + WriteConstructor string `json:"writeConstructor,omitempty"` // Fields are the request tree's accesses, write direction only. Fields []FieldBinding `json:"fields,omitempty"` } @@ -233,17 +233,17 @@ type Call struct { Parameters []CallParameter `json:"parameters,omitempty"` // ResponseType is the finished Go type expression of the success // payload, empty when the call yields none. - ResponseType string `json:"response_type,omitempty"` + ResponseType string `json:"responseType,omitempty"` // RequestType is the finished type expression the body travels as, // empty when the call takes none. - RequestType string `json:"request_type,omitempty"` + RequestType string `json:"requestType,omitempty"` // Results are the call's result types in order, error included, so // the emitter renders the right assignment shape. Results []string `json:"results,omitempty"` // QueryParameters are the required query parameters the operation // sends as constants, carried from the intermediate representation so // pruning can settle them onto the SDK's request configuration type. - QueryParameters []ir.QueryParameter `json:"query_parameters,omitempty"` + QueryParameters []ir.QueryParameter `json:"queryParameters,omitempty"` } // Segment is one hop of a call: a field selection or a method call. @@ -260,7 +260,7 @@ type CallParameter struct { // Local is the Go identifier the expression uses. Local string `json:"local"` // GoType is the finished type the emitter declares it with. - GoType string `json:"go_type"` + GoType string `json:"goType"` // Wire is the path parameter's wire name, for tracing. Wire string `json:"wire"` } @@ -292,19 +292,19 @@ type FieldBinding struct { // speaks. Attr string `json:"attr"` Wire string `json:"wire"` - // Kind and ElementType mirror the attribute's terraform kinds, so + // Type and ElementType mirror the attribute's terraform kinds, so // pruning and the emitter know the framework side without // re-deriving it. - Kind ir.AttributeType `json:"kind,omitempty"` - ElementType ir.AttributeType `json:"element_type,omitempty"` + Type ir.AttributeType `json:"type,omitempty"` + ElementType ir.AttributeType `json:"elementType,omitempty"` // Access is the rendered accessor and conversion set. Access FieldAccess `json:"access"` // NestedModel is the SDK type one nested object is read as; // NestedWriteModel and NestedConstructor are its write-side twin. // All empty for a scalar. - NestedModel string `json:"nested_model,omitempty"` - NestedWriteModel string `json:"nested_write_model,omitempty"` - NestedConstructor string `json:"nested_constructor,omitempty"` + NestedModel string `json:"nestedModel,omitempty"` + NestedWriteModel string `json:"nestedWriteModel,omitempty"` + NestedConstructor string `json:"nestedConstructor,omitempty"` // Nested are the child field bindings of an object attribute or a // list of objects. Nested []FieldBinding `json:"nested,omitempty"` @@ -314,7 +314,7 @@ type FieldBinding struct { // to, or the document declares it write-only. Set on the root attribute, // whose whole subtree is then never read back — a member that cannot be // read makes the object it sits in one the response cannot rebuild. - KeptFromPlan bool `json:"kept_from_plan,omitempty"` + KeptFromPlan bool `json:"keptFromPlan,omitempty"` } // FieldAccess is how generated code reads and writes one field. Every @@ -332,25 +332,25 @@ type FieldAccess struct { // "*string", "int32", "[]string", "*models.Tag_kind". Where the getter // and the setter disagree it is the getter's, because state mapping is // what most consumers read it for. - SDKType string `json:"sdk_type,omitempty"` + SDKType string `json:"sdkType,omitempty"` // SDKWriteType is the type the setter takes, set only where it differs // from SDKType. Construction builds a value of this type; using SDKType // there types the value for the read side and does not compile. - SDKWriteType string `json:"sdk_write_type,omitempty"` + SDKWriteType string `json:"sdkWriteType,omitempty"` // ConvertGet and ConvertSet name the convert functions bridging each // direction, e.g. "FromPtrString" / "ToPtrString". - ConvertGet string `json:"convert_get,omitempty"` - ConvertSet string `json:"convert_set,omitempty"` + ConvertGet string `json:"convertGet,omitempty"` + ConvertSet string `json:"convertSet,omitempty"` // ParseFunc is a generated enumeration's parse companion, finished // with its package qualifier, e.g. "models.ParseTag_kind"; empty for // anything that is not an enumeration. - ParseFunc string `json:"parse_func,omitempty"` + ParseFunc string `json:"parseFunc,omitempty"` // NestedNilable reports whether a nested-object accessor's return can be // nil — a pointer, an interface (every kiota model is one) or another // reference type — so the state mapping guards the read before it // dereferences. False for a value-typed nested accessor, which cannot be // nil. Meaningful only on a nested-object field. - NestedNilable bool `json:"nested_nilable,omitempty"` + NestedNilable bool `json:"nestedNilable,omitempty"` } // Removal is one thing pruning deleted, with the SDK's reason. diff --git a/internal/sdkbind/prune_edge_test.go b/internal/sdkbind/prune_edge_test.go index d0577c6..beb14ac 100644 --- a/internal/sdkbind/prune_edge_test.go +++ b/internal/sdkbind/prune_edge_test.go @@ -73,7 +73,7 @@ func TestPruneKiotaShapes(t *testing.T) { // TestLoadSDKRefusesBrokenTree reports a tree that does not type-check // as a load error carrying the tool's own reason. -func TestLoadSDKRefusesBrokenTree(t *testing.T) { +func TestUnit_LoadSDK_RefusesABrokenTree(t *testing.T) { _, err := loadSDK(testdataDir(t, "brokensdk")) if err == nil { t.Fatal("loadSDK accepted a tree that does not type-check") @@ -91,9 +91,9 @@ func TestPruneLookupDatasource(t *testing.T) { Datasources: []ir.Datasource{{ Names: names("tags", "tags"), Operations: ir.Operations{ - Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("id", "id", ir.TypeString, ir.Required), attribute("name", "name", ir.TypeString, ir.Computed), }}, @@ -123,14 +123,14 @@ func TestPruneKiotaListResource(t *testing.T) { { Names: names("tags", "tags"), ListOperation: *operation(ir.OperationList, "GET", "/tags", ""), - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Computed), }}, }, { Names: names("widgets", "widgets"), ListOperation: *operation(ir.OperationList, "GET", "/widgets", ""), - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Computed), }}, }, @@ -159,9 +159,9 @@ func TestPruneDatasourceRemovals(t *testing.T) { Datasources: []ir.Datasource{{ Names: names("widgets", "widgets"), Operations: ir.Operations{ - Read: operation(ir.OperationRead, "GET", "/widgets/{widgetId}", "", ir.Parameter{Name: "widgetId", Type: ir.TypeString}), + Read: operation(ir.OperationRead, "GET", "/widgets/{widgetId}", "", ir.URLPathParameter{Name: "widgetId", Type: ir.TypeString}), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("id", "id", ir.TypeString, ir.Required), }}, LookupByKey: true, @@ -190,7 +190,7 @@ func TestPruneKiotaWrappedList(t *testing.T) { }} items := attribute("items", "items", ir.TypeList, ir.Computed) items.ElementType = ir.TypeObject - items.Nested = itemTree + items.NestedAttributes = itemTree t.Run("the envelope key names the getter", func(t *testing.T) { m := &ir.Model{ @@ -198,7 +198,7 @@ func TestPruneKiotaWrappedList(t *testing.T) { Datasources: []ir.Datasource{{ Names: names("gizmos", "gizmos"), Operations: ir.Operations{List: operation(ir.OperationList, "GET", "/gizmos", "")}, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("filter_type", "filter_type", ir.TypeString, ir.Required), attribute("filter_value", "filter_value", ir.TypeString, ir.Optional), items, @@ -227,7 +227,7 @@ func TestPruneKiotaWrappedList(t *testing.T) { ListResources: []ir.ListResource{{ Names: names("gizmos", "gizmos"), ListOperation: *operation(ir.OperationList, "GET", "/gizmos", ""), - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Computed), }}, }}, @@ -253,7 +253,7 @@ func TestPruneKiotaAmbiguousWrapper(t *testing.T) { ListResources: []ir.ListResource{{ Names: names("blobs", "blobs"), ListOperation: *operation(ir.OperationList, "GET", "/blobs", ""), - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Computed), }}, // No envelope key, and the envelope carries two slice getters. @@ -276,7 +276,7 @@ func TestPruneActionRemovals(t *testing.T) { Provider: ir.Provider{Name: "example"}, Actions: []ir.Action{{ Names: names("tags_assign", "tags"), - InvokeOperation: *operation(ir.OperationInvoke, "POST", "/tags/{tagId}/assign", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + InvokeOperation: *operation(ir.OperationAction, "POST", "/tags/{tagId}/assign", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), // No request schema: the drafted call passes (ctx, nil) where // the SDK's Post takes three arguments. }}, @@ -300,9 +300,9 @@ func TestPruneUnconstructibleBody(t *testing.T) { Names: names("orphans", "orphans"), Operations: ir.Operations{ Create: operation(ir.OperationCreate, "POST", "/orphans", ""), - Read: operation(ir.OperationRead, "GET", "/orphans/{orphanId}", "", ir.Parameter{Name: "orphanId", Type: ir.TypeString}), + Read: operation(ir.OperationRead, "GET", "/orphans/{orphanId}", "", ir.URLPathParameter{Name: "orphanId", Type: ir.TypeString}), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("name", "name", ir.TypeString, ir.Required), }}, }}, @@ -327,9 +327,9 @@ func TestPruneUnbuildableEntities(t *testing.T) { Names: names("tags", "tags"), Operations: ir.Operations{ Create: operation(ir.OperationCreate, "POST", "/tags", ""), - Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("legacy", "legacy", ir.TypeString, ir.Optional), }}, }}, @@ -351,9 +351,9 @@ func TestPruneUnbuildableEntities(t *testing.T) { Names: names("tags", "tags"), Operations: ir.Operations{ Create: operation(ir.OperationCreate, "POST", "/tags", ""), - Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.Parameter{Name: "tagId", Type: ir.TypeString}), + Read: operation(ir.OperationRead, "GET", "/tags/{tagId}", "", ir.URLPathParameter{Name: "tagId", Type: ir.TypeString}), }, - Schema: &ir.AttributeTree{Attributes: []ir.Attribute{ + Attributes: &ir.AttributeTree{Attributes: []ir.Attribute{ attribute("id", "id", ir.TypeString, ir.Computed), }}, }}, @@ -397,7 +397,7 @@ func TestUnit_SettleScalar_BridgesDateOnlyTimeSliceAndBytes(t *testing.T) { {"byte slice", ir.TypeString, "", types.NewSlice(types.Typ[types.Byte]), "FromBytesBase64", "ToBytesBase64", "[]byte"}, } { fb := FieldBinding{ - Attr: "x", Wire: "x", Kind: testCase.kind, ElementType: testCase.element, + Attr: "x", Wire: "x", Type: testCase.kind, ElementType: testCase.element, Access: FieldAccess{Get: "GetX", Set: "SetX"}, } p := &pruner{} @@ -434,7 +434,7 @@ func TestUnit_IsKiotaDateOnly_MatchesOnThePackagePath(t *testing.T) { func TestUnit_SettleScalar_BridgesUUID(t *testing.T) { uuidType := structNamed("github.com/google/uuid", "uuid", "UUID") - scalar := FieldBinding{Attr: "x", Wire: "x", Kind: ir.TypeString, Access: FieldAccess{Get: "GetX", Set: "SetX"}} + scalar := FieldBinding{Attr: "x", Wire: "x", Type: ir.TypeString, Access: FieldAccess{Get: "GetX", Set: "SetX"}} if why := (&pruner{}).settleScalar(&scalar, types.NewPointer(uuidType)); why.refused() { t.Fatalf("a uuid scalar was refused: %s", why.Reason) } @@ -443,7 +443,7 @@ func TestUnit_SettleScalar_BridgesUUID(t *testing.T) { scalar.Access.ConvertGet, scalar.Access.ConvertSet) } - list := FieldBinding{Attr: "x", Wire: "x", Kind: ir.TypeList, ElementType: ir.TypeString, + list := FieldBinding{Attr: "x", Wire: "x", Type: ir.TypeList, ElementType: ir.TypeString, Access: FieldAccess{Get: "GetX", Set: "SetX"}} if why := (&pruner{}).settleScalar(&list, types.NewSlice(uuidType)); why.refused() { t.Fatalf("a uuid slice was refused: %s", why.Reason) @@ -479,7 +479,7 @@ func TestUnit_SettleScalar_MapThroughAdditionalData(t *testing.T) { types.NewTuple(types.NewVar(0, nil, "", types.NewMap(types.Typ[types.String], types.NewInterfaceType(nil, nil)))), false) named.AddMethod(types.NewFunc(0, goPackage, "GetAdditionalData", sig)) - fb := FieldBinding{Attr: "headers", Wire: "headers", Kind: ir.TypeMap, ElementType: ir.TypeString, + fb := FieldBinding{Attr: "headers", Wire: "headers", Type: ir.TypeMap, ElementType: ir.TypeString, Access: FieldAccess{Get: "GetHeaders"}} if why := (&pruner{}).settleScalar(&fb, named); why.refused() { t.Fatalf("a map carried through additionalData was refused: %s", why.Reason) @@ -492,13 +492,13 @@ func TestUnit_SettleScalar_MapThroughAdditionalData(t *testing.T) { } } -// TestUnit_SettleScalar_MapWithoutABagIsRefused proves a map attribute whose +// TestUnit_SettleScalar_MapWithoutABagIsExcluded proves a map attribute whose // SDK type carries neither a Go map nor an additionalData bag still names // itself in the reason rather than settling to something that will not // compile. -func TestUnit_SettleScalar_MapWithoutABagIsRefused(t *testing.T) { +func TestUnit_SettleScalar_MapWithoutABagIsExcluded(t *testing.T) { named := structNamed("example.com/sdk/models", "models", "Opaque") - fb := FieldBinding{Attr: "headers", Wire: "headers", Kind: ir.TypeMap, ElementType: ir.TypeString, + fb := FieldBinding{Attr: "headers", Wire: "headers", Type: ir.TypeMap, ElementType: ir.TypeString, Access: FieldAccess{Get: "GetHeaders"}} why := (&pruner{}).settleScalar(&fb, named) @@ -514,7 +514,7 @@ func TestUnit_SettleScalar_MapWithoutABagIsRefused(t *testing.T) { // reads and writes as different scalar types keeps both directions, each // bridged against its own type. func TestUnit_SettleEachDirection_BridgesAScalarPair(t *testing.T) { - fb := FieldBinding{Attr: "count", Wire: "count", Kind: ir.TypeInt64, + fb := FieldBinding{Attr: "count", Wire: "count", Type: ir.TypeInt64, Access: FieldAccess{Get: "GetCount", Set: "SetCount"}} why := (&pruner{}).settleEachDirection(&fb, @@ -533,12 +533,12 @@ func TestUnit_SettleEachDirection_BridgesAScalarPair(t *testing.T) { } } -// TestUnit_SettleEachDirection_RefusesWhatTheCatalogCannotCarry proves a +// TestUnit_SettleEachDirection_ExcludesWhatTheCatalogCannotCarry proves a // pair one side of which has no bridge is still refused, naming both types // so the pair can be read from the report. -func TestUnit_SettleEachDirection_RefusesWhatTheCatalogCannotCarry(t *testing.T) { +func TestUnit_SettleEachDirection_ExcludesWhatTheCatalogCannotCarry(t *testing.T) { opaque := structNamed("example.com/sdk/models", "models", "Opaque") - fb := FieldBinding{Attr: "x", Wire: "x", Kind: ir.TypeString, + fb := FieldBinding{Attr: "x", Wire: "x", Type: ir.TypeString, Access: FieldAccess{Get: "GetX", Set: "SetX"}} why := (&pruner{}).settleEachDirection(&fb, @@ -569,7 +569,7 @@ func TestUnit_SettleEachDirection_CarriesTheWriteConstructor(t *testing.T) { return named } - fb := FieldBinding{Attr: "headers", Wire: "headers", Kind: ir.TypeMap, ElementType: ir.TypeString, + fb := FieldBinding{Attr: "headers", Wire: "headers", Type: ir.TypeMap, ElementType: ir.TypeString, Access: FieldAccess{Get: "GetHeaders", Set: "SetHeaders"}} // An empty loader is enough: writeModelFromNamed falls through to the diff --git a/internal/sdkbind/prune_fields.go b/internal/sdkbind/prune_fields.go index 2e4baa1..e5d22c9 100644 --- a/internal/sdkbind/prune_fields.go +++ b/internal/sdkbind/prune_fields.go @@ -343,11 +343,11 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { } cannot := func(shape string) refusal { return because(CauseUnbridgeableType, shape, - "the SDK carries it as %s, which cannot be bridged to a %s attribute", shape, fb.Kind) + "the SDK carries it as %s, which cannot be bridged to a %s attribute", shape, fb.Type) } // A slice of scalars or enumerations. - if slice, ok := t.Underlying().(*types.Slice); ok && fb.Kind == ir.TypeList { + if slice, ok := t.Underlying().(*types.Slice); ok && fb.Type == ir.TypeList { element := slice.Elem() if basic, ok := element.Underlying().(*types.Basic); ok { if named, isNamed := element.(*types.Named); isNamed { @@ -381,7 +381,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { // untyped additionalData bag, so the map is reached through that rather // than through a Go map. The bag is map[string]any, so the element // conversion asserts each value's type at runtime. - if fb.Kind == ir.TypeMap { + if fb.Type == ir.TypeMap { if named, ok := t.(*types.Named); ok { if _, hasGet := methodOn(named, "GetAdditionalData"); hasGet { title := exportedName(string(fb.ElementType)) @@ -405,7 +405,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { // A map of scalars the SDK does carry as a Go map. Only a string key can // address a terraform map, and only a scalar value has a catalog bridge. - if mapType, ok := t.Underlying().(*types.Map); ok && fb.Kind == ir.TypeMap { + if mapType, ok := t.Underlying().(*types.Map); ok && fb.Type == ir.TypeMap { if basic, isBasic := mapType.Key().Underlying().(*types.Basic); !isBasic || basic.Kind() != types.String { return cannot(shortType(t)) } @@ -420,7 +420,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { // OpenAPI's `format: byte` is base64, which derives a string attribute // rather than a list of numbers, so the bridge is base64 too. - if slice, ok := t.Underlying().(*types.Slice); ok && fb.Kind == ir.TypeString { + if slice, ok := t.Underlying().(*types.Slice); ok && fb.Type == ir.TypeString { if basic, isBasic := slice.Elem().Underlying().(*types.Basic); isBasic && basic.Kind() == types.Byte { settle("[]byte", "FromBytesBase64", "ToBytesBase64", "") return refusal{} @@ -448,7 +448,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { if named, ok := current.(*types.Named); ok { if parse, isEnum := p.enumParse(named); isEnum { - if fb.Kind != ir.TypeString { + if fb.Type != ir.TypeString { return cannot(shortType(t)) } get, set := convert("Enum") @@ -456,7 +456,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { return refusal{} } if isStdTime(named) { - if fb.Kind != ir.TypeString { + if fb.Type != ir.TypeString { return cannot(shortType(t)) } get, set := convert("Time") @@ -466,7 +466,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { // kiota mints its own date-only type rather than using time.Time // for a `format: date` field. if isKiotaDateOnly(named) { - if fb.Kind != ir.TypeString { + if fb.Type != ir.TypeString { return cannot(shortType(t)) } get, set := convert("DateOnly") @@ -476,7 +476,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { // A uuid is a string to a practitioner. Parsing one back can fail, // which the write bridge reports rather than guessing at. if isGoogleUUID(named) { - if fb.Kind != ir.TypeString { + if fb.Type != ir.TypeString { return cannot(shortType(t)) } get, set := convert("UUID") @@ -487,7 +487,7 @@ func (p *pruner) settleScalar(fb *FieldBinding, t types.Type) refusal { } if basic, ok := current.(*types.Basic); ok { - if !kindCompatible(fb.Kind, basic) { + if !kindCompatible(fb.Type, basic) { return cannot(shortType(t)) } get, set := convert(exportedName(basic.Name())) diff --git a/internal/sdkbind/prune_test.go b/internal/sdkbind/prune_test.go index 8fc2200..f1af4fa 100644 --- a/internal/sdkbind/prune_test.go +++ b/internal/sdkbind/prune_test.go @@ -393,7 +393,7 @@ func TestUnit_Prune_RepairsAnIndexerTheGeneratorSpeltDifferently(t *testing.T) { } } -func TestUnit_Prune_RefusesAnAmbiguousIndexer(t *testing.T) { +func TestUnit_Prune_ExcludesAnAmbiguousIndexer(t *testing.T) { // Two indexers is a guess, and a guess is what this must never make. seg := &Segment{Name: "ByThingId", Call: true, Args: []string{"id"}} p := &pruner{} diff --git a/internal/sdkbind/reconcile_test.go b/internal/sdkbind/reconcile_test.go index f22ebbd..5766494 100644 --- a/internal/sdkbind/reconcile_test.go +++ b/internal/sdkbind/reconcile_test.go @@ -99,11 +99,11 @@ func reconciliationLess(a, z Reconciliation) bool { } } -// TestUnit_Prune_RefusalsOnOneModelShareOneCause is the point of a cause. A +// TestUnit_Prune_ExclusionsOnOneModelShareOneCause is the point of a cause. A // model that carries none of an entity's fields refuses every one of them, // and reported without a cause that is one finding per attribute — leaving a // reader to notice for themselves that they are one fact. -func TestUnit_Prune_RefusalsOnOneModelShareOneCause(t *testing.T) { +func TestUnit_Prune_ExclusionsOnOneModelShareOneCause(t *testing.T) { _, removed := prunedKiota(t) byCause := map[ir.Cause][]string{} diff --git a/internal/templates/services/resource/crud.go.tmpl b/internal/templates/services/resource/crud.go.tmpl index 9110d5b..0183e89 100644 --- a/internal/templates/services/resource/crud.go.tmpl +++ b/internal/templates/services/resource/crud.go.tmpl @@ -3,12 +3,12 @@ package {{ .Package }} {{ .CRUDImports }} -{{ if .HasEC }} -// eventualConsistency is how long a read may lag a write, as the document +{{ if .HasReadAfterWriteDelay }} +// readAfterWriteDelay is how long a read may lag a write, as the document // declares it. The predicate below keeps the read-after-write loop // retrying inside the operation timeout while a lagging read still // answers "gone". -const eventualConsistency = {{ .ECDuration }} +const readAfterWriteDelay = {{ .ReadAfterWriteDelay }} // consistencyPredicate treats a state the read removed as "not consistent // yet" rather than "does not exist": within the retry window a 404 is @@ -69,7 +69,7 @@ func (r *{{ .Type }}) Create(ctx context.Context, req resource.CreateRequest, re readReq := resource.ReadRequest{State: resp.State, ProviderMeta: req.ProviderMeta} opts := crud.ReadWithRetryOptions{Operation: errors.OperationCreate, ResourceTypeName: ResourceName} -{{- if .HasEC }} +{{- if .HasReadAfterWriteDelay }} opts.ConsistencyPredicate = consistencyPredicate {{- end }} if retryErr := crud.ReadWithRetry(ctx, r.Read, readReq, crud.CreateResponseContainer{Response: resp}, opts); retryErr != nil { @@ -179,7 +179,7 @@ func (r *{{ .Type }}) Update(ctx context.Context, req resource.UpdateRequest, re readReq := resource.ReadRequest{State: resp.State, ProviderMeta: req.ProviderMeta} opts := crud.ReadWithRetryOptions{Operation: errors.OperationUpdate, ResourceTypeName: ResourceName} -{{- if .HasEC }} +{{- if .HasReadAfterWriteDelay }} opts.ConsistencyPredicate = consistencyPredicate {{- end }} if retryErr := crud.ReadWithRetry(ctx, r.Read, readReq, crud.UpdateResponseContainer{Response: resp}, opts); retryErr != nil {