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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions cmd/tfpfgen/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"strings"

"github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/docpatch"
"github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/kiota"
"github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/snapshot"
)
Expand Down Expand Up @@ -97,6 +98,15 @@ func runSDKGenerate(args []string) error {
return err
}

// Document patches: curated, recording-justified corrections applied to a copy
// of the snapshot when the published document is provably wrong about the
// live API. The snapshot's own bytes never change; generation reads the
// patched copy, and with no patches present the snapshot is read directly.
patches, err := docpatch.Load(filepath.Join(o.openapiDir, docpatch.DirName))
if err != nil {
return err
}

module := o.module
if module == "" {
module, err = embedModulePath(o.out)
Expand All @@ -117,12 +127,25 @@ func runSDKGenerate(args []string) error {

if o.dryRun {
log.Printf("snapshot: %s", snap.SpecPath())
for _, p := range patches {
log.Printf("would apply %s: %s", p.File, p.Justification)
}
log.Printf("would run: kiota generate -l go -d %s -o %s -n %s -c %s --exclude-backward-compatible",
gen.Description, gen.Out, gen.Module, gen.ClientName)
log.Printf("dry run; nothing was generated")
return nil
}

if len(patches) > 0 {
patched, err := patchedDocument(snap, patches)
if err != nil {
return err
}
defer func() { _ = os.RemoveAll(filepath.Dir(patched)) }()
gen.Description = patched
log.Printf("applied %d document patch(es) from %s", len(patches), filepath.Join(o.openapiDir, docpatch.DirName))
}

// The version gate: a committed tree names the kiota that produced it, and
// running any other version rewrites the whole tree as an inexplicable diff.
lock, hadLock, err := kiota.ReadLock(o.out)
Expand All @@ -149,6 +172,19 @@ func runSDKGenerate(args []string) error {
return err
}

// A patched generation read a temporary copy, and a temp path must not
// reach the committed lock: point descriptionLocation back at the pinned
// snapshot the patched copy was derived from.
if gen.Description != snap.SpecPath() {
rel, err := relDescription(o.out, snap.SpecPath())
if err != nil {
return err
}
if err := kiota.SetDescriptionLocation(o.out, rel); err != nil {
return err
}
}

if o.mode == sdkModeExternal {
if err := ensureSDKModule(o.out, module); err != nil {
return err
Expand All @@ -158,6 +194,43 @@ func runSDKGenerate(args []string) error {
return sdkPostcheck(o.out)
}

// patchedDocument applies the loaded patches to the snapshot's document and
// writes the result into a fresh temp directory, returning the file path.
func patchedDocument(snap snapshot.Snapshot, patches []docpatch.Patch) (string, error) {
raw, err := os.ReadFile(snap.SpecPath()) //nolint:gosec // the verified snapshot's own path
if err != nil {
return "", err
}
patched, err := docpatch.Apply(raw, patches)
if err != nil {
return "", err
}

dir, err := os.MkdirTemp("", "tfpfgen-patched-spec-*")
if err != nil {
return "", err
}
path := filepath.Join(dir, "api.yaml")
if err := os.WriteFile(path, patched, 0o600); err != nil {
return "", err
}
return path, nil
}

// relDescription is the lock-file spelling of the snapshot's location: the
// same relative-to-the-SDK-root form kiota itself writes.
func relDescription(out, spec string) (string, error) {
absOut, err := filepath.Abs(out)
if err != nil {
return "", err
}
absSpec, err := filepath.Abs(spec)
if err != nil {
return "", err
}
return filepath.Rel(absOut, absSpec)
}

// embedModulePath derives kiota's -n for an embedded SDK: the enclosing
// module's path joined with the SDK root's relative directory -- kiota's own
// rule for generating inside an existing module.
Expand Down
15 changes: 15 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ one module. Under `-mode external` the tree gets a minimal generated `go.mod`
own repository. `-check` regenerates into scratch and byte-compares — the only
proof that the committed tree still follows from the pinned snapshot.

**Document patches.** When a published document is provably wrong about the
live API (a probe recording shows the API accepting and echoing an enum value
the document omits), or shaped in a way that provably defeats deterministic
generation (an anonymous schema kiota names unstably), a *document patch*
records the divergence:
`<openapi-dir>/patches/*.patch.json`, each holding a `justification` naming
the evidence and RFC 6902 `operations` scoped to it. Patches apply to a copy
(the snapshot's bytes and checksum never change; application preserves
document order, which kiota's inline-schema naming depends on), generation
reads the copy, and `-check` applies them identically, so a patch is part of
what the committed tree provably follows from. When the vendor fixes the
document, an `add` that finds its value already present refuses as *stale* —
the prompt to delete the patch. With no patches directory, the snapshot is
read directly.

## `blueprint`

### `blueprint draft`
Expand Down
1 change: 1 addition & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ without adding it here first.
|---|---|
| OpenAPI document | The upstream API description as its vendor publishes it; never called a spec or a specification. |
| snapshot | A pinned, committed copy of an OpenAPI document, fetched by `openapi fetch` and stored under `openapi/` so every derivation is reproducible. |
| document patch | A curated, evidence-justified RFC 6902 correction under `openapi/<provider>/patches/`, applied to a copy of the snapshot before `sdk generate` reads it — for a document that is provably wrong about the live API, or shaped in a way that provably defeats deterministic generation. |
| blueprint | The committed JSON intermediate representation the generator builds a provider from — schema plus bindings, wire mappings and observed behaviour. |
| draft | A blueprint or scenario not yet fit for the pipeline, held under a `.draft.json` name the pipeline's loaders cannot open. |
| promote | To rename a draft to its canonical filename, and nothing else — a git-visible act with no content change. |
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/pb33f/libopenapi v0.38.7
golang.org/x/mod v0.38.0
golang.org/x/tools v0.48.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/gofumpt v0.11.0
)

Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/gofumpt v0.11.0 h1:0H01XB95PnN2QgCSR9ELdZyTlJqNZ7181B0BTMh5VZc=
Expand Down
Loading
Loading