feat(02-02): capture, scrub, and strictly diff stateful flows
Resolve named placeholders from a private variable store, mask dates and ids after shape checks, and keep comparing independent steps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
133
tide/normalize.go
Normal file
133
tide/normalize.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package tide
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var carbonOffsetRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$`)
|
||||
|
||||
const (
|
||||
maskDatetime = "<datetime>"
|
||||
maskID = "<id>"
|
||||
)
|
||||
|
||||
func normalizeJSON(raw []byte, step Step) ([]byte, []Diff) {
|
||||
if len(strings.TrimSpace(string(raw))) == 0 {
|
||||
return raw, nil
|
||||
}
|
||||
val, err := decodeJSON(raw)
|
||||
if err != nil {
|
||||
return raw, nil
|
||||
}
|
||||
var diffs []Diff
|
||||
masked := maskValue("$", val, step, &diffs)
|
||||
out, err := json.Marshal(masked)
|
||||
if err != nil {
|
||||
return raw, diffs
|
||||
}
|
||||
return out, diffs
|
||||
}
|
||||
|
||||
func maskValue(path string, val any, step Step, diffs *[]Diff) any {
|
||||
switch v := val.(type) {
|
||||
case map[string]any:
|
||||
out := make(map[string]any, len(v))
|
||||
for k, child := range v {
|
||||
out[k] = maskValue(pathJoin(path, k), child, step, diffs)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(v))
|
||||
for i, child := range v {
|
||||
out[i] = maskValue(fmt.Sprintf("%s[%d]", path, i), child, step, diffs)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return maskLeaf(path, val, step, diffs)
|
||||
}
|
||||
}
|
||||
|
||||
func maskLeaf(path string, val any, step Step, diffs *[]Diff) any {
|
||||
key := lastPathKey(path)
|
||||
if key == "slug" || disabledPath(step, path, key) {
|
||||
return val
|
||||
}
|
||||
if isDateKey(key) {
|
||||
return maskDate(path, val, diffs)
|
||||
}
|
||||
if isIDKey(key) {
|
||||
return maskIDValue(path, val, diffs)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func maskDate(path string, val any, diffs *[]Diff) any {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
s, ok := val.(string)
|
||||
if !ok {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: "Carbon +00:00 string or null", Actual: formatValue(val)})
|
||||
return val
|
||||
}
|
||||
if !carbonOffsetRe.MatchString(s) {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: "Carbon +00:00", Actual: strconvQuote(s)})
|
||||
return val
|
||||
}
|
||||
return maskDatetime
|
||||
}
|
||||
|
||||
func maskIDValue(path string, val any, diffs *[]Diff) any {
|
||||
n, ok := val.(json.Number)
|
||||
if !ok {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: "integer id", Actual: formatValue(val)})
|
||||
return val
|
||||
}
|
||||
if strings.Contains(string(n), ".") {
|
||||
*diffs = append(*diffs, Diff{Path: path, Expected: "integer id", Actual: "number " + string(n)})
|
||||
return val
|
||||
}
|
||||
return maskID
|
||||
}
|
||||
|
||||
func isDateKey(key string) bool {
|
||||
return strings.HasSuffix(key, "_at")
|
||||
}
|
||||
|
||||
func isIDKey(key string) bool {
|
||||
if key == "id" {
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(key, "_id") && !strings.HasSuffix(key, "_at")
|
||||
}
|
||||
|
||||
func lastPathKey(path string) string {
|
||||
path = strings.TrimPrefix(path, "$.")
|
||||
if i := strings.LastIndex(path, "."); i >= 0 {
|
||||
path = path[i+1:]
|
||||
}
|
||||
if i := strings.IndexByte(path, '['); i >= 0 {
|
||||
path = path[:i]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func disabledPath(step Step, jsonPath, key string) bool {
|
||||
for _, rule := range step.Normalize {
|
||||
if !rule.Disable {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(rule.Path)
|
||||
if p == jsonPath || p == key || strings.TrimPrefix(p, "$.") == strings.TrimPrefix(jsonPath, "$.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func strconvQuote(s string) string {
|
||||
return `"` + s + `"`
|
||||
}
|
||||
Reference in New Issue
Block a user