diff --git a/arazzo/arazzo.go b/arazzo/arazzo.go index ebc4ff3..a81aa7c 100644 --- a/arazzo/arazzo.go +++ b/arazzo/arazzo.go @@ -12,7 +12,9 @@ import ( "github.com/speakeasy-api/openapi/extensions" "github.com/speakeasy-api/openapi/internal/interfaces" "github.com/speakeasy-api/openapi/internal/utils" + "github.com/speakeasy-api/openapi/jsonschema/oas3" "github.com/speakeasy-api/openapi/marshaller" + "github.com/speakeasy-api/openapi/pointer" "github.com/speakeasy-api/openapi/validation" ) @@ -98,6 +100,7 @@ func (a *Arazzo) Sync(ctx context.Context) error { // Validate will validate the Arazzo document against the Arazzo Specification. func (a *Arazzo) Validate(ctx context.Context, opts ...validation.Option) []error { opts = append(opts, validation.WithContextObject(a)) + opts = append(opts, validation.WithContextObject(&oas3.ParentDocumentVersion{Arazzo: pointer.From(a.Arazzo)})) core := a.GetCore() errs := []error{} diff --git a/arazzo/components.go b/arazzo/components.go index 9fb1b43..a15bf77 100644 --- a/arazzo/components.go +++ b/arazzo/components.go @@ -47,12 +47,7 @@ func (c *Components) Validate(ctx context.Context, opts ...validation.Option) [] errs = append(errs, validation.NewMapKeyError(validation.NewValueValidationError("components field inputs key must be a valid key [%s]: %s", componentNameRegex.String(), key), core, core.Inputs, key)) } - if input.IsLeft() { - jsOpts := opts - jsOpts = append(jsOpts, validation.WithContextObject(&componentKey{name: key})) - - errs = append(errs, input.Left.Validate(ctx, jsOpts...)...) - } + errs = append(errs, input.Validate(ctx, opts...)...) } for key, parameter := range c.Parameters.All() { diff --git a/jsonschema/oas3/schema30.json b/jsonschema/oas3/schema30.json new file mode 100644 index 0000000..4553e5c --- /dev/null +++ b/jsonschema/oas3/schema30.json @@ -0,0 +1,234 @@ +{ + "id": "https://spec.openapis.org/oas/3.0/dialect/2024-10-18", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "OpenAPI 3.0 Schema Object based on official https://spec.openapis.org/oas/3.0/schema/2024-10-18.html specification", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "format": "uri", + "description": "Added unofficial support for $schema property to assist testing" + }, + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": {}, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": ["array", "boolean", "integer", "number", "object", "string"] + }, + "not": { + "oneOf": [{ "$ref": "#" }, { "$ref": "#/definitions/Reference" }] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [{ "$ref": "#" }, { "$ref": "#/definitions/Reference" }] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [{ "$ref": "#" }, { "$ref": "#/definitions/Reference" }] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [{ "$ref": "#" }, { "$ref": "#/definitions/Reference" }] + } + }, + "items": { + "oneOf": [{ "$ref": "#" }, { "$ref": "#/definitions/Reference" }] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [{ "$ref": "#" }, { "$ref": "#/definitions/Reference" }] + } + }, + "additionalProperties": { + "oneOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/Reference" }, + { "type": "boolean" } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": {}, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": {}, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": ["$ref"], + "patternProperties": { + "^\\$ref$": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Discriminator": { + "type": "object", + "required": ["propertyName"], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": ["url"], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + } + } +} diff --git a/jsonschema/oas3/schema_exclusive_validation_test.go b/jsonschema/oas3/schema_exclusive_validation_test.go new file mode 100644 index 0000000..0692901 --- /dev/null +++ b/jsonschema/oas3/schema_exclusive_validation_test.go @@ -0,0 +1,356 @@ +package oas3_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/speakeasy-api/openapi/jsonschema/oas3" + "github.com/speakeasy-api/openapi/marshaller" + "github.com/speakeasy-api/openapi/pointer" + "github.com/speakeasy-api/openapi/validation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSchema_ExclusiveMinimumMaximum_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yaml string + openAPIVersion *string // Optional OpenAPI document version + expectedExclusiveMinimum interface{} // bool or float64 + expectedExclusiveMaximum interface{} // bool or float64 + shouldValidate bool + }{ + // Boolean values with OpenAPI 3.0 context + { + name: "boolean exclusiveMinimum and exclusiveMaximum with OpenAPI 3.0 document version", + yaml: ` +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: true +exclusiveMaximum: false +`, + openAPIVersion: pointer.From("3.0.3"), + expectedExclusiveMinimum: true, + expectedExclusiveMaximum: false, + shouldValidate: true, + }, + { + name: "both boolean values true with OpenAPI 3.0 document version", + yaml: ` +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: true +exclusiveMaximum: true +`, + openAPIVersion: pointer.From("3.0.3"), + expectedExclusiveMinimum: true, + expectedExclusiveMaximum: true, + shouldValidate: true, + }, + { + name: "both boolean values false with OpenAPI 3.0 document version", + yaml: ` +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: false +exclusiveMaximum: false +`, + openAPIVersion: pointer.From("3.0.3"), + expectedExclusiveMinimum: false, + expectedExclusiveMaximum: false, + shouldValidate: true, + }, + // Boolean values with explicit 3.0 $schema + { + name: "boolean exclusiveMinimum and exclusiveMaximum with 3.0 $schema", + yaml: ` +$schema: "https://spec.openapis.org/oas/3.0/dialect/2024-10-18" +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: true +exclusiveMaximum: false +`, + expectedExclusiveMinimum: true, + expectedExclusiveMaximum: false, + shouldValidate: true, + }, + { + name: "both boolean values true with 3.0 $schema", + yaml: ` +$schema: "https://spec.openapis.org/oas/3.0/dialect/2024-10-18" +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: true +exclusiveMaximum: true +`, + expectedExclusiveMinimum: true, + expectedExclusiveMaximum: true, + shouldValidate: true, + }, + // Numeric values (should work with any version) + { + name: "numeric exclusiveMinimum and exclusiveMaximum", + yaml: ` +type: number +exclusiveMinimum: 0.5 +exclusiveMaximum: 99.5 +`, + expectedExclusiveMinimum: 0.5, + expectedExclusiveMaximum: 99.5, + shouldValidate: true, + }, + { + name: "numeric exclusiveMinimum and exclusiveMaximum as integers", + yaml: ` +type: number +exclusiveMinimum: 1 +exclusiveMaximum: 99 +`, + expectedExclusiveMinimum: 1.0, + expectedExclusiveMaximum: 99.0, + shouldValidate: true, + }, + { + name: "numeric exclusiveMinimum and exclusiveMaximum with OpenAPI 3.1", + yaml: ` +type: number +exclusiveMinimum: 0.5 +exclusiveMaximum: 99.5 +`, + openAPIVersion: pointer.From("3.1.0"), + expectedExclusiveMinimum: 0.5, + expectedExclusiveMaximum: 99.5, + shouldValidate: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var schema oas3.Schema + + validationErrs, err := marshaller.Unmarshal(t.Context(), bytes.NewBufferString(tt.yaml), &schema) + require.NoError(t, err, "Unmarshaling should succeed") + require.Empty(t, validationErrs, "Should have no validation errors during unmarshaling") + + // Test schema validation with optional document version context + var validationErrors []error + if tt.openAPIVersion != nil { + docVersion := &oas3.ParentDocumentVersion{ + OpenAPI: tt.openAPIVersion, + } + validationErrors = schema.Validate(t.Context(), validation.WithContextObject(docVersion)) + } else { + validationErrors = schema.Validate(t.Context()) + } + + if tt.shouldValidate { + assert.Empty(t, validationErrors, "Schema validation should pass for: %s", tt.name) + } else { + assert.NotEmpty(t, validationErrors, "Schema validation should fail for: %s", tt.name) + } + + // Verify parsed values + if tt.expectedExclusiveMinimum != nil { + require.NotNil(t, schema.ExclusiveMinimum, "ExclusiveMinimum should not be nil") + + switch expected := tt.expectedExclusiveMinimum.(type) { + case bool: + assert.True(t, schema.ExclusiveMinimum.IsLeft(), "ExclusiveMinimum should be boolean (Left side)") + if schema.ExclusiveMinimum.IsLeft() { + actual := *schema.ExclusiveMinimum.Left + assert.Equal(t, expected, actual, "ExclusiveMinimum boolean value should match expected") + } + case float64: + assert.True(t, schema.ExclusiveMinimum.IsRight(), "ExclusiveMinimum should be number (Right side)") + if schema.ExclusiveMinimum.IsRight() { + actual := *schema.ExclusiveMinimum.Right + assert.InDelta(t, expected, actual, 0.001, "ExclusiveMinimum number value should match expected") + } + } + } + + if tt.expectedExclusiveMaximum != nil { + require.NotNil(t, schema.ExclusiveMaximum, "ExclusiveMaximum should not be nil") + + switch expected := tt.expectedExclusiveMaximum.(type) { + case bool: + assert.True(t, schema.ExclusiveMaximum.IsLeft(), "ExclusiveMaximum should be boolean (Left side)") + if schema.ExclusiveMaximum.IsLeft() { + actual := *schema.ExclusiveMaximum.Left + assert.Equal(t, expected, actual, "ExclusiveMaximum boolean value should match expected") + } + case float64: + assert.True(t, schema.ExclusiveMaximum.IsRight(), "ExclusiveMaximum should be number (Right side)") + if schema.ExclusiveMaximum.IsRight() { + actual := *schema.ExclusiveMaximum.Right + assert.InDelta(t, expected, actual, 0.001, "ExclusiveMaximum number value should match expected") + } + } + } + + // Verify $schema property if present + if tt.yaml != "" && strings.Contains(tt.yaml, "$schema:") { + require.NotNil(t, schema.Schema, "Schema property should not be nil when $schema is specified") + } + }) + } +} + +func TestSchema_ExclusiveMinimumMaximum_Error(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yaml string + openAPIVersion *string + expectError bool + errorContains string + }{ + // Boolean values should fail with OpenAPI 3.1 + { + name: "boolean exclusiveMinimum with OpenAPI 3.1 document version should fail", + yaml: ` +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: true +exclusiveMaximum: false +`, + openAPIVersion: pointer.From("3.1.0"), + expectError: true, + errorContains: "got boolean, want number", + }, + { + name: "boolean exclusiveMinimum with 3.1 $schema should fail", + yaml: ` +$schema: "https://spec.openapis.org/oas/3.1/dialect/base" +type: number +minimum: 0 +maximum: 100 +exclusiveMinimum: true +exclusiveMaximum: false +`, + expectError: true, + errorContains: "got boolean, want number", + }, + // Invalid types should always fail + { + name: "invalid string type for exclusiveMinimum", + yaml: ` +type: number +exclusiveMinimum: "invalid" +`, + expectError: true, + errorContains: "exclusiveMinimum", + }, + { + name: "invalid string type for exclusiveMaximum", + yaml: ` +type: number +exclusiveMaximum: "invalid" +`, + expectError: true, + errorContains: "exclusiveMaximum", + }, + { + name: "invalid array type for exclusiveMinimum", + yaml: ` +type: number +exclusiveMinimum: [1, 2, 3] +`, + expectError: true, + errorContains: "exclusiveMinimum", + }, + // Mixed boolean and numeric should fail with OpenAPI 3.0 (only supports boolean) + { + name: "mixed boolean exclusiveMinimum and numeric exclusiveMaximum with OpenAPI 3.0 should fail", + yaml: ` +type: number +minimum: 0 +exclusiveMinimum: true +exclusiveMaximum: 50.5 +`, + openAPIVersion: pointer.From("3.0.3"), + expectError: true, + errorContains: "got number, want boolean", + }, + { + name: "mixed numeric exclusiveMinimum and boolean exclusiveMaximum with OpenAPI 3.0 should fail", + yaml: ` +type: number +maximum: 100 +exclusiveMinimum: 0.5 +exclusiveMaximum: true +`, + openAPIVersion: pointer.From("3.0.3"), + expectError: true, + errorContains: "got number, want boolean", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var schema oas3.Schema + + validationErrs, err := marshaller.Unmarshal(t.Context(), bytes.NewBufferString(tt.yaml), &schema) + + if tt.expectError { + // We expect either unmarshaling to fail or validation errors + if err == nil && len(validationErrs) == 0 { + // If unmarshaling succeeded, check schema validation + var validationErrors []error + if tt.openAPIVersion != nil { + docVersion := &oas3.ParentDocumentVersion{ + OpenAPI: tt.openAPIVersion, + } + validationErrors = schema.Validate(t.Context(), validation.WithContextObject(docVersion)) + } else { + validationErrors = schema.Validate(t.Context()) + } + + assert.NotEmpty(t, validationErrors, "Should have validation errors for: %s", tt.name) + + // Check if any error contains the expected string + found := false + for _, validationErr := range validationErrors { + if assert.Contains(t, validationErr.Error(), tt.errorContains) { + found = true + break + } + } + assert.True(t, found, "Should find error containing '%s'", tt.errorContains) + } else { + // Unmarshaling failed or had validation errors, which is expected + assert.True(t, err != nil || len(validationErrs) > 0, "Should have errors during unmarshaling") + } + } else { + require.NoError(t, err, "Unmarshaling should succeed") + require.Empty(t, validationErrs, "Should have no validation errors during unmarshaling") + + var validationErrors []error + if tt.openAPIVersion != nil { + docVersion := &oas3.ParentDocumentVersion{ + OpenAPI: tt.openAPIVersion, + } + validationErrors = schema.Validate(t.Context(), validation.WithContextObject(docVersion)) + } else { + validationErrors = schema.Validate(t.Context()) + } + assert.Empty(t, validationErrors, "Schema validation should pass for: %s", tt.name) + } + }) + } +} diff --git a/jsonschema/oas3/validation.go b/jsonschema/oas3/validation.go index f01467d..dc4bcb6 100644 --- a/jsonschema/oas3/validation.go +++ b/jsonschema/oas3/validation.go @@ -27,9 +27,22 @@ var schema31JSON string //go:embed schema31.base.json var schema31BaseJSON string -var oasSchemaValidator *jsValidator.Schema +//go:embed schema30.json +var schema30JSON string + +var oasSchemaValidator = make(map[string]*jsValidator.Schema) var defaultPrinter = message.NewPrinter(language.English) +const ( + JSONSchema31SchemaID = "https://spec.openapis.org/oas/3.1/dialect/base" + JSONSchema30SchemaID = "https://spec.openapis.org/oas/3.0/dialect/2024-10-18" +) + +type ParentDocumentVersion struct { + OpenAPI *string + Arazzo *string +} + func Validate[T Referenceable | Concrete](ctx context.Context, schema *JSONSchema[T], opts ...validation.Option) []error { if schema == nil { return nil @@ -43,7 +56,42 @@ func Validate[T Referenceable | Concrete](ctx context.Context, schema *JSONSchem } func (js *Schema) Validate(ctx context.Context, opts ...validation.Option) []error { - initValidation() + o := validation.NewOptions(opts...) + + dv := validation.GetContextObject[ParentDocumentVersion](o) + + var schema string + if js.Schema != nil { + switch *js.Schema { + case JSONSchema31SchemaID: + schema = *js.Schema + case JSONSchema30SchemaID: + schema = *js.Schema + default: + // Currently not supported + } + } + if schema == "" && dv != nil { + switch { + case dv.OpenAPI != nil: + switch { + case strings.HasPrefix(*dv.OpenAPI, "3.1"): + schema = JSONSchema31SchemaID + case strings.HasPrefix(*dv.OpenAPI, "3.0"): + schema = JSONSchema30SchemaID + default: + // Currently not supported + } + case dv.Arazzo != nil: + // Currently not supported for Arazzo documents + } + } + if schema == "" { + // Default to OpenAPI 3.1 schema TODO: consider maybe defaulting to draft-2020-12 instead + schema = JSONSchema31SchemaID + } + + oasSchemaValidator := initValidation(schema) buf := bytes.NewBuffer([]byte{}) core := js.GetCore() @@ -84,7 +132,16 @@ func getRootCauses(err *jsValidator.ValidationError, js core.Schema) []error { for _, cause := range err.Causes { if len(cause.Causes) == 0 { - errJP := jsonpointer.PartsToJSONPointer(cause.InstanceLocation) + var errJP jsonpointer.JSONPointer + switch { + case len(cause.InstanceLocation) > 0: + errJP = jsonpointer.PartsToJSONPointer(cause.InstanceLocation) + case cause.ErrorKind != nil: + errJP = jsonpointer.PartsToJSONPointer(cause.ErrorKind.KeywordPath()) + default: + errJP = jsonpointer.JSONPointer("/") + } + t, err := jsonpointer.GetTarget(js, errJP, jsonpointer.WithStructTags("key")) if err != nil { // TODO need to potentially handle this in another way @@ -123,33 +180,49 @@ func getRootCauses(err *jsValidator.ValidationError, js core.Schema) []error { return errs } -var validationInitialized bool +var validationInitialized = make(map[string]bool) var initMutex sync.Mutex -func initValidation() { +func initValidation(schema string) *jsValidator.Schema { initMutex.Lock() defer initMutex.Unlock() - if validationInitialized { - return + if validationInitialized[schema] { + return oasSchemaValidator[schema] } - oasSchema, err := jsValidator.UnmarshalJSON(bytes.NewReader([]byte(schema31JSON))) - if err != nil { - panic(err) - } - - oasSchemaBase, err := jsValidator.UnmarshalJSON(bytes.NewReader([]byte(schema31BaseJSON))) - if err != nil { - panic(err) - } + var schemaResource any c := jsValidator.NewCompiler() - if err := c.AddResource("https://spec.openapis.org/oas/3.1/meta/base", oasSchemaBase); err != nil { - panic(err) + + switch schema { + case JSONSchema31SchemaID: + oasSchemaBase, err := jsValidator.UnmarshalJSON(bytes.NewBufferString(schema31BaseJSON)) + if err != nil { + panic(err) + } + if err := c.AddResource("https://spec.openapis.org/oas/3.1/meta/base", oasSchemaBase); err != nil { + panic(err) + } + + schemaResource, err = jsValidator.UnmarshalJSON(bytes.NewBufferString(schema31JSON)) + if err != nil { + panic(err) + } + case JSONSchema30SchemaID: + var err error + schemaResource, err = jsValidator.UnmarshalJSON(bytes.NewBufferString(schema30JSON)) + if err != nil { + panic(err) + } + default: + panic("unsupported schema") } - if err := c.AddResource("schema.json", oasSchema); err != nil { + + if err := c.AddResource("schema.json", schemaResource); err != nil { panic(err) } - oasSchemaValidator = c.MustCompile("schema.json") - validationInitialized = true + oasSchemaValidator[schema] = c.MustCompile("schema.json") + validationInitialized[schema] = true + + return oasSchemaValidator[schema] } diff --git a/openapi/components.go b/openapi/components.go index 73357f2..8adf79e 100644 --- a/openapi/components.go +++ b/openapi/components.go @@ -138,9 +138,7 @@ func (c *Components) Validate(ctx context.Context, opts ...validation.Option) [] if c.Schemas != nil { for _, schema := range c.Schemas.All() { - if schema.IsLeft() { - errs = append(errs, schema.Left.Validate(ctx, opts...)...) - } + errs = append(errs, schema.Validate(ctx, opts...)...) } } diff --git a/openapi/header.go b/openapi/header.go index 19e7a9b..91f14f1 100644 --- a/openapi/header.go +++ b/openapi/header.go @@ -136,7 +136,7 @@ func (h *Header) Validate(ctx context.Context, opts ...validation.Option) []erro } if core.Schema.Present { - errs = append(errs, oas3.Validate(ctx, h.Schema)...) + errs = append(errs, h.Schema.Validate(ctx, opts...)...) } for _, obj := range h.Content.All() { diff --git a/openapi/links_validate_test.go b/openapi/links_validate_test.go index 0b6e792..2a92c17 100644 --- a/openapi/links_validate_test.go +++ b/openapi/links_validate_test.go @@ -7,6 +7,7 @@ import ( "github.com/speakeasy-api/openapi/marshaller" "github.com/speakeasy-api/openapi/openapi" + "github.com/speakeasy-api/openapi/pointer" "github.com/speakeasy-api/openapi/validation" "github.com/stretchr/testify/require" ) @@ -24,13 +25,13 @@ func TestLink_Validate_Success(t *testing.T) { // Add GET operation with getUserById operation1 := &openapi.Operation{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } pathItem.Set("get", operation1) // Add PUT operation with updateUser to the same path operation2 := &openapi.Operation{ - OperationID: stringPtr("updateUser"), + OperationID: pointer.From("updateUser"), } pathItem.Set("put", operation2) @@ -141,13 +142,13 @@ func TestLink_Validate_Error(t *testing.T) { // Add GET operation with getUserById operation1 := &openapi.Operation{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } pathItem.Set("get", operation1) // Add PUT operation with updateUser to the same path operation2 := &openapi.Operation{ - OperationID: stringPtr("updateUser"), + OperationID: pointer.From("updateUser"), } pathItem.Set("put", operation2) @@ -250,13 +251,13 @@ func TestLink_Validate_OperationID_NotFound(t *testing.T) { // Add a path with an operation pathItem := openapi.NewPathItem() operation := &openapi.Operation{ - OperationID: stringPtr("existingOperation"), + OperationID: pointer.From("existingOperation"), } pathItem.Set("get", operation) openAPIDoc.Paths.Set("/users/{id}", &openapi.ReferencedPathItem{Object: pathItem}) link := &openapi.Link{ - OperationID: stringPtr("nonExistentOperation"), + OperationID: pointer.From("nonExistentOperation"), } errs := link.Validate(t.Context(), validation.WithContextObject(openAPIDoc)) @@ -275,13 +276,13 @@ func TestLink_Validate_OperationID_Found(t *testing.T) { // Add a path with an operation pathItem := openapi.NewPathItem() operation := &openapi.Operation{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } pathItem.Set("get", operation) openAPIDoc.Paths.Set("/users/{id}", &openapi.ReferencedPathItem{Object: pathItem}) link := &openapi.Link{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } errs := link.Validate(t.Context(), validation.WithContextObject(openAPIDoc)) @@ -292,7 +293,7 @@ func TestLink_Validate_OperationID_WithoutOpenAPIContext_Panics(t *testing.T) { t.Parallel() link := &openapi.Link{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } require.Panics(t, func() { @@ -313,13 +314,13 @@ func TestLink_Validate_ComplexExpressions(t *testing.T) { // Add GET operation with getUserById operation1 := &openapi.Operation{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } pathItem.Set("get", operation1) // Add PUT operation with updateUser to the same path operation2 := &openapi.Operation{ - OperationID: stringPtr("updateUser"), + OperationID: pointer.From("updateUser"), } pathItem.Set("put", operation2) @@ -390,13 +391,13 @@ func TestLink_Validate_NilParameters(t *testing.T) { // Add a path with an operation pathItem := openapi.NewPathItem() operation := &openapi.Operation{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } pathItem.Set("get", operation) openAPIDoc.Paths.Set("/users/{id}", &openapi.ReferencedPathItem{Object: pathItem}) link := &openapi.Link{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), Parameters: nil, // Explicitly nil RequestBody: nil, // Explicitly nil Server: nil, // Explicitly nil @@ -417,7 +418,7 @@ func TestLink_Validate_EmptyParameters(t *testing.T) { // Add a path with an operation pathItem := openapi.NewPathItem() operation := &openapi.Operation{ - OperationID: stringPtr("getUserById"), + OperationID: pointer.From("getUserById"), } pathItem.Set("get", operation) openAPIDoc.Paths.Set("/users/{id}", &openapi.ReferencedPathItem{Object: pathItem}) @@ -436,8 +437,3 @@ description: Empty parameters map errs := link.Validate(t.Context(), validation.WithContextObject(openAPIDoc)) require.Empty(t, errs, "Expected no validation errors for empty parameters") } - -// Helper function to create string pointers -func stringPtr(s string) *string { - return &s -} diff --git a/openapi/mediatype.go b/openapi/mediatype.go index 85adc5d..ecf1668 100644 --- a/openapi/mediatype.go +++ b/openapi/mediatype.go @@ -67,7 +67,7 @@ func (m *MediaType) Validate(ctx context.Context, opts ...validation.Option) []e errs := []error{} if core.Schema.Present { - errs = append(errs, oas3.Validate(ctx, m.Schema)...) + errs = append(errs, m.Schema.Validate(ctx, opts...)...) } for _, obj := range m.Examples.All() { diff --git a/openapi/openapi.go b/openapi/openapi.go index 4e8645f..ef4d8e6 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -11,6 +11,7 @@ import ( "github.com/speakeasy-api/openapi/jsonschema/oas3" "github.com/speakeasy-api/openapi/marshaller" "github.com/speakeasy-api/openapi/openapi/core" + "github.com/speakeasy-api/openapi/pointer" "github.com/speakeasy-api/openapi/sequencedmap" "github.com/speakeasy-api/openapi/validation" ) @@ -158,6 +159,7 @@ func (o *OpenAPI) Validate(ctx context.Context, opts ...validation.Option) []err errs := []error{} opts = append(opts, validation.WithContextObject(o)) + opts = append(opts, validation.WithContextObject(&oas3.ParentDocumentVersion{OpenAPI: pointer.From(o.OpenAPI)})) openAPIMajor, openAPIMinor, openAPIPatch, err := utils.ParseVersion(o.OpenAPI) if err != nil { diff --git a/openapi/parameter.go b/openapi/parameter.go index 3110b5a..96aafd8 100644 --- a/openapi/parameter.go +++ b/openapi/parameter.go @@ -252,7 +252,7 @@ func (p *Parameter) Validate(ctx context.Context, opts ...validation.Option) []e } if core.Schema.Present { - errs = append(errs, oas3.Validate(ctx, p.Schema)...) + errs = append(errs, p.Schema.Validate(ctx, opts...)...) } for _, obj := range p.Content.All() { diff --git a/values/eithervalue_integration_test.go b/values/eithervalue_integration_test.go index 404ab77..ed76e44 100644 --- a/values/eithervalue_integration_test.go +++ b/values/eithervalue_integration_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/speakeasy-api/openapi/jsonpointer" + "github.com/speakeasy-api/openapi/pointer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -121,7 +122,7 @@ func TestEitherValue_JSONPointer_UnsupportedNavigation(t *testing.T) { // Test with value that doesn't support the requested navigation type eitherValue := &EitherValue[string, string, string, string]{ - Left: stringPtr("simple string"), + Left: pointer.From("simple string"), } // Try to navigate with key (should fail because string doesn't support navigation) diff --git a/values/eithervalue_jsonpointer_test.go b/values/eithervalue_jsonpointer_test.go index cc80d2a..0bca8e8 100644 --- a/values/eithervalue_jsonpointer_test.go +++ b/values/eithervalue_jsonpointer_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/speakeasy-api/openapi/jsonpointer" + "github.com/speakeasy-api/openapi/pointer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -103,7 +104,7 @@ func TestEitherValue_JSONPointer_UnsupportedType(t *testing.T) { // Test with Left value that doesn't support key navigation eitherValue := &EitherValue[string, string, string, string]{ - Left: stringPtr("simple string"), + Left: pointer.From("simple string"), } // Try to navigate with key (should fail because string doesn't support navigation) @@ -223,8 +224,3 @@ func TestEitherValue_JSONPointer_BothNavigationTypes(t *testing.T) { require.NoError(t, err) assert.Equal(t, "slicevalue", result) } - -// Helper function to create string pointer -func stringPtr(s string) *string { - return &s -}