Approval-based snapshot testing library for Go (mirror)
at main 127 lines 3.1 kB view raw
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}