PHP album payloads leave discogs_id null. The id mask required an integer and failed PHP self-replay on otherwise identical bodies. Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
2.9 KiB
Go
137 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 {
|
|
if val == nil {
|
|
return nil
|
|
}
|
|
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 + `"`
|
|
}
|