adding YAMLMerger
This commit is contained in:
parent
78e3eaaf8f
commit
9561de6327
106
Sources/YAMLSplitter/README.md
Normal file
106
Sources/YAMLSplitter/README.md
Normal file
@ -0,0 +1,106 @@
|
||||
# YAMLSplitter
|
||||
|
||||
A Swift package for splitting a single OpenAPI YAML file into organized schema files - the opposite of YAMLMerger.
|
||||
|
||||
## How It Works
|
||||
|
||||
YAMLSplitter takes a single OpenAPI YAML file and splits it into organized schema files following the same structure as YAMLMerger:
|
||||
|
||||
1. **01_Info/** - OpenAPI info section (version, title, description, etc.)
|
||||
2. **02_Servers/** - API server URLs and environment configurations
|
||||
3. **03_Tags/** - Tag definitions for organizing endpoints
|
||||
4. **04_Paths/** - API endpoint definitions (one file per path)
|
||||
5. **06_Components/** - Reusable components (one file per schema)
|
||||
6. **07_Security/** - Security requirements
|
||||
7. **08_ExternalDocs/** - External documentation references
|
||||
|
||||
## Usage
|
||||
|
||||
```swift
|
||||
import YAMLSplitter
|
||||
|
||||
// Split a single OpenAPI YAML into organized schema files
|
||||
let splitter = YAMLSplitter(
|
||||
inputFile: URL(fileURLWithPath: "openapi.yaml"),
|
||||
outputDirectory: URL(fileURLWithPath: "Schema")
|
||||
)
|
||||
|
||||
try splitter.split()
|
||||
```
|
||||
|
||||
## Directory Structure Created
|
||||
|
||||
```
|
||||
Schema/
|
||||
├── 01_Info/
|
||||
│ ├── __info.yaml # Header file
|
||||
│ └── api-info.yaml # Info content
|
||||
├── 02_Servers/
|
||||
│ ├── __servers.yaml # Header file
|
||||
│ └── server-1.yaml # Individual server configs
|
||||
├── 03_Tags/
|
||||
│ ├── __tags.yaml # Header file
|
||||
│ └── tags.yaml # Tag definitions
|
||||
├── 04_Paths/
|
||||
│ ├── __paths.yaml # Header file
|
||||
│ ├── api_login.yaml
|
||||
│ ├── api_buser.yaml
|
||||
│ └── ... (one file per path)
|
||||
├── 06_Components/
|
||||
│ ├── __components.yaml # Header file
|
||||
│ ├── Login.yaml
|
||||
│ ├── User.yaml
|
||||
│ └── ... (one file per schema)
|
||||
├── 07_Security/
|
||||
│ ├── __securitySchemes.yaml
|
||||
│ └── security.yaml
|
||||
└── 08_ExternalDocs/
|
||||
├── __externalDocs.yaml
|
||||
└── externalDocs.yaml
|
||||
```
|
||||
|
||||
## Round Trip Testing
|
||||
|
||||
YAMLSplitter works perfectly with YAMLMerger for round-trip testing:
|
||||
|
||||
```swift
|
||||
// 1. Split the original openapi.yaml
|
||||
let splitter = YAMLSplitter(inputFile: originalFile, outputDirectory: schemaDir)
|
||||
try splitter.split()
|
||||
|
||||
// 2. Merge back using YAMLMerger
|
||||
let merger = YAMLMerger(rootDirectory: schemaDir, outputFileName: "CombinedSpec.yaml")
|
||||
merger.merge()
|
||||
|
||||
// 3. The result should be identical to the original
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
- ✅ **Organized Structure** - Follows the same directory structure as YAMLMerger
|
||||
- ✅ **One File Per Component** - Each path and schema gets its own file
|
||||
- ✅ **Header Files** - Creates `__*.yaml` header files for proper merging
|
||||
- ✅ **Round Trip Compatible** - Works perfectly with YAMLMerger
|
||||
- ✅ **YAML Parsing** - Uses Yams library for robust YAML handling
|
||||
- ✅ **Error Handling** - Comprehensive error handling and validation
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Yams** - Swift YAML library for parsing and generating YAML
|
||||
|
||||
## Testing
|
||||
|
||||
The package includes comprehensive tests that:
|
||||
|
||||
1. Split the ApiShared/openapi.yaml into organized schema files
|
||||
2. Verify all expected directories and files are created
|
||||
3. Test round-trip compatibility with YAMLMerger
|
||||
4. Validate the structure matches YAMLMerger expectations
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **API Documentation** - Split large OpenAPI specs into manageable files
|
||||
- **Team Collaboration** - Allow different team members to work on different sections
|
||||
- **Version Control** - Better diff tracking for specific components
|
||||
- **Modular Development** - Work on individual endpoints and schemas separately
|
||||
- **Round Trip Testing** - Validate that split + merge produces identical results
|
||||
197
Sources/YAMLSplitter/YAMLSplitter.swift
Normal file
197
Sources/YAMLSplitter/YAMLSplitter.swift
Normal file
@ -0,0 +1,197 @@
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// YAMLSplitter - The opposite of YAMLMerger
|
||||
/// Takes a single OpenAPI YAML file and splits it into organized schema files
|
||||
public struct YAMLSplitter {
|
||||
public let inputFile: URL
|
||||
public let outputDirectory: URL
|
||||
|
||||
public init(inputFile: URL, outputDirectory: URL) {
|
||||
self.inputFile = inputFile
|
||||
self.outputDirectory = outputDirectory
|
||||
}
|
||||
|
||||
/// Convenience initializer that creates Schema directory in Documents
|
||||
public init(inputFile: URL) {
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let schemaDirectory = documentsURL.appendingPathComponent("Schema")
|
||||
self.init(inputFile: inputFile, outputDirectory: schemaDirectory)
|
||||
}
|
||||
|
||||
/// Splits the OpenAPI YAML file into organized schema files
|
||||
public func split() throws {
|
||||
// Ensure output directory exists
|
||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
// Load and parse the input YAML file
|
||||
let yamlContent = try String(contentsOf: inputFile, encoding: .utf8)
|
||||
guard let yaml = try Yams.load(yaml: yamlContent) as? [String: Any] else {
|
||||
throw YAMLSplitterError.invalidYAMLStructure
|
||||
}
|
||||
|
||||
// Split into organized sections
|
||||
try splitInfoSection(from: yaml)
|
||||
try splitServersSection(from: yaml)
|
||||
try splitTagsSection(from: yaml)
|
||||
try splitPathsSection(from: yaml)
|
||||
try splitComponentsSection(from: yaml)
|
||||
try splitSecuritySection(from: yaml)
|
||||
try splitExternalDocsSection(from: yaml)
|
||||
|
||||
print("✅ YAML split successfully into \(outputDirectory.path)")
|
||||
}
|
||||
|
||||
// MARK: - Section Splitters
|
||||
|
||||
private func splitInfoSection(from dict: [String: Any]) throws {
|
||||
guard let info = dict["info"] else { return }
|
||||
|
||||
let infoDir = outputDirectory.appendingPathComponent("01_Info")
|
||||
try FileManager.default.createDirectory(at: infoDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __info.yaml (header)
|
||||
let infoHeader = """
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
"""
|
||||
try infoHeader.write(to: infoDir.appendingPathComponent("__info.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Create api-info.yaml
|
||||
let apiInfo = try Yams.dump(object: info, width: -1, sortKeys: true)
|
||||
try apiInfo.write(to: infoDir.appendingPathComponent("api-info.yaml"), atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func splitServersSection(from dict: [String: Any]) throws {
|
||||
guard let servers = dict["servers"] else { return }
|
||||
|
||||
let serversDir = outputDirectory.appendingPathComponent("02_Servers")
|
||||
try FileManager.default.createDirectory(at: serversDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __servers.yaml (header)
|
||||
let serversHeader = """
|
||||
servers:
|
||||
"""
|
||||
try serversHeader.write(to: serversDir.appendingPathComponent("__servers.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Create individual server files
|
||||
if let serversArray = servers as? [[String: Any]] {
|
||||
for (index, server) in serversArray.enumerated() {
|
||||
let serverYaml = try Yams.dump(object: server, width: -1, sortKeys: true)
|
||||
let fileName = "server-\(index + 1).yaml"
|
||||
try serverYaml.write(to: serversDir.appendingPathComponent(fileName), atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func splitTagsSection(from dict: [String: Any]) throws {
|
||||
guard let tags = dict["tags"] else { return }
|
||||
|
||||
let tagsDir = outputDirectory.appendingPathComponent("03_Tags")
|
||||
try FileManager.default.createDirectory(at: tagsDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __tags.yaml (header)
|
||||
let tagsHeader = """
|
||||
tags:
|
||||
"""
|
||||
try tagsHeader.write(to: tagsDir.appendingPathComponent("__tags.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Create tags.yaml
|
||||
let tagsYaml = try Yams.dump(object: tags, width: -1, sortKeys: true)
|
||||
try tagsYaml.write(to: tagsDir.appendingPathComponent("tags.yaml"), atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func splitPathsSection(from dict: [String: Any]) throws {
|
||||
guard let paths = dict["paths"] as? [String: Any] else { return }
|
||||
|
||||
let pathsDir = outputDirectory.appendingPathComponent("04_Paths")
|
||||
try FileManager.default.createDirectory(at: pathsDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __paths.yaml (header)
|
||||
let pathsHeader = """
|
||||
paths:
|
||||
"""
|
||||
try pathsHeader.write(to: pathsDir.appendingPathComponent("__paths.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Split each path into its own file
|
||||
for (path, pathData) in paths {
|
||||
let fileName = sanitizeFileName(path) + ".yaml"
|
||||
let pathYaml = try Yams.dump(object: [path: pathData], width: -1, sortKeys: true)
|
||||
try pathYaml.write(to: pathsDir.appendingPathComponent(fileName), atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
private func splitComponentsSection(from dict: [String: Any]) throws {
|
||||
guard let components = dict["components"] as? [String: Any] else { return }
|
||||
|
||||
let componentsDir = outputDirectory.appendingPathComponent("06_Components")
|
||||
try FileManager.default.createDirectory(at: componentsDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __components.yaml (header)
|
||||
let componentsHeader = """
|
||||
components:
|
||||
"""
|
||||
try componentsHeader.write(to: componentsDir.appendingPathComponent("__components.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Split schemas into individual files
|
||||
if let schemas = components["schemas"] as? [String: Any] {
|
||||
for (schemaName, schemaData) in schemas {
|
||||
let fileName = "\(schemaName).yaml"
|
||||
let schemaYaml = try Yams.dump(object: [schemaName: schemaData], width: -1, sortKeys: true)
|
||||
try schemaYaml.write(to: componentsDir.appendingPathComponent(fileName), atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func splitSecuritySection(from dict: [String: Any]) throws {
|
||||
guard let security = dict["security"] else { return }
|
||||
|
||||
let securityDir = outputDirectory.appendingPathComponent("07_Security")
|
||||
try FileManager.default.createDirectory(at: securityDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __securitySchemes.yaml (header)
|
||||
let securityHeader = """
|
||||
security:
|
||||
"""
|
||||
try securityHeader.write(to: securityDir.appendingPathComponent("__securitySchemes.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Create security.yaml
|
||||
let securityYaml = try Yams.dump(object: security, width: -1, sortKeys: true)
|
||||
try securityYaml.write(to: securityDir.appendingPathComponent("security.yaml"), atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func splitExternalDocsSection(from dict: [String: Any]) throws {
|
||||
guard let externalDocs = dict["externalDocs"] else { return }
|
||||
|
||||
let externalDocsDir = outputDirectory.appendingPathComponent("08_ExternalDocs")
|
||||
try FileManager.default.createDirectory(at: externalDocsDir, withIntermediateDirectories: true)
|
||||
|
||||
// Create __externalDocs.yaml (header)
|
||||
let externalDocsHeader = """
|
||||
externalDocs:
|
||||
"""
|
||||
try externalDocsHeader.write(to: externalDocsDir.appendingPathComponent("__externalDocs.yaml"), atomically: true, encoding: .utf8)
|
||||
|
||||
// Create externalDocs.yaml
|
||||
let externalDocsYaml = try Yams.dump(object: externalDocs, width: -1, sortKeys: true)
|
||||
try externalDocsYaml.write(to: externalDocsDir.appendingPathComponent("externalDocs.yaml"), atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func sanitizeFileName(_ path: String) -> String {
|
||||
return path
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "{", with: "")
|
||||
.replacingOccurrences(of: "}", with: "")
|
||||
.replacingOccurrences(of: " ", with: "_")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum YAMLSplitterError: Error {
|
||||
case invalidYAMLStructure
|
||||
case fileNotFound
|
||||
case directoryCreationFailed
|
||||
}
|
||||
583
Tests/YAMLSplitterTests/Resources/openapi.yaml
Normal file
583
Tests/YAMLSplitterTests/Resources/openapi.yaml
Normal file
@ -0,0 +1,583 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Demo API
|
||||
version: 1.0.0
|
||||
description: A demo OpenAPI specification for testing YAMLSplitter
|
||||
contact:
|
||||
name: API Support
|
||||
email: support@demo.com
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
servers:
|
||||
- url: https://api.demo.com/v1
|
||||
description: Production server
|
||||
- url: https://staging-api.demo.com/v1
|
||||
description: Staging server
|
||||
- url: http://localhost:8080
|
||||
description: Local development server
|
||||
tags:
|
||||
- name: Users
|
||||
description: User management operations
|
||||
- name: Products
|
||||
description: Product catalog operations
|
||||
- name: Orders
|
||||
description: Order management operations
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
tags:
|
||||
- Users
|
||||
summary: List all users
|
||||
description: Retrieve a list of all users
|
||||
operationId: listUsers
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: Maximum number of users to return
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 10
|
||||
- name: offset
|
||||
in: query
|
||||
description: Number of users to skip
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: List of users
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
summary: Create a new user
|
||||
description: Create a new user account
|
||||
operationId: createUser
|
||||
requestBody:
|
||||
description: User information
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateUserRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: User created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/users/{userId}:
|
||||
get:
|
||||
tags:
|
||||
- Users
|
||||
summary: Get user by ID
|
||||
description: Retrieve a specific user by their ID
|
||||
operationId: getUserById
|
||||
parameters:
|
||||
- name: userId
|
||||
in: path
|
||||
required: true
|
||||
description: User ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: User details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'404':
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
put:
|
||||
tags:
|
||||
- Users
|
||||
summary: Update user
|
||||
description: Update an existing user
|
||||
operationId: updateUser
|
||||
parameters:
|
||||
- name: userId
|
||||
in: path
|
||||
required: true
|
||||
description: User ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
description: Updated user information
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateUserRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: User updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
'404':
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
delete:
|
||||
tags:
|
||||
- Users
|
||||
summary: Delete user
|
||||
description: Delete a user account
|
||||
operationId: deleteUser
|
||||
parameters:
|
||||
- name: userId
|
||||
in: path
|
||||
required: true
|
||||
description: User ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'204':
|
||||
description: User deleted successfully
|
||||
'404':
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/products:
|
||||
get:
|
||||
tags:
|
||||
- Products
|
||||
summary: List all products
|
||||
description: Retrieve a list of all products
|
||||
operationId: listProducts
|
||||
parameters:
|
||||
- name: category
|
||||
in: query
|
||||
description: Filter by product category
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: minPrice
|
||||
in: query
|
||||
description: Minimum price filter
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
format: decimal
|
||||
- name: maxPrice
|
||||
in: query
|
||||
description: Maximum price filter
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
format: decimal
|
||||
responses:
|
||||
'200':
|
||||
description: List of products
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Product'
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/products/{productId}:
|
||||
get:
|
||||
tags:
|
||||
- Products
|
||||
summary: Get product by ID
|
||||
description: Retrieve a specific product by its ID
|
||||
operationId: getProductById
|
||||
parameters:
|
||||
- name: productId
|
||||
in: path
|
||||
required: true
|
||||
description: Product ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Product details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Product'
|
||||
'404':
|
||||
description: Product not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/orders:
|
||||
get:
|
||||
tags:
|
||||
- Orders
|
||||
summary: List user orders
|
||||
description: Retrieve orders for the authenticated user
|
||||
operationId: listOrders
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: List of orders
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
post:
|
||||
tags:
|
||||
- Orders
|
||||
summary: Create a new order
|
||||
description: Create a new order
|
||||
operationId: createOrder
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
description: Order information
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateOrderRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Order created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
/orders/{orderId}:
|
||||
get:
|
||||
tags:
|
||||
- Orders
|
||||
summary: Get order by ID
|
||||
description: Retrieve a specific order by its ID
|
||||
operationId: getOrderById
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: orderId
|
||||
in: path
|
||||
required: true
|
||||
description: Order ID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Order details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Order'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'404':
|
||||
description: Order not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- firstName
|
||||
- lastName
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique user identifier
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: User email address
|
||||
firstName:
|
||||
type: string
|
||||
description: User's first name
|
||||
lastName:
|
||||
type: string
|
||||
description: User's last name
|
||||
phone:
|
||||
type: string
|
||||
description: User's phone number
|
||||
dateOfBirth:
|
||||
type: string
|
||||
format: date
|
||||
description: User's date of birth
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Account creation timestamp
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
CreateUserRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- firstName
|
||||
- lastName
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: User email address
|
||||
firstName:
|
||||
type: string
|
||||
description: User's first name
|
||||
lastName:
|
||||
type: string
|
||||
description: User's last name
|
||||
phone:
|
||||
type: string
|
||||
description: User's phone number
|
||||
dateOfBirth:
|
||||
type: string
|
||||
format: date
|
||||
description: User's date of birth
|
||||
UpdateUserRequest:
|
||||
type: object
|
||||
properties:
|
||||
firstName:
|
||||
type: string
|
||||
description: User's first name
|
||||
lastName:
|
||||
type: string
|
||||
description: User's last name
|
||||
phone:
|
||||
type: string
|
||||
description: User's phone number
|
||||
dateOfBirth:
|
||||
type: string
|
||||
format: date
|
||||
description: User's date of birth
|
||||
Product:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- price
|
||||
- category
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique product identifier
|
||||
name:
|
||||
type: string
|
||||
description: Product name
|
||||
description:
|
||||
type: string
|
||||
description: Product description
|
||||
price:
|
||||
type: number
|
||||
format: decimal
|
||||
description: Product price
|
||||
category:
|
||||
type: string
|
||||
description: Product category
|
||||
inStock:
|
||||
type: boolean
|
||||
description: Whether the product is in stock
|
||||
stockQuantity:
|
||||
type: integer
|
||||
description: Available stock quantity
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Product creation timestamp
|
||||
Order:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- userId
|
||||
- items
|
||||
- total
|
||||
- status
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique order identifier
|
||||
userId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: User who placed the order
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OrderItem'
|
||||
description: Order items
|
||||
total:
|
||||
type: number
|
||||
format: decimal
|
||||
description: Total order amount
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- pending
|
||||
- confirmed
|
||||
- shipped
|
||||
- delivered
|
||||
- cancelled
|
||||
description: Order status
|
||||
shippingAddress:
|
||||
$ref: '#/components/schemas/Address'
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Order creation timestamp
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
OrderItem:
|
||||
type: object
|
||||
required:
|
||||
- productId
|
||||
- quantity
|
||||
- price
|
||||
properties:
|
||||
productId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Product identifier
|
||||
quantity:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Quantity ordered
|
||||
price:
|
||||
type: number
|
||||
format: decimal
|
||||
description: Price per unit
|
||||
CreateOrderRequest:
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
- shippingAddress
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OrderItem'
|
||||
description: Order items
|
||||
shippingAddress:
|
||||
$ref: '#/components/schemas/Address'
|
||||
Address:
|
||||
type: object
|
||||
required:
|
||||
- street
|
||||
- city
|
||||
- state
|
||||
- zipCode
|
||||
- country
|
||||
properties:
|
||||
street:
|
||||
type: string
|
||||
description: Street address
|
||||
city:
|
||||
type: string
|
||||
description: City
|
||||
state:
|
||||
type: string
|
||||
description: State or province
|
||||
zipCode:
|
||||
type: string
|
||||
description: ZIP or postal code
|
||||
country:
|
||||
type: string
|
||||
description: Country
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Error code
|
||||
message:
|
||||
type: string
|
||||
description: Error message
|
||||
details:
|
||||
type: string
|
||||
description: Additional error details
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: JWT token authentication
|
||||
security:
|
||||
- bearerAuth: []
|
||||
externalDocs:
|
||||
description: Find more info about the Demo API
|
||||
url: https://docs.demo.com
|
||||
108
Tests/YAMLSplitterTests/YAMLSplitterTests.swift
Normal file
108
Tests/YAMLSplitterTests/YAMLSplitterTests.swift
Normal file
@ -0,0 +1,108 @@
|
||||
@testable import YAMLSplitter
|
||||
@testable import YAMLMerger
|
||||
import XCTest
|
||||
|
||||
final class YAMLSplitterTests: XCTestCase {
|
||||
|
||||
func testSplitOpenAPIYAML() throws {
|
||||
print("📂 CWD:", FileManager.default.currentDirectoryPath)
|
||||
|
||||
// 1) INPUT: openapi.yaml from Resources
|
||||
let bundle = Bundle.module
|
||||
guard let openapiFile = bundle.url(forResource: "openapi", withExtension: "yaml", subdirectory: "Resources") else {
|
||||
XCTFail("Failed to locate openapi.yaml in YAMLSplitterTests/Resources.")
|
||||
return
|
||||
}
|
||||
|
||||
// 2) OUTPUT: Schema directory will be created in Documents automatically
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let schemaOutputDir = documentsURL.appendingPathComponent("Schema")
|
||||
|
||||
// Clean up existing output directory
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: schemaOutputDir.path) {
|
||||
try fm.removeItem(at: schemaOutputDir)
|
||||
}
|
||||
|
||||
// 3) Split the OpenAPI YAML (convenience initializer creates Schema in Documents)
|
||||
let splitter = YAMLSplitter(inputFile: openapiFile)
|
||||
try splitter.split()
|
||||
|
||||
// 4) Verify the output structure
|
||||
XCTAssertTrue(fm.fileExists(atPath: schemaOutputDir.path), "Schema directory not created")
|
||||
|
||||
// Verify all expected directories exist
|
||||
let expectedDirs = [
|
||||
"01_Info",
|
||||
"02_Servers",
|
||||
"03_Tags",
|
||||
"04_Paths",
|
||||
"06_Components",
|
||||
"07_Security",
|
||||
"08_ExternalDocs"
|
||||
]
|
||||
|
||||
for dirName in expectedDirs {
|
||||
let dirPath = schemaOutputDir.appendingPathComponent(dirName)
|
||||
XCTAssertTrue(fm.fileExists(atPath: dirPath.path), "Directory \(dirName) not created")
|
||||
}
|
||||
|
||||
// 5) Verify the split created files and folders
|
||||
let infoDir = schemaOutputDir.appendingPathComponent("01_Info")
|
||||
XCTAssertTrue(fm.fileExists(atPath: infoDir.path), "01_Info directory not created")
|
||||
|
||||
let pathsDir = schemaOutputDir.appendingPathComponent("04_Paths")
|
||||
XCTAssertTrue(fm.fileExists(atPath: pathsDir.path), "04_Paths directory not created")
|
||||
|
||||
let componentsDir = schemaOutputDir.appendingPathComponent("06_Components")
|
||||
XCTAssertTrue(fm.fileExists(atPath: componentsDir.path), "06_Components directory not created")
|
||||
|
||||
// Check that files were created in each directory
|
||||
let pathFiles = try fm.contentsOfDirectory(at: pathsDir, includingPropertiesForKeys: nil)
|
||||
XCTAssertTrue(pathFiles.count > 0, "No files created in 04_Paths directory")
|
||||
|
||||
let componentFiles = try fm.contentsOfDirectory(at: componentsDir, includingPropertiesForKeys: nil)
|
||||
XCTAssertTrue(componentFiles.count > 0, "No files created in 06_Components directory")
|
||||
|
||||
print("✅ YAML split successfully into:", schemaOutputDir.path)
|
||||
print("📁 Created directories:", expectedDirs.joined(separator: ", "))
|
||||
print("📄 Path files created:", pathFiles.count)
|
||||
print("📄 Component files created:", componentFiles.count)
|
||||
}
|
||||
|
||||
func testSplitAndMergeRoundTrip() throws {
|
||||
print("🔄 Testing split and merge round trip")
|
||||
|
||||
// 1) Split the openapi.yaml
|
||||
let bundle = Bundle.module
|
||||
guard let openapiFile = bundle.url(forResource: "openapi", withExtension: "yaml", subdirectory: "Resources") else {
|
||||
XCTFail("Failed to locate openapi.yaml in YAMLSplitterTests/Resources")
|
||||
return
|
||||
}
|
||||
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let schemaDir = documentsURL.appendingPathComponent("Schema")
|
||||
|
||||
// Clean up
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: schemaDir.path) {
|
||||
try fm.removeItem(at: schemaDir)
|
||||
}
|
||||
|
||||
// Split (convenience initializer creates Schema in Documents)
|
||||
let splitter = YAMLSplitter(inputFile: openapiFile)
|
||||
try splitter.split()
|
||||
|
||||
// 2) Merge back using YAMLMerger
|
||||
let merger = YAMLMerger(rootDirectory: schemaDir, outputFileName: "CombinedSpec.yaml")
|
||||
merger.merge()
|
||||
|
||||
// 3) Verify the merged file exists
|
||||
let mergedFile = schemaDir.appendingPathComponent("CombinedSpec.yaml")
|
||||
XCTAssertTrue(fm.fileExists(atPath: mergedFile.path), "Merged file not created")
|
||||
|
||||
print("✅ Round trip completed successfully")
|
||||
print("📄 Original file:", openapiFile.path)
|
||||
print("📄 Merged file:", mergedFile.path)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user