Files
summercms/tide/normalize.go
Jakub Zych aa165fe3d0 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>
2026-09-17 12:32:49 +02:00

134 lines
2.9 KiB
Go

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 + `"`
}