-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathschema_property.go
More file actions
343 lines (292 loc) · 10.8 KB
/
Copy pathschema_property.go
File metadata and controls
343 lines (292 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
// Copyright 2025 DoorDash, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
package codegen
import (
"fmt"
"slices"
"strings"
"github.com/doordash-oss/oapi-codegen-dd/v3/pkg/runtime"
)
type Property struct {
GoName string
Description string
JsonFieldName string
Schema GoSchema
Extensions map[string]any
Deprecated bool
Constraints Constraints
SensitiveData *runtime.SensitiveDataConfig
ParentType string // Name of the parent type (for detecting recursive references)
}
func (p Property) IsEqual(other Property) bool {
return p.JsonFieldName == other.JsonFieldName &&
p.Schema.TypeDecl() == other.Schema.TypeDecl() &&
p.Constraints.IsEqual(other.Constraints)
}
// reservedTemplateVars are variable names used in handler templates that should be avoided.
var reservedTemplateVars = map[string]bool{
"ctx": true, "opts": true, "pathParams": true, "query": true, "queryParams": true,
"headerParams": true, "headers": true, "body": true, "resp": true, "status": true,
"w": true, "r": true, "err": true, "result": true, "values": true,
}
// GoVariableName returns a safe Go variable name for this property.
// It handles Go keywords by prefixing with "p" and numbers by prefixing with "n".
// It also avoids reserved template variable names.
func (p Property) GoVariableName() string {
// Use GoName (already sanitized) and convert to lowerCamelCase
name := p.GoName
if len(name) > 0 {
name = strings.ToLower(string(name[0])) + name[1:]
}
if isGoKeyword(name) || reservedTemplateVars[name] {
name = "p" + p.GoName
}
if len(name) > 0 && name[0] >= '0' && name[0] <= '9' {
name = "n" + name
}
return name
}
// IsFile returns true if this property is a file type (format: binary).
func (p Property) IsFile() bool {
return p.Schema.GoType == "runtime.File"
}
func (p Property) GoTypeDef() string {
typeDef := p.Schema.TypeDecl()
if p.IsPointerType() {
typeDef = "*" + strings.TrimPrefix(typeDef, "*")
}
return typeDef
}
// IsPointerType returns true if this property's Go type is a pointer.
func (p Property) IsPointerType() bool {
typeDef := p.Schema.TypeDecl()
// Check for recursive references FIRST: if this property's type is the same as its parent type,
// it MUST be a pointer to avoid infinite size structs, even if it has additional properties
if p.ParentType != "" {
// Check both RefType and GoType for matches
if (p.Schema.RefType != "" && p.Schema.RefType == p.ParentType) ||
(p.Schema.GoType != "" && p.Schema.GoType == p.ParentType) {
return true
}
}
// Arrays, maps, and objects with additional properties are not pointers
if p.Schema.OpenAPISchema != nil && slices.Contains(p.Schema.OpenAPISchema.Type, "array") {
return false
}
if p.Schema.OpenAPISchema != nil && slices.Contains(p.Schema.OpenAPISchema.Type, "object") {
if schemaHasAdditionalProperties(p.Schema.OpenAPISchema) {
return false
}
}
if strings.HasPrefix(typeDef, "map[") || strings.HasPrefix(typeDef, "[]") {
return false
}
// Check if it's a pointer based on nullable and SkipOptionalPointer
return !p.Schema.SkipOptionalPointer && p.Constraints.Nullable != nil && *p.Constraints.Nullable
}
// needsCustomValidation returns true if this property needs custom validation logic
// (i.e., calling Validate() method) instead of just using validator tags.
//
// The logic:
// 1. Check the type first (primitive vs custom)
// 2. If primitive type with validation tags → use validator.Var() (return false)
// 3. If custom type (even with validation tags) → call .Validate() (return true)
// 4. If primitive type without validation tags → skip validation (return false)
// 5. Special case: arrays/maps with item types that need validation → return true
func (p Property) needsCustomValidation() bool {
// Get the type definition
typeDef := p.Schema.TypeDecl()
// Empty type means no validation needed
if typeDef == "" {
return false
}
// Check if it's an array with items that need validation
// This must be checked before the general "primitive" check because arrays
// of custom types (e.g., []DisputeInfo) need custom validation to iterate
// over elements and call their Validate() methods
if p.Schema.ArrayType != nil {
if p.Schema.ArrayType.NeedsValidation() {
return true
}
}
// Check if it's a map with values that need validation
if p.Schema.AdditionalPropertiesType != nil {
if p.Schema.AdditionalPropertiesType.NeedsValidation() {
return true
}
}
// Check if it's a primitive type or primitive alias
isPrimitive := p.Schema.IsPrimitiveAlias ||
isPrimitiveType(typeDef) ||
strings.HasPrefix(typeDef, "[]") ||
strings.HasPrefix(typeDef, "map[")
// If it's a primitive type with validation tags, use validator.Var()
if isPrimitive && len(p.Constraints.ValidationTags) > 0 {
return false
}
// If it's a primitive type without validation tags, skip validation
if isPrimitive {
return false
}
// For custom types (structs, unions, etc.), always call .Validate()
// even if they have validation tags (the tags are ignored for custom types)
return true
}
// needsPatternValidation reports whether this property carries an enforceable regex pattern
// (resolved from its schema, including $ref targets).
func (p Property) needsPatternValidation() bool {
return p.Constraints.hasPattern() &&
isPatternValidatable(p.Schema.TypeDecl()) &&
patternCompiles(*p.Constraints.Pattern)
}
func createPropertyGoFieldName(jsonName string, extensions map[string]any) string {
goFieldName := jsonName
if extension, ok := extensions[extGoName]; ok {
if extGoFieldName, err := parseString(extension); err == nil {
goFieldName = extGoFieldName
}
}
if extension, ok := extensions[extOapiCodegenOnlyHonourGoName]; ok {
if use, err := parseBooleanValue(extension); err == nil {
if use {
return goFieldName
}
}
}
// convert some special names needed for interfaces
// "error" (lowercase) conflicts with the error interface
// "Error" (capitalized) conflicts with the Error() method that we generate for error response types
if goFieldName == "error" || goFieldName == "Error" {
goFieldName = "ErrorData"
}
// "Validate" conflicts with the Validate() method that we generate for validation
typeName := schemaNameToTypeName(goFieldName)
if typeName == "Validate" {
return "ValidateData"
}
return typeName
}
// deduplicateProperties removes duplicate properties based on GoName,
// keeping the last occurrence (which takes precedence in allOf merging)
func deduplicateProperties(props []Property) []Property {
if len(props) == 0 {
return props
}
// Use a map to track the last occurrence of each GoName
seen := make(map[string]int)
for i, p := range props {
seen[p.GoName] = i
}
// Build result with only the last occurrence of each GoName
result := make([]Property, 0, len(seen))
for i, p := range props {
if seen[p.GoName] == i {
result = append(result, p)
}
}
return result
}
// genFieldsFromProperties produce corresponding field names with JSON annotations,
// given a list of schema descriptors
func genFieldsFromProperties(props []Property, options ParseOptions) []string {
// Deduplicate properties to avoid generating duplicate struct fields
// This handles cases where allOf merging results in duplicate property names
props = deduplicateProperties(props)
var fields []string
for i, p := range props {
field := ""
goFieldName := p.GoName
// Add a comment to a field in case we have one, otherwise skip.
if !options.OmitDescription && p.Description != "" {
// Separate the comment from a previous-defined, unrelated field.
// Make sure the actual field is separated by a newline.
if i != 0 {
field += "\n"
}
field += fmt.Sprintf("%s\n", stringWithTypeNameToGoComment(p.Description, p.GoName))
}
if p.Deprecated {
// This comment has to be on its own line for godoc & IDEs to pick up
var deprecationReason string
if extension, ok := p.Extensions[extDeprecationReason]; ok {
if extOmitEmpty, err := parseString(extension); err == nil {
deprecationReason = extOmitEmpty
}
}
field += fmt.Sprintf("%s\n", deprecationComment(deprecationReason))
}
// Check x-go-type-skip-optional-pointer, which will override if the type
// should be a pointer or not when the field is optional.
if extension, ok := p.Extensions[extPropGoTypeSkipOptionalPointer]; ok {
if skipOptionalPointer, err := parseBooleanValue(extension); err == nil {
p.Schema.SkipOptionalPointer = skipOptionalPointer
}
}
field += fmt.Sprintf(" %s %s", goFieldName, p.GoTypeDef())
c := p.Constraints
omitEmpty := c.Nullable != nil && *c.Nullable
if p.Schema.SkipOptionalPointer {
omitEmpty = false
}
// Support x-omitempty
if extOmitEmptyValue, ok := p.Extensions[extPropOmitEmpty]; ok {
if extOmitEmpty, err := parseBooleanValue(extOmitEmptyValue); err == nil {
omitEmpty = extOmitEmpty
}
}
fieldTags := make(map[string]string)
if !options.SkipValidation && len(p.Constraints.ValidationTags) > 0 {
fieldTags["validate"] = strings.Join(c.ValidationTags, ",")
}
jsonFieldName := p.JsonFieldName
if jsonFieldName == "" {
jsonFieldName = "-"
}
fieldTags["json"] = jsonFieldName
if omitEmpty && jsonFieldName != "-" {
fieldTags["json"] += ",omitempty"
}
// Support x-go-json-ignore
if extension, ok := p.Extensions[extPropGoJsonIgnore]; ok {
if goJsonIgnore, err := parseBooleanValue(extension); err == nil && goJsonIgnore {
fieldTags["json"] = "-"
}
}
// Support x-oapi-codegen-extra-tags
if extension, ok := p.Extensions[extPropExtraTags]; ok {
if tags, err := extExtraTags(extension); err == nil {
keys := sortedMapKeys(tags)
for _, k := range keys {
fieldTags[k] = tags[k]
}
}
}
// Emit additional tags (mirrors json tag value, but doesn't overwrite extra-tags)
for _, tag := range options.AdditionalTags {
if _, exists := fieldTags[tag]; !exists {
fieldTags[tag] = fieldTags["json"]
}
}
// Support x-sensitive-data - add a simple marker tag
// The actual masking is handled via custom MarshalJSON generation
if _, ok := p.Extensions[extSensitiveData]; ok {
fieldTags["sensitive"] = ""
}
// Convert the fieldTags map into Go field annotations.
keys := sortedMapKeys(fieldTags)
tags := make([]string, len(keys))
for j, k := range keys {
tags[j] = fmt.Sprintf(`%s:"%s"`, k, fieldTags[k])
}
field += "`" + strings.Join(tags, " ") + "`"
fields = append(fields, field)
}
return fields
}