diff --git a/internal/audit/plan/synth.go b/internal/audit/plan/synth.go index 0856eaf..65e1aff 100644 --- a/internal/audit/plan/synth.go +++ b/internal/audit/plan/synth.go @@ -95,6 +95,17 @@ func (sy synth) value(field string, s *specmodel.Schema, depth int, topLevel boo return nil, false } r := s.Resolved() + // A name must be unique per run and must carry the prefix cleanup matches + // on, so the invented token beats a declared example. An example is a + // value the API accepted once; an API that requires a name to be unique + // refuses it every time after, and an object created under it is invisible + // to the prefix pass and stays in the tenant. An enum, a format or a + // pattern leaves the ordinary priority alone: the token satisfies none of + // those shapes, so a field carrying one is not a field to invent a name + // for. + if NameBearing(field) && len(r.Enum) == 0 && r.Format == "" && r.Pattern == "" { + return sy.nameToken(field), true + } if r.Example != nil { return r.Example, true } @@ -139,7 +150,7 @@ func (sy synth) formatValue(format string) (any, bool) { func (sy synth) typeValue(field string, r *specmodel.Schema, depth int) (any, bool) { switch { case r.Type == "string": - if nameBearing(field) { + if NameBearing(field) { return sy.nameToken(field), true } return "sample-" + field, true @@ -268,9 +279,9 @@ func (sy synth) nameToken(field string) string { return sy.prefix + "-" + RunIDToken + "-" + sy.entity + "-" + field } -// nameBearing reports whether a string field names its object — the +// NameBearing reports whether a string field names its object — the // fields whose synthesized values must carry the cleanup prefix. -func nameBearing(field string) bool { +func NameBearing(field string) bool { lf := strings.ToLower(field) for _, suffix := range []string{"name", "title", "label"} { if lf == suffix || strings.HasSuffix(lf, suffix) { diff --git a/internal/audit/plan/synth_test.go b/internal/audit/plan/synth_test.go index 7df5ad9..9afe188 100644 --- a/internal/audit/plan/synth_test.go +++ b/internal/audit/plan/synth_test.go @@ -381,3 +381,45 @@ components: t.Errorf("minimal body carries the optional self-reference: %#v", body) } } + +func TestUnit_Plan_AnInventedNameOutranksADeclaredExample(t *testing.T) { + spec := resourceSpec( + "name, gated_name, shaped_name, summary", + ` name: + type: string + example: My thing + gated_name: + type: string + example: My thing + enum: [alpha, beta] + shaped_name: + type: string + example: my.thing + pattern: "^[a-z.]+$" + summary: + type: string + example: some words +`) + p := mustDerive(t, loadDoc(t, spec), testConfig(), nil) + body := entityByKey(t, p, "thing").Steps[0].Body + + // The example is a value the API took once; a unique name refuses it + // thereafter, and an object created under it never carries the prefix + // cleanup matches on. + if got := body["name"]; got != "tfpfgen-"+RunIDToken+"-thing-name" { + t.Errorf("name = %#v, want the invented token", got) + } + // A field an enum or a pattern constrains keeps the ordinary priority, in + // which a declared example already outranks both: the token satisfies + // neither shape, so such a field is not one to invent a name for. + if got := body["gated_name"]; got != "My thing" { + t.Errorf("gated_name = %#v, want the declared example", got) + } + if got := body["shaped_name"]; got != "my.thing" { + t.Errorf("shaped_name = %#v, want the declared example", got) + } + // A field that names nothing keeps its example. + if got := body["summary"]; got != "some words" { + t.Errorf("summary = %#v, want the declared example", got) + } +} diff --git a/internal/audit/run/adjust_unit_test.go b/internal/audit/run/adjust_unit_test.go index 3a5b369..700d75e 100644 --- a/internal/audit/run/adjust_unit_test.go +++ b/internal/audit/run/adjust_unit_test.go @@ -195,6 +195,23 @@ func TestUnit_Strategize_SynthesisHelpers(t *testing.T) { if v := synthValue(strategy.SynthHint{Field: "label", Type: "string"}, "ent", "tfpfgen"); v != "tfpfgen--ent-label" { t.Errorf("name-bearing string = %v", v) } + // A name-bearing field takes the invented token over a declared example: + // this is the path a live run synthesises from, and an API that requires a + // unique name refuses the example every run after the first. + if v := synthValue(strategy.SynthHint{Field: "name", Type: "string", Example: "My thing"}, + "ent", "tfpfgen"); v != "tfpfgen--ent-name" { + t.Errorf("a name-bearing example = %#v, want the invented token", v) + } + // A field an enum, a format or a pattern constrains is not one to invent a + // name for, so the ordinary priority stands. + if v := synthValue(strategy.SynthHint{Field: "name", Type: "string", Example: "My thing", + Pattern: "^[A-Za-z ]+$"}, "ent", "tfpfgen"); v != "My thing" { + t.Errorf("a constrained name = %#v, want the declared example", v) + } + if v := synthValue(strategy.SynthHint{Field: "name", Type: "string", Format: "email"}, + "ent", "tfpfgen"); v != "tfpfgen-@example.invalid" { + t.Errorf("a formatted name = %#v, want the format-driven value", v) + } if v := synthValue(strategy.SynthHint{Field: "color", Type: "string"}, "ent", "p"); v != "sample-color" { t.Errorf("plain string = %v", v) } diff --git a/internal/audit/run/strategize.go b/internal/audit/run/strategize.go index 7a3e43f..4710673 100644 --- a/internal/audit/run/strategize.go +++ b/internal/audit/run/strategize.go @@ -21,7 +21,6 @@ import ( "fmt" "math/bits" "strconv" - "strings" "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/audit/observe" "github.com/deploymenttheory/terraform-plugin-framework-codegen/internal/audit/plan" @@ -336,7 +335,7 @@ func (r *runner) synthField(ent *entityState, field string) any { return synthValue(h, ent.plan.Entity, r.opts.NamePrefix) } } - if nameBearing(field) { + if plan.NameBearing(field) { return nameToken(r.opts.NamePrefix, ent.plan.Entity, field) } return "sample-" + field @@ -346,6 +345,17 @@ func (r *runner) synthField(ent *entityState, field string) any { // plan package's synthesis: example, then default, then the first enum member, // then a format-driven value, then a type-driven one. func synthValue(h strategy.SynthHint, entity, prefix string) any { + // A name must be unique per run and must carry the prefix cleanup matches + // on, so the invented token beats a declared example. An example is a + // value the API accepted once; an API that requires a name to be unique + // refuses it every time after, and an object created under it is invisible + // to the prefix pass and stays in the tenant. An enum, a format or a + // pattern leaves the ordinary priority alone: the token satisfies none of + // those shapes, so a field carrying one is not a field to invent a name + // for. + if plan.NameBearing(h.Field) && len(h.Enum) == 0 && h.Format == "" && h.Pattern == "" { + return nameToken(prefix, entity, h.Field) + } if h.Example != nil { return h.Example } @@ -370,12 +380,12 @@ func synthValue(h strategy.SynthHint, entity, prefix string) any { case "object": return map[string]any{} case "string": - if nameBearing(h.Field) { + if plan.NameBearing(h.Field) { return nameToken(prefix, entity, h.Field) } return "sample-" + h.Field default: - if nameBearing(h.Field) { + if plan.NameBearing(h.Field) { return nameToken(prefix, entity, h.Field) } return "sample-" + h.Field @@ -540,18 +550,6 @@ func nameToken(prefix, entity, field string) string { return prefix + "-" + plan.RunIDToken + "-" + entity + "-" + field } -// nameBearing reports whether a string field names its object — the fields -// whose synthesised values must carry the cleanup prefix. -func nameBearing(field string) bool { - lf := strings.ToLower(field) - for _, suffix := range []string{"name", "title", "label"} { - if lf == suffix || strings.HasSuffix(lf, suffix) { - return true - } - } - return false -} - // bisectionAllowance is the extra createMaximal attempts worth reserving to // halve the optional set down to one field, plus the retry that confirms it. func bisectionAllowance(optional int) int {