From a2c22a5aa24578f3fb85a3c94de1023752c03e90 Mon Sep 17 00:00:00 2001 From: ShocOne <62835948+ShocOne@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:01:03 +0100 Subject: [PATCH] feat: a fixture value is the shape its format declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every synthesised string was the attribute path behind a prefix, whatever the document said the string held. A generated SDK parses these on the way in — a timestamp becomes time.Time, an identifier becomes uuid.UUID — so the value was refused before any assertion in a generated test ran, and the failure named the parse rather than the field. The format now decides the shape. One that leaves room keeps the prefix, so a value left behind on a live API is still recognisable as debris; a timestamp and a uuid have no room and are fixed instead, which keeps them deterministic. An address is TEST-NET-1 and a host is under example.invalid, so a value that escapes into a request reaches nothing real. Co-Authored-By: Claude Opus 5 (1M context) --- internal/fixtures/fixtures.go | 51 ++++++++++++++++++++++-- internal/fixtures/fixtures_test.go | 62 ++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/internal/fixtures/fixtures.go b/internal/fixtures/fixtures.go index 1c41708..1654862 100644 --- a/internal/fixtures/fixtures.go +++ b/internal/fixtures/fixtures.go @@ -18,6 +18,7 @@ package fixtures import ( + "encoding/base64" "strings" ir "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/intermediate_representation" @@ -320,8 +321,9 @@ func deriveTree(tree *ir.AttributeTree, path []string) ([]Entry, []Omission) { } // scalarFor synthesises one scalar value: enum-driven when the document -// declares values, type-driven otherwise, with strings carrying the test -// prefix and the attribute path so no two attributes share a value. +// declares values, format-driven when it declares what the string holds, +// type-driven otherwise. A plain string carries the test prefix and the +// attribute path so no two attributes share a value. func scalarFor(kind ir.AttributeType, a ir.Attribute, path []string) any { switch kind { case ir.TypeBool: @@ -337,8 +339,51 @@ func scalarFor(kind ir.AttributeType, a ir.Attribute, path []string) any { if len(a.AdvisoryValues) > 0 { return a.AdvisoryValues[0] } - return NamePrefix + strings.ReplaceAll(strings.Join(path, "-"), "_", "-") + name := NamePrefix + strings.ReplaceAll(strings.Join(path, "-"), "_", "-") + if formatted, ok := formatValue(a.Format, name); ok { + return formatted + } + return name + } +} + +// formatValue synthesises a string the document says is more than a string, +// and reports whether the format is one it knows. +// +// A generated SDK parses these on the way in: a timestamp becomes time.Time +// and an identifier becomes uuid.UUID, so a value of the wrong shape is +// refused before any assertion in a generated test runs, and the failure +// names the parse rather than the field. +// +// A format with room for the prefix keeps it, so a name a test leaves behind +// on a live API is still recognisable. A timestamp and a uuid have no such +// room; they are fixed instead, which keeps them deterministic. +func formatValue(format, name string) (string, bool) { + switch format { + case "date-time": + return "2026-01-02T03:04:05Z", true + case "date": + return "2026-01-02", true + case "time": + return "03:04:05Z", true + case "uuid": + return "00000000-0000-4000-8000-000000000000", true + case "byte", "base64": + return base64.StdEncoding.EncodeToString([]byte(name)), true + case "email", "idn-email": + return name + "@example.invalid", true + case "hostname", "idn-hostname": + return name + ".example.invalid", true + case "uri", "url", "uri-reference", "iri": + return "https://example.invalid/" + name, true + case "ipv4": + // TEST-NET-1 and the documentation prefix: reserved for exactly this, + // so a value that escapes into a request reaches nothing real. + return "192.0.2.1", true + case "ipv6": + return "2001:db8::1", true } + return "", false } // wanted reports whether a value's attribute travels in the given diff --git a/internal/fixtures/fixtures_test.go b/internal/fixtures/fixtures_test.go index 03083a6..d62be24 100644 --- a/internal/fixtures/fixtures_test.go +++ b/internal/fixtures/fixtures_test.go @@ -3,9 +3,12 @@ package fixtures import ( "bytes" "encoding/json" + "net/netip" "reflect" + "regexp" "strings" "testing" + "time" ir "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/intermediate_representation" ) @@ -344,3 +347,62 @@ func TestUnit_Fixturespec_SingleVariantEntityIsUngated(t *testing.T) { t.Fatalf("ungated enum keeps its first value basic, got %v", got) } } + +// TestUnit_Fixturespec_AFormatDecidesTheValueShape proves a string the +// document says is more than a string is synthesised as that thing. A +// generated SDK parses these on the way in, so a value of the wrong shape is +// 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}, + }} + + got := map[string]any{} + for _, e := range Derive(tree).Entries { + got[e.Name] = e.Scalar + } + + if _, err := time.Parse(time.RFC3339, got["created_at"].(string)); err != nil { + t.Errorf("created_at = %v, which no SDK will parse as a timestamp: %v", got["created_at"], err) + } + if _, err := time.Parse(time.DateOnly, got["born_on"].(string)); err != nil { + t.Errorf("born_on = %v: %v", got["born_on"], err) + } + // Matched by shape rather than parsed: the toolkit takes no uuid + // dependency, and the shape is what an SDK's parser accepts. + uuidShape := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) + if value, _ := got["agent_id"].(string); !uuidShape.MatchString(value) { + t.Errorf("agent_id = %q, which no SDK will parse as a uuid", value) + } + if _, err := netip.ParseAddr(got["address"].(string)); err != nil { + t.Errorf("address = %v: %v", got["address"], err) + } + + // A format with room for the prefix keeps it, so a value left behind on a + // live API is still recognisable as toolkit debris. + for _, name := range []string{"owner_email", "home", "label"} { + if value, _ := got[name].(string); !strings.Contains(value, NamePrefix) { + t.Errorf("%s = %q, which carries no %q", name, value, NamePrefix) + } + } + if value, _ := got["owner_email"].(string); !strings.HasSuffix(value, "@example.invalid") { + t.Errorf("owner_email = %q, which is not an address", value) + } + if value, _ := got["home"].(string); !strings.HasPrefix(value, "https://") { + t.Errorf("home = %q, which is not a url", value) + } + + // Determinism is the whole scheme: a regenerated fixture is byte-identical + // or the document changed. + for _, e := range Derive(tree).Entries { + if e.Scalar != got[e.Name] { + t.Errorf("%s derived %v then %v", e.Name, got[e.Name], e.Scalar) + } + } +}