Skip to content

Commit fcd97d5

Browse files
refactor(sanitize): address PR feedback and improve code quality
- Simplify nested conditions in removeExtensions by consolidating checks - Add SanitizeResult type to collect and return warnings for invalid glob patterns - Pre-allocate unknownProperties slice capacity for better performance - Use map[string]struct{} instead of map[string]bool for memory efficiency - Add PrintWarning method to OpenAPIProcessor for warning display - Update all tests and CLI to handle new return signature Addresses Copilot review feedback in PR #59
1 parent 5846405 commit fcd97d5

File tree

5 files changed

+106
-38
lines changed

5 files changed

+106
-38
lines changed

marshaller/unmarshaller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ func unmarshalModel(ctx context.Context, parentName string, node *yaml.Node, str
349349

350350
// Track unknown properties (non-extension, non-field, non-embedded map properties)
351351
var unknownPropertiesMutex sync.Mutex
352-
var unknownProperties []string
352+
unknownProperties := make([]string, 0, numJobs)
353353

354354
// Mutex to protect concurrent access to extensionsField
355355
var extensionsMutex sync.Mutex

openapi/cmd/sanitize.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,16 @@ func sanitizeOpenAPI(ctx context.Context, processor *OpenAPIProcessor) error {
143143
}
144144

145145
// Perform the sanitization
146-
if err := openapi.Sanitize(ctx, doc, opts); err != nil {
146+
result, err := openapi.Sanitize(ctx, doc, opts)
147+
if err != nil {
147148
return fmt.Errorf("failed to sanitize OpenAPI document: %w", err)
148149
}
149150

151+
// Report any warnings
152+
for _, warning := range result.Warnings {
153+
processor.PrintWarning(warning)
154+
}
155+
150156
// Report success
151157
reportSanitizationResults(processor, opts)
152158

openapi/cmd/shared.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,10 @@ func (p *OpenAPIProcessor) PrintInfo(message string) {
114114
fmt.Printf("📋 %s\n", message)
115115
}
116116
}
117+
118+
// PrintWarning prints a warning message if not writing to stdout
119+
func (p *OpenAPIProcessor) PrintWarning(message string) {
120+
if !p.WriteToStdout {
121+
fmt.Printf("⚠️ Warning: %s\n", message)
122+
}
123+
}

openapi/sanitize.go

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

openapi/sanitize_test.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ func TestSanitize_RemoveAllExtensions_Success(t *testing.T) {
2626
require.Empty(t, validationErrs, "Input document should be valid")
2727

2828
// Sanitize with default options (remove all extensions and clean components)
29-
err = openapi.Sanitize(ctx, inputDoc, nil)
29+
result, err := openapi.Sanitize(ctx, inputDoc, nil)
3030
require.NoError(t, err)
31+
assert.Empty(t, result.Warnings, "Should not have warnings")
3132

3233
// Marshal the sanitized document to YAML
3334
var buf bytes.Buffer
@@ -63,8 +64,9 @@ func TestSanitize_PatternBased_Success(t *testing.T) {
6364
KeepUnusedComponents: true,
6465
KeepUnknownProperties: true,
6566
}
66-
err = openapi.Sanitize(ctx, inputDoc, opts)
67+
result, err := openapi.Sanitize(ctx, inputDoc, opts)
6768
require.NoError(t, err)
69+
assert.Empty(t, result.Warnings, "Should not have warnings")
6870

6971
// Marshal the sanitized document to YAML
7072
var buf bytes.Buffer
@@ -99,8 +101,9 @@ func TestSanitize_MultiplePatterns_Success(t *testing.T) {
99101
ExtensionPatterns: []string{"x-go-*", "x-internal-*"},
100102
KeepUnusedComponents: true,
101103
}
102-
err = openapi.Sanitize(ctx, inputDoc, opts)
104+
result, err := openapi.Sanitize(ctx, inputDoc, opts)
103105
require.NoError(t, err)
106+
assert.Empty(t, result.Warnings, "Should not have warnings")
104107

105108
// Marshal the sanitized document to YAML
106109
var buf bytes.Buffer
@@ -134,8 +137,9 @@ func TestSanitize_KeepComponents_Success(t *testing.T) {
134137
opts := &openapi.SanitizeOptions{
135138
KeepUnusedComponents: true,
136139
}
137-
err = openapi.Sanitize(ctx, inputDoc, opts)
140+
result, err := openapi.Sanitize(ctx, inputDoc, opts)
138141
require.NoError(t, err)
142+
assert.Empty(t, result.Warnings, "Should not have warnings")
139143

140144
// Marshal the sanitized document to YAML
141145
var buf bytes.Buffer
@@ -157,8 +161,9 @@ func TestSanitize_EmptyDocument_Success(t *testing.T) {
157161
ctx := t.Context()
158162

159163
// Test with nil document
160-
err := openapi.Sanitize(ctx, nil, nil)
164+
result, err := openapi.Sanitize(ctx, nil, nil)
161165
require.NoError(t, err)
166+
assert.Empty(t, result.Warnings, "Should not have warnings")
162167

163168
// Test with minimal document (no components, no extensions)
164169
doc := &openapi.OpenAPI{
@@ -169,8 +174,9 @@ func TestSanitize_EmptyDocument_Success(t *testing.T) {
169174
},
170175
}
171176

172-
err = openapi.Sanitize(ctx, doc, nil)
177+
result, err = openapi.Sanitize(ctx, doc, nil)
173178
require.NoError(t, err)
179+
assert.Empty(t, result.Warnings, "Should not have warnings")
174180
}
175181

176182
func TestSanitize_NoExtensions_Success(t *testing.T) {
@@ -188,8 +194,9 @@ func TestSanitize_NoExtensions_Success(t *testing.T) {
188194
require.Empty(t, validationErrs, "Input document should be valid")
189195

190196
// Sanitize (should be a no-op for extensions)
191-
err = openapi.Sanitize(ctx, inputDoc, nil)
197+
result, err := openapi.Sanitize(ctx, inputDoc, nil)
192198
require.NoError(t, err)
199+
assert.Empty(t, result.Warnings, "Should not have warnings")
193200

194201
// Document should still be valid
195202
assert.NotNil(t, inputDoc)
@@ -265,8 +272,9 @@ keepUnknownProperties: true
265272
require.NoError(t, err)
266273

267274
// Sanitize using config
268-
err = openapi.Sanitize(ctx, inputDoc, opts)
275+
result, err := openapi.Sanitize(ctx, inputDoc, opts)
269276
require.NoError(t, err)
277+
assert.Empty(t, result.Warnings, "Should not have warnings")
270278

271279
// Marshal the sanitized document to YAML
272280
var buf bytes.Buffer
@@ -304,8 +312,9 @@ func TestSanitize_KeepExtensionsRemoveUnknownProperties_Success(t *testing.T) {
304312
KeepUnusedComponents: true,
305313
}
306314

307-
err = openapi.Sanitize(ctx, inputDoc, opts)
315+
result, err := openapi.Sanitize(ctx, inputDoc, opts)
308316
require.NoError(t, err)
317+
assert.Empty(t, result.Warnings, "Should not have warnings")
309318

310319
// Marshal the sanitized document
311320
var buf bytes.Buffer

0 commit comments

Comments
 (0)