Skip to content

Commit 68d5db7

Browse files
committed
feat: const validation support
fixes #472
1 parent 5be1479 commit 68d5db7

File tree

5 files changed

+306
-4
lines changed

5 files changed

+306
-4
lines changed

pkg/generator/schema_generator.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ func (g *schemaGenerator) generateDeclaredType(t *schemas.Type, scope nameScope)
373373
return &codegen.NamedType{Decl: &decl}, nil
374374
}
375375

376+
//nolint:gocyclo // todo: reduce cyclomatic complexity
376377
func (g *schemaGenerator) structFieldValidators(
377378
validators []validator,
378379
f codegen.StructField,
@@ -390,12 +391,23 @@ func (g *schemaGenerator) structFieldValidators(
390391
validators = g.structFieldValidators(validators, f, v.Type, v.IsNillable())
391392

392393
case codegen.PrimitiveType:
393-
if v.Type == schemas.TypeNameString {
394+
switch {
395+
case v.Type == schemas.TypeNameString:
394396
hasPattern := len(f.SchemaType.Pattern) != 0
395-
if f.SchemaType.MinLength != 0 || f.SchemaType.MaxLength != 0 || hasPattern {
397+
if f.SchemaType.MinLength != 0 || f.SchemaType.MaxLength != 0 || hasPattern || f.SchemaType.Const != nil {
396398
// Double escape the escape characters so we don't effectively parse the escapes within the value.
397399
escapedPattern := f.SchemaType.Pattern
398400

401+
var constVal *string
402+
403+
if f.SchemaType.Const != nil {
404+
if s, ok := f.SchemaType.Const.(string); ok {
405+
constVal = &s
406+
} else {
407+
g.warner(fmt.Sprintf("Ignoring non string const value: %v", f.SchemaType.Const))
408+
}
409+
}
410+
399411
replaceJSONCharactersBy := []string{"\\b", "\\f", "\\n", "\\r", "\\t"}
400412

401413
replaceJSONCharacters := []string{"\b", "\f", "\n", "\r", "\t"}
@@ -410,19 +422,22 @@ func (g *schemaGenerator) structFieldValidators(
410422
minLength: f.SchemaType.MinLength,
411423
maxLength: f.SchemaType.MaxLength,
412424
pattern: escapedPattern,
425+
constVal: constVal,
413426
isNillable: isNillable,
414427
})
415428
}
416429

417430
if hasPattern {
418431
g.output.file.Package.AddImport("regexp", "")
419432
}
420-
} else if strings.Contains(v.Type, "int") || v.Type == float64Type {
433+
434+
case strings.Contains(v.Type, "int") || v.Type == float64Type:
421435
if f.SchemaType.MultipleOf != nil ||
422436
f.SchemaType.Maximum != nil ||
423437
f.SchemaType.ExclusiveMaximum != nil ||
424438
f.SchemaType.Minimum != nil ||
425-
f.SchemaType.ExclusiveMinimum != nil {
439+
f.SchemaType.ExclusiveMinimum != nil ||
440+
f.SchemaType.Const != nil {
426441
validators = append(validators, &numericValidator{
427442
jsonName: f.JSONName,
428443
fieldName: f.Name,
@@ -432,13 +447,34 @@ func (g *schemaGenerator) structFieldValidators(
432447
exclusiveMaximum: f.SchemaType.ExclusiveMaximum,
433448
minimum: f.SchemaType.Minimum,
434449
exclusiveMinimum: f.SchemaType.ExclusiveMinimum,
450+
constVal: f.SchemaType.Const,
435451
roundToInt: strings.Contains(v.Type, "int"),
436452
})
437453
}
438454

439455
if f.SchemaType.MultipleOf != nil && v.Type == float64Type {
440456
g.output.file.Package.AddImport("math", "")
441457
}
458+
459+
case v.Type == "bool":
460+
if f.SchemaType.Const != nil {
461+
var constVal *bool
462+
463+
if f.SchemaType.Const != nil {
464+
if b, ok := f.SchemaType.Const.(bool); ok {
465+
constVal = &b
466+
} else {
467+
g.warner(fmt.Sprintf("Ignoring non boolean const value: %v", f.SchemaType.Const))
468+
}
469+
}
470+
471+
validators = append(validators, &booleanValidator{
472+
jsonName: f.JSONName,
473+
fieldName: f.Name,
474+
isNillable: isNillable,
475+
constVal: constVal,
476+
})
477+
}
442478
}
443479

444480
case *codegen.ArrayType:

pkg/generator/validator.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ type stringValidator struct {
348348
maxLength int
349349
isNillable bool
350350
pattern string
351+
constVal *string
351352
}
352353

353354
func (v *stringValidator) generate(out *codegen.Emitter, unmarshalTemplate string) error {
@@ -385,6 +386,14 @@ func (v *stringValidator) generate(out *codegen.Emitter, unmarshalTemplate strin
385386
}
386387
}
387388

389+
if v.constVal != nil {
390+
out.Printlnf(`if %s%s%s != "%s" {`, checkPointer, pointerPrefix, value, *v.constVal)
391+
out.Indent(1)
392+
out.Printlnf(`return fmt.Errorf("field %%s: must be equal to %%s", "%s", "%s")`, fieldName, *v.constVal)
393+
out.Indent(-1)
394+
out.Printlnf("}")
395+
}
396+
388397
if v.minLength == 0 && v.maxLength == 0 {
389398
return nil
390399
}
@@ -424,6 +433,7 @@ type numericValidator struct {
424433
exclusiveMaximum *any
425434
minimum *float64
426435
exclusiveMinimum *any
436+
constVal any
427437
roundToInt bool
428438
}
429439

@@ -437,6 +447,14 @@ func (v *numericValidator) generate(out *codegen.Emitter, unmarshalTemplate stri
437447
pointerPrefix = "*"
438448
}
439449

450+
if v.constVal != nil {
451+
out.Printlnf(`if %s%s%s != %v {`, checkPointer, pointerPrefix, value, v.constVal)
452+
out.Indent(1)
453+
out.Printlnf(`return fmt.Errorf("field %%s: must be equal to %%v", "%s", %v)`, v.jsonName, v.constVal)
454+
out.Indent(-1)
455+
out.Printlnf("}")
456+
}
457+
440458
if v.multipleOf != nil {
441459
if v.roundToInt {
442460
out.Printlnf(`if %s %s%s %% %v != 0 {`, checkPointer, pointerPrefix, value, v.valueOf(*v.multipleOf))
@@ -519,6 +537,42 @@ func (v *numericValidator) valueOf(val float64) any {
519537
return val
520538
}
521539

540+
type booleanValidator struct {
541+
jsonName string
542+
fieldName string
543+
isNillable bool
544+
constVal *bool
545+
}
546+
547+
func (v *booleanValidator) generate(out *codegen.Emitter, unmarshalTemplate string) error {
548+
value := getPlainName(v.fieldName)
549+
fieldName := v.jsonName
550+
checkPointer := ""
551+
pointerPrefix := ""
552+
553+
if v.isNillable {
554+
checkPointer = fmt.Sprintf("%s != nil && ", value)
555+
pointerPrefix = "*"
556+
}
557+
558+
if v.constVal != nil {
559+
out.Printlnf(`if %s%s%s != %t {`, checkPointer, pointerPrefix, value, *v.constVal)
560+
out.Indent(1)
561+
out.Printlnf(`return fmt.Errorf("field %%s: must be equal to %%t", "%s", %t)`, fieldName, *v.constVal)
562+
out.Indent(-1)
563+
out.Printlnf("}")
564+
}
565+
566+
return nil
567+
}
568+
569+
func (v *booleanValidator) desc() *validatorDesc {
570+
return &validatorDesc{
571+
hasError: true,
572+
beforeJSONUnmarshal: false,
573+
}
574+
}
575+
522576
func getPlainName(fieldName string) string {
523577
if fieldName == "" {
524578
return varNamePlainStruct

pkg/schemas/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ type Type struct {
170170
AdditionalProperties *Type `json:"additionalProperties,omitempty"` // Section 5.18.
171171
Enum []interface{} `json:"enum,omitempty"` // Section 5.20.
172172
Type TypeList `json:"type,omitempty"` // Section 5.21.
173+
Const interface{} `json:"const,omitempty"`
173174
// RFC draft-bhutton-json-schema-01, section 10.
174175
AllOf []*Type `json:"allOf,omitempty"` // Section 10.2.1.1.
175176
AnyOf []*Type `json:"anyOf,omitempty"` // Section 10.2.1.2.

tests/data/core/const/const.go

Lines changed: 159 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)