Skip to content

Commit a06eab4

Browse files
Parameterize content types for models
Allow users to control which content types are included in model generation for requests, responses.
1 parent 63de366 commit a06eab4

File tree

5 files changed

+139
-4
lines changed

5 files changed

+139
-4
lines changed

experimental/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ import-mapping:
175175
https://example.com/specs/shared.yaml: github.com/org/shared
176176
# Use "-" to indicate types should stay in the current package
177177
./local-types.yaml: "-"
178+
179+
# Content types: filter which media types generate models (regexp patterns)
180+
# Only request/response bodies with matching content types will have types generated.
181+
# Defaults to JSON types if not specified.
182+
content-types:
183+
- "^application/json$"
184+
- "^application/.*\\+json$"
185+
# Add custom patterns as needed:
186+
# - "^application/xml$"
187+
# - "^text/plain$"
178188
```
179189

180190
### External References

experimental/internal/codegen/codegen.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import (
1515
func Generate(doc libopenapi.Document, cfg Configuration) (string, error) {
1616
cfg.ApplyDefaults()
1717

18+
// Create content type matcher for filtering request/response bodies
19+
contentTypeMatcher := NewContentTypeMatcher(cfg.ContentTypes)
20+
1821
// Pass 1: Gather all schemas that need types
19-
schemas, err := GatherSchemas(doc)
22+
schemas, err := GatherSchemas(doc, contentTypeMatcher)
2023
if err != nil {
2124
return "", fmt.Errorf("gathering schemas: %w", err)
2225
}

experimental/internal/codegen/configuration.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package codegen
33
import (
44
"crypto/sha256"
55
"encoding/hex"
6+
"regexp"
67
"sort"
78
)
89

@@ -21,12 +22,57 @@ type Configuration struct {
2122
// Example: {"../common/api.yaml": "github.com/org/project/common"}
2223
// Use "-" as the value to indicate types should be in the current package.
2324
ImportMapping map[string]string `yaml:"import-mapping,omitempty"`
25+
// ContentTypes is a list of regexp patterns for media types to generate models for.
26+
// Only request/response bodies with matching content types will have types generated.
27+
// Defaults to common JSON and YAML types if not specified.
28+
ContentTypes []string `yaml:"content-types,omitempty"`
29+
}
30+
31+
// DefaultContentTypes returns the default list of content type patterns.
32+
// These match common JSON and YAML media types.
33+
func DefaultContentTypes() []string {
34+
return []string{
35+
`^application/json$`,
36+
`^application/.*\+json$`,
37+
}
2438
}
2539

2640
// ApplyDefaults merges user configuration on top of default values.
2741
func (c *Configuration) ApplyDefaults() {
2842
c.TypeMapping = DefaultTypeMapping.Merge(c.TypeMapping)
2943
c.NameMangling = DefaultNameMangling().Merge(c.NameMangling)
44+
if len(c.ContentTypes) == 0 {
45+
c.ContentTypes = DefaultContentTypes()
46+
}
47+
}
48+
49+
// ContentTypeMatcher checks if content types match configured patterns.
50+
type ContentTypeMatcher struct {
51+
patterns []*regexp.Regexp
52+
}
53+
54+
// NewContentTypeMatcher creates a matcher from a list of regexp patterns.
55+
// Invalid patterns are silently ignored.
56+
func NewContentTypeMatcher(patterns []string) *ContentTypeMatcher {
57+
m := &ContentTypeMatcher{
58+
patterns: make([]*regexp.Regexp, 0, len(patterns)),
59+
}
60+
for _, p := range patterns {
61+
if re, err := regexp.Compile(p); err == nil {
62+
m.patterns = append(m.patterns, re)
63+
}
64+
}
65+
return m
66+
}
67+
68+
// Matches returns true if the content type matches any of the configured patterns.
69+
func (m *ContentTypeMatcher) Matches(contentType string) bool {
70+
for _, re := range m.patterns {
71+
if re.MatchString(contentType) {
72+
return true
73+
}
74+
}
75+
return false
3076
}
3177

3278
// ExternalImport represents an external package import with its alias.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package codegen
2+
3+
import "testing"
4+
5+
func TestContentTypeMatcher(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
patterns []string
9+
contentType string
10+
want bool
11+
}{
12+
// Default patterns - JSON only (YAML not supported without custom unmarshalers)
13+
{"json exact", DefaultContentTypes(), "application/json", true},
14+
{"json+suffix", DefaultContentTypes(), "application/vnd.api+json", true},
15+
{"problem+json", DefaultContentTypes(), "application/problem+json", true},
16+
17+
// YAML not in defaults (would need custom unmarshalers)
18+
{"yaml not default", DefaultContentTypes(), "application/yaml", false},
19+
{"text/yaml not default", DefaultContentTypes(), "text/yaml", false},
20+
21+
// Non-matching
22+
{"text/plain", DefaultContentTypes(), "text/plain", false},
23+
{"text/html", DefaultContentTypes(), "text/html", false},
24+
{"application/xml", DefaultContentTypes(), "application/xml", false},
25+
{"application/octet-stream", DefaultContentTypes(), "application/octet-stream", false},
26+
{"multipart/form-data", DefaultContentTypes(), "multipart/form-data", false},
27+
{"image/png", DefaultContentTypes(), "image/png", false},
28+
29+
// Custom patterns
30+
{"custom xml", []string{`^application/xml$`}, "application/xml", true},
31+
{"custom xml no match", []string{`^application/xml$`}, "application/json", false},
32+
{"custom wildcard", []string{`^text/.*`}, "text/plain", true},
33+
{"custom wildcard html", []string{`^text/.*`}, "text/html", true},
34+
{"custom yaml", []string{`^application/yaml$`}, "application/yaml", true},
35+
36+
// Empty patterns
37+
{"empty patterns", []string{}, "application/json", false},
38+
39+
// Invalid pattern (silently ignored)
40+
{"invalid pattern", []string{`[invalid`}, "application/json", false},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
m := NewContentTypeMatcher(tt.patterns)
46+
got := m.Matches(tt.contentType)
47+
if got != tt.want {
48+
t.Errorf("Matches(%q) = %v, want %v", tt.contentType, got, tt.want)
49+
}
50+
})
51+
}
52+
}
53+
54+
func TestDefaultContentTypes(t *testing.T) {
55+
defaults := DefaultContentTypes()
56+
if len(defaults) == 0 {
57+
t.Error("DefaultContentTypes() returned empty slice")
58+
}
59+
60+
// Verify all patterns are valid regexps
61+
m := NewContentTypeMatcher(defaults)
62+
if len(m.patterns) != len(defaults) {
63+
t.Errorf("Some default patterns failed to compile: got %d patterns, want %d",
64+
len(m.patterns), len(defaults))
65+
}
66+
}

experimental/internal/codegen/gather.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// GatherSchemas traverses an OpenAPI document and collects all schemas into a list.
12-
func GatherSchemas(doc libopenapi.Document) ([]*SchemaDescriptor, error) {
12+
func GatherSchemas(doc libopenapi.Document, contentTypeMatcher *ContentTypeMatcher) ([]*SchemaDescriptor, error) {
1313
model, errs := doc.BuildV3Model()
1414
if len(errs) > 0 {
1515
return nil, fmt.Errorf("building v3 model: %v", errs[0])
@@ -19,15 +19,17 @@ func GatherSchemas(doc libopenapi.Document) ([]*SchemaDescriptor, error) {
1919
}
2020

2121
g := &gatherer{
22-
schemas: make([]*SchemaDescriptor, 0),
22+
schemas: make([]*SchemaDescriptor, 0),
23+
contentTypeMatcher: contentTypeMatcher,
2324
}
2425

2526
g.gatherFromDocument(&model.Model)
2627
return g.schemas, nil
2728
}
2829

2930
type gatherer struct {
30-
schemas []*SchemaDescriptor
31+
schemas []*SchemaDescriptor
32+
contentTypeMatcher *ContentTypeMatcher
3133
}
3234

3335
func (g *gatherer) gatherFromDocument(doc *v3.Document) {
@@ -141,6 +143,10 @@ func (g *gatherer) gatherFromRequestBody(rb *v3.RequestBody, basePath SchemaPath
141143

142144
for pair := rb.Content.First(); pair != nil; pair = pair.Next() {
143145
contentType := pair.Key()
146+
// Skip content types that don't match the configured patterns
147+
if g.contentTypeMatcher != nil && !g.contentTypeMatcher.Matches(contentType) {
148+
continue
149+
}
144150
mediaType := pair.Value()
145151
g.gatherFromMediaType(mediaType, basePath.Append("content", contentType))
146152
}
@@ -154,6 +160,10 @@ func (g *gatherer) gatherFromResponse(response *v3.Response, basePath SchemaPath
154160
if response.Content != nil {
155161
for pair := response.Content.First(); pair != nil; pair = pair.Next() {
156162
contentType := pair.Key()
163+
// Skip content types that don't match the configured patterns
164+
if g.contentTypeMatcher != nil && !g.contentTypeMatcher.Matches(contentType) {
165+
continue
166+
}
157167
mediaType := pair.Value()
158168
g.gatherFromMediaType(mediaType, basePath.Append("content", contentType))
159169
}

0 commit comments

Comments
 (0)