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
3 changes: 3 additions & 0 deletions .changelog/pr-128.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
`resource/pingone_davinci_flow`: Resolved DaVinci form node theme references (`theme.value` and the indirect `themeId.value` rich-text-wrapped UUID used when `theme.value` is "useThemeId") to `pingone_branding_theme`, matching the existing `form.value` behavior. Until `pingone_branding_theme` export support lands, the theme ID is emitted as an overridable Terraform variable.
```
68 changes: 63 additions & 5 deletions contributing/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,24 @@ type EmbeddedReferenceRule struct {
TargetResourceType string // what the UUID references (e.g., "pingone_davinci_flow")
JSONKeyPath string // path inside JSON blob (e.g., "subFlowId.value.value")
ReferenceField string // TF attribute to reference (e.g., "id")

Strategy string // "" / "reference" (default), "reference_with_fallback", or "variable"
VariablePrefix string // combined with VariableNamingPath's suffix to name a fallback variable
VariableNamingPath string // JSON key path used to derive a human-readable variable suffix

// PreconditionKeyPath / PreconditionValue: optional sibling JSON key path
// (resolved against the same parsed blob as JSONKeyPath) that must equal
// PreconditionValue before the rule fires. Zero value ("") means no
// precondition — fires unconditionally (pre-existing behavior).
PreconditionKeyPath string
PreconditionValue string

// UnwrapMode: "" (default, JSONKeyPath resolves directly to a plain
// string) or "rich_text" (the value at JSONKeyPath is itself a JSON
// string containing a Slate-style rich-text wrapper —
// `[{"children":[{"text":"<value>"}]}]` — unwrapped before resolution
// and re-embedded inside the wrapper on write).
UnwrapMode string
}

type EmbeddedReferenceRegistry struct { ... }
Expand All @@ -508,11 +526,51 @@ type EmbeddedReferenceRegistry struct { ... }

1. **Walk attribute path**: Navigate `ResourceData.Attributes` following dot-notation path segments. `*` matches all map keys at that level.
2. **Extract JSON**: For each `RawHCLValue`, parse the `jsonencode(...)` expression to extract the JSON object inside.
3. **Walk JSON**: Navigate the JSON structure following `JSONKeyPath` (dot-notation) to locate the UUID string.
4. **Lookup**: Query the dependency graph for a resource matching `TargetResourceType` with the extracted UUID.
5. **Replace**: Update the JSON blob, replacing the UUID with `${resource_type.label.reference_field}` (Terraform interpolation syntax with single `$`).
6. **Serialize**: Re-marshal the JSON and update the `RawHCLValue` in the attributes map.
7. **Add Graph Edge**: Record the dependency in the graph so `--include-upstream` and cycle detection work correctly.
3. **Check precondition** (if `PreconditionKeyPath` is set): walk `PreconditionKeyPath` against the same parsed JSON; if it doesn't resolve to exactly `PreconditionValue` (including when the key is absent), the rule no-ops for this value — no change, no fallback variable, no graph edge.
4. **Walk JSON**: Navigate the JSON structure following `JSONKeyPath` (dot-notation) to locate the value. When `UnwrapMode == "rich_text"`, this locates the Slate-style wrapper string, not the raw UUID directly.
5. **Unwrap** (if `UnwrapMode == "rich_text"`): parse the wrapper `[{"children":[{"text":"<value>"}]}]` and extract the inner `text` value. Any shape mismatch (not an array, empty array, missing `children`, missing/non-string `text`) results in a no-op, mirroring `walkJSONPath`'s "return empty on mismatch" convention — never a panic.
6. **UUID-format guard** (unconditional): the extracted (or unwrapped) string is validated via `looksLikeUUID` (backed by `github.com/google/uuid`'s `Validate`). Only a UUID-shaped string is treated as a resolvable reference/fallback-variable target — anything else (a sentinel/mode-flag string such as `"useThemeId"` or `"activeTheme"`, or any future sentinel) no-ops exactly like "no value found," **regardless of `Strategy`**. This is a single check point applied uniformly, not a per-rule opt-in, so no enumerated skip-list is needed and no future rule registration can accidentally reintroduce the bug this guard closes.
7. **Lookup**: Query the dependency graph for a resource matching `TargetResourceType` with the extracted UUID.
8. **Replace**: Update the JSON blob, replacing the UUID with `${resource_type.label.reference_field}` (Terraform interpolation syntax with single `$`) or a fallback `${var.name}` reference, depending on `Strategy`. For `UnwrapMode == "rich_text"`, the replacement happens *inside* the wrapper (see below) rather than as a bare substring swap.
9. **Serialize**: Re-marshal the JSON and update the `RawHCLValue` in the attributes map.
10. **Add Graph Edge**: Record the dependency in the graph so `--include-upstream` and cycle detection work correctly (skipped for `Strategy: "variable"` or when only a fallback variable is emitted).

**When to use `PreconditionKeyPath`**: when a rule should only fire based on a sibling key's value — e.g., a mode-flag key that switches which of two sibling keys holds the real reference (`theme.value == "useThemeId"` gates whether `themeId.value` should be resolved). Leave both fields at their zero value (`""`) for rules that should fire unconditionally, exactly like `subFlowId`/`form.value` do today.

**When to use `UnwrapMode: "rich_text"`**: when the value at `JSONKeyPath` is not a plain UUID string but a JSON-encoded Slate/rich-text wrapper (as produced by some DaVinci node editor fields). Because the outer `properties` map is itself re-encoded via `jsonencode_raw`, the wrapper's inner quotes appear **backslash-escaped** in the final `RawHCLValue` text — a plain substring replace (as `replaceUUIDInRawHCL` does for the no-unwrap path) would not match. The rich-text path instead swaps the UUID for the resolved reference inside the *unescaped* wrapper string, then re-derives both the old and new wrapper's escaped form via `json.Marshal` (which normalizes escaping identically to how the original text was produced) before substituting inside the `RawHCLValue`.

**Worked example — `pingone_branding_theme` reference on a `showForm` node** (registered against `internal/platform/pingone/resource_flow.go`): a node's `theme.value` may hold a direct UUID, be absent, or be the literal string `"useThemeId"` with the real UUID embedded in a rich-text wrapper at `themeId.value`. Two rules cover this:

```go
// Case 1: direct UUID at theme.value — no precondition, no unwrap.
registerEmbeddedReferenceRule(core.EmbeddedReferenceRule{
ResourceType: "pingone_davinci_flow",
AttributePath: "graph_data.elements.nodes.*.data.properties",
TargetResourceType: "pingone_branding_theme",
JSONKeyPath: "theme.value",
ReferenceField: "id",
Strategy: "reference_with_fallback",
VariablePrefix: "davinci_theme",
VariableNamingPath: "nodeTitle.value",
})

// Case 3: rich-text-wrapped UUID at themeId.value, gated on theme.value == "useThemeId".
registerEmbeddedReferenceRule(core.EmbeddedReferenceRule{
ResourceType: "pingone_davinci_flow",
AttributePath: "graph_data.elements.nodes.*.data.properties",
TargetResourceType: "pingone_branding_theme",
JSONKeyPath: "themeId.value",
ReferenceField: "id",
Strategy: "reference_with_fallback",
VariablePrefix: "davinci_theme",
VariableNamingPath: "nodeTitle.value",
PreconditionKeyPath: "theme.value",
PreconditionValue: "useThemeId",
UnwrapMode: "rich_text",
})
```

Case 1's rule fires unconditionally on `theme.value`, but the format guard rejects non-UUID values like `"useThemeId"` or `"activeTheme"` before `Strategy` is consulted — no `SkipValues` list is needed. Case 3's rule only fires when `theme.value` is exactly `"useThemeId"`, and only ever touches the UUID nested inside `themeId.value`'s wrapper — `theme.value` itself is left untouched by this rule.

#### Example: DaVinci Flow `subFlowId` Rule

Expand Down
72 changes: 67 additions & 5 deletions contributing/DEVELOPER_HANDBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,26 @@ type EmbeddedReferenceRule struct {
TargetResourceType string // resource type the UUID references ("pingone_davinci_flow")
JSONKeyPath string // path inside the JSON object ("subFlowId.value.value")
ReferenceField string // attribute on target resource ("id")

Strategy string // "" / "reference" (default), "reference_with_fallback", or "variable"
VariablePrefix string // combined with a VariableNamingPath-derived suffix to name a fallback variable
VariableNamingPath string // JSON key path used to derive a human-readable variable suffix

// PreconditionKeyPath / PreconditionValue — optional. A sibling JSON key
// path (resolved against the same parsed blob as JSONKeyPath) that must
// equal PreconditionValue before the rule fires. Zero value ("") means
// no precondition — the rule fires unconditionally (matches pre-existing
// rules like subFlowId/form.value, which need no changes).
PreconditionKeyPath string
PreconditionValue string

// UnwrapMode — optional. "" (default) means JSONKeyPath resolves
// directly to a plain string. "rich_text" means the value at
// JSONKeyPath is itself a JSON string holding a Slate-style rich-text
// wrapper (`[{"children":[{"text":"<value>"}]}]`); the inner value is
// unwrapped before resolution and the resolved reference/variable is
// re-embedded inside the wrapper on write.
UnwrapMode string
}
```

Expand All @@ -445,6 +465,12 @@ type EmbeddedReferenceRule struct {
- Keys must exist in the JSON for the subFlowId to be found and replaced
- Missing keys are silently skipped (not an error)

**The unconditional UUID-format guard**: regardless of which fields are set, the value ultimately extracted from `JSONKeyPath` (after unwrap, if `UnwrapMode == "rich_text"`) is always validated by a `looksLikeUUID` check before any `Strategy` branch runs. If it isn't shaped like a UUID, the rule no-ops exactly as if no value had been found at all — no change, no fallback variable, no graph edge. This means you never need to enumerate sentinel/mode-flag strings (e.g., `"useThemeId"`, `"activeTheme"`) in a rule registration to keep them from being misresolved — the guard rejects any non-UUID string generically, including future sentinels no one has thought of yet.

**Using `PreconditionKeyPath`/`PreconditionValue`**: set these when a rule should only act if a *different* key in the same JSON blob equals a specific value — for example, a mode-flag key that determines which of two sibling keys holds the live reference. Leave both unset (`""`) for rules that should fire unconditionally.

**Using `UnwrapMode: "rich_text"`**: set this when the value at `JSONKeyPath` is not a plain UUID string but a JSON-encoded Slate/rich-text wrapper. Because the outer JSON blob is itself passed through `jsonencode_raw` a second time, the wrapper's inner quotes are backslash-escaped inside the final `RawHCLValue` text — a plain substring replace would not match, so the rich-text path re-derives the escaped form via `json.Marshal` before substituting. You do not need to do anything special in your rule literal beyond setting `UnwrapMode: "rich_text"`; the engine handles both extraction and re-embedding.

### Step 1: Analyze the Structure

Find the API struct and the YAML attribute:
Expand Down Expand Up @@ -507,6 +533,40 @@ func init() {
}
```

**Worked example — precondition + rich-text unwrap**: a `showForm` node's theme reference can be a direct UUID at `theme.value`, absent, or an indirect mode flag (`theme.value == "useThemeId"`) pointing at a rich-text-wrapped UUID in `themeId.value`. This is expressed as two rules, both targeting `pingone_branding_theme`:

```go
// Direct UUID case — no precondition, no unwrap.
registerEmbeddedReferenceRule(core.EmbeddedReferenceRule{
ResourceType: "pingone_davinci_flow",
AttributePath: "graph_data.elements.nodes.*.data.properties",
TargetResourceType: "pingone_branding_theme",
JSONKeyPath: "theme.value",
ReferenceField: "id",
Strategy: "reference_with_fallback",
VariablePrefix: "davinci_theme",
VariableNamingPath: "nodeTitle.value",
})

// Indirect mode-flag case — fires only when theme.value == "useThemeId";
// the UUID lives inside themeId.value's rich-text wrapper.
registerEmbeddedReferenceRule(core.EmbeddedReferenceRule{
ResourceType: "pingone_davinci_flow",
AttributePath: "graph_data.elements.nodes.*.data.properties",
TargetResourceType: "pingone_branding_theme",
JSONKeyPath: "themeId.value",
ReferenceField: "id",
Strategy: "reference_with_fallback",
VariablePrefix: "davinci_theme",
VariableNamingPath: "nodeTitle.value",
PreconditionKeyPath: "theme.value",
PreconditionValue: "useThemeId",
UnwrapMode: "rich_text",
})
```

Neither rule needs an enumerated skip list for sentinel values like `"activeTheme"` — the unconditional UUID-format guard rejects any non-UUID string extracted from `theme.value` before `Strategy` is ever consulted.

### Step 3: Verify

Run the pipeline and check the output:
Expand Down Expand Up @@ -551,11 +611,13 @@ resource "pingone_davinci_flow" "parent_flow_label" {
1. **Rule Matching**: After all resources are processed and added to the dependency graph, `ResolveEmbeddedReferences()` iterates all rules.
2. **Attribute Navigation**: For each rule, walk `ResourceData.Attributes` following `AttributePath`. The `*` wildcard matches all map keys at that level.
3. **JSON Extraction**: For each `RawHCLValue` at the final path, extract the JSON object from inside the `jsonencode(...)` expression.
4. **UUID Lookup**: Navigate the JSON blob via `JSONKeyPath`, find the UUID string.
5. **Graph Resolution**: Query the dependency graph for a `TargetResourceType` resource with that UUID to get its Terraform label.
6. **Replacement**: Replace the UUID with `${resource_type.label.reference_field}` (Terraform interpolation).
7. **Serialization**: Re-marshal the JSON and update the `RawHCLValue`.
8. **Graph Update**: Record the dependency edge so `--include-upstream` and cycle detection work correctly.
4. **Precondition Check** (if `PreconditionKeyPath` is set): walk `PreconditionKeyPath` against the same parsed JSON; a mismatch (or absent key) no-ops the rule for this value.
5. **JSON/UUID Extraction**: Navigate the JSON blob via `JSONKeyPath` to find the value. If `UnwrapMode == "rich_text"`, unwrap the Slate wrapper to get the inner string; any shape mismatch no-ops without a panic.
6. **UUID-Format Guard**: validate the extracted string via `looksLikeUUID`. Non-UUID-shaped strings (sentinels, mode flags, anything unexpected) no-op here, regardless of `Strategy`.
7. **Graph Resolution**: Query the dependency graph for a `TargetResourceType` resource with that UUID to get its Terraform label.
8. **Replacement**: Replace the UUID with `${resource_type.label.reference_field}` (Terraform interpolation) or a fallback `${var.name}`, depending on `Strategy`. For `UnwrapMode == "rich_text"`, the replacement is re-embedded inside the wrapper via an escaping-aware helper rather than a plain substring swap.
9. **Serialization**: Re-marshal the JSON and update the `RawHCLValue`.
10. **Graph Update**: Record the dependency edge so `--include-upstream` and cycle detection work correctly.

### Debugging Embedded References

Expand Down
Loading
Loading