Skip to content

Commit 4280c90

Browse files
committed
feat: const validation support
fixes #472
1 parent 740f5fe commit 4280c90

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
@@ -376,6 +376,7 @@ func (g *schemaGenerator) generateDeclaredType(t *schemas.Type, scope nameScope)
376376
return &codegen.NamedType{Decl: &decl}, nil
377377
}
378378

379+
//nolint:gocyclo // todo: reduce cyclomatic complexity
379380
func (g *schemaGenerator) structFieldValidators(
380381
validators []validator,
381382
f codegen.StructField,
@@ -393,12 +394,23 @@ func (g *schemaGenerator) structFieldValidators(
393394
validators = g.structFieldValidators(validators, f, v.Type, v.IsNillable())
394395

395396
case codegen.PrimitiveType:
396-
if v.Type == schemas.TypeNameString {
397+
switch {
398+
case v.Type == schemas.TypeNameString:
397399
hasPattern := len(f.SchemaType.Pattern) != 0
398-
if f.SchemaType.MinLength != 0 || f.SchemaType.MaxLength != 0 || hasPattern {
400+
if f.SchemaType.MinLength != 0 || f.SchemaType.MaxLength != 0 || hasPattern || f.SchemaType.Const != nil {
399401
// Double escape the escape characters so we don't effectively parse the escapes within the value.
400402
escapedPattern := f.SchemaType.Pattern
401403

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

404416
replaceJSONCharacters := []string{"\b", "\f", "\n", "\r", "\t"}
@@ -413,19 +425,22 @@ func (g *schemaGenerator) structFieldValidators(
413425
minLength: f.SchemaType.MinLength,
414426
maxLength: f.SchemaType.MaxLength,
415427
pattern: escapedPattern,
428+
constVal: constVal,
416429
isNillable: isNillable,
417430
})
418431
}
419432

420433
if hasPattern {
421434
g.output.file.Package.AddImport("regexp", "")
422435
}
423-
} else if strings.Contains(v.Type, "int") || v.Type == float64Type {
436+
437+
case strings.Contains(v.Type, "int") || v.Type == float64Type:
424438
if f.SchemaType.MultipleOf != nil ||
425439
f.SchemaType.Maximum != nil ||
426440
f.SchemaType.ExclusiveMaximum != nil ||
427441
f.SchemaType.Minimum != nil ||
428-
f.SchemaType.ExclusiveMinimum != nil {
442+
f.SchemaType.ExclusiveMinimum != nil ||
443+
f.SchemaType.Const != nil {
429444
validators = append(validators, &numericValidator{
430445
jsonName: f.JSONName,
431446
fieldName: f.Name,
@@ -435,13 +450,34 @@ func (g *schemaGenerator) structFieldValidators(
435450
exclusiveMaximum: f.SchemaType.ExclusiveMaximum,
436451
minimum: f.SchemaType.Minimum,
437452
exclusiveMinimum: f.SchemaType.ExclusiveMinimum,
453+
constVal: f.SchemaType.Const,
438454
roundToInt: strings.Contains(v.Type, "int"),
439455
})
440456
}
441457

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

447483
case *codegen.ArrayType:

pkg/generator/validator.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ type stringValidator struct {
356356
maxLength int
357357
isNillable bool
358358
pattern string
359+
constVal *string
359360
}
360361

361362
func (v *stringValidator) generate(out *codegen.Emitter, format string) error {
@@ -393,6 +394,14 @@ func (v *stringValidator) generate(out *codegen.Emitter, format string) error {
393394
}
394395
}
395396

397+
if v.constVal != nil {
398+
out.Printlnf(`if %s%s%s != "%s" {`, checkPointer, pointerPrefix, value, *v.constVal)
399+
out.Indent(1)
400+
out.Printlnf(`return fmt.Errorf("field %%s: must be equal to %%s", "%s", "%s")`, fieldName, *v.constVal)
401+
out.Indent(-1)
402+
out.Printlnf("}")
403+
}
404+
396405
if v.minLength == 0 && v.maxLength == 0 {
397406
return nil
398407
}
@@ -432,6 +441,7 @@ type numericValidator struct {
432441
exclusiveMaximum *any
433442
minimum *float64
434443
exclusiveMinimum *any
444+
constVal any
435445
roundToInt bool
436446
}
437447

@@ -445,6 +455,14 @@ func (v *numericValidator) generate(out *codegen.Emitter, format string) error {
445455
pointerPrefix = "*"
446456
}
447457

458+
if v.constVal != nil {
459+
out.Printlnf(`if %s%s%s != %v {`, checkPointer, pointerPrefix, value, v.constVal)
460+
out.Indent(1)
461+
out.Printlnf(`return fmt.Errorf("field %%s: must be equal to %%v", "%s", %v)`, v.jsonName, v.constVal)
462+
out.Indent(-1)
463+
out.Printlnf("}")
464+
}
465+
448466
if v.multipleOf != nil {
449467
if v.roundToInt {
450468
out.Printlnf(`if %s %s%s %% %v != 0 {`, checkPointer, pointerPrefix, value, v.valueOf(*v.multipleOf))
@@ -527,6 +545,42 @@ func (v *numericValidator) valueOf(val float64) any {
527545
return val
528546
}
529547

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

pkg/schemas/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ type Type struct {
177177
AdditionalProperties *Type `json:"additionalProperties,omitempty"` // Section 5.18.
178178
Enum []interface{} `json:"enum,omitempty"` // Section 5.20.
179179
Type TypeList `json:"type,omitempty"` // Section 5.21.
180+
Const interface{} `json:"const,omitempty"`
180181
// RFC draft-bhutton-json-schema-01, section 10.
181182
AllOf []*Type `json:"allOf,omitempty"` // Section 10.2.1.1.
182183
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)