Skip to content
Open
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
49 changes: 49 additions & 0 deletions pkg/coop/blueprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import (
"embed"
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
)

//go:embed blueprints/*.json
var blueprintFS embed.FS

var blueprintNodeRefRE = regexp.MustCompile(`\$\{node\.([^}:]+):[^}]+\}`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only validates placeholders that already match the full ${node.:<field?} shape: it wouldn't catch something like ${node.setup.create-product}.


// BlueprintStep is a step definition within a blueprint.
type BlueprintStep struct {
StepDefinition
Expand Down Expand Up @@ -74,10 +77,56 @@ func LoadBlueprint(id string) (*Blueprint, error) {
if err := json.Unmarshal(data, &bp); err != nil {
return nil, fmt.Errorf("parsing blueprint %q: %w", id, err)
}
if err := validateBlueprintReferences(&bp); err != nil {
return nil, fmt.Errorf("validating blueprint %q: %w", id, err)
}

return &bp, nil
}

func validateBlueprintReferences(bp *Blueprint) error {
validRefs := map[string]bool{}
for _, step := range bp.Steps {
for _, node := range step.Nodes {
validRefs[step.Key+"."+node.Key] = true
for _, request := range node.TestRequests {
if request.Key != "" {
validRefs[step.Key+"."+node.Key+"."+request.Key] = true
}
}
}
}

data, err := json.Marshal(bp)
if err != nil {
return err
}
for _, match := range blueprintNodeRefRE.FindAllSubmatch(data, -1) {
ref := string(match[1])
if validRefs[ref] {
continue
}
parts := strings.Split(ref, ".")
if len(parts) == 3 && validRefs[parts[0]+"."+parts[1]] && isIntegerRefSegment(parts[2]) {
continue
}
return fmt.Errorf("unknown node reference %q", ref)
}
return nil
}

func isIntegerRefSegment(value string) bool {
if value == "" {
return false
}
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return true
}

func prefixMatchBlueprint(prefix string) (string, error) {
ids, err := ListBlueprints()
if err != nil {
Expand Down
35 changes: 35 additions & 0 deletions pkg/coop/blueprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,41 @@ func TestLoadBlueprintNotFound(t *testing.T) {
assert.Error(t, err)
}

func TestValidateBlueprintReferences(t *testing.T) {
bp := &Blueprint{
ID: "test",
Steps: []BlueprintStep{
{
StepDefinition: StepDefinition{Key: "setup", Title: "Setup"},
Nodes: []NodeDefinition{
{Key: "create-product", Request: &APIRequest{Path: "/v1/products", Method: "post"}},
{Key: "create-clock", TestRequests: []TestHelperRequest{{
Key: "create-clock-request",
APIRequest: APIRequest{
Path: "/v1/test_helpers/test_clocks",
Method: "post",
},
}}},
{Key: "create-checkout", Request: &APIRequest{
Path: "/v1/checkout/sessions",
Method: "post",
Params: map[string]string{
"price": "${node.setup.create-product:default_price}",
},
}},
{Key: "view-clock", Request: &APIRequest{Path: "/v1/test_helpers/test_clocks/${node.setup.create-clock.create-clock-request:id}", Method: "get"}},
},
},
},
}
require.NoError(t, validateBlueprintReferences(bp))

bp.Steps[0].Nodes[3].Request.Path = "/v1/products/${node.old.create-product:id}"
err := validateBlueprintReferences(bp)
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown node reference")
}

func TestLoadBlueprintPrefixMatch(t *testing.T) {
bp, err := LoadBlueprint("setup-future")
require.NoError(t, err)
Expand Down
Loading