Approval-based snapshot testing library for Go (mirror)
1package transform
2
3import (
4 "encoding/json"
5 "fmt"
6)
7
8// Scrubber transforms content before snapshotting.
9type Scrubber interface {
10 Scrub(content string) string
11}
12
13// IgnorePattern determines whether a key-value pair should be excluded.
14type IgnorePattern interface {
15 ShouldIgnore(key, value string) bool
16}
17
18// Config holds the transformation configuration.
19type Config struct {
20 Scrubbers []Scrubber
21 Ignore []IgnorePattern
22}
23
24// ApplyScrubbers applies all scrubbers to the content in order.
25func ApplyScrubbers(content string, scrubbers []Scrubber) string {
26 result := content
27 for _, scrubber := range scrubbers {
28 result = scrubber.Scrub(result)
29 }
30 return result
31}
32
33// TransformJSON applies scrubbers and ignore patterns to JSON data.
34func TransformJSON(jsonStr string, config *Config) (string, error) {
35 var data any
36 if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
37 return "", fmt.Errorf("failed to unmarshal JSON: %w", err)
38 }
39
40 // Apply ignore patterns first (removes fields)
41 if len(config.Ignore) > 0 {
42 data = walkAndFilter(data, config.Ignore)
43 }
44
45 // Marshal back to JSON
46 prettyJSON, err := json.MarshalIndent(data, "", " ")
47 if err != nil {
48 return "", fmt.Errorf("failed to marshal JSON: %w", err)
49 }
50
51 result := string(prettyJSON)
52
53 // Apply scrubbers to the final string
54 result = ApplyScrubbers(result, config.Scrubbers)
55
56 return result, nil
57}
58
59// walkAndFilter recursively walks the data structure and filters out ignored fields.
60func walkAndFilter(data any, ignorePatterns []IgnorePattern) any {
61 switch v := data.(type) {
62 case map[string]any:
63 return filterMap(v, ignorePatterns)
64 case []any:
65 return filterSlice(v, ignorePatterns)
66 default:
67 return data
68 }
69}
70
71// filterMap filters a map, removing entries that match ignore patterns.
72func filterMap(m map[string]any, ignorePatterns []IgnorePattern) map[string]any {
73 result := make(map[string]any)
74 for key, value := range m {
75 // Convert value to string for comparison
76 valueStr := valueToString(value)
77
78 // Check if this key-value pair should be ignored
79 shouldIgnore := false
80 for _, pattern := range ignorePatterns {
81 if pattern.ShouldIgnore(key, valueStr) {
82 shouldIgnore = true
83 break
84 }
85 }
86
87 if !shouldIgnore {
88 // Recursively filter nested structures
89 result[key] = walkAndFilter(value, ignorePatterns)
90 }
91 }
92 return result
93}
94
95// filterSlice filters a slice, recursively processing each element.
96func filterSlice(s []any, ignorePatterns []IgnorePattern) []any {
97 result := make([]any, len(s))
98 for i, item := range s {
99 result[i] = walkAndFilter(item, ignorePatterns)
100 }
101 return result
102}
103
104// valueToString converts various value types to string for comparison.
105func valueToString(value any) string {
106 switch v := value.(type) {
107 case string:
108 return v
109 case nil:
110 return "null"
111 case bool:
112 if v {
113 return "true"
114 }
115 return "false"
116 case float64:
117 return fmt.Sprintf("%v", v)
118 case int, int64:
119 return fmt.Sprintf("%d", v)
120 default:
121 // For complex types, marshal to JSON
122 if bytes, err := json.Marshal(v); err == nil {
123 return string(bytes)
124 }
125 return fmt.Sprintf("%v", v)
126 }
127}