Skip to content

Commit

Permalink
Support validation by enum constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
0xC0D3D00D committed Feb 21, 2025
1 parent 803d00a commit 844ee11
Show file tree
Hide file tree
Showing 11 changed files with 937 additions and 50 deletions.
285 changes: 245 additions & 40 deletions internal/gen/proto/buf/protoschema/test/v1/constraints.pb.go

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions internal/proto/buf/protoschema/test/v1/constraints.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,45 @@ message ConstraintTest {
optional string string_value = 2 [(buf.validate.field).required = true];
}

enum Enum {
ENUM_UNSPECIFIED = 0;
ENUM_VAL1 = 1;
ENUM_VAL2 = 2;
ENUM_VAL7 = 7;
}

oneof test_case {
RequiredImplicit required_implicit = 1;
RequiredOptional required_optional = 2;
bool const_bool = 3 [(buf.validate.field).bool.const = false];
Enum const_enum = 119 [(buf.validate.field).enum.const = 2];
Enum defined_only_enum = 120 [(buf.validate.field).enum.defined_only = true];
Enum in_enum = 121 [(buf.validate.field).enum = {
in: [
1,
2
]
}];
Enum not_in_enum = 122 [(buf.validate.field).enum = {
not_in: [
0,
7
]
}];
Enum defined_only_not_in_enum = 123 [(buf.validate.field).enum = {
defined_only: true
not_in: [0]
}];
Enum in_and_not_in_enum = 124 [(buf.validate.field).enum = {
in: [
1,
7
]
not_in: [
0,
7
]
}];
string const_string = 4 [(buf.validate.field).string.const = "const"];
string len_string = 5 [(buf.validate.field).string.len = 5];
string min_len_string = 6 [(buf.validate.field).string.min_len = 5];
Expand Down
2 changes: 1 addition & 1 deletion internal/protoschema/golden/golden.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func GetTestDescriptors(testdataPath string) ([]protoreflect.MessageDescriptor,
inputPath := filepath.Join(filepath.FromSlash(testdataPath), "codegenrequest", "input.json")
input, err := os.ReadFile(inputPath)
if err != nil {
return nil, fmt.Errorf("failed to open input file descritpor set at %q: %w", inputPath, err)
return nil, fmt.Errorf("failed to open input file descriptor set at %q: %w", inputPath, err)
}
fdset := &descriptorpb.FileDescriptorSet{}
if err = (&protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(input, fdset); err != nil {
Expand Down
83 changes: 75 additions & 8 deletions internal/protoschema/jsonschema/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package jsonschema
import (
"fmt"
"math"
"slices"
"strings"
"unicode"

Expand Down Expand Up @@ -179,7 +180,7 @@ func (p *jsonSchemaGenerator) generateValidation(field protoreflect.FieldDescrip
case protoreflect.BoolKind:
p.generateBoolValidation(field, constraints, schema)
case protoreflect.EnumKind:
p.generateEnumValidation(field, schema)
p.generateEnumValidation(field, constraints, schema)
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
p.generateInt32Validation(field, constraints, schema)
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
Expand Down Expand Up @@ -244,16 +245,82 @@ func generateTitle(name protoreflect.Name) string {
return result.String()
}

func (p *jsonSchemaGenerator) generateEnumValidation(field protoreflect.FieldDescriptor, schema map[string]interface{}) {
var enum = make([]interface{}, 0)
type enumFieldSelector struct {
selected bool
index int
}

func (p *jsonSchemaGenerator) generateEnumValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
enumFieldSelectors := make(map[int32]enumFieldSelector, field.Enum().Values().Len())
for i := range field.Enum().Values().Len() {
enum = append(enum, field.Enum().Values().Get(i).Name())
val := field.Enum().Values().Get(i)
enumFieldSelectors[int32(val.Number())] = enumFieldSelector{
selected: true,
index: i,
}
}
anyOf := []map[string]interface{}{
{"type": jsString, "enum": enum, "title": generateTitle(field.Enum().Name())},
{"type": jsInteger, "minimum": math.MinInt32, "maximum": math.MaxInt32},

if constraints.GetEnum() != nil && constraints.GetEnum().HasConst() {
for number := range enumFieldSelectors {
if number != constraints.GetEnum().GetConst() {
enumFieldSelectors[number] = enumFieldSelector{}
}
}
}
schema["anyOf"] = anyOf

if constraints.GetEnum() != nil && len(constraints.GetEnum().In) > 0 {
inMap := make(map[int32]struct{}, len(constraints.GetEnum().In))
for _, value := range constraints.GetEnum().In {
inMap[value] = struct{}{}
}

for number := range enumFieldSelectors {
if _, ok := inMap[number]; !ok {
enumFieldSelectors[number] = enumFieldSelector{}
}
}
}

if constraints.GetEnum() != nil && len(constraints.GetEnum().NotIn) > 0 {
for _, value := range constraints.GetEnum().NotIn {
enumFieldSelectors[value] = enumFieldSelector{}
}
}

onlySelectIntValues := constraints.GetEnum() != nil &&
(constraints.GetEnum().GetDefinedOnly() ||
constraints.GetEnum().HasConst() ||
constraints.GetEnum().GetIn() != nil)

validIntegers := map[string]interface{}{"type": jsInteger, "minimum": math.MinInt32, "maximum": math.MaxInt32}
if onlySelectIntValues {
var integerValues = make([]int32, 0)
for number, val := range enumFieldSelectors {
if val.selected {
integerValues = append(integerValues, number)
}
}
slices.Sort(integerValues)

validIntegers = map[string]interface{}{"type": jsInteger, "enum": integerValues}
}

validIndexes := make([]int, 0, len(enumFieldSelectors))
for _, val := range enumFieldSelectors {
if val.selected {
validIndexes = append(validIndexes, val.index)
}
}
slices.Sort(validIndexes)

var stringValues = make([]string, 0)
for _, index := range validIndexes {
stringValues = append(stringValues, string(field.Enum().Values().Get(index).Name()))
}

validStrings := map[string]interface{}{"type": jsString, "enum": stringValues, "title": generateTitle(field.Enum().Name())}

schema["anyOf"] = []map[string]interface{}{validStrings, validIntegers}
}

type baseRule[T comparable] interface {
Expand Down
2 changes: 1 addition & 1 deletion internal/testdata/codegenrequest/input.json

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions internal/testdata/jsonschema-doc/test.ConstraintTests.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions internal/testdata/jsonschema-doc/test.ConstraintTests.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 844ee11

Please sign in to comment.