Repository: ogen-go/ogen
Pull request #529 (2022-08-10 12:18:48)

feat(jsonschema): validate min-max constraints


Patch:
diff --git a/internal/location/print.go b/internal/location/print.go
index 34ef82b7..5079ffd6 100644
--- a/internal/location/print.go
+++ b/internal/location/print.go
@@ -76,6 +76,9 @@ func (o *PrintListingOptions) setDefaults() {
 	}
 }
 
+// BugLine is a fallback line when the line is not available.
+const BugLine = `Cannot render line properly, please fill a bug report`
+
 // PrintListing prints given message with line number and file listing to the writer.
 //
 // The context parameter defines the number of lines to print before and after.
@@ -112,7 +115,7 @@ func (l Lines) PrintListing(w io.Writer, msg string, loc Location, opts PrintLis
 	line := func(n int) []byte {
 		start, end := l.Line(n)
 		if start < 0 || end < 0 {
-			return []byte(`Cannot render line properly, please fill a bug report`)
+			return []byte(BugLine)
 		}
 		return bytes.Trim(l.data[start:end], "\r\n")
 	}
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 4fd095aa..2de16f2d 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -9,6 +9,7 @@ import (
 	"github.com/go-faster/jx"
 
 	"github.com/ogen-go/ogen/internal/location"
+	ogenjson "github.com/ogen-go/ogen/json"
 )
 
 // Parser parses JSON schemas.
@@ -66,11 +67,26 @@ func (p *Parser) parse1(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *
 	}
 
 	if schema != nil && s != nil {
-		if len(schema.Enum) > 0 {
-			values, err := parseEnumValues(s, schema.Enum)
+		if enum := schema.Enum; len(enum) > 0 {
+			loc := schema.Locator.Field("enum")
+			for i := range enum {
+				for j := range enum {
+					if i == j {
+						continue
+					}
+					a, b := enum[i], enum[j]
+					if ok, _ := ogenjson.Equal(a, b); ok {
+						loc := loc.Index(i)
+						err := errors.Errorf("duplicate enum value: %q", a)
+						return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+					}
+				}
+			}
+
+			values, err := parseEnumValues(s, enum)
 			if err != nil {
 				err := errors.Wrap(err, "parse enum values")
-				return nil, p.wrapField("enum", ctx.lastLoc(), schema.Locator, err)
+				return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
 			}
 			s.Enum = values
 			handleNullableEnum(s)
@@ -114,6 +130,22 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 		return p.wrapField(field, ctx.lastLoc(), schema.Locator, err)
 	}
 
+	validateMinMax := func(prop string, min, max *uint64) (rerr error) {
+		if min == nil || max == nil {
+			return nil
+		}
+		defer func() {
+			if rerr != nil {
+				rerr = wrapField("min"+prop, rerr)
+			}
+		}()
+
+		if *min > *max {
+			return errors.Errorf("min%s (%d) is greater than max%s (%d)", prop, *min, prop, *max)
+		}
+		return nil
+	}
+
 	if ref := schema.Ref; ref != "" {
 		s, err := p.resolve(ref, ctx)
 		if err != nil {
@@ -226,6 +258,14 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 			err := errors.New("object cannot contain 'items' field")
 			return nil, wrapField("items", err)
 		}
+		if err := validateMinMax(
+			"Properties",
+			schema.MinProperties,
+			schema.MaxProperties,
+		); err != nil {
+			return nil, err
+		}
+
 		required := func(name string) bool {
 			for _, p := range schema.Required {
 				if p == name {
@@ -304,6 +344,14 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 		return s, nil
 
 	case "array":
+		if err := validateMinMax(
+			"Items",
+			schema.MinItems,
+			schema.MaxItems,
+		); err != nil {
+			return nil, err
+		}
+
 		array := hook(&Schema{
 			Type:        Array,
 			MinItems:    schema.MinItems,
@@ -329,7 +377,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 		return array, nil
 
 	case "number", "integer":
-		if mul := schema.MultipleOf; mul != nil {
+		if mul := schema.MultipleOf; len(mul) > 0 {
 			if err := func() error {
 				rat := new(big.Rat)
 				if err := rat.UnmarshalText(mul); err != nil {
@@ -362,6 +410,14 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 		}), nil
 
 	case "string":
+		if err := validateMinMax(
+			"Length",
+			schema.MinLength,
+			schema.MaxLength,
+		); err != nil {
+			return nil, err
+		}
+
 		return hook(&Schema{
 			Type:      String,
 			Format:    schema.Format,
diff --git a/jsonschema/parser_enum.go b/jsonschema/parser_enum.go
index 37ae0514..eb3ed08c 100644
--- a/jsonschema/parser_enum.go
+++ b/jsonschema/parser_enum.go
@@ -5,8 +5,6 @@ import (
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
-
-	ogenjson "github.com/ogen-go/ogen/json"
 )
 
 func inferJSONType(v json.RawMessage) (string, error) {
@@ -26,18 +24,6 @@ func inferJSONType(v json.RawMessage) (string, error) {
 }
 
 func parseEnumValues(s *Schema, rawValues []json.RawMessage) ([]interface{}, error) {
-	for i := range rawValues {
-		for j := range rawValues {
-			if i == j {
-				continue
-			}
-			a, b := rawValues[i], rawValues[j]
-			if ok, _ := ogenjson.Equal(a, b); ok {
-				return nil, errors.Errorf("duplicate enum values: %q, at index %d and %d", a, i, j)
-			}
-		}
-	}
-
 	var values []interface{}
 	for _, raw := range rawValues {
 		val, err := parseJSONValue(s, raw)


Test patch:
diff --git a/jsonschema/_testdata/negative/array/invalid_minItems.json b/jsonschema/_testdata/negative/array/invalid_minItems.json
new file mode 100644
index 00000000..7f69fa69
--- /dev/null
+++ b/jsonschema/_testdata/negative/array/invalid_minItems.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "array",
+  "minItems": 10,
+  "maxItems": 1
+}
diff --git a/jsonschema/_testdata/negative/enum/duplicate_integer_value.json b/jsonschema/_testdata/negative/enum/duplicate_integer_value.json
new file mode 100644
index 00000000..70cd306c
--- /dev/null
+++ b/jsonschema/_testdata/negative/enum/duplicate_integer_value.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "string",
+  "enum": [
+    1, 10, 1e1, 2
+  ]
+}
diff --git a/jsonschema/_testdata/negative/object/invalid_minProperties.json b/jsonschema/_testdata/negative/object/invalid_minProperties.json
new file mode 100644
index 00000000..f52046db
--- /dev/null
+++ b/jsonschema/_testdata/negative/object/invalid_minProperties.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "minProperties": 10,
+  "maxProperties": 1
+}
diff --git a/jsonschema/_testdata/negative/string/invalid_minLength.json b/jsonschema/_testdata/negative/string/invalid_minLength.json
new file mode 100644
index 00000000..b5b182f9
--- /dev/null
+++ b/jsonschema/_testdata/negative/string/invalid_minLength.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "string",
+  "minLength": 10,
+  "maxLength": 1
+}
diff --git a/jsonschema/jsonschema_test.go b/jsonschema/jsonschema_test.go
index 60262e56..5ac92c01 100644
--- a/jsonschema/jsonschema_test.go
+++ b/jsonschema/jsonschema_test.go
@@ -43,6 +43,9 @@ func TestNegative(t *testing.T) {
 		//
 		// There should be a good reason to remove this line.
 		a.True(ok)
-		t.Log(buf.String())
+		pretty := buf.String()
+		a.NotEmpty(pretty)
+		a.NotContains(pretty, location.BugLine)
+		t.Log(pretty)
 	})
 }
diff --git a/openapi/parser/parser_test.go b/openapi/parser/parser_test.go
index 570c31bf..95512f0f 100644
--- a/openapi/parser/parser_test.go
+++ b/openapi/parser/parser_test.go
@@ -41,6 +41,9 @@ func TestNegative(t *testing.T) {
 		//
 		// There should be a good reason to remove this line.
 		a.True(ok)
-		t.Log(buf.String())
+		pretty := buf.String()
+		a.NotEmpty(pretty)
+		a.NotContains(pretty, location.BugLine)
+		t.Log(pretty)
 	})
 }