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