diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml index 2962974..320c31d 100644 --- a/.github/workflows/commits.yml +++ b/.github/workflows/commits.yml @@ -10,11 +10,9 @@ on: - ready_for_review jobs: build: - name: Conventional Commits + name: Conventional PR Title runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: webiny/action-conventional-commits@v1.3.0 - uses: amannn/action-semantic-pull-request@v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/jsonschema/oas3/schema_validate_test.go b/jsonschema/oas3/schema_validate_test.go index 20bec09..9e8b2e3 100644 --- a/jsonschema/oas3/schema_validate_test.go +++ b/jsonschema/oas3/schema_validate_test.go @@ -334,6 +334,14 @@ properties: - type: number - type: boolean required: ["user"] +`, + }, + { + name: "valid schema with $ref and additional properties (OpenAPI 3.1)", + yml: ` +$ref: "#/components/schemas/User" +required: ["name", "email"] +description: "User schema with additional validation requirements" `, }, } @@ -474,6 +482,15 @@ oneOf: "invalid" `, wantErrs: []string{"schema field oneOf got string, want array"}, }, + { + name: "$ref with additional properties not allowed in OpenAPI 3.0", + yml: ` +$schema: "https://spec.openapis.org/oas/3.0/dialect/2024-10-18" +$ref: "#/components/schemas/User" +required: ["name", "email"] +`, + wantErrs: []string{"additional properties '$ref' not allowed"}, + }, } for _, tt := range tests { diff --git a/marshaller/model.go b/marshaller/model.go index 09ca7a2..daee7ce 100644 --- a/marshaller/model.go +++ b/marshaller/model.go @@ -98,6 +98,10 @@ func (m *Model[T]) GetRootNode() *yaml.Node { } func (m *Model[T]) GetRootNodeLine() int { + if m == nil { + return -1 + } + if rootNode := m.GetRootNode(); rootNode != nil { return rootNode.Line } @@ -105,6 +109,10 @@ func (m *Model[T]) GetRootNodeLine() int { } func (m *Model[T]) GetRootNodeColumn() int { + if m == nil { + return -1 + } + if rootNode := m.GetRootNode(); rootNode != nil { return rootNode.Column } @@ -161,7 +169,7 @@ func (m *Model[T]) SetCoreAny(core any) { } func (m *Model[T]) GetCachedReferencedObject(key string) (any, bool) { - if m.objectCache == nil { + if m == nil || m.objectCache == nil { return nil, false } return m.objectCache.Load(key) @@ -172,7 +180,7 @@ func (m *Model[T]) StoreReferencedObjectInCache(key string, obj any) { } func (m *Model[T]) GetCachedReferenceDocument(key string) ([]byte, bool) { - if m.documentCache == nil { + if m == nil || m.documentCache == nil { return nil, false } value, ok := m.documentCache.Load(key) diff --git a/openapi/bundle.go b/openapi/bundle.go index 058b34c..41f2a78 100644 --- a/openapi/bundle.go +++ b/openapi/bundle.go @@ -2,6 +2,7 @@ package openapi import ( "context" + "errors" "fmt" "path/filepath" "regexp" @@ -253,7 +254,10 @@ func bundleSchema(ctx context.Context, schema *oas3.JSONSchema[oas3.Referenceabl resolvedHash := hashing.Hash(resolvedRefSchema) // Generate component name with smart conflict resolution - componentName := generateComponentNameWithHashConflictResolution(ref, namingStrategy, componentStorage.componentNames, componentStorage.schemaHashes, resolvedHash) + componentName, err := generateComponentNameWithHashConflictResolution(ref, namingStrategy, componentStorage.componentNames, componentStorage.schemaHashes, resolvedHash, opts.TargetLocation) + if err != nil { + return fmt.Errorf("failed to generate component name for %s: %w", ref, err) + } // Store the mapping componentStorage.externalRefs[ref] = componentName @@ -366,7 +370,10 @@ func bundleGenericReference[T any, V interfaces.Validator[T], C marshaller.CoreM } // Generate component name - componentName := generateComponentName(refStr, namingStrategy, componentStorage.componentNames) + componentName, err := generateComponentName(refStr, namingStrategy, componentStorage.componentNames, opts.TargetLocation) + if err != nil { + return fmt.Errorf("failed to generate component name for %s: %w", refStr, err) + } componentStorage.componentNames[componentName] = true // Store the mapping @@ -405,19 +412,19 @@ func bundleGenericReference[T any, V interfaces.Validator[T], C marshaller.CoreM } // generateComponentName creates a new component name based on the reference and naming strategy -func generateComponentName(ref string, strategy BundleNamingStrategy, usedNames map[string]bool) string { +func generateComponentName(ref string, strategy BundleNamingStrategy, usedNames map[string]bool, targetLocation string) (string, error) { switch strategy { case BundleNamingFilePath: - return generateFilePathBasedNameWithConflictResolution(ref, usedNames) + return generateFilePathBasedNameWithConflictResolution(ref, usedNames, targetLocation) case BundleNamingCounter: - return generateCounterBasedName(ref, usedNames) + return generateCounterBasedName(ref, usedNames), nil default: - return generateCounterBasedName(ref, usedNames) + return generateCounterBasedName(ref, usedNames), nil } } // generateComponentNameWithHashConflictResolution creates a component name with smart conflict resolution based on content hashes -func generateComponentNameWithHashConflictResolution(ref string, strategy BundleNamingStrategy, usedNames map[string]bool, schemaHashes map[string]string, resolvedHash string) string { +func generateComponentNameWithHashConflictResolution(ref string, strategy BundleNamingStrategy, usedNames map[string]bool, schemaHashes map[string]string, resolvedHash string, targetLocation string) (string, error) { // Parse the reference to extract the simple name parts := strings.Split(ref, "#") if len(parts) == 0 { @@ -454,24 +461,24 @@ func generateComponentNameWithHashConflictResolution(ref string, strategy Bundle if existingHash, exists := schemaHashes[simpleName]; exists { if existingHash == resolvedHash { // Same content, reuse existing schema - return simpleName + return simpleName, nil } // Different content with same name - need conflict resolution // Fall back to the configured naming strategy for conflict resolution - return generateComponentName(ref, strategy, usedNames) + return generateComponentName(ref, strategy, usedNames, targetLocation) } // No conflict, use simple name - return simpleName + return simpleName, nil } // generateFilePathBasedNameWithConflictResolution tries to use simple names first, falling back to file-path-based names for conflicts -func generateFilePathBasedNameWithConflictResolution(ref string, usedNames map[string]bool) string { +func generateFilePathBasedNameWithConflictResolution(ref string, usedNames map[string]bool, targetLocation string) (string, error) { // Parse the reference to extract file path and fragment parts := strings.Split(ref, "#") if len(parts) == 0 { // This should never happen as strings.Split never returns nil or empty slice - return "unknown" + return "unknown", nil } fragment := "" if len(parts) > 1 { @@ -502,20 +509,20 @@ func generateFilePathBasedNameWithConflictResolution(ref string, usedNames map[s // Try simple name first if !usedNames[simpleName] { - return simpleName + return simpleName, nil } // If there's a conflict, fall back to file-path-based naming - return generateFilePathBasedName(ref, usedNames) + return generateFilePathBasedName(ref, usedNames, targetLocation) } // generateFilePathBasedName creates names like "some_path_external_yaml~User" or "some_path_external_yaml" for top-level refs -func generateFilePathBasedName(ref string, usedNames map[string]bool) string { +func generateFilePathBasedName(ref string, usedNames map[string]bool, targetLocation string) (string, error) { // Parse the reference to extract file path and fragment parts := strings.Split(ref, "#") if len(parts) == 0 { // This should never happen as strings.Split never returns nil or empty slice - return "unknown" + return "unknown", nil } filePath := parts[0] fragment := "" @@ -530,6 +537,14 @@ func generateFilePathBasedName(ref string, usedNames map[string]bool) string { // Remove leading "./" if present cleanPath = strings.TrimPrefix(cleanPath, "./") + // Handle parent directory references more elegantly + // Instead of converting "../" to "___", we'll normalize the path + normalizedPath, err := normalizePathForComponentName(cleanPath, targetLocation) + if err != nil { + return "", fmt.Errorf("failed to normalize path %s: %w", cleanPath, err) + } + cleanPath = normalizedPath + // Replace extension dot with underscore to keep it but make it safe ext := filepath.Ext(cleanPath) if ext != "" { @@ -559,7 +574,99 @@ func generateFilePathBasedName(ref string, usedNames map[string]bool) string { counter++ } - return componentName + return componentName, nil +} + +// normalizePathForComponentName normalizes a file path to create a more readable component name +// by resolving relative paths to their actual directory names using absolute path resolution +func normalizePathForComponentName(path, targetLocation string) (string, error) { + if targetLocation == "" { + return "", errors.New("target location cannot be empty for path normalization") + } + + // Get the directory of the target location + targetDir := filepath.Dir(targetLocation) + + // Resolve the relative path against the target directory to get absolute path + resolvedAbsPath, err := filepath.Abs(filepath.Join(targetDir, path)) + if err != nil { + return "", fmt.Errorf("failed to resolve relative path: %w", err) + } + + // Split the original relative path to find where the real path starts (after all the ../) + // Handle both Unix and Windows path separators + normalizedPath := strings.ReplaceAll(path, "\\", "/") + pathParts := strings.Split(normalizedPath, "/") + + // Count parent directory navigations and find the start of the real path + parentCount := 0 + realPathStart := len(pathParts) // Default to end if no real path found + foundRealPath := false + + for i, part := range pathParts { + if foundRealPath { + break + } + + switch part { + case "..": + parentCount++ + case ".": + // Skip current directory references + continue + case "": + // Skip empty parts + continue + default: + // Found the start of the real path + realPathStart = i + foundRealPath = true + } + } + + // Get the real path parts (everything after the ../ navigation) + var realPathParts []string + if realPathStart < len(pathParts) { + realPathParts = pathParts[realPathStart:] + } + + // Use the absolute path to get the meaningful directory structure + // Split the absolute path and take the last meaningful parts + absParts := strings.Split(strings.ReplaceAll(resolvedAbsPath, "\\", "/"), "/") + + // We want to include the directory we land on after navigation plus the real path + // For "../../../other/api.yaml" from "openapi/a/b/c/spec.yaml", we want "openapi/other/api.yaml" + // So we need: landing directory (openapi) + real path parts (other/api.yaml) + + var resultParts []string + + if parentCount > 0 { + // Find the landing directory after going up parentCount levels + // We need at least parentCount + len(realPathParts) parts in the absolute path + requiredParts := 1 + len(realPathParts) // 1 for landing directory + real path parts + + if len(absParts) < requiredParts { + return "", fmt.Errorf("not enough path components in resolved absolute path: got %d, need at least %d", len(absParts), requiredParts) + } + + // Take the landing directory (the directory we end up in after going up) + landingDirIndex := len(absParts) - len(realPathParts) - 1 + if landingDirIndex >= 0 && landingDirIndex < len(absParts) { + landingDir := absParts[landingDirIndex] + resultParts = append(resultParts, landingDir) + } + } + + // Add the real path parts + resultParts = append(resultParts, realPathParts...) + + // Join and clean up the result + result := strings.Join(resultParts, "/") + + // Remove leading "./" if present + result = strings.TrimPrefix(result, "./") + + return result, nil } // generateCounterBasedName creates names like "User_1", "User_2" for conflicts diff --git a/openapi/marshalling.go b/openapi/marshalling.go index 24a644a..e5ab384 100644 --- a/openapi/marshalling.go +++ b/openapi/marshalling.go @@ -5,6 +5,7 @@ import ( "io" "github.com/speakeasy-api/openapi/marshaller" + "github.com/speakeasy-api/openapi/openapi/core" "github.com/speakeasy-api/openapi/validation" ) @@ -57,7 +58,7 @@ func Marshal(ctx context.Context, openapi *OpenAPI, w io.Writer) error { // Sync will sync the high-level model to the core model. // This is useful when creating or mutating a high-level model and wanting access to the yaml nodes that back it. -func Sync(ctx context.Context, model marshaller.Marshallable[OpenAPI]) error { +func Sync(ctx context.Context, model marshaller.Marshallable[core.OpenAPI]) error { _, err := marshaller.SyncValue(ctx, model, model.GetCore(), model.GetRootNode(), false) return err } diff --git a/openapi/testdata/inline/bundled_counter_expected.yaml b/openapi/testdata/inline/bundled_counter_expected.yaml index 52faa13..399fc8e 100644 --- a/openapi/testdata/inline/bundled_counter_expected.yaml +++ b/openapi/testdata/inline/bundled_counter_expected.yaml @@ -284,8 +284,8 @@ paths: tags: [external] summary: Test external parameter with complex reference chain parameters: - - $ref: '#/components/parameters/ComplexFilterParam' - - $ref: '#/components/parameters/PaginationParam' + - $ref: "#/components/parameters/ComplexFilterParam" + - $ref: "#/components/parameters/PaginationParam" responses: "200": description: Filtered results @@ -305,6 +305,54 @@ paths: type: integer page: type: integer + # Test parent directory reference to another testdata folder - this should reproduce issue #50 + /parent-dir-test: + get: + tags: [external] + summary: Test parent directory reference to clean folder (should reproduce issue #50) + responses: + "200": + description: Success with parent directory reference (conflicts with existing User) + content: + application/json: + schema: + $ref: "#/components/schemas/User_2" + "400": + description: Error from parent directory (conflicts with existing Error) + content: + application/json: + schema: + $ref: "#/components/schemas/Error_1" + "404": + description: Non-conflicting schema from parent directory + content: + application/json: + schema: + $ref: "#/components/schemas/UnusedSchema" + # Test complex conflict resolution with parent directory references + /product-conflict-test: + get: + tags: [external] + summary: Test complex conflict resolution with parent directory references + requestBody: + description: Product creation data + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + responses: + "200": + description: Product from root testdata (should conflict and get counter) + content: + application/json: + schema: + $ref: "#/components/schemas/Product_1" + "201": + description: Product from intermediate testdata (should conflict and get counter) + content: + application/json: + schema: + $ref: "#/components/schemas/Product_2" components: parameters: LimitParam: @@ -469,6 +517,28 @@ components: type: object additionalProperties: true description: Additional error details + Product: + type: object + description: Product information from inline directory + required: + - id + - name + - sku + properties: + id: + type: integer + description: Product identifier (integer format) + name: + type: string + maxLength: 100 + description: Product name + sku: + type: string + description: Product SKU + active: + type: boolean + default: true + description: Product status User_1: type: object properties: @@ -712,6 +782,73 @@ components: properties: {} title: Abstract Body description: An abstract schema used to define other request and response body model schemas. + User_2: + type: object + properties: + id: + type: integer + name: + type: string + Error_1: + type: object + properties: + message: + type: string + code: + type: integer + UnusedSchema: + type: string + description: This schema is not referenced anywhere + Product_1: + type: object + properties: + id: + type: integer + format: int64 + description: Product identifier + name: + type: string + maxLength: 200 + description: Product name + price: + type: number + minimum: 0 + format: double + description: Product price + category: + type: string + description: Product category + required: + - id + - name + - price + description: Product information from root testdata + x-test: some-value + Product_2: + type: object + properties: + id: + type: string + format: uuid + description: Product identifier (UUID format) + name: + type: string + maxLength: 150 + description: Product name + description: + type: string + maxLength: 1000 + description: Product description + tags: + type: array + items: + type: string + description: Product tags + required: + - id + - name + - description + description: Product information from inline/testdata directory requestBodies: CreateUserRequest: description: Request body for creating a user diff --git a/openapi/testdata/inline/bundled_expected.yaml b/openapi/testdata/inline/bundled_expected.yaml index e8dd430..3f94402 100644 --- a/openapi/testdata/inline/bundled_expected.yaml +++ b/openapi/testdata/inline/bundled_expected.yaml @@ -284,8 +284,8 @@ paths: tags: [external] summary: Test external parameter with complex reference chain parameters: - - $ref: '#/components/parameters/ComplexFilterParam' - - $ref: '#/components/parameters/PaginationParam' + - $ref: "#/components/parameters/ComplexFilterParam" + - $ref: "#/components/parameters/PaginationParam" responses: "200": description: Filtered results @@ -305,6 +305,54 @@ paths: type: integer page: type: integer + # Test parent directory reference to another testdata folder - this should reproduce issue #50 + /parent-dir-test: + get: + tags: [external] + summary: Test parent directory reference to clean folder (should reproduce issue #50) + responses: + "200": + description: Success with parent directory reference (conflicts with existing User) + content: + application/json: + schema: + $ref: "#/components/schemas/testdata_clean_clean_input_yaml~components_schemas_User" + "400": + description: Error from parent directory (conflicts with existing Error) + content: + application/json: + schema: + $ref: "#/components/schemas/testdata_clean_clean_input_yaml~components_schemas_Error" + "404": + description: Non-conflicting schema from parent directory + content: + application/json: + schema: + $ref: "#/components/schemas/UnusedSchema" + # Test complex conflict resolution with parent directory references + /product-conflict-test: + get: + tags: [external] + summary: Test complex conflict resolution with parent directory references + requestBody: + description: Product creation data + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + responses: + "200": + description: Product from root testdata (should conflict and get counter) + content: + application/json: + schema: + $ref: "#/components/schemas/testdata_test_openapi_yaml~components_schemas_Product" + "201": + description: Product from intermediate testdata (should conflict and get counter) + content: + application/json: + schema: + $ref: "#/components/schemas/testdata_test_openapi_yaml~components_schemas_Product_1" components: parameters: LimitParam: @@ -469,6 +517,28 @@ components: type: object additionalProperties: true description: Additional error details + Product: + type: object + description: Product information from inline directory + required: + - id + - name + - sku + properties: + id: + type: integer + description: Product identifier (integer format) + name: + type: string + maxLength: 100 + description: Product name + sku: + type: string + description: Product SKU + active: + type: boolean + default: true + description: Product status external_conflicting_user_yaml~User: type: object properties: @@ -712,6 +782,73 @@ components: properties: {} title: Abstract Body description: An abstract schema used to define other request and response body model schemas. + testdata_clean_clean_input_yaml~components_schemas_User: + type: object + properties: + id: + type: integer + name: + type: string + testdata_clean_clean_input_yaml~components_schemas_Error: + type: object + properties: + message: + type: string + code: + type: integer + UnusedSchema: + type: string + description: This schema is not referenced anywhere + testdata_test_openapi_yaml~components_schemas_Product: + type: object + properties: + id: + type: integer + format: int64 + description: Product identifier + name: + type: string + maxLength: 200 + description: Product name + price: + type: number + minimum: 0 + format: double + description: Product price + category: + type: string + description: Product category + required: + - id + - name + - price + description: Product information from root testdata + x-test: some-value + testdata_test_openapi_yaml~components_schemas_Product_1: + type: object + properties: + id: + type: string + format: uuid + description: Product identifier (UUID format) + name: + type: string + maxLength: 150 + description: Product name + description: + type: string + maxLength: 1000 + description: Product description + tags: + type: array + items: + type: string + description: Product tags + required: + - id + - name + - description + description: Product information from inline/testdata directory requestBodies: CreateUserRequest: description: Request body for creating a user diff --git a/openapi/testdata/inline/inline_expected.yaml b/openapi/testdata/inline/inline_expected.yaml index a8cdfae..f541b85 100644 --- a/openapi/testdata/inline/inline_expected.yaml +++ b/openapi/testdata/inline/inline_expected.yaml @@ -842,6 +842,131 @@ paths: type: integer page: type: integer + # Test parent directory reference to another testdata folder - this should reproduce issue #50 + /parent-dir-test: + get: + tags: [external] + summary: Test parent directory reference to clean folder (should reproduce issue #50) + responses: + "200": + description: Success with parent directory reference (conflicts with existing User) + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + "400": + description: Error from parent directory (conflicts with existing Error) + content: + application/json: + schema: + type: object + properties: + message: + type: string + code: + type: integer + "404": + description: Non-conflicting schema from parent directory + content: + application/json: + schema: + type: string + description: This schema is not referenced anywhere + # Test complex conflict resolution with parent directory references + /product-conflict-test: + get: + tags: [external] + summary: Test complex conflict resolution with parent directory references + requestBody: + description: Product creation data + content: + application/json: + schema: + type: object + properties: + id: + type: integer + description: Product identifier (integer format) + name: + type: string + maxLength: 100 + description: Product name + sku: + type: string + description: Product SKU + active: + type: boolean + description: Product status + default: true + required: + - id + - name + - sku + description: Product information from inline directory + responses: + "200": + description: Product from root testdata (should conflict and get counter) + content: + application/json: + schema: + type: object + properties: + id: + type: integer + format: int64 + description: Product identifier + name: + type: string + maxLength: 200 + description: Product name + price: + type: number + minimum: 0 + format: double + description: Product price + category: + type: string + description: Product category + required: + - id + - name + - price + description: Product information from root testdata + x-test: some-value + "201": + description: Product from intermediate testdata (should conflict and get counter) + content: + application/json: + schema: + type: object + properties: + id: + type: string + format: uuid + description: Product identifier (UUID format) + name: + type: string + maxLength: 150 + description: Product name + description: + type: string + maxLength: 1000 + description: Product description + tags: + type: array + items: + type: string + description: Product tags + required: + - id + - name + - description + description: Product information from inline/testdata directory components: schemas: User: diff --git a/openapi/testdata/inline/inline_input.yaml b/openapi/testdata/inline/inline_input.yaml index 2a2cc73..42cf0bb 100644 --- a/openapi/testdata/inline/inline_input.yaml +++ b/openapi/testdata/inline/inline_input.yaml @@ -299,8 +299,8 @@ paths: tags: [external] summary: Test external parameter with complex reference chain parameters: - - $ref: 'external/parameters.yaml#/components/parameters/ComplexFilterParam' - - $ref: 'external/parameters.yaml#/components/parameters/PaginationParam' + - $ref: "external/parameters.yaml#/components/parameters/ComplexFilterParam" + - $ref: "external/parameters.yaml#/components/parameters/PaginationParam" responses: "200": description: Filtered results @@ -321,6 +321,56 @@ paths: page: type: integer + # Test parent directory reference to another testdata folder - this should reproduce issue #50 + /parent-dir-test: + get: + tags: [external] + summary: Test parent directory reference to clean folder (should reproduce issue #50) + responses: + "200": + description: Success with parent directory reference (conflicts with existing User) + content: + application/json: + schema: + $ref: "../clean/clean_input.yaml#/components/schemas/User" + "400": + description: Error from parent directory (conflicts with existing Error) + content: + application/json: + schema: + $ref: "../clean/clean_input.yaml#/components/schemas/Error" + "404": + description: Non-conflicting schema from parent directory + content: + application/json: + schema: + $ref: "../clean/clean_input.yaml#/components/schemas/UnusedSchema" + + # Test complex conflict resolution with parent directory references + /product-conflict-test: + get: + tags: [external] + summary: Test complex conflict resolution with parent directory references + requestBody: + description: Product creation data + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + responses: + "200": + description: Product from root testdata (should conflict and get counter) + content: + application/json: + schema: + $ref: "../test.openapi.yaml#/components/schemas/Product" + "201": + description: Product from intermediate testdata (should conflict and get counter) + content: + application/json: + schema: + $ref: "./testdata/test.openapi.yaml#/components/schemas/Product" + components: parameters: LimitParam: @@ -482,6 +532,29 @@ components: additionalProperties: true description: Additional error details + Product: + type: object + description: Product information from inline directory + required: + - id + - name + - sku + properties: + id: + type: integer + description: Product identifier (integer format) + name: + type: string + maxLength: 100 + description: Product name + sku: + type: string + description: Product SKU + active: + type: boolean + default: true + description: Product status + requestBodies: CreateUserRequest: description: Request body for creating a user diff --git a/openapi/testdata/inline/testdata/test.openapi.yaml b/openapi/testdata/inline/testdata/test.openapi.yaml new file mode 100644 index 0000000..9e7e712 --- /dev/null +++ b/openapi/testdata/inline/testdata/test.openapi.yaml @@ -0,0 +1,35 @@ +openapi: 3.1.0 +info: + title: Intermediate Test API + version: 1.0.0 + description: Intermediate test file to create conflict chain + +paths: {} + +components: + schemas: + Product: + type: object + description: Product information from inline/testdata directory + required: + - id + - name + - description + properties: + id: + type: string + format: uuid + description: Product identifier (UUID format) + name: + type: string + maxLength: 150 + description: Product name + description: + type: string + maxLength: 1000 + description: Product description + tags: + type: array + items: + type: string + description: Product tags diff --git a/openapi/testdata/test.openapi.yaml b/openapi/testdata/test.openapi.yaml index 18adb04..5ecf5c7 100644 --- a/openapi/testdata/test.openapi.yaml +++ b/openapi/testdata/test.openapi.yaml @@ -325,6 +325,31 @@ components: message: type: string x-test: some-value + Product: + type: object + description: Product information from root testdata + required: + - id + - name + - price + properties: + id: + type: integer + format: int64 + description: Product identifier + name: + type: string + maxLength: 200 + description: Product name + price: + type: number + format: double + minimum: 0 + description: Product price + category: + type: string + description: Product category + x-test: some-value responses: BadRequest: description: Bad request