From 9561de63278843aa86ef4352bc89f3ddb761c22f Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 26 Oct 2025 00:45:59 +0200 Subject: [PATCH] adding YAMLMerger --- Sources/YAMLSplitter/README.md | 106 ++++ Sources/YAMLSplitter/YAMLSplitter.swift | 197 ++++++ .../YAMLSplitterTests/Resources/openapi.yaml | 583 ++++++++++++++++++ .../YAMLSplitterTests/YAMLSplitterTests.swift | 108 ++++ 4 files changed, 994 insertions(+) create mode 100644 Sources/YAMLSplitter/README.md create mode 100644 Sources/YAMLSplitter/YAMLSplitter.swift create mode 100644 Tests/YAMLSplitterTests/Resources/openapi.yaml create mode 100644 Tests/YAMLSplitterTests/YAMLSplitterTests.swift diff --git a/Sources/YAMLSplitter/README.md b/Sources/YAMLSplitter/README.md new file mode 100644 index 0000000..b8bcc7c --- /dev/null +++ b/Sources/YAMLSplitter/README.md @@ -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 diff --git a/Sources/YAMLSplitter/YAMLSplitter.swift b/Sources/YAMLSplitter/YAMLSplitter.swift new file mode 100644 index 0000000..24d1208 --- /dev/null +++ b/Sources/YAMLSplitter/YAMLSplitter.swift @@ -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 +} diff --git a/Tests/YAMLSplitterTests/Resources/openapi.yaml b/Tests/YAMLSplitterTests/Resources/openapi.yaml new file mode 100644 index 0000000..6235ed6 --- /dev/null +++ b/Tests/YAMLSplitterTests/Resources/openapi.yaml @@ -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 \ No newline at end of file diff --git a/Tests/YAMLSplitterTests/YAMLSplitterTests.swift b/Tests/YAMLSplitterTests/YAMLSplitterTests.swift new file mode 100644 index 0000000..e3b636d --- /dev/null +++ b/Tests/YAMLSplitterTests/YAMLSplitterTests.swift @@ -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) + } +}