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
51 changes: 48 additions & 3 deletions internal/fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package fixtures

import (
"encoding/base64"
"strings"

ir "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/intermediate_representation"
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
62 changes: 62 additions & 0 deletions internal/fixtures/fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
}
}