@@ -35,6 +35,13 @@ type SanitizeOptions struct {
3535 KeepUnknownProperties bool `yaml:"keepUnknownProperties,omitempty"`
3636}
3737
38+ // SanitizeResult contains the results of a sanitization operation.
39+ type SanitizeResult struct {
40+ // Warnings contains non-fatal issues encountered during sanitization.
41+ // These typically include invalid glob patterns that were skipped.
42+ Warnings []string
43+ }
44+
3845// Sanitize cleans an OpenAPI document by removing unwanted elements.
3946// By default (nil options or zero values), it provides aggressive cleanup:
4047// - Removes all extensions (x-*)
@@ -64,18 +71,21 @@ type SanitizeOptions struct {
6471// Example usage:
6572//
6673// // Default sanitization: remove all extensions, unused components, and unknown properties
67- // err := Sanitize(ctx, doc, nil)
74+ // result, err := Sanitize(ctx, doc, nil)
6875// if err != nil {
6976// return fmt.Errorf("failed to sanitize document: %w", err)
7077// }
78+ // for _, warning := range result.Warnings {
79+ // fmt.Fprintf(os.Stderr, "Warning: %s\n", warning)
80+ // }
7181//
7282// // Remove only x-go-* extensions, keep everything else
7383// opts := &SanitizeOptions{
7484// ExtensionPatterns: []string{"x-go-*"},
7585// KeepUnusedComponents: true,
7686// KeepUnknownProperties: true,
7787// }
78- // err := Sanitize(ctx, doc, opts)
88+ // result, err := Sanitize(ctx, doc, opts)
7989// if err != nil {
8090// return fmt.Errorf("failed to sanitize document: %w", err)
8191// }
@@ -84,18 +94,21 @@ type SanitizeOptions struct {
8494// opts := &SanitizeOptions{
8595// KeepUnusedComponents: true,
8696// }
87- // err := Sanitize(ctx, doc, opts)
97+ // result, err := Sanitize(ctx, doc, opts)
8898//
8999// Parameters:
90100// - ctx: Context for the operation
91101// - doc: The OpenAPI document to sanitize (modified in place)
92102// - opts: Sanitization options (nil uses defaults: aggressive cleanup)
93103//
94104// Returns:
105+ // - *SanitizeResult: Result containing any warnings from the operation
95106// - error: Any error that occurred during sanitization
96- func Sanitize (ctx context.Context , doc * OpenAPI , opts * SanitizeOptions ) error {
107+ func Sanitize (ctx context.Context , doc * OpenAPI , opts * SanitizeOptions ) (* SanitizeResult , error ) {
108+ result := & SanitizeResult {}
109+
97110 if doc == nil {
98- return nil
111+ return result , nil
99112 }
100113
101114 // Use default options if nil
@@ -104,25 +117,27 @@ func Sanitize(ctx context.Context, doc *OpenAPI, opts *SanitizeOptions) error {
104117 }
105118
106119 // Remove extensions based on configuration
107- if err := removeExtensions (ctx , doc , opts ); err != nil {
108- return fmt .Errorf ("failed to remove extensions: %w" , err )
120+ warnings , err := removeExtensions (ctx , doc , opts )
121+ if err != nil {
122+ return result , fmt .Errorf ("failed to remove extensions: %w" , err )
109123 }
124+ result .Warnings = append (result .Warnings , warnings ... )
110125
111126 // Remove unknown properties if not keeping them
112127 if ! opts .KeepUnknownProperties {
113128 if err := removeUnknownProperties (ctx , doc ); err != nil {
114- return fmt .Errorf ("failed to remove unknown properties: %w" , err )
129+ return result , fmt .Errorf ("failed to remove unknown properties: %w" , err )
115130 }
116131 }
117132
118133 // Clean unused components if not keeping them
119134 if ! opts .KeepUnusedComponents {
120135 if err := Clean (ctx , doc ); err != nil {
121- return fmt .Errorf ("failed to clean unused components: %w" , err )
136+ return result , fmt .Errorf ("failed to clean unused components: %w" , err )
122137 }
123138 }
124139
125- return nil
140+ return result , nil
126141}
127142
128143// LoadSanitizeConfig loads sanitize configuration from a YAML reader.
@@ -152,21 +167,23 @@ func LoadSanitizeConfigFromFile(path string) (*SanitizeOptions, error) {
152167}
153168
154169// removeExtensions walks through the document and removes extensions based on options.
155- func removeExtensions (ctx context.Context , doc * OpenAPI , opts * SanitizeOptions ) error {
170+ // Returns a slice of warnings for any invalid glob patterns encountered.
171+ func removeExtensions (ctx context.Context , doc * OpenAPI , opts * SanitizeOptions ) ([]string , error ) {
156172 // Determine removal strategy:
157173 // - nil ExtensionPatterns: remove ALL extensions (default)
158174 // - empty array []: keep ALL extensions (explicit no-op)
159175 // - non-empty array: remove only matching patterns
160176
161- if opts != nil && opts .ExtensionPatterns != nil && len (opts .ExtensionPatterns ) == 0 {
162- // Empty array explicitly set = keep all extensions
163- return nil
164- }
165-
166177 var patterns []string
167178 removeAll := true
179+ var invalidPatterns []string
168180
169- if opts != nil && opts .ExtensionPatterns != nil && len (opts .ExtensionPatterns ) > 0 {
181+ // Handle extension patterns if explicitly set
182+ if opts != nil && opts .ExtensionPatterns != nil {
183+ if len (opts .ExtensionPatterns ) == 0 {
184+ // Empty array explicitly set = keep all extensions
185+ return nil , nil
186+ }
170187 // Use patterns for selective removal
171188 patterns = opts .ExtensionPatterns
172189 removeAll = false
@@ -189,7 +206,14 @@ func removeExtensions(ctx context.Context, doc *OpenAPI, opts *SanitizeOptions)
189206 shouldRemove = true
190207 } else {
191208 // Check if extension matches any pattern
192- shouldRemove = matchesAnyPattern (key , patterns )
209+ matched , invalid := matchesAnyPattern (key , patterns )
210+ shouldRemove = matched
211+ // Collect invalid patterns (only once per pattern)
212+ for _ , pattern := range invalid {
213+ if ! contains (invalidPatterns , pattern ) {
214+ invalidPatterns = append (invalidPatterns , pattern )
215+ }
216+ }
193217 }
194218
195219 if shouldRemove {
@@ -206,11 +230,17 @@ func removeExtensions(ctx context.Context, doc *OpenAPI, opts *SanitizeOptions)
206230 },
207231 })
208232 if err != nil {
209- return fmt .Errorf ("failed to process extensions: %w" , err )
233+ return nil , fmt .Errorf ("failed to process extensions: %w" , err )
210234 }
211235 }
212236
213- return nil
237+ // Convert invalid patterns to warnings
238+ var warnings []string
239+ for _ , pattern := range invalidPatterns {
240+ warnings = append (warnings , fmt .Sprintf ("invalid glob pattern '%s' was skipped" , pattern ))
241+ }
242+
243+ return warnings , nil
214244}
215245
216246// removeUnknownProperties removes properties that are not defined in the OpenAPI specification.
@@ -370,9 +400,9 @@ func removePropertiesFromNode(node *yaml.Node, keysToRemove []string) {
370400 }
371401
372402 // Build a set of keys to remove for efficient lookup
373- removeSet := make (map [string ]bool , len (keysToRemove ))
403+ removeSet := make (map [string ]struct {} , len (keysToRemove ))
374404 for _ , key := range keysToRemove {
375- removeSet [key ] = true
405+ removeSet [key ] = struct {}{}
376406 }
377407
378408 // Filter content to exclude keys in the remove set
@@ -385,9 +415,11 @@ func removePropertiesFromNode(node *yaml.Node, keysToRemove []string) {
385415 keyNode := node .Content [i ]
386416 valueNode := node .Content [i + 1 ]
387417
388- if keyNode .Kind == yaml .ScalarNode && removeSet [keyNode .Value ] {
389- // Skip this key-value pair (it's unknown)
390- continue
418+ if keyNode .Kind == yaml .ScalarNode {
419+ if _ , shouldRemove := removeSet [keyNode .Value ]; shouldRemove {
420+ // Skip this key-value pair (it's unknown)
421+ continue
422+ }
391423 }
392424
393425 // Keep this key-value pair
@@ -399,14 +431,28 @@ func removePropertiesFromNode(node *yaml.Node, keysToRemove []string) {
399431}
400432
401433// matchesAnyPattern checks if a string matches any of the provided glob patterns.
402- func matchesAnyPattern (str string , patterns []string ) bool {
434+ // Returns (matched bool, invalidPatterns []string) where invalidPatterns contains
435+ // any patterns that failed to compile.
436+ func matchesAnyPattern (str string , patterns []string ) (bool , []string ) {
437+ var invalidPatterns []string
403438 for _ , pattern := range patterns {
404439 matched , err := filepath .Match (pattern , str )
405440 if err != nil {
406- // Invalid pattern, skip it
441+ // Collect invalid pattern
442+ invalidPatterns = append (invalidPatterns , pattern )
407443 continue
408444 }
409445 if matched {
446+ return true , invalidPatterns
447+ }
448+ }
449+ return false , invalidPatterns
450+ }
451+
452+ // contains checks if a string slice contains a specific string.
453+ func contains (slice []string , str string ) bool {
454+ for _ , s := range slice {
455+ if s == str {
410456 return true
411457 }
412458 }
0 commit comments