diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..680623b --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,16 @@ +name: Deployment + +on: + push: + tags: + - 'v*' + - '[0-9]*' + +jobs: + + create-docc-and-deploy: + uses: BinaryBirds/github-workflows/.github/workflows/docc_deploy.yml@main + permissions: + contents: read + pages: write + id-token: write \ No newline at end of file diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml deleted file mode 100644 index b10006f..0000000 --- a/.github/workflows/run-checks.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Run checks - -on: - pull_request: - branches: - - main - -jobs: - - run-checks: - # runs-on: macOS-latest - runs-on: self-hosted - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Install Dependencies - run: | - brew install mint - mint install NickLockwood/SwiftFormat@0.53.4 --no-link - - - name: run script - run: ./scripts/run-checks.sh diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index d53cde7..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Run tests - -on: - push: - branches: - - main - paths: - - '**.swift' - pull_request: - branches: - - main - -jobs: - - macOS-tests: - runs-on: self-hosted - steps: - - - name: Checkout - uses: actions/checkout@v4 - - # - name: Cache - # uses: actions/cache@v3 - # with: - # path: server/.build - # key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - # restore-keys: ${{ runner.os }}-spm- - - - name: Test - run: swift test --parallel --enable-code-coverage - - linux-tests: - runs-on: ubuntu-latest - strategy: - matrix: - image: - - 'swift:5.9' - - 'swift:5.10' - container: - image: ${{ matrix.image }} - steps: - - - name: Checkout - uses: actions/checkout@v4 - - # - name: Cache - # uses: actions/cache@v3 - # with: - # path: server/.build - # key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - # restore-keys: ${{ runner.os }}-spm- - - - name: Test - run: swift test --parallel --enable-code-coverage diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..ebfae0b --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,39 @@ +name: Testing + +on: + pull_request: + branches: + - main + +jobs: + + swiftlang_checks: + name: Swiftlang Checks + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "project" + format_check_enabled : true + broken_symlink_check_enabled : true + unacceptable_language_check_enabled : true + shell_check_enabled : true + docs_check_enabled : false + api_breakage_check_enabled : false + license_header_check_enabled : false + yamllint_check_enabled : false + python_lint_check_enabled : false + + bb_checks: + name: BB Checks + uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main + with: + local_swift_dependencies_check_enabled : true + headers_check_enabled : true + docc_warnings_check_enabled : true + + swiftlang_tests: + name: Swiftlang Tests + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_windows_checks : false + linux_build_command: "swift test --parallel --enable-code-coverage" + linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"nightly-6.0\"}, {\"swift_version\": \"nightly-6.1\"}, {\"swift_version\": \"nightly-6.3\"}]" \ No newline at end of file diff --git a/.swift-format b/.swift-format index e3cbb89..6a37415 100644 --- a/.swift-format +++ b/.swift-format @@ -1,58 +1,64 @@ { - "fileScopedDeclarationPrivacy" : { - "accessLevel" : "private" + "version": 1, + "lineLength": 80, + "maximumBlankLines": 1, + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" }, - "indentation" : { - "spaces" : 4 + "tabWidth": 4, + "indentation": { + "spaces": 4 }, - "indentConditionalCompilationBlocks" : false, - "indentSwitchCaseLabels" : false, - "lineBreakAroundMultilineExpressionChainComponents" : true, - "lineBreakBeforeControlFlowKeywords" : true, - "lineBreakBeforeEachArgument" : true, - "lineBreakBeforeEachGenericRequirement" : true, - "lineLength" : 80, - "maximumBlankLines" : 1, - "prioritizeKeepingFunctionOutputTogether" : false, - "respectsExistingLineBreaks" : true, - "rules" : { - "AllPublicDeclarationsHaveDocumentation" : false, - "AlwaysUseLowerCamelCase" : false, - "AmbiguousTrailingClosureOverload" : true, - "BeginDocumentationCommentWithOneLineSummary" : false, - "DoNotUseSemicolons" : true, - "DontRepeatTypeInStaticProperties" : false, - "FileScopedDeclarationPrivacy" : true, - "FullyIndirectEnum" : true, - "GroupNumericLiterals" : true, - "IdentifiersMustBeASCII" : true, - "NeverForceUnwrap" : false, - "NeverUseForceTry" : false, - "NeverUseImplicitlyUnwrappedOptionals" : false, - "NoAccessLevelOnExtensionDeclaration" : false, - "NoAssignmentInExpressions" : true, - "NoBlockComments" : true, - "NoCasesWithOnlyFallthrough" : true, - "NoEmptyTrailingClosureParentheses" : true, - "NoLabelsInCasePatterns" : false, - "NoLeadingUnderscores" : false, - "NoParensAroundConditions" : true, - "NoVoidReturnOnFunctionSignature" : true, - "OneCasePerLine" : true, - "OneVariableDeclarationPerLine" : true, - "OnlyOneTrailingClosureArgument" : true, - "OrderedImports" : false, - "ReturnVoidInsteadOfEmptyTuple" : true, - "UseEarlyExits" : false, - "UseLetInEveryBoundCaseVariable" : false, - "UseShorthandTypeNames" : true, - "UseSingleLinePropertyGetter" : false, - "UseSynthesizedInitializer" : true, - "UseTripleSlashForDocumentationComments" : true, - "UseWhereClausesInForLoops" : false, - "ValidateDocumentationComments" : true - }, - "spacesAroundRangeFormationOperators" : false, - "tabWidth" : 4, - "version" : 1 + "indentConditionalCompilationBlocks": false, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": true, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "spacesAroundRangeFormationOperators": false, + "multiElementCollectionTrailingCommas": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": false, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true + } } diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 0000000..4308420 --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1 @@ +Package.swift diff --git a/.unacceptablelanguageignore b/.unacceptablelanguageignore new file mode 100644 index 0000000..48821ec --- /dev/null +++ b/.unacceptablelanguageignore @@ -0,0 +1,3 @@ +Tests/FeatherOpenAPITests/Petstore/Petstore.swift +Petstore/openapi@3.0.yaml +Petstore/openapi@3.1.yaml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d5c5226 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,86 @@ +// +// AGENTS.md +// feather-openapi +// +// Created by Binary Birds on 2026. 01. 20.. + +# Repository Guidelines + +This repository contains an project written with Swift 6. Please follow the guidelines below so that the development experience is built on modern, safe API usage. + + +## Role + +You are a **Senior Swift Engineer**, specializing in server-side Swift development, and related frameworks (Vapor, Hummingbird). + + +## Core instructions + +- Swift 6.2 or later, using modern Swift concurrency. +- Do not introduce third-party frameworks without asking first. +- Avoid Foundation unless requested. +- Build system: Swift Package Manager. +- Testing framework: Swift Testing (`swift test`). + + +## Swift instructions + +- Assume strict Swift concurrency rules are being applied. +- Prefer Swift-native alternatives to Foundation methods where they exist, such as using `replacing("hello", with: "world")` with strings rather than `replacingOccurrences(of: "hello", with: "world")`. +- Prefer modern Foundation API, if Foundation can not be avoided, for example `URL.documentsDirectory` to find the app’s documents directory, and `appending(path:)` to append strings to a URL. +- Never use C-style number formatting such as `Text(String(format: "%.2f", abs(myNumber)))`; always use `Text(abs(change), format: .number.precision(.fractionLength(2)))` instead. +- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`. +- Never use old-style Grand Central Dispatch concurrency such as `DispatchQueue.main.async()`. If behavior like this is needed, always use modern Swift concurrency. +- Avoid force unwraps and force `try` unless it is unrecoverable. +- Never use `Task.sleep(nanoseconds:)`; always use `Task.sleep(for:)` instead. + + +## Project structure + +- Use a consistent project structure. +- Follow strict naming conventions for types, properties, methods, and data models. +- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file. +- Write unit tests for core application logic. +- Add code comments and documentation comments as needed. +- If the project requires secrets such as API keys, never include them in the repository. + + +## Build, Test, and Development Commands + +- Preferred workflow: after any code change run `swift build` to rebuild the project, and run `swift test` when you actually need tests. + + +## PR instructions + +- If installed, make sure the `make format` & `make check` commands returns no warnings or errors before committing. + + +## Coding Style & Naming Conventions + +- Enforce formatting with the `make format` command. +- Swift 6.2, prefer strict typing and small files (<500 LOC as a guardrail) +- Naming: types UpperCamelCase; methods/properties lowerCamelCase; tests mirror subject names; avoid abbreviations except common GitHub/API terms. + + +## Testing Guidelines + +- Framework: Swift Testing via `swift test`. Name suites `Tests` and functions `behavior()`. +- Cover new logic. Use deterministic fixtures/mocks for data. +- Run `swift test` before pushing; prefer adding tests alongside bug fixes. + + +## Commit & Pull Request Guidelines + +- Commit messages follow the existing short, imperative style; optional scoped prefixes. Keep them concise; present tense; no trailing period. +- PRs: include a brief summary, linked issue ticket if any. + + +## Security & Configuration Tips + +- Keep GitHub App secrets/private key out of the repo; +- Do not log tokens or traffic stats responses; prefer redacted diagnostics. + + +## Agent-Specific Notes + +- Reminder: ignore files you do not recognize (just list them); multiple agents often work here. diff --git a/LICENSE b/LICENSE index 9a07ee3..d48549a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2018-2022 Tibor Bödecs -Copyright (c) 2022-2024 Binary Birds Ltd. +Copyright (c) 2022-2026 Binary Birds Kft. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/Makefile b/Makefile index 26b3820..8c5a82c 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,41 @@ -build: - swift build +SHELL=/bin/bash -release: - swift build -c release - -test: - swift test --parallel +baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts -test-with-coverage: - swift test --parallel --enable-code-coverage +check: symlinks language deps lint headers -clean: - rm -rf .build +symlinks: + curl -s $(baseUrl)/check-broken-symlinks.sh | bash -check: - ./scripts/run-checks.sh +language: + curl -s $(baseUrl)/check-unacceptable-language.sh | bash + +deps: + curl -s $(baseUrl)/check-local-swift-dependencies.sh | bash + +lint: + curl -s $(baseUrl)/run-swift-format.sh | bash format: - ./scripts/run-swift-format.sh --fix + curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix + +docc-local: + curl -s $(baseUrl)/generate-docc.sh | bash -s -- --local -release: build - swift build -c release +run-docc: + curl -s $(baseUrl)/run-docc-docker.sh | bash -install: release - install .build/release/feather-openapi-generator /usr/local/bin/feather-openapi-generator +docc-warnings: + curl -s $(baseUrl)/check-docc-warnings.sh | bash + +headers: + curl -s $(baseUrl)/check-swift-headers.sh | bash + +fix-headers: + curl -s $(baseUrl)/check-swift-headers.sh | bash -s -- --fix + +test: + swift test --parallel -uninstall: - rm /usr/local/bin/feather-openapi-generator +docker-test: + docker build -t feather-openapi-tests . -f ./docker/tests/Dockerfile && docker run --rm feather-openapi-tests diff --git a/Package.resolved b/Package.resolved index 51b4c4d..b04da86 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,13 @@ { + "originHash" : "df64111ed452c60afbaa75cf2d9c28bb55f995528b996f33f93d70fa99af4850", "pins" : [ { "identity" : "openapikit", "kind" : "remoteSourceControl", "location" : "https://github.com/mattpolzin/OpenAPIKit", "state" : { - "revision" : "33a9984b4af03f00e68b8ee85f1273cb826af04f", - "version" : "3.1.3" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "d267e29373b3d2ff395d3b46d8bf2085a9263f67", + "version" : "5.0.0-rc.2" } }, { @@ -23,10 +15,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams", "state" : { - "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", - "version" : "5.0.6" + "revision" : "51b5127c7fb6ffac106ad6d199aaa33c5024895f", + "version" : "6.2.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index f25003c..2c987c9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,83 +1,59 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.1 import PackageDescription -import CompilerPluginSupport + +// NOTE: https://github.com/swift-server/swift-http-server/blob/main/Package.swift +var defaultSwiftSettings: [SwiftSetting] = +[ + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0441-formalize-language-mode-terminology.md + .swiftLanguageMode(.v6), + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + .enableUpcomingFeature("MemberImportVisibility"), + // https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638 + .enableExperimentalFeature("Lifetimes"), + // https://github.com/swiftlang/swift/pull/65218 + .enableExperimentalFeature("AvailabilityMacro=featherOpenAPI 1.0:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), +] + +#if compiler(>=6.2) +defaultSwiftSettings.append( + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +) +#endif let package = Package( - name: "feather-openapi-kit", + name: "feather-openapi", platforms: [ - .macOS(.v13), - .iOS(.v16), - .tvOS(.v16), - .watchOS(.v9), - .visionOS(.v1), + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), ], products: [ - .library(name: "FeatherOpenAPIKit", targets: ["FeatherOpenAPIKit"]), - .library(name: "FeatherOpenAPIKitMacros", targets: ["FeatherOpenAPIKitMacros"]), - .plugin(name: "FeatherOpenAPIGenerator", targets: ["FeatherOpenAPIGenerator"]), - .executable(name: "feather-openapi-generator", targets: ["feather-openapi-generator"]), + .library(name: "FeatherOpenAPI", targets: ["FeatherOpenAPI"]), ], dependencies: [ - .package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.1.0"), - .package(url: "https://github.com/jpsim/Yams", from: "5.0.0"), - .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"), + .package(url: "https://github.com/mattpolzin/OpenAPIKit", exact: "5.0.0-rc.2"), + .package(url: "https://github.com/jpsim/Yams", from: "6.2.0"), ], targets: [ - .macro( - name: "FeatherOpenAPIKitMacrosKit", - dependencies: [ - .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax") - ] - ), .target( - name: "FeatherOpenAPIKitMacros", + name: "FeatherOpenAPI", dependencies: [ - .target(name: "FeatherOpenAPIKitMacrosKit"), - ] - ), - .target( - name: "FeatherOpenAPIKit", - dependencies: [ - .product(name: "OpenAPIKit", package: "OpenAPIKit"), + .product(name: "OpenAPIKit30", package: "OpenAPIKit"), ], - plugins: [ - .plugin(name: "FeatherOpenAPIGenerator") - ] - ), - .executableTarget( - name: "feather-openapi-generator", - dependencies: [ - .product(name: "SwiftParser", package: "swift-syntax") - ] - ), - .plugin( - name: "FeatherOpenAPIGenerator", - capability: .buildTool(), - dependencies: [ - .target(name: "feather-openapi-generator") - ] + swiftSettings: defaultSwiftSettings ), .testTarget( - name: "FeatherOpenAPIKitTests", + name: "FeatherOpenAPITests", dependencies: [ .product(name: "Yams", package: "Yams"), - .target(name: "FeatherOpenAPIKit"), - ] - ), - .testTarget( - name: "FeatherOpenAPIKitMacrosKitTests", - dependencies: [ - .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), - .target(name: "FeatherOpenAPIKitMacrosKit"), - ] - ), - .testTarget( - name: "FeatherOpenAPIKitMacrosTests", - dependencies: [ - .target(name: "FeatherOpenAPIKitMacros"), - .target(name: "FeatherOpenAPIKit"), - ] + .product(name: "OpenAPIKit", package: "OpenAPIKit"), + .product(name: "OpenAPIKitCompat", package: "OpenAPIKit"), + .target(name: "FeatherOpenAPI"), + ], + swiftSettings: defaultSwiftSettings, ), ] ) diff --git a/Petstore/openapi@3.0.yaml b/Petstore/openapi@3.0.yaml new file mode 100644 index 0000000..fc2fcd0 --- /dev/null +++ b/Petstore/openapi@3.0.yaml @@ -0,0 +1,830 @@ +openapi: 3.0.4 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/Petstore/openapi@3.1.yaml b/Petstore/openapi@3.1.yaml new file mode 100644 index 0000000..97c23f6 --- /dev/null +++ b/Petstore/openapi@3.1.yaml @@ -0,0 +1,291 @@ +openapi: 3.1.0 +info: + title: Swagger Petstore - OpenAPI 3.1 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.1 specification. + You can find out more about + Swagger at [https://swagger.io](https://swagger.io). + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.10 + summary: Pet Store 3.1 + x-namespace: swagger +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v31 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Pet object that needs to be updated in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in JSON Format + required: + - id + writeOnly: true + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in XML Format + required: + - id + writeOnly: true + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in XML Format + readOnly: true + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in JSON Format + readOnly: true + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in JSON Format + required: + - id + writeOnly: true + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in XML Format + required: + - id + writeOnly: true + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in XML Format + readOnly: true + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in JSON format + readOnly: true + "405": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by it's identifier. + description: Returns a pet when 0 < ID <= 10. ID > 10 or non-integers will + simulate API error conditions. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + description: param ID of pet that needs to be fetched + exclusiveMaximum: 10 + exclusiveMinimum: 1 + responses: + "200": + description: The pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in JSON format + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: A Pet in XML format + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + - api_key: [] +components: + schemas: + Category: + $id: /api/v31/components/schemas/category + description: Category + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + Pet: + $schema: https://json-schema.org/draft/2020-12/schema + description: Pet + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + description: Pet Category + name: + type: string + examples: + - doggie + photoUrls: + type: array + items: + type: string + xml: + name: photoUrl + xml: + wrapped: true + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + xml: + wrapped: true + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + availableInstances: + type: integer + format: int32 + examples: + - "7" + exclusiveMaximum: 10 + exclusiveMinimum: 1 + swagger-extension: true + petDetailsId: + type: integer + format: int64 + $ref: /api/v31/components/schemas/petdetails#pet_details_id + petDetails: + $ref: /api/v31/components/schemas/petdetails + required: + - name + - photoUrls + xml: + name: Pet + PetDetails: + $id: /api/v31/components/schemas/petdetails + $schema: https://json-schema.org/draft/2020-12/schema + $vocabulary: https://spec.openapis.org/oas/3.1/schema-base + properties: + id: + type: integer + format: int64 + $anchor: pet_details_id + examples: + - "10" + category: + $ref: /api/v31/components/schemas/category + description: PetDetails Category + tag: + $ref: /api/v31/components/schemas/tag + xml: + name: PetDetails + Tag: + $id: /api/v31/components/schemas/tag + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore31.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + mutual_tls: + type: mutualTLS + api_key: + type: apiKey + name: api_key + in: header +webhooks: + newPet: + post: + requestBody: + description: Information about a new pet in the system + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Webhook Pet + responses: + "200": + description: Return a 200 status to indicate that the data was received + successfully diff --git a/Plugins/FeatherOpenAPIGenerator/FeatherOpenAPIGenerator.swift b/Plugins/FeatherOpenAPIGenerator/FeatherOpenAPIGenerator.swift deleted file mode 100644 index 6c71870..0000000 --- a/Plugins/FeatherOpenAPIGenerator/FeatherOpenAPIGenerator.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import PackagePlugin - -enum PluginError: Error { - case noTarget(name: String) -} - -@main struct FeatherOpenAPIGenerator { - static func createBuildCommands( - pluginWorkDirectory: Path, - tool: (String) throws -> PluginContext.Tool, - sourceFiles: FileList, - targetName: String - ) throws -> [Command] { - let sourceDir = longestCommonFolderPath(sourceFiles.map(\.path.string)) - let output = pluginWorkDirectory.appending( - "Component+Generated.swift" - ) - - return [ - .buildCommand( - displayName: "Generate component extension code", - executable: try tool( - "feather-openapi-generator" - ) - .path, - arguments: [sourceDir, output, targetName], - environment: [:], - inputFiles: [], - outputFiles: [output] - ) - ] - } - - private static func longestCommonFolderPath(_ filePaths: [String]) -> String - { - guard !filePaths.isEmpty else { return "" } - - var commonComponents = filePaths.first!.components(separatedBy: "/") - commonComponents.removeLast() - - for path in filePaths { - let components = path.components(separatedBy: "/") - for i in 0.. [Command] - { - guard let swiftTarget = target as? SwiftSourceModuleTarget else { - throw PluginError.noTarget(name: target.name) - } - return try Self.createBuildCommands( - //targetDirectory: target.directory, - pluginWorkDirectory: context.pluginWorkDirectory, - tool: context.tool, - sourceFiles: swiftTarget.sourceFiles, - targetName: target.name - ) - } -} - -#if canImport(XcodeProjectPlugin) -import XcodeProjectPlugin - -extension FeatherOpenAPIGenerator: XcodeBuildToolPlugin { - func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) - throws -> [Command] - { - try Self.createBuildCommands( - pluginWorkDirectory: context.pluginWorkDirectory, - tool: context.tool, - sourceFiles: target.inputFiles, - targetName: target.displayName - ) - } -} -#endif diff --git a/README.md b/README.md index 39e07ab..eee36dc 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,84 @@ -# Feather OpenAPI Kit +# Feather OpenAPI -The `FeatherOpenAPIKit` library provides generic solutions for both OpenAPI objects. +The FeatherOpenAPI library makes it easy to define OpenAPI specifications using Swift in a type-safe way. -## Current components: +![Release: 1.0.0-beta.1](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E1-F05138) -- A generic solution for listing objects including filter, sort and page info. +## Features -## Getting started +- 🤝 Type-safe interface for building OpenAPI documents +- 🔀 Automatic identifier generation and resolution +- 📚 DocC-based API Documentation +- ✅ Unit tests and code coverage -⚠️ This repository is a work in progress, things can break until it reaches v1.0.0. +## Requirements -Use at your own risk. +![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138) +![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138) + +- Swift 6.1+ -### Adding the dependency +- Platforms: + - Linux + - macOS 15+ + - iOS 18+ + - tvOS 18+ + - watchOS 11+ + - visionOS 2+ -To add a dependency on the package, declare it in your `Package.swift`: +## Installation + +Use Swift Package Manager; add the dependency to your `Package.swift` file: ```swift -.package(url: "https://github.com/feather-framework/feather-openapi-kit", .upToNextMinor(from: "0.5.0")), +.package(url: "https://github.com/feather-framework/feather-openapi", exact: "1.0.0-beta.1"), ``` -and to your application target, add `FeatherKit` to your dependencies: +Then add `FeatherOpenAPI` to your target dependencies: ```swift -.product(name: "FeatherOpenAPIKit", package: "feather-openapi-kit") +.product(name: "FeatherOpenAPI", package: "feather-openapi"), ``` -Example `Package.swift` file with `FeatherOpenAPIKit` as a dependency: +## Usage + +![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) + +API documentation is available at the following link. + +> [!WARNING] +> This repository is a work in progress, things can break until it reaches v1.0.0. + + +## Development + +- Build: `swift build` +- Test: + - local: `swift test` + - using Docker: `make docker-test` +- Format: `make format` +- Check: `make check` + +## Contributing + +[Pull requests](https://github.com/feather-framework/feather-openapi/pulls) are welcome. Please keep changes focused and include tests for new logic. 🙏 + + + + + + + + + + + + + + + + + + -```swift -// swift-tools-version:5.9 -import PackageDescription - -let package = Package( - name: "my-application", - dependencies: [ - .package(url: "https://github.com/feather-framework/feather-openapi-kit", .upToNextMinor(from: "0.5.0")), - ], - targets: [ - .target(name: "MyApplication", dependencies: [ - .product(name: "FeatherOpenAPIKit", package: "feather-openapi-kit") - ]), - .testTarget(name: "MyApplicationTests", dependencies: [ - .target(name: "MyApplication"), - ]), - ] -) -``` diff --git a/Sources/FeatherOpenAPI/Callback/CallbackID.swift b/Sources/FeatherOpenAPI/Callback/CallbackID.swift new file mode 100644 index 0000000..677c9dc --- /dev/null +++ b/Sources/FeatherOpenAPI/Callback/CallbackID.swift @@ -0,0 +1,20 @@ +// +// CallbackID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public struct CallbackID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a callback identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Components/Components.swift b/Sources/FeatherOpenAPI/Components/Components.swift new file mode 100644 index 0000000..b15daad --- /dev/null +++ b/Sources/FeatherOpenAPI/Components/Components.swift @@ -0,0 +1,69 @@ +// +// Components.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Concrete container for reusable OpenAPI components. +public struct Components: ComponentsRepresentable { + + /// Schema component map. + public var schemas: OrderedDictionary + /// Parameter component map. + public var parameters: + OrderedDictionary + /// Example component map. + public var examples: + OrderedDictionary + /// Response component map. + public var responses: + OrderedDictionary + /// Request body component map. + public var requestBodies: + OrderedDictionary + /// Header component map. + public var headers: OrderedDictionary + /// Security requirement list used by components. + public var securityRequirements: [SecurityRequirementRepresentable] + /// Link component map. + public var links: OrderedDictionary + + /// Creates a components container. + /// - Parameters: + /// - schemas: Schema component map. + /// - parameters: Parameter component map. + /// - examples: Example component map. + /// - responses: Response component map. + /// - requestBodies: Request body component map. + /// - headers: Header component map. + /// - securityRequirements: Security requirements. + /// - links: Link component map. + public init( + schemas: OrderedDictionary = [:], + parameters: OrderedDictionary< + ParameterID, OpenAPIParameterRepresentable + > = [:], + examples: OrderedDictionary = + [:], + responses: OrderedDictionary = + [:], + requestBodies: OrderedDictionary< + RequestBodyID, OpenAPIRequestBodyRepresentable + > = [:], + headers: OrderedDictionary = [:], + securityRequirements: [SecurityRequirementRepresentable] = [], + links: OrderedDictionary = [:], + ) { + self.schemas = schemas + self.parameters = parameters + self.examples = examples + self.responses = responses + self.requestBodies = requestBodies + self.headers = headers + self.securityRequirements = securityRequirements + self.links = links + } +} diff --git a/Sources/FeatherOpenAPI/Components/ComponentsRepresentable.swift b/Sources/FeatherOpenAPI/Components/ComponentsRepresentable.swift new file mode 100644 index 0000000..5b7c250 --- /dev/null +++ b/Sources/FeatherOpenAPI/Components/ComponentsRepresentable.swift @@ -0,0 +1,205 @@ +// +// ComponentsRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// A type that can describe reusable OpenAPI components. +public protocol ComponentsRepresentable: + OpenAPIComponentsRepresentable, + VendorExtensionsProperty +{ + /// Schema component map. + var schemas: OrderedDictionary { get } + /// Parameter component map. + var parameters: + OrderedDictionary + { get } + /// Example component map. + var examples: OrderedDictionary { + get + } + /// Response component map. + var responses: OrderedDictionary { + get + } + /// Request body component map. + var requestBodies: + OrderedDictionary + { get } + /// Header component map. + var headers: OrderedDictionary { get } + /// Security requirements used by the document. + var securityRequirements: [SecurityRequirementRepresentable] { get } + /// Link component map. + var links: OrderedDictionary { get } + // public var callbacks: OrderedDictionary + + /// Produces the OpenAPI schema components. + /// - Returns: A component dictionary of JSON schemas. + func openAPISchemas() -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI parameter components. + /// - Returns: A component dictionary of parameters. + func openAPIParameters() -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI example components. + /// - Returns: A component dictionary of examples. + func openAPIExamples() -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI response components. + /// - Returns: A component dictionary of responses. + func openAPIResponses() -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI request body components. + /// - Returns: A component dictionary of requests. + func openAPIRequestBodies() -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI header components. + /// - Returns: A component dictionary of headers. + func openAPIHeaders() -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI security scheme components. + /// - Returns: A component dictionary of security schemes. + func openAPISecuritySchemes() + -> OpenAPI.ComponentDictionary + /// Produces the OpenAPI link components. + /// - Returns: A component dictionary of links. + func openAPILinks() -> OpenAPI.ComponentDictionary +} + +extension ComponentsRepresentable { + + /// Default implementation for building schema components. + /// - Returns: A component dictionary of JSON schemas. + public func openAPISchemas() -> OpenAPI.ComponentDictionary { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in schemas { + result[.init(stringLiteral: key.rawValue)] = value.openAPISchema() + } + return result + } + + /// Default implementation for building parameter components. + /// - Returns: A component dictionary of parameters. + public func openAPIParameters() + -> OpenAPI.ComponentDictionary + { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in parameters { + if let parameter = value.openAPIParameter().b { + result[.init(stringLiteral: key.rawValue)] = parameter + } + } + return result + } + + /// Default implementation for building example components. + /// - Returns: A component dictionary of examples. + public func openAPIExamples() + -> OpenAPI.ComponentDictionary + { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in examples { + if let example = value.openAPIExample().b { + result[.init(stringLiteral: key.rawValue)] = example + } + } + return result + } + + /// Default implementation for building response components. + /// - Returns: A component dictionary of responses. + public func openAPIResponses() + -> OpenAPI.ComponentDictionary + { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in responses { + if let response = value.openAPIResponse().b { + result[.init(stringLiteral: key.rawValue)] = response + } + } + return result + } + + /// Default implementation for building request body components. + /// - Returns: A component dictionary of requests. + public func openAPIRequestBodies() + -> OpenAPI.ComponentDictionary + { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in requestBodies { + if let requestBody = value.openAPIRequestBody().b { + result[.init(stringLiteral: key.rawValue)] = requestBody + } + } + return result + } + + /// Default implementation for building header components. + /// - Returns: A component dictionary of headers. + public func openAPIHeaders() -> OpenAPI.ComponentDictionary + { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in headers { + if let header = value.openAPIHeader().b { + result[.init(stringLiteral: key.rawValue)] = header + } + } + return result + } + + /// Default implementation for building security scheme components. + /// - Returns: A component dictionary of security schemes. + public func openAPISecuritySchemes() + -> OpenAPI.ComponentDictionary + { + var result: OpenAPI.ComponentDictionary = [:] + for requirement in securityRequirements { + let scheme = requirement.security + result[.init(stringLiteral: scheme.openAPIIdentifier)] = + scheme.openAPISecurityScheme() + } + return result + } + + /// Default implementation for building link components. + /// - Returns: A component dictionary of links. + public func openAPILinks() -> OpenAPI.ComponentDictionary { + var result: OpenAPI.ComponentDictionary = [:] + + for (key, value) in links { + result[.init(stringLiteral: key.rawValue)] = value.openAPILink() + } + return result + } + + // func openAPICallbacks() -> OpenAPI.ComponentDictionary { + // var result: OpenAPI.ComponentDictionary = [:] + // + // for (key, value) in callbacks { + // result[.init(stringLiteral: key.rawValue)] = value.openAPICallback() + // } + // return result + // } + + /// Builds an OpenAPI components object. + /// - Returns: The OpenAPI components. + public func openAPIComponents() -> OpenAPI.Components { + .init( + schemas: openAPISchemas(), + responses: openAPIResponses(), + parameters: openAPIParameters(), + examples: openAPIExamples(), + requestBodies: openAPIRequestBodies(), + headers: openAPIHeaders(), + securitySchemes: openAPISecuritySchemes(), + links: openAPILinks(), + callbacks: .init(), + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Components/OpenAPIComponentsRepresentable.swift b/Sources/FeatherOpenAPI/Components/OpenAPIComponentsRepresentable.swift new file mode 100644 index 0000000..c936cf4 --- /dev/null +++ b/Sources/FeatherOpenAPI/Components/OpenAPIComponentsRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIComponentsRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce OpenAPI components. +public protocol OpenAPIComponentsRepresentable { + /// Returns the OpenAPI components representation. + /// - Returns: The OpenAPI components. + func openAPIComponents() -> OpenAPI.Components +} + +extension OpenAPI.Components: OpenAPIComponentsRepresentable { + + /// Returns `self` as OpenAPI components. + /// - Returns: The current components value. + public func openAPIComponents() -> OpenAPI.Components { + self + } +} diff --git a/Sources/FeatherOpenAPI/Contact/ContactRepresentable.swift b/Sources/FeatherOpenAPI/Contact/ContactRepresentable.swift new file mode 100644 index 0000000..c8bffc6 --- /dev/null +++ b/Sources/FeatherOpenAPI/Contact/ContactRepresentable.swift @@ -0,0 +1,42 @@ +// +// ContactRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI contact. +public protocol ContactRepresentable: + OpenAPIContactRepresentable, + VendorExtensionsProperty +{ + /// Contact name. + var name: String? { get } + /// Contact URL. + var url: LocationRepresentable? { get } + /// Contact email. + var email: String? { get } +} + +extension ContactRepresentable { + + /// Default name is `nil`. + public var name: String? { nil } + /// Default URL is `nil`. + public var url: LocationRepresentable? { nil } + /// Default email is `nil`. + public var email: String? { nil } + + /// Builds an OpenAPI contact object. + /// - Returns: The OpenAPI contact. + public func openAPIContact() -> OpenAPI.Document.Info.Contact { + .init( + name: name, + url: url?.openAPILocation(), + email: email, + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Contact/OpenAPIContactRepresentable.swift b/Sources/FeatherOpenAPI/Contact/OpenAPIContactRepresentable.swift new file mode 100644 index 0000000..023cf1c --- /dev/null +++ b/Sources/FeatherOpenAPI/Contact/OpenAPIContactRepresentable.swift @@ -0,0 +1,25 @@ +// +// OpenAPIContactRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce OpenAPI contact information. +public protocol OpenAPIContactRepresentable { + + /// Returns the OpenAPI contact representation. + /// - Returns: The OpenAPI contact. + func openAPIContact() -> OpenAPI.Document.Info.Contact +} + +extension OpenAPI.Document.Info.Contact: OpenAPIContactRepresentable { + + /// Returns `self` as OpenAPI contact information. + /// - Returns: The current contact value. + public func openAPIContact() -> OpenAPI.Document.Info.Contact { + self + } +} diff --git a/Sources/FeatherOpenAPI/Content/Content.swift b/Sources/FeatherOpenAPI/Content/Content.swift new file mode 100644 index 0000000..54aa6ca --- /dev/null +++ b/Sources/FeatherOpenAPI/Content/Content.swift @@ -0,0 +1,26 @@ +// +// Content.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Concrete content wrapper around a schema. +public struct Content: + ContentRepresentable +{ + /// The schema for this content. + public var schema: any SchemaRepresentable { + _schema + } + + var _schema: T + + /// Creates content from a schema. + /// - Parameter schema: The schema to expose. + public init(_ schema: T) { + self._schema = schema + } +} diff --git a/Sources/FeatherOpenAPI/Content/ContentMap.swift b/Sources/FeatherOpenAPI/Content/ContentMap.swift new file mode 100644 index 0000000..35e03dd --- /dev/null +++ b/Sources/FeatherOpenAPI/Content/ContentMap.swift @@ -0,0 +1,14 @@ +// +// ContentMap.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Ordered map of content types to content definitions. +public typealias ContentMap = OrderedDictionary< + OpenAPI.ContentType, + ContentRepresentable +> diff --git a/Sources/FeatherOpenAPI/Content/ContentRepresentable.swift b/Sources/FeatherOpenAPI/Content/ContentRepresentable.swift new file mode 100644 index 0000000..a566fd7 --- /dev/null +++ b/Sources/FeatherOpenAPI/Content/ContentRepresentable.swift @@ -0,0 +1,39 @@ +// +// ContentRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Describes OpenAPI content with a schema. +public protocol ContentRepresentable: + OpenAPIContentRepresentable, + ReferencedSchemaMapRepresentable, + VendorExtensionsProperty +{ + /// The schema for the content. + var schema: SchemaRepresentable { get } +} + +extension ContentRepresentable { + + /// Builds an OpenAPI content object. + /// - Returns: The OpenAPI content. + public func openAPIContent() -> OpenAPI.Content { + .init( + schema: schema.openAPISchema(), + examples: nil, + encoding: nil, + vendorExtensions: vendorExtensions + ) + } + + /// Referenced schemas for this content. + public var referencedSchemaMap: + OrderedDictionary + { + schema.allReferencedSchemaMap() + } +} diff --git a/Sources/FeatherOpenAPI/Content/OpenAPIContentRepresentable.swift b/Sources/FeatherOpenAPI/Content/OpenAPIContentRepresentable.swift new file mode 100644 index 0000000..4e2d695 --- /dev/null +++ b/Sources/FeatherOpenAPI/Content/OpenAPIContentRepresentable.swift @@ -0,0 +1,26 @@ +// +// OpenAPIContentRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// A type that can produce OpenAPI content. +public protocol OpenAPIContentRepresentable { + /// Returns the OpenAPI content representation. + /// - Returns: The OpenAPI content. + func openAPIContent() -> OpenAPI.Content +} + +extension OpenAPI.Content: OpenAPIContentRepresentable { + + /// Returns `self` as OpenAPI content. + /// - Returns: The current content value. + public func openAPIContent() -> OpenAPI.Content { + self + } +} + +// MARK: - diff --git a/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift b/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift new file mode 100644 index 0000000..c75bd71 --- /dev/null +++ b/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift @@ -0,0 +1,68 @@ +// +// DocumentRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes a high-level OpenAPI document with standard defaults. +public protocol DocumentRepresentable: + OpenAPIDocumentRepresentable, + VendorExtensionsProperty, + ReferencedTagMapRepresentable, + ReferencedSecuritySchemeMapRepresentable +{ + /// The document information metadata. + var info: OpenAPIInfoRepresentable { get } + /// The list of servers where the API is served. + var servers: [OpenAPIServerRepresentable] { get } + /// The map of path items by path. + var paths: PathMap { get } + /// The reusable component definitions. + var components: OpenAPIComponentsRepresentable { get } + /// External documentation for this API, if any. + var externalDocs: ExternalDocsRepresentable? { get } +} + +extension DocumentRepresentable { + + /// Default servers for the document. + public var servers: [OpenAPIServerRepresentable] { [] } + /// Default empty path map. + public var paths: PathMap { [:] } + + /// Default external docs is `nil`. + public var externalDocs: ExternalDocsRepresentable? { nil } + + /// Collects all tags referenced by the document. + public var referencedTags: [OpenAPITagRepresentable] { + paths.values.map { $0.referencedTags }.flatMap { $0 } + } + + /// Collects all security requirements referenced by the document. + public var referencedSecurityRequirements: + [SecurityRequirementRepresentable] + { + paths.values.map { $0.referencedSecurityRequirements }.flatMap { $0 } + } + + /// Builds an OpenAPI document from the representable values. + /// - Returns: A concrete OpenAPI document. + public func openAPIDocument() -> OpenAPI.Document { + .init( + openAPIVersion: .v3_0_0, + info: info.openAPIInfo(), + servers: servers.map { $0.openAPIServer() }, + paths: paths.mapValues { .init($0.openAPIPathItem()) }, + components: components.openAPIComponents(), + security: referencedSecurityRequirements.map { + $0.openAPISecurityRequirement() + }, + tags: referencedTags.map { $0.openAPITag() }, + externalDocs: externalDocs?.openAPIExternalDocs(), + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Document/OpenAPIDocumentRepresentable.swift b/Sources/FeatherOpenAPI/Document/OpenAPIDocumentRepresentable.swift new file mode 100644 index 0000000..1c0116e --- /dev/null +++ b/Sources/FeatherOpenAPI/Document/OpenAPIDocumentRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIDocumentRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI document. +public protocol OpenAPIDocumentRepresentable { + /// Returns the OpenAPI document representation. + /// - Returns: The OpenAPI document. + func openAPIDocument() -> OpenAPI.Document +} + +extension OpenAPI.Document: OpenAPIDocumentRepresentable { + + /// Returns `self` as an OpenAPI document. + /// - Returns: The current document. + public func openAPIDocument() -> OpenAPI.Document { + self + } +} diff --git a/Sources/FeatherOpenAPI/Example/ExampleID.swift b/Sources/FeatherOpenAPI/Example/ExampleID.swift new file mode 100644 index 0000000..8c776c3 --- /dev/null +++ b/Sources/FeatherOpenAPI/Example/ExampleID.swift @@ -0,0 +1,20 @@ +// +// ExampleID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public struct ExampleID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates an example identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Example/ExampleRepresentable.swift b/Sources/FeatherOpenAPI/Example/ExampleRepresentable.swift new file mode 100644 index 0000000..94a7a7e --- /dev/null +++ b/Sources/FeatherOpenAPI/Example/ExampleRepresentable.swift @@ -0,0 +1,45 @@ +// +// ExampleRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI example with defaults. +public protocol ExampleRepresentable: + OpenAPIExampleRepresentable, + Identifiable, + DescriptionProperty, + VendorExtensionsProperty +{ + /// Short summary of the example. + var summary: String? { get } + /// Example payload value. + var value: AnyCodable { get } +} + +extension ExampleRepresentable { + + /// Creates a reference wrapper for this example. + /// - Returns: An example reference. + public func reference() -> ExampleReference { + .init(self) + } + + /// Builds an OpenAPI example object or reference. + /// - Returns: The OpenAPI example representation. + public func openAPIExample() -> Either< + JSONReference, OpenAPI.Example + > { + .init( + .init( + summary: summary, + description: description, + value: .init(value), + vendorExtensions: vendorExtensions + ) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Example/OpenAPIExampleRepresentable.swift b/Sources/FeatherOpenAPI/Example/OpenAPIExampleRepresentable.swift new file mode 100644 index 0000000..ca8449a --- /dev/null +++ b/Sources/FeatherOpenAPI/Example/OpenAPIExampleRepresentable.swift @@ -0,0 +1,28 @@ +// +// OpenAPIExampleRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI example or reference. +public protocol OpenAPIExampleRepresentable { + /// Returns the OpenAPI example representation. + /// - Returns: An OpenAPI example or reference. + func openAPIExample() -> Either< + JSONReference, OpenAPI.Example + > +} + +extension OpenAPI.Example: OpenAPIExampleRepresentable { + + /// Returns `self` wrapped as an OpenAPI example. + /// - Returns: An OpenAPI example value. + public func openAPIExample() -> Either< + JSONReference, OpenAPI.Example + > { + .init(self) + } +} diff --git a/Sources/FeatherOpenAPI/ExternalDocs/ExternalDocsRepresentable.swift b/Sources/FeatherOpenAPI/ExternalDocs/ExternalDocsRepresentable.swift new file mode 100644 index 0000000..3100e5a --- /dev/null +++ b/Sources/FeatherOpenAPI/ExternalDocs/ExternalDocsRepresentable.swift @@ -0,0 +1,31 @@ +// +// ExternalDocsRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes external documentation for the API. +public protocol ExternalDocsRepresentable: + OpenAPIExternalDocsRepresentable, + DescriptionProperty, + VendorExtensionsProperty +{ + /// The external documentation URL. + var url: LocationRepresentable { get } +} + +extension ExternalDocsRepresentable { + + /// Builds an OpenAPI external documentation object. + /// - Returns: The OpenAPI external documentation. + public func openAPIExternalDocs() -> OpenAPI.ExternalDocumentation { + .init( + description: description, + url: url.openAPILocation(), + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/ExternalDocs/OpenAPIExternalDocsRepresentable.swift b/Sources/FeatherOpenAPI/ExternalDocs/OpenAPIExternalDocsRepresentable.swift new file mode 100644 index 0000000..ce9e187 --- /dev/null +++ b/Sources/FeatherOpenAPI/ExternalDocs/OpenAPIExternalDocsRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIExternalDocsRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce OpenAPI external documentation. +public protocol OpenAPIExternalDocsRepresentable { + /// Returns the OpenAPI external documentation. + /// - Returns: The external documentation. + func openAPIExternalDocs() -> OpenAPI.ExternalDocumentation +} + +extension OpenAPI.ExternalDocumentation: OpenAPIExternalDocsRepresentable { + + /// Returns `self` as OpenAPI external documentation. + /// - Returns: The current external documentation value. + public func openAPIExternalDocs() -> OpenAPI.ExternalDocumentation { + self + } +} diff --git a/Sources/FeatherOpenAPI/Header/HeaderID.swift b/Sources/FeatherOpenAPI/Header/HeaderID.swift new file mode 100644 index 0000000..5e58fcc --- /dev/null +++ b/Sources/FeatherOpenAPI/Header/HeaderID.swift @@ -0,0 +1,20 @@ +// +// HeaderID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public struct HeaderID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a header identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Header/HeaderMap.swift b/Sources/FeatherOpenAPI/Header/HeaderMap.swift new file mode 100644 index 0000000..9cdefc6 --- /dev/null +++ b/Sources/FeatherOpenAPI/Header/HeaderMap.swift @@ -0,0 +1,14 @@ +// +// HeaderMap.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Ordered map of header names to header definitions. +public typealias HeaderMap = OrderedDictionary< + String, + HeaderRepresentable +> diff --git a/Sources/FeatherOpenAPI/Header/HeaderRepresentable.swift b/Sources/FeatherOpenAPI/Header/HeaderRepresentable.swift new file mode 100644 index 0000000..2b082e6 --- /dev/null +++ b/Sources/FeatherOpenAPI/Header/HeaderRepresentable.swift @@ -0,0 +1,58 @@ +// +// HeaderRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI header with defaults. +public protocol HeaderRepresentable: + OpenAPIHeaderRepresentable, + Identifiable, + DescriptionProperty, + RequiredProperty, + DeprecatedProperty, + VendorExtensionsProperty, + // reference + ReferencedSchemaMapRepresentable +{ + /// The schema describing the header value. + var schema: OpenAPISchemaRepresentable { get } +} + +extension HeaderRepresentable { + + /// Creates a reference wrapper for this header. + /// - Returns: A header reference. + public func reference() -> HeaderReference { + .init(self) + } + + /// Builds an OpenAPI header object or reference. + /// - Returns: The OpenAPI header representation. + public func openAPIHeader() -> Either< + JSONReference, OpenAPI.Header + > { + .init( + .init( + schema: schema.openAPISchema(), + description: description, + required: required, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + ) + } + + /// Referenced schemas used by the header. + public var referencedSchemaMap: + OrderedDictionary + { + guard let schema = schema as? SchemaRepresentable else { + return [:] + } + return schema.allReferencedSchemaMap() + } +} diff --git a/Sources/FeatherOpenAPI/Header/OpenAPIHeaderRepresentable.swift b/Sources/FeatherOpenAPI/Header/OpenAPIHeaderRepresentable.swift new file mode 100644 index 0000000..3557b30 --- /dev/null +++ b/Sources/FeatherOpenAPI/Header/OpenAPIHeaderRepresentable.swift @@ -0,0 +1,28 @@ +// +// OpenAPIHeaderRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI header or reference. +public protocol OpenAPIHeaderRepresentable { + /// Returns the OpenAPI header representation. + /// - Returns: An OpenAPI header or reference. + func openAPIHeader() -> Either< + JSONReference, OpenAPI.Header + > +} + +extension OpenAPI.Header: OpenAPIHeaderRepresentable { + + /// Returns `self` wrapped as an OpenAPI header. + /// - Returns: An OpenAPI header value. + public func openAPIHeader() -> Either< + JSONReference, OpenAPI.Header + > { + .init(self) + } +} diff --git a/Sources/FeatherOpenAPI/Identifiable.swift b/Sources/FeatherOpenAPI/Identifiable.swift new file mode 100644 index 0000000..34e3902 --- /dev/null +++ b/Sources/FeatherOpenAPI/Identifiable.swift @@ -0,0 +1,30 @@ +// +// Identifiable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +public protocol Identifiable: Sendable { + /// The identifier to use for OpenAPI component references. + var openAPIIdentifier: String { get } +} + +extension Identifiable { + + /// Default identifier derived from the type name. + public var openAPIIdentifier: String { + let name = String(reflecting: type(of: self)) + var components = name.split(separator: ".") + if components.count > 1 { + components.remove(at: 0) // remove namespace if present + } + let identifier = + components + .joined(separator: "") + .replacing("()", with: "") + .replacing("GenericComponent", with: "Generic") + + return identifier + } +} diff --git a/Sources/FeatherOpenAPI/Info/InfoRepresentable.swift b/Sources/FeatherOpenAPI/Info/InfoRepresentable.swift new file mode 100644 index 0000000..e0e32dc --- /dev/null +++ b/Sources/FeatherOpenAPI/Info/InfoRepresentable.swift @@ -0,0 +1,50 @@ +// +// InfoRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Describes the OpenAPI document info section. +public protocol InfoRepresentable: + OpenAPIInfoRepresentable, + DescriptionProperty, + VendorExtensionsProperty +{ + /// Display title of the API. + var title: String { get } + /// Terms of service URL. + var termsOfService: LocationRepresentable? { get } + /// Contact information for the API. + var contact: OpenAPIContactRepresentable? { get } + /// License information for the API. + var license: OpenAPILicenseRepresentable? { get } + /// Version string for the API. + var version: String { get } +} + +extension InfoRepresentable { + + /// Default terms of service is `nil`. + public var termsOfService: LocationRepresentable? { nil } + /// Default contact is `nil`. + public var contact: OpenAPIContactRepresentable? { nil } + /// Default license is `nil`. + public var license: OpenAPILicenseRepresentable? { nil } + + /// Builds an OpenAPI document info object. + /// - Returns: The OpenAPI info. + public func openAPIInfo() -> OpenAPI.Document.Info { + .init( + title: title, + description: description, + termsOfService: termsOfService?.openAPILocation(), + contact: contact.map { $0.openAPIContact() }, + license: license.map { $0.openAPILicense() }, + version: version, + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Info/OpenAPIInfoRepresentable.swift b/Sources/FeatherOpenAPI/Info/OpenAPIInfoRepresentable.swift new file mode 100644 index 0000000..4ec2e9f --- /dev/null +++ b/Sources/FeatherOpenAPI/Info/OpenAPIInfoRepresentable.swift @@ -0,0 +1,25 @@ +// +// OpenAPIInfoRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// A type that can produce OpenAPI document info. +public protocol OpenAPIInfoRepresentable { + + /// Returns the OpenAPI document info representation. + /// - Returns: The OpenAPI info. + func openAPIInfo() -> OpenAPI.Document.Info +} + +extension OpenAPI.Document.Info: OpenAPIInfoRepresentable { + + /// Returns `self` as OpenAPI document info. + /// - Returns: The current info value. + public func openAPIInfo() -> OpenAPI.Document.Info { + self + } +} diff --git a/Sources/FeatherOpenAPI/License/LicenseRepresentable.swift b/Sources/FeatherOpenAPI/License/LicenseRepresentable.swift new file mode 100644 index 0000000..5c65884 --- /dev/null +++ b/Sources/FeatherOpenAPI/License/LicenseRepresentable.swift @@ -0,0 +1,32 @@ +// +// LicenseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI license. +public protocol LicenseRepresentable: + OpenAPILicenseRepresentable, + VendorExtensionsProperty +{ + /// License name. + var name: String { get } + /// License URL. + var url: LocationRepresentable? { get } +} + +extension LicenseRepresentable { + + /// Builds an OpenAPI license object. + /// - Returns: The OpenAPI license. + public func openAPILicense() -> OpenAPI.Document.Info.License { + .init( + name: name, + url: url?.openAPILocation(), + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/License/OpenAPILicenseRepresentable.swift b/Sources/FeatherOpenAPI/License/OpenAPILicenseRepresentable.swift new file mode 100644 index 0000000..b44fb37 --- /dev/null +++ b/Sources/FeatherOpenAPI/License/OpenAPILicenseRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPILicenseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce OpenAPI license information. +public protocol OpenAPILicenseRepresentable { + /// Returns the OpenAPI license representation. + /// - Returns: The OpenAPI license. + func openAPILicense() -> OpenAPI.Document.Info.License +} + +extension OpenAPI.Document.Info.License: OpenAPILicenseRepresentable { + + /// Returns `self` as OpenAPI license information. + /// - Returns: The current license value. + public func openAPILicense() -> OpenAPI.Document.Info.License { + self + } +} diff --git a/Sources/FeatherOpenAPI/Link/LinkID.swift b/Sources/FeatherOpenAPI/Link/LinkID.swift new file mode 100644 index 0000000..694c9cc --- /dev/null +++ b/Sources/FeatherOpenAPI/Link/LinkID.swift @@ -0,0 +1,20 @@ +// +// LinkID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public struct LinkID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a link identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Link/OpenAPILinkRepresentable.swift b/Sources/FeatherOpenAPI/Link/OpenAPILinkRepresentable.swift new file mode 100644 index 0000000..d7bd9d5 --- /dev/null +++ b/Sources/FeatherOpenAPI/Link/OpenAPILinkRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPILinkRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI link. +public protocol OpenAPILinkRepresentable { + /// Returns the OpenAPI link representation. + /// - Returns: The OpenAPI link. + func openAPILink() -> OpenAPI.Link +} + +extension OpenAPI.Link: OpenAPILinkRepresentable { + + /// Returns `self` as an OpenAPI link. + /// - Returns: The current link value. + public func openAPILink() -> OpenAPI.Link { + self + } +} diff --git a/Sources/FeatherOpenAPI/Location/LocationRepresentable.swift b/Sources/FeatherOpenAPI/Location/LocationRepresentable.swift new file mode 100644 index 0000000..58e6759 --- /dev/null +++ b/Sources/FeatherOpenAPI/Location/LocationRepresentable.swift @@ -0,0 +1,31 @@ +// +// LocationRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A type that can provide a URL location string. +public protocol LocationRepresentable: OpenAPILocationRepresentable { + /// The URL string for the location. + var location: String { get } +} + +extension LocationRepresentable { + + /// Converts the location string into a `URL`. + /// - Returns: A URL created from the location string. + public func openAPILocation() -> URL { + .init(string: location)! + } +} + +//extension String: URLRepresentable { +// public var rawURL: String { self } +//} diff --git a/Sources/FeatherOpenAPI/Location/OpenAPILocationRepresentable.swift b/Sources/FeatherOpenAPI/Location/OpenAPILocationRepresentable.swift new file mode 100644 index 0000000..213e165 --- /dev/null +++ b/Sources/FeatherOpenAPI/Location/OpenAPILocationRepresentable.swift @@ -0,0 +1,28 @@ +// +// OpenAPILocationRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +/// A type that can produce a URL location. +public protocol OpenAPILocationRepresentable { + /// Returns the URL representation. + /// - Returns: The URL. + func openAPILocation() -> URL +} + +extension URL: OpenAPILocationRepresentable { + + /// Returns `self` as a URL location. + /// - Returns: The current URL. + public func openAPILocation() -> URL { + self + } +} diff --git a/Sources/FeatherOpenAPI/Operation/OpenAPIOperationRepresentable.swift b/Sources/FeatherOpenAPI/Operation/OpenAPIOperationRepresentable.swift new file mode 100644 index 0000000..9aebb06 --- /dev/null +++ b/Sources/FeatherOpenAPI/Operation/OpenAPIOperationRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIOperationRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI operation. +public protocol OpenAPIOperationRepresentable { + /// Returns the OpenAPI operation representation. + /// - Returns: The OpenAPI operation. + func openAPIOperation() -> OpenAPI.Operation +} + +extension OpenAPI.Operation: OpenAPIOperationRepresentable { + + /// Returns `self` as an OpenAPI operation. + /// - Returns: The current operation value. + public func openAPIOperation() -> OpenAPI.Operation { + self + } +} diff --git a/Sources/FeatherOpenAPI/Operation/OperationRepresentable.swift b/Sources/FeatherOpenAPI/Operation/OperationRepresentable.swift new file mode 100644 index 0000000..66ac92f --- /dev/null +++ b/Sources/FeatherOpenAPI/Operation/OperationRepresentable.swift @@ -0,0 +1,255 @@ +// +// OperationRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +extension String { + + fileprivate func lowercasedFirstLetter() -> String { + guard !isEmpty else { + return self + } + return prefix(1).lowercased() + dropFirst() + } +} + +/// Describes an OpenAPI operation with defaults and reference aggregation. +public protocol OperationRepresentable: + OpenAPIOperationRepresentable, + // properties + DescriptionProperty, + DeprecatedProperty, + VendorExtensionsProperty, + // references + ReferencedSchemaMapRepresentable, + ReferencedHeaderMapRepresentable, + ReferencedRequestBodyMapRepresentable, + ReferencedParameterMapRepresentable, + ReferencedResponseMapRepresentable, + ReferencedTagMapRepresentable, + ReferencedSecuritySchemeMapRepresentable +{ + // associatedtype RequestBodyType: RequestBodyRepresentable + + /// Tags associated with the operation. + var tags: [TagRepresentable] { get } + /// Short summary of the operation. + var summary: String? { get } + /// Optional explicit operation identifier. + var operationId: String? { get } + + /// Parameters accepted by the operation. + var parameters: [ParameterRepresentable] { get } + /// Optional request body. + var requestBody: RequestBodyRepresentable? { get } + /// Response map keyed by status code. + var responseMap: ResponseMap { get } + + /// Optional security requirements. + var security: [SecurityRequirementRepresentable]? { get } + /// Optional per-operation servers. + var servers: [ServerRepresentable]? { get } +} + +extension OperationRepresentable { + + /// Default tags are empty. + public var tags: [TagRepresentable] { [] } + /// Default summary is `nil`. + public var summary: String? { nil } + + /// Default operation identifier is `nil`. + public var operationId: String? { nil } + /// Default parameters are empty. + public var parameters: [ParameterRepresentable] { [] } + + /// Computes a default operation identifier from the type name. + public static var operationId: String { + var components = String(reflecting: self) + .split(separator: ".") + .dropFirst() + .map(String.init) + + components.remove(at: 2) + if let last = components.popLast()?.lowercasedFirstLetter() { + components.insert(last, at: 0) + } + return components.joined(separator: "") + } + + /// Default request body is `nil`. + public var requestBody: RequestBodyRepresentable? { nil } + /// Default security requirements are `nil`. + public var security: [SecurityRequirementRepresentable]? { nil } + /// Default servers list is `nil`. + public var servers: [ServerRepresentable]? { nil } + + private var openAPITags: [String]? { + tags.isEmpty ? nil : tags.map { $0.name } + } + + private var openAPISecurityRequirements: [OpenAPI.SecurityRequirement]? { + guard let security, !security.isEmpty else { + return nil + } + + return security.map { $0.openAPISecurityRequirement() } + } + + /// Builds an OpenAPI operation. + /// - Returns: The OpenAPI operation. + public func openAPIOperation() -> OpenAPI.Operation { + if let requestBody { + return .init( + tags: openAPITags, + summary: summary, + description: description, + externalDocs: nil, + operationId: operationId, + parameters: parameters.map { $0.openAPIParameter() }, + requestBody: requestBody.openAPIRequestBody(), + responses: responseMap.mapValues { $0.openAPIResponse() }, + callbacks: [:], + deprecated: deprecated, + security: openAPISecurityRequirements, + servers: servers?.map { $0.openAPIServer() }, + vendorExtensions: vendorExtensions + ) + } + return .init( + tags: openAPITags, + summary: summary, + description: description, + externalDocs: nil, + operationId: operationId, + parameters: parameters.map { $0.openAPIParameter() }, + responses: responseMap.mapValues { $0.openAPIResponse() }, + callbacks: [:], + deprecated: deprecated, + security: openAPISecurityRequirements, + servers: servers?.map { $0.openAPIServer() }, + vendorExtensions: vendorExtensions + ) + } + + // MARK: - refs + + /// Aggregated referenced schemas from parameters, request body, and responses. + public var referencedSchemaMap: + OrderedDictionary + { + var results = OrderedDictionary() + + for parameter in parameters { + results.merge(parameter.referencedSchemaMap) + } + + if let schemaMap = requestBody?.referencedSchemaMap { + results.merge(schemaMap) + } + + let headers = responseMap.values + .map { $0.headerMap.values } + .flatMap { $0 } + + for header in headers { + results.merge(header.referencedSchemaMap) + } + + let contents = responseMap.values + .map { $0.contentMap.values } + .flatMap { $0 } + + for content in contents { + results.merge(content.referencedSchemaMap) + } + + return results + } + + /// Aggregated referenced parameters used by the operation. + public var referencedParameterMap: + OrderedDictionary + { + var results = OrderedDictionary< + ParameterID, OpenAPIParameterRepresentable + >() + + for parameter in parameters { + if let ref = parameter as? ParameterReferenceRepresentable { + if case .b(let parameter) = ref.object.openAPIParameter() { + results[ref.id] = parameter + } + } + } + return results + } + + /// Aggregated referenced request bodies used by the operation. + public var referencedRequestBodyMap: + OrderedDictionary + { + var results = OrderedDictionary< + RequestBodyID, OpenAPIRequestBodyRepresentable + >() + + if let ref = requestBody as? RequestBodyReferenceRepresentable { + results[ref.id] = ref.object + } + return results + } + + /// Aggregated referenced headers used by responses. + public var referencedHeaderMap: + OrderedDictionary + { + var results = OrderedDictionary() + + let headers = responseMap.values + .map { $0.headerMap.values } + .flatMap { $0 } + + for header in headers { + if let ref = header as? HeaderReferenceRepresentable { + if case .b(let header) = ref.object.openAPIHeader() { + results[ref.id] = header + } + } + } + return results + } + + /// Aggregated referenced responses used by the operation. + public var referencedResponseMap: + OrderedDictionary + { + var results = OrderedDictionary< + ResponseID, OpenAPIResponseRepresentable + >() + + for response in responseMap.values { + if let ref = response as? ResponseReferenceRepresentable { + if case .b(let response) = ref.object.openAPIResponse() { + results[ref.id] = response + } + } + } + return results + } + + /// Referenced tags for the operation. + public var referencedTags: [OpenAPITagRepresentable] { + tags + } + + /// Referenced security requirements for the operation. + public var referencedSecurityRequirements: + [SecurityRequirementRepresentable] + { + security?.map { $0 } ?? [] + } +} diff --git a/Sources/FeatherOpenAPI/OrderedDictionary+Merge.swift b/Sources/FeatherOpenAPI/OrderedDictionary+Merge.swift new file mode 100644 index 0000000..367141e --- /dev/null +++ b/Sources/FeatherOpenAPI/OrderedDictionary+Merge.swift @@ -0,0 +1,14 @@ +// +// OrderedDictionary+Merge.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import OpenAPIKit30 + +extension OrderedDictionary { + + mutating func merge(_ other: Self) { + merge(other, uniquingKeysWith: { _, new in new }) + } +} diff --git a/Sources/FeatherOpenAPI/Parameter/Abstraction/OpenAPIParameterRepresentable.swift b/Sources/FeatherOpenAPI/Parameter/Abstraction/OpenAPIParameterRepresentable.swift new file mode 100644 index 0000000..f4ac8d5 --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/Abstraction/OpenAPIParameterRepresentable.swift @@ -0,0 +1,28 @@ +// +// OpenAPIParameterRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI parameter or reference. +public protocol OpenAPIParameterRepresentable { + /// Returns the OpenAPI parameter representation. + /// - Returns: An OpenAPI parameter or reference. + func openAPIParameter() -> Either< + JSONReference, OpenAPI.Parameter + > +} + +extension OpenAPI.Parameter: OpenAPIParameterRepresentable { + + /// Returns `self` wrapped as an OpenAPI parameter. + /// - Returns: An OpenAPI parameter value. + public func openAPIParameter() -> Either< + JSONReference, OpenAPI.Parameter + > { + .init(self) + } +} diff --git a/Sources/FeatherOpenAPI/Parameter/Abstraction/ParameterID.swift b/Sources/FeatherOpenAPI/Parameter/Abstraction/ParameterID.swift new file mode 100644 index 0000000..7521ec4 --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/Abstraction/ParameterID.swift @@ -0,0 +1,20 @@ +// +// ParameterID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public struct ParameterID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a parameter identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Parameter/Abstraction/ParameterRepresentable.swift b/Sources/FeatherOpenAPI/Parameter/Abstraction/ParameterRepresentable.swift new file mode 100644 index 0000000..d381ecd --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/Abstraction/ParameterRepresentable.swift @@ -0,0 +1,64 @@ +// +// ParameterRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI parameter with defaults. +public protocol ParameterRepresentable: + OpenAPIParameterRepresentable, + Identifiable, + // property + DescriptionProperty, + DeprecatedProperty, + VendorExtensionsProperty, + // reference + ReferencedSchemaMapRepresentable +{ + /// The parameter name. + var name: String { get } + /// The parameter context (path, query, header, cookie). + var context: OpenAPI.Parameter.Context { get } + /// The schema describing the parameter value. + var schema: OpenAPISchemaRepresentable { get } + +} + +extension ParameterRepresentable { + + /// Creates a reference wrapper for this parameter. + /// - Returns: A parameter reference. + public func reference() -> ParameterReference { + .init(self) + } + + /// Builds an OpenAPI parameter object or reference. + /// - Returns: The OpenAPI parameter representation. + public func openAPIParameter() -> Either< + JSONReference, OpenAPI.Parameter + > { + .init( + .init( + name: name, + context: context, + schema: schema.openAPISchema(), + description: description, + deprecated: deprecated, + vendorExtensions: vendorExtensions + ) + ) + } + + /// Referenced schemas used by the parameter. + public var referencedSchemaMap: + OrderedDictionary + { + guard let schema = schema as? SchemaRepresentable else { + return [:] + } + return schema.allReferencedSchemaMap() + } +} diff --git a/Sources/FeatherOpenAPI/Parameter/CookieParameterRepresentable.swift b/Sources/FeatherOpenAPI/Parameter/CookieParameterRepresentable.swift new file mode 100644 index 0000000..0d44959 --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/CookieParameterRepresentable.swift @@ -0,0 +1,24 @@ +// +// CookieParameterRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Parameter located in a cookie. +public protocol CookieParameterRepresentable: + ParameterRepresentable, + RequiredProperty +{ + +} + +extension CookieParameterRepresentable { + + /// Cookie parameter context. + public var context: OpenAPI.Parameter.Context { + .cookie(required: `required`) + } +} diff --git a/Sources/FeatherOpenAPI/Parameter/HeaderParameterRepresentable.swift b/Sources/FeatherOpenAPI/Parameter/HeaderParameterRepresentable.swift new file mode 100644 index 0000000..e600dbe --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/HeaderParameterRepresentable.swift @@ -0,0 +1,24 @@ +// +// HeaderParameterRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Parameter located in a header. +public protocol HeaderParameterRepresentable: + ParameterRepresentable, + RequiredProperty +{ + +} + +extension HeaderParameterRepresentable { + + /// Header parameter context. + public var context: OpenAPI.Parameter.Context { + .header(required: `required`) + } +} diff --git a/Sources/FeatherOpenAPI/Parameter/PathParameterRepresentable.swift b/Sources/FeatherOpenAPI/Parameter/PathParameterRepresentable.swift new file mode 100644 index 0000000..764bb54 --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/PathParameterRepresentable.swift @@ -0,0 +1,19 @@ +// +// PathParameterRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Parameter located in the path. +public protocol PathParameterRepresentable: ParameterRepresentable { + +} + +extension PathParameterRepresentable { + + /// Path parameter context. + public var context: OpenAPI.Parameter.Context { .path } +} diff --git a/Sources/FeatherOpenAPI/Parameter/QueryParameterRepresentable.swift b/Sources/FeatherOpenAPI/Parameter/QueryParameterRepresentable.swift new file mode 100644 index 0000000..9447b18 --- /dev/null +++ b/Sources/FeatherOpenAPI/Parameter/QueryParameterRepresentable.swift @@ -0,0 +1,31 @@ +// +// QueryParameterRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Parameter located in the query string. +public protocol QueryParameterRepresentable: + ParameterRepresentable, + RequiredProperty +{ + /// Whether empty values are allowed. + var allowEmptyValue: Bool { get } +} + +extension QueryParameterRepresentable { + + /// Default empty value allowance is `true`. + public var allowEmptyValue: Bool { true } + + /// Query parameter context. + public var context: OpenAPI.Parameter.Context { + .query( + required: `required`, + allowEmptyValue: allowEmptyValue + ) + } +} diff --git a/Sources/FeatherOpenAPI/Path.swift b/Sources/FeatherOpenAPI/Path.swift new file mode 100644 index 0000000..767e229 --- /dev/null +++ b/Sources/FeatherOpenAPI/Path.swift @@ -0,0 +1,61 @@ +// +// Path.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +/// A lightweight OpenAPI path wrapper with composition helpers. +public struct Path: ExpressibleByStringLiteral { + + /// The underlying path string. + public let value: String + + /// Creates a new path from a string. + /// - Parameter value: The path string. + public init(_ value: String) { + self.value = value + } + + /// Creates a new path from a string literal. + /// - Parameter value: The path string. + public init(stringLiteral value: StringLiteralType) { + self.value = value + } + + /// Joins two paths with a `/` separator. + /// - Parameters: + /// - lhs: The left-hand path. + /// - rhs: The right-hand path. + /// - Returns: The combined path. + public static func / (lhs: Self, rhs: Self) -> Self { + .init(lhs.value + "/" + rhs.value) + } + + /// Joins a path and a string segment. + /// - Parameters: + /// - lhs: The left-hand path. + /// - rhs: The right-hand path segment. + /// - Returns: The combined path. + public static func / (lhs: Self, rhs: String) -> Self { + lhs / Self(rhs) + } + + /// Joins a string segment and a path. + /// - Parameters: + /// - lhs: The left-hand path segment. + /// - rhs: The right-hand path. + /// - Returns: The combined path. + public static func / (lhs: String, rhs: Self) -> Self { + Self(lhs) / rhs + } +} + +extension Path { + + /// Builds a path parameter segment like `{id}`. + /// - Parameter param: The parameter name. + /// - Returns: The parameterized path segment. + public static func parameter(_ param: String) -> Self { + .init("{" + param + "}") + } +} diff --git a/Sources/FeatherOpenAPI/PathCollection/PathCollectionRepresentable.swift b/Sources/FeatherOpenAPI/PathCollection/PathCollectionRepresentable.swift new file mode 100644 index 0000000..9d8e41e --- /dev/null +++ b/Sources/FeatherOpenAPI/PathCollection/PathCollectionRepresentable.swift @@ -0,0 +1,137 @@ +// +// PathCollectionRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Describes a collection of paths and their derived components. +public protocol PathCollectionRepresentable: + ReferencedSchemaMapRepresentable, + ReferencedParameterMapRepresentable, + ReferencedRequestBodyMapRepresentable, + ReferencedHeaderMapRepresentable, + ReferencedResponseMapRepresentable, + ReferencedTagMapRepresentable, + ReferencedSecuritySchemeMapRepresentable +{ + /// The map of paths to path items. + var pathMap: PathMap { get } + /// Derived components from the path collection. + var components: FeatherOpenAPI.Components { get } +} + +extension PathCollectionRepresentable { + + /// Aggregated referenced schemas from the path map. + public var referencedSchemaMap: + OrderedDictionary + { + var results = OrderedDictionary() + + let schemaMaps = pathMap.values + .map { $0.referencedSchemaMap } + .flatMap { $0 } + + for (k, v) in schemaMaps { + results[k] = v + } + return results + } + + /// Aggregated referenced parameters from the path map. + public var referencedParameterMap: + OrderedDictionary + { + var results = OrderedDictionary< + ParameterID, OpenAPIParameterRepresentable + >() + + let parameterMaps = pathMap.values + .map { $0.referencedParameterMap } + .flatMap { $0 } + + for (k, v) in parameterMaps { + results[k] = v + } + return results + } + + /// Aggregated referenced request bodies from the path map. + public var referencedRequestBodyMap: + OrderedDictionary + { + var results = OrderedDictionary< + RequestBodyID, OpenAPIRequestBodyRepresentable + >() + + let requestBodyMaps = pathMap.values + .map { $0.referencedRequestBodyMap } + .flatMap { $0 } + + for (k, v) in requestBodyMaps { + results[k] = v + } + return results + } + + /// Aggregated referenced headers from the path map. + public var referencedHeaderMap: + OrderedDictionary + { + var results = OrderedDictionary() + + let headerMaps = pathMap.values + .map { $0.referencedHeaderMap } + .flatMap { $0 } + + for (k, v) in headerMaps { + results[k] = v + } + return results + } + + /// Aggregated referenced responses from the path map. + public var referencedResponseMap: + OrderedDictionary + { + var results = OrderedDictionary< + ResponseID, OpenAPIResponseRepresentable + >() + + let responseMaps = pathMap.values + .map { $0.referencedResponseMap } + .flatMap { $0 } + + for (k, v) in responseMaps { + results[k] = v + } + return results + } + + /// Aggregated referenced tags from the path map. + public var referencedTags: [OpenAPITagRepresentable] { + pathMap.values.map { $0.referencedTags }.flatMap { $0 } + } + + /// Aggregated referenced security requirements from the path map. + public var referencedSecurityRequirements: + [SecurityRequirementRepresentable] + { + pathMap.values.map { $0.referencedSecurityRequirements }.flatMap { $0 } + } + + /// Builds components from all referenced objects in the path map. + public var components: FeatherOpenAPI.Components { + .init( + schemas: referencedSchemaMap, + parameters: referencedParameterMap, + responses: referencedResponseMap, + requestBodies: referencedRequestBodyMap, + headers: referencedHeaderMap, + securityRequirements: referencedSecurityRequirements + ) + } +} diff --git a/Sources/FeatherOpenAPI/PathItem/OpenAPIPathItemRepresentable.swift b/Sources/FeatherOpenAPI/PathItem/OpenAPIPathItemRepresentable.swift new file mode 100644 index 0000000..37b0353 --- /dev/null +++ b/Sources/FeatherOpenAPI/PathItem/OpenAPIPathItemRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIPathItemRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI path item. +public protocol OpenAPIPathItemRepresentable { + /// Returns the OpenAPI path item representation. + /// - Returns: The OpenAPI path item. + func openAPIPathItem() -> OpenAPI.PathItem +} + +extension OpenAPI.PathItem: OpenAPIPathItemRepresentable { + + /// Returns `self` as an OpenAPI path item. + /// - Returns: The current path item value. + public func openAPIPathItem() -> OpenAPI.PathItem { + self + } +} diff --git a/Sources/FeatherOpenAPI/PathItem/PathItemRepresentable.swift b/Sources/FeatherOpenAPI/PathItem/PathItemRepresentable.swift new file mode 100644 index 0000000..557c95f --- /dev/null +++ b/Sources/FeatherOpenAPI/PathItem/PathItemRepresentable.swift @@ -0,0 +1,196 @@ +// +// PathItemRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI path item with HTTP operation bindings. +public protocol PathItemRepresentable: + OpenAPIPathItemRepresentable, + // properties + DescriptionProperty, + VendorExtensionsProperty, + // reference + ReferencedSchemaMapRepresentable, + ReferencedParameterMapRepresentable, + ReferencedRequestBodyMapRepresentable, + ReferencedHeaderMapRepresentable, + ReferencedResponseMapRepresentable, + ReferencedTagMapRepresentable, + ReferencedSecuritySchemeMapRepresentable +{ + /// Short summary for the path item. + var summary: String? { get } + + /// Optional servers that override document-level servers. + var servers: [OpenAPIServerRepresentable]? { get } + /// GET operation for this path. + var get: OperationRepresentable? { get } + /// PUT operation for this path. + var put: OperationRepresentable? { get } + /// POST operation for this path. + var post: OperationRepresentable? { get } + /// DELETE operation for this path. + var delete: OperationRepresentable? { get } + /// OPTIONS operation for this path. + var options: OperationRepresentable? { get } + /// HEAD operation for this path. + var head: OperationRepresentable? { get } + /// PATCH operation for this path. + var patch: OperationRepresentable? { get } + /// TRACE operation for this path. + var trace: OperationRepresentable? { get } +} + +extension PathItemRepresentable { + + /// Default summary is `nil`. + public var summary: String? { nil } + + /// Default servers list is `nil`. + public var servers: [OpenAPIServerRepresentable]? { nil } + /// Default GET operation is `nil`. + public var get: OperationRepresentable? { nil } + /// Default PUT operation is `nil`. + public var put: OperationRepresentable? { nil } + /// Default POST operation is `nil`. + public var post: OperationRepresentable? { nil } + /// Default DELETE operation is `nil`. + public var delete: OperationRepresentable? { nil } + /// Default OPTIONS operation is `nil`. + public var options: OperationRepresentable? { nil } + /// Default HEAD operation is `nil`. + public var head: OperationRepresentable? { nil } + /// Default PATCH operation is `nil`. + public var patch: OperationRepresentable? { nil } + /// Default TRACE operation is `nil`. + public var trace: OperationRepresentable? { nil } + + /// Builds an OpenAPI path item. + /// - Returns: The OpenAPI path item. + public func openAPIPathItem() -> OpenAPI.PathItem { + .init( + summary: summary, + description: description, + servers: servers?.map { $0.openAPIServer() }, + parameters: [], + get: get?.openAPIOperation(), + put: put?.openAPIOperation(), + post: post?.openAPIOperation(), + delete: delete?.openAPIOperation(), + options: options?.openAPIOperation(), + head: head?.openAPIOperation(), + patch: patch?.openAPIOperation(), + trace: trace?.openAPIOperation(), + vendorExtensions: vendorExtensions + ) + } + + /// All non-nil operations declared on the path item. + public var allOperations: [OperationRepresentable] { + [ + get, + put, + post, + delete, + options, + head, + patch, + trace, + ] + .compactMap { $0 } + } + + /// Aggregated referenced schemas from operations. + public var referencedSchemaMap: + OrderedDictionary + { + var results = OrderedDictionary() + + let maps = allOperations.map { $0.referencedSchemaMap }.flatMap { $0 } + + for (k, v) in maps { + results[k] = v + } + return results + } + + /// Aggregated referenced parameters from operations. + public var referencedParameterMap: + OrderedDictionary + { + var results = OrderedDictionary< + ParameterID, OpenAPIParameterRepresentable + >() + + let maps = allOperations.map { $0.referencedParameterMap } + .flatMap { $0 } + + for (k, v) in maps { + results[k] = v + } + return results + } + + /// Aggregated referenced request bodies from operations. + public var referencedRequestBodyMap: + OrderedDictionary + { + var results = OrderedDictionary< + RequestBodyID, OpenAPIRequestBodyRepresentable + >() + + let maps = allOperations.map { $0.referencedRequestBodyMap } + .flatMap { $0 } + + for (k, v) in maps { + results[k] = v + } + return results + } + + /// Aggregated referenced headers from operations. + public var referencedHeaderMap: + OrderedDictionary + { + var results = OrderedDictionary() + + let maps = allOperations.map { $0.referencedHeaderMap }.flatMap { $0 } + + for (k, v) in maps { + results[k] = v + } + return results + } + + /// Aggregated referenced responses from operations. + public var referencedResponseMap: + OrderedDictionary + { + var results = OrderedDictionary< + ResponseID, OpenAPIResponseRepresentable + >() + + let maps = allOperations.map { $0.referencedResponseMap }.flatMap { $0 } + + for (k, v) in maps { + results[k] = v + } + return results + } + + /// Aggregated referenced tags from operations. + public var referencedTags: [OpenAPITagRepresentable] { + allOperations.map { $0.referencedTags }.flatMap { $0 } + } + + /// Aggregated referenced security requirements from operations. + public var referencedSecurityRequirements: + [SecurityRequirementRepresentable] + { + allOperations.map { $0.referencedSecurityRequirements }.flatMap { $0 } + } +} diff --git a/Sources/FeatherOpenAPI/PathItem/PathMap.swift b/Sources/FeatherOpenAPI/PathItem/PathMap.swift new file mode 100644 index 0000000..0419e46 --- /dev/null +++ b/Sources/FeatherOpenAPI/PathItem/PathMap.swift @@ -0,0 +1,14 @@ +// +// PathMap.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Ordered map of OpenAPI paths to path items. +public typealias PathMap = OrderedDictionary< + OpenAPI.Path, + PathItemRepresentable +> diff --git a/Sources/FeatherOpenAPI/Reference/ExampleReferenceRepresentable.swift b/Sources/FeatherOpenAPI/Reference/ExampleReferenceRepresentable.swift new file mode 100644 index 0000000..e1eac36 --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/ExampleReferenceRepresentable.swift @@ -0,0 +1,58 @@ +// +// ExampleReferenceRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 24.. +// + +import OpenAPIKit30 + +/// A type that exposes a referenced example. +public protocol ExampleReferenceRepresentable { + /// The identifier for the example reference. + var id: ExampleID { get } + /// The underlying example object. + var object: ExampleRepresentable { get } +} + +/// Wrapper that exposes an example as a reusable reference. +public struct ExampleReference: + ExampleRepresentable, + ExampleReferenceRepresentable +{ + /// Example summary. + public var summary: String? { object.summary } + /// Example value. + public var value: AnyCodable { object.value } + /// Example description. + public var description: String? { object.description } + /// Example vendor extensions. + public var vendorExtensions: [String: AnyCodable] { + object.vendorExtensions + } + + /// The underlying example object. + public var object: ExampleRepresentable { + _object + } + + /// The example identifier. + public var id: ExampleID + /// The concrete example instance. + public var _object: T + + internal init( + _ object: T + ) { + self.id = .init(object.openAPIIdentifier) + self._object = object + } + + /// Returns a component reference for the example. + /// - Returns: An example reference. + public func openAPIExample() -> Either< + JSONReference, OpenAPI.Example + > { + .reference(.component(named: id.rawValue)) + } +} diff --git a/Sources/FeatherOpenAPI/Reference/HeaderReferenceRepresentable.swift b/Sources/FeatherOpenAPI/Reference/HeaderReferenceRepresentable.swift new file mode 100644 index 0000000..c531228 --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/HeaderReferenceRepresentable.swift @@ -0,0 +1,52 @@ +// +// HeaderReferenceRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that exposes a referenced header. +public protocol HeaderReferenceRepresentable { + /// The identifier for the header reference. + var id: HeaderID { get } + /// The underlying header object. + var object: HeaderRepresentable { get } +} + +/// Wrapper that exposes a header as a reusable reference. +public struct HeaderReference: + HeaderRepresentable, + HeaderReferenceRepresentable +{ + /// Header schema. + public var schema: OpenAPISchemaRepresentable { object.schema } + + /// The underlying header object. + public var object: HeaderRepresentable { + _object + } + + /// The header identifier. + public var id: HeaderID + /// The concrete header instance. + public var _object: T + + /// Creates a header reference. + /// - Parameter object: The header to reference. + public init( + _ object: T + ) { + self.id = .init(object.openAPIIdentifier) + self._object = object + } + + /// Returns a component reference for the header. + /// - Returns: A header reference. + public func openAPIHeader() -> Either< + JSONReference, OpenAPI.Header + > { + .reference(.component(named: id.rawValue)) + } +} diff --git a/Sources/FeatherOpenAPI/Reference/ParameterReferenceRepresentable.swift b/Sources/FeatherOpenAPI/Reference/ParameterReferenceRepresentable.swift new file mode 100644 index 0000000..2c8feab --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/ParameterReferenceRepresentable.swift @@ -0,0 +1,56 @@ +// +// ParameterReferenceRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that exposes a referenced parameter. +public protocol ParameterReferenceRepresentable { + /// The identifier for the parameter reference. + var id: ParameterID { get } + /// The underlying parameter object. + var object: ParameterRepresentable { get } +} + +/// Wrapper that exposes a parameter as a reusable reference. +public struct ParameterReference: + ParameterRepresentable, + ParameterReferenceRepresentable +{ + /// Parameter name. + public var name: String { object.name } + /// Parameter context. + public var context: OpenAPIKit30.OpenAPI.Parameter.Context { + object.context + } + /// Parameter schema. + public var schema: any OpenAPISchemaRepresentable { object.schema } + + /// The underlying parameter object. + public var object: ParameterRepresentable { + _object + } + + /// The parameter identifier. + public var id: ParameterID + /// The concrete parameter instance. + public var _object: T + + internal init( + _ object: T + ) { + self.id = .init(object.openAPIIdentifier) + self._object = object + } + + /// Returns a component reference for the parameter. + /// - Returns: A parameter reference. + public func openAPIParameter() -> Either< + JSONReference, OpenAPI.Parameter + > { + .reference(.component(named: id.rawValue)) + } +} diff --git a/Sources/FeatherOpenAPI/Reference/ReferencedSchemaMapRepresentable.swift b/Sources/FeatherOpenAPI/Reference/ReferencedSchemaMapRepresentable.swift new file mode 100644 index 0000000..b79c122 --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/ReferencedSchemaMapRepresentable.swift @@ -0,0 +1,62 @@ +// +// ReferencedSchemaMapRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Exposes referenced schemas. +public protocol ReferencedSchemaMapRepresentable { + /// Map of referenced schemas. + var referencedSchemaMap: + OrderedDictionary + { get } +} + +/// Exposes referenced parameters. +public protocol ReferencedParameterMapRepresentable { + /// Map of referenced parameters. + var referencedParameterMap: + OrderedDictionary + { get } +} + +/// Exposes referenced request bodies. +public protocol ReferencedRequestBodyMapRepresentable { + /// Map of referenced request bodies. + var referencedRequestBodyMap: + OrderedDictionary + { get } +} + +/// Exposes referenced headers. +public protocol ReferencedHeaderMapRepresentable { + /// Map of referenced headers. + var referencedHeaderMap: + OrderedDictionary + { get } +} + +/// Exposes referenced responses. +public protocol ReferencedResponseMapRepresentable { + /// Map of referenced responses. + var referencedResponseMap: + OrderedDictionary + { get } +} + +/// Exposes referenced security requirements. +public protocol ReferencedSecuritySchemeMapRepresentable { + /// List of referenced security requirements. + var referencedSecurityRequirements: [SecurityRequirementRepresentable] { + get + } +} + +/// Exposes referenced tags. +public protocol ReferencedTagMapRepresentable { + /// List of referenced tags. + var referencedTags: [OpenAPITagRepresentable] { get } +} diff --git a/Sources/FeatherOpenAPI/Reference/RequestBodyReferenceRepresentable.swift b/Sources/FeatherOpenAPI/Reference/RequestBodyReferenceRepresentable.swift new file mode 100644 index 0000000..bcb44df --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/RequestBodyReferenceRepresentable.swift @@ -0,0 +1,50 @@ +// +// RequestBodyReferenceRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that exposes a referenced request body. +public protocol RequestBodyReferenceRepresentable { + /// The identifier for the request body reference. + var id: RequestBodyID { get } + /// The underlying request body object. + var object: RequestBodyRepresentable { get } +} + +/// Wrapper that exposes a request body as a reusable reference. +public struct RequestBodyReference: + RequestBodyRepresentable, + RequestBodyReferenceRepresentable +{ + /// Request body content map. + public var contentMap: ContentMap { object.contentMap } + + /// The underlying request body object. + public var object: RequestBodyRepresentable { + _object + } + + /// The request body identifier. + public var id: RequestBodyID + /// The concrete request body instance. + public var _object: T + + internal init( + _ object: T + ) { + self.id = .init(object.openAPIIdentifier) + self._object = object + } + + /// Returns a component reference for the request body. + /// - Returns: A request body reference. + public func openAPIRequestBody() -> Either< + JSONReference, OpenAPI.Request + > { + .reference(.component(named: id.rawValue)) + } +} diff --git a/Sources/FeatherOpenAPI/Reference/ResponseReferenceRepresentable.swift b/Sources/FeatherOpenAPI/Reference/ResponseReferenceRepresentable.swift new file mode 100644 index 0000000..5f0a5e2 --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/ResponseReferenceRepresentable.swift @@ -0,0 +1,54 @@ +// +// ResponseReferenceRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that exposes a referenced response. +public protocol ResponseReferenceRepresentable { + /// The identifier for the response reference. + var id: ResponseID { get } + /// The underlying response object. + var object: ResponseRepresentable { get } +} + +/// Wrapper that exposes a response as a reusable reference. +public struct ResponseReference: + ResponseRepresentable, + ResponseReferenceRepresentable +{ + /// Response description. + public var description: String { object.description } + /// Response header map. + public var headerMap: HeaderMap { object.headerMap } + /// Response content map. + public var contentMap: ContentMap { object.contentMap } + + /// The underlying response object. + public var object: ResponseRepresentable { + _object + } + + /// The response identifier. + public var id: ResponseID + /// The concrete response instance. + public var _object: T + + internal init( + _ object: T + ) { + self.id = .init(object.openAPIIdentifier) + self._object = object + } + + /// Returns a component reference for the response. + /// - Returns: A response reference. + public func openAPIResponse() -> Either< + JSONReference, OpenAPI.Response + > { + .reference(.component(named: id.rawValue)) + } +} diff --git a/Sources/FeatherOpenAPI/Reference/SchemaReference.swift b/Sources/FeatherOpenAPI/Reference/SchemaReference.swift new file mode 100644 index 0000000..0a220b1 --- /dev/null +++ b/Sources/FeatherOpenAPI/Reference/SchemaReference.swift @@ -0,0 +1,56 @@ +// +// SchemaReference.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// A type that exposes a referenced schema. +public protocol SchemaReferenceRepresentable { + /// The identifier for the schema reference. + var id: SchemaID { get } + /// The underlying schema object. + var object: SchemaRepresentable { get } +} + +/// Wrapper that exposes a schema as a reusable reference. +public struct SchemaReference: + SchemaRepresentable, + SchemaReferenceRepresentable +{ + /// The underlying schema object. + public var object: any SchemaRepresentable { + _object + } + + /// The schema identifier. + public var id: SchemaID + /// The concrete schema instance. + public var _object: T + /// Indicates whether the reference is required. + public var required: Bool + + internal init( + _ object: T, + required: Bool = true + ) { + self.id = .init(object.openAPIIdentifier) + self._object = object + self.required = required + } + + /// Returns a JSON Schema reference to the component. + /// - Returns: The JSON schema reference. + public func openAPISchema() -> JSONSchema { + .reference(.component(named: id.rawValue), required: required) + } + + /// Referenced schema map for this reference. + public var referencedSchemaMap: + OrderedDictionary + { + [id: object] + } +} diff --git a/Sources/FeatherOpenAPI/RequestBody/Abstraction/OpenAPIRequestBodyRepresentable.swift b/Sources/FeatherOpenAPI/RequestBody/Abstraction/OpenAPIRequestBodyRepresentable.swift new file mode 100644 index 0000000..96d6f3c --- /dev/null +++ b/Sources/FeatherOpenAPI/RequestBody/Abstraction/OpenAPIRequestBodyRepresentable.swift @@ -0,0 +1,28 @@ +// +// OpenAPIRequestBodyRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI request body or reference. +public protocol OpenAPIRequestBodyRepresentable { + /// Returns the OpenAPI request body representation. + /// - Returns: An OpenAPI request body or reference. + func openAPIRequestBody() -> Either< + JSONReference, OpenAPI.Request + > +} + +extension OpenAPI.Request: OpenAPIRequestBodyRepresentable { + + /// Returns `self` wrapped as an OpenAPI request body. + /// - Returns: An OpenAPI request body value. + public func openAPIRequestBody() -> Either< + JSONReference, OpenAPI.Request + > { + .init(self) + } +} diff --git a/Sources/FeatherOpenAPI/RequestBody/Abstraction/RequestBodyID.swift b/Sources/FeatherOpenAPI/RequestBody/Abstraction/RequestBodyID.swift new file mode 100644 index 0000000..28aecf8 --- /dev/null +++ b/Sources/FeatherOpenAPI/RequestBody/Abstraction/RequestBodyID.swift @@ -0,0 +1,20 @@ +// +// RequestBodyID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +public struct RequestBodyID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a request body identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/RequestBody/Abstraction/RequestBodyRepresentable.swift b/Sources/FeatherOpenAPI/RequestBody/Abstraction/RequestBodyRepresentable.swift new file mode 100644 index 0000000..a96f5a0 --- /dev/null +++ b/Sources/FeatherOpenAPI/RequestBody/Abstraction/RequestBodyRepresentable.swift @@ -0,0 +1,57 @@ +// +// RequestBodyRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI request body with defaults. +public protocol RequestBodyRepresentable: + Identifiable, + OpenAPIRequestBodyRepresentable, + DescriptionProperty, + RequiredProperty, + VendorExtensionsProperty, + // reference + ReferencedSchemaMapRepresentable +{ + /// Map of request body content by content type. + var contentMap: ContentMap { get } +} + +extension RequestBodyRepresentable { + + /// Creates a reference wrapper for this request body. + /// - Returns: A request body reference. + public func reference() -> RequestBodyReference { + .init(self) + } + + /// Builds an OpenAPI request body object or reference. + /// - Returns: The OpenAPI request body representation. + public func openAPIRequestBody() -> Either< + JSONReference, OpenAPI.Request + > { + .init( + .init( + description: description, + content: contentMap.mapValues { $0.openAPIContent() }, + required: `required`, + vendorExtensions: vendorExtensions + ) + ) + } + + /// Aggregated referenced schemas from the content map. + public var referencedSchemaMap: + OrderedDictionary + { + var results = OrderedDictionary() + for content in contentMap.values { + results.merge(content.referencedSchemaMap) + } + return results + } +} diff --git a/Sources/FeatherOpenAPI/RequestBody/BinaryRequestBodyRepresentable.swift b/Sources/FeatherOpenAPI/RequestBody/BinaryRequestBodyRepresentable.swift new file mode 100644 index 0000000..762750a --- /dev/null +++ b/Sources/FeatherOpenAPI/RequestBody/BinaryRequestBodyRepresentable.swift @@ -0,0 +1,23 @@ +// +// BinaryRequestBodyRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Request body with binary content. +public protocol BinaryRequestBodyRepresentable: RequestBodyRepresentable { + +} + +extension BinaryRequestBodyRepresentable { + + /// Builds a binary content map using an octet-stream schema. + public var contentMap: ContentMap { + [ + .other("application/octet-stream"): Content(BinarySchema()) + ] + } +} diff --git a/Sources/FeatherOpenAPI/RequestBody/FormRequestBodyRepresentable.swift b/Sources/FeatherOpenAPI/RequestBody/FormRequestBodyRepresentable.swift new file mode 100644 index 0000000..769c183 --- /dev/null +++ b/Sources/FeatherOpenAPI/RequestBody/FormRequestBodyRepresentable.swift @@ -0,0 +1,27 @@ +// +// FormRequestBodyRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Request body with form-encoded content. +public protocol FormRequestBodyRepresentable: RequestBodyRepresentable { + /// The schema type used in the form content. + associatedtype SchemaType: SchemaRepresentable + + /// The schema instance for the form request body. + var schema: SchemaType { get } +} + +extension FormRequestBodyRepresentable { + + /// Builds a form content map from the schema. + public var contentMap: ContentMap { + [ + .form: Content(schema) + ] + } +} diff --git a/Sources/FeatherOpenAPI/RequestBody/JSONRequestBodyRepresentable.swift b/Sources/FeatherOpenAPI/RequestBody/JSONRequestBodyRepresentable.swift new file mode 100644 index 0000000..2e7cd16 --- /dev/null +++ b/Sources/FeatherOpenAPI/RequestBody/JSONRequestBodyRepresentable.swift @@ -0,0 +1,27 @@ +// +// JSONRequestBodyRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Request body with JSON content. +public protocol JSONRequestBodyRepresentable: RequestBodyRepresentable { + /// The JSON schema type used in the request body. + associatedtype SchemaType: SchemaRepresentable + + /// The schema instance for the JSON request body. + var schema: SchemaType { get } +} + +extension JSONRequestBodyRepresentable { + + /// Builds a JSON content map from the schema. + public var contentMap: ContentMap { + [ + .json: Content(schema) + ] + } +} diff --git a/Sources/FeatherOpenAPI/Response/Abstraction/OpenAPIResponseRepresentable.swift b/Sources/FeatherOpenAPI/Response/Abstraction/OpenAPIResponseRepresentable.swift new file mode 100644 index 0000000..38204c6 --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/Abstraction/OpenAPIResponseRepresentable.swift @@ -0,0 +1,28 @@ +// +// OpenAPIResponseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI response or reference. +public protocol OpenAPIResponseRepresentable { + /// Returns the OpenAPI response representation. + /// - Returns: An OpenAPI response or reference. + func openAPIResponse() -> Either< + JSONReference, OpenAPI.Response + > +} + +extension OpenAPI.Response: OpenAPIResponseRepresentable { + + /// Returns `self` wrapped as an OpenAPI response. + /// - Returns: An OpenAPI response value. + public func openAPIResponse() -> Either< + JSONReference, OpenAPI.Response + > { + .init(self) + } +} diff --git a/Sources/FeatherOpenAPI/Response/Abstraction/ResponseID.swift b/Sources/FeatherOpenAPI/Response/Abstraction/ResponseID.swift new file mode 100644 index 0000000..6aae4bf --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/Abstraction/ResponseID.swift @@ -0,0 +1,20 @@ +// +// ResponseID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +public struct ResponseID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a response identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Response/Abstraction/ResponseMap.swift b/Sources/FeatherOpenAPI/Response/Abstraction/ResponseMap.swift new file mode 100644 index 0000000..7962a6a --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/Abstraction/ResponseMap.swift @@ -0,0 +1,14 @@ +// +// ResponseMap.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Ordered map of response status codes to response definitions. +public typealias ResponseMap = OrderedDictionary< + OpenAPI.Response.StatusCode, + ResponseRepresentable +> diff --git a/Sources/FeatherOpenAPI/Response/Abstraction/ResponseRepresentable.swift b/Sources/FeatherOpenAPI/Response/Abstraction/ResponseRepresentable.swift new file mode 100644 index 0000000..ec5c156 --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/Abstraction/ResponseRepresentable.swift @@ -0,0 +1,50 @@ +// +// ResponseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI response with defaults. +public protocol ResponseRepresentable: + OpenAPIResponseRepresentable, + Identifiable, + VendorExtensionsProperty +{ + /// Response description. + var description: String { get } + /// Map of response headers. + var headerMap: HeaderMap { get } + /// Map of response content by content type. + var contentMap: ContentMap { get } +} + +extension ResponseRepresentable { + + /// Default header map is empty. + public var headerMap: HeaderMap { [:] } + + /// Creates a reference wrapper for this response. + /// - Returns: A response reference. + public func reference() -> ResponseReference { + .init(self) + } + + /// Builds an OpenAPI response object or reference. + /// - Returns: The OpenAPI response representation. + public func openAPIResponse() -> Either< + JSONReference, OpenAPI.Response + > { + .init( + .init( + description: description, + headers: headerMap.mapValues { $0.openAPIHeader() }, + content: contentMap.mapValues { $0.openAPIContent() }, + links: [:], + vendorExtensions: vendorExtensions + ) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Response/BinaryResponseRepresentable.swift b/Sources/FeatherOpenAPI/Response/BinaryResponseRepresentable.swift new file mode 100644 index 0000000..6f8743e --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/BinaryResponseRepresentable.swift @@ -0,0 +1,23 @@ +// +// BinaryResponseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Response with binary content. +public protocol BinaryResponseRepresentable: ResponseRepresentable { + +} + +extension BinaryResponseRepresentable { + + /// Builds a binary content map using an octet-stream schema. + public var contentMap: ContentMap { + [ + .other("application/octet-stream"): Content(BinarySchema()) + ] + } +} diff --git a/Sources/FeatherOpenAPI/Response/FormResponseRepresentable.swift b/Sources/FeatherOpenAPI/Response/FormResponseRepresentable.swift new file mode 100644 index 0000000..ca00ede --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/FormResponseRepresentable.swift @@ -0,0 +1,27 @@ +// +// FormResponseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Response with form-encoded content. +public protocol FormResponseRepresentable: ResponseRepresentable { + /// The schema type used in the form content. + associatedtype SchemaType: SchemaRepresentable + + /// The schema instance for the form response. + var schema: SchemaType { get } +} + +extension FormResponseRepresentable { + + /// Builds a form content map from the schema. + public var contentMap: ContentMap { + [ + .form: Content(schema) + ] + } +} diff --git a/Sources/FeatherOpenAPI/Response/JSONResponseRepresentable.swift b/Sources/FeatherOpenAPI/Response/JSONResponseRepresentable.swift new file mode 100644 index 0000000..b338180 --- /dev/null +++ b/Sources/FeatherOpenAPI/Response/JSONResponseRepresentable.swift @@ -0,0 +1,27 @@ +// +// JSONResponseRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Response with JSON content. +public protocol JSONResponseRepresentable: ResponseRepresentable { + /// The JSON schema type used in the response content. + associatedtype SchemaType: SchemaRepresentable + + /// The schema instance for the JSON response. + var schema: SchemaType { get } +} + +extension JSONResponseRepresentable { + + /// Builds a JSON content map from the schema. + public var contentMap: ContentMap { + [ + .json: Content(schema) + ] + } +} diff --git a/Sources/FeatherOpenAPI/Schema/Abstraction/OpenAPISchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/Abstraction/OpenAPISchemaRepresentable.swift new file mode 100644 index 0000000..a0888a2 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/Abstraction/OpenAPISchemaRepresentable.swift @@ -0,0 +1,23 @@ +// +// OpenAPISchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI JSON schema. +public protocol OpenAPISchemaRepresentable { + + /// Returns the JSON schema representation. + /// - Returns: The JSON schema. + func openAPISchema() -> JSONSchema +} + +extension JSONSchema: OpenAPISchemaRepresentable { + + /// Returns `self` as a JSON schema. + /// - Returns: The current schema value. + public func openAPISchema() -> JSONSchema { self } +} diff --git a/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaID.swift b/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaID.swift new file mode 100644 index 0000000..08c9133 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaID.swift @@ -0,0 +1,20 @@ +// +// SchemaID.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +public struct SchemaID: Sendable, Equatable, Hashable, Codable { + + /// The raw identifier value. + public var rawValue: String + + /// Creates a schema identifier. + /// - Parameter rawValue: The raw identifier value. + public init( + _ rawValue: String + ) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaMap.swift b/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaMap.swift new file mode 100644 index 0000000..6151b52 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaMap.swift @@ -0,0 +1,14 @@ +// +// SchemaMap.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Ordered map of schema names to schema definitions. +public typealias SchemaMap = OrderedDictionary< + String, + SchemaRepresentable +> diff --git a/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaRepresentable.swift new file mode 100644 index 0000000..e93e589 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/Abstraction/SchemaRepresentable.swift @@ -0,0 +1,73 @@ +// +// SchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI schema with common properties. +public protocol SchemaRepresentable: + OpenAPISchemaRepresentable, + Identifiable, + ReferencedSchemaMapRepresentable, + // shared properties + RequiredProperty, + TitleProperty, + DescriptionProperty, + NullableProperty +{ + /// Indicates whether the schema is deprecated. + var deprecated: Bool? { get } +} + +extension SchemaRepresentable { + + /// Creates a reference wrapper for this schema. + /// - Parameter required: Whether the reference is required. + /// - Returns: A schema reference. + public func reference( + required: Bool = true + ) -> SchemaReference { + .init(self) + } + + /// Default deprecated flag is `nil`. + public var deprecated: Bool? { nil } + + /// Referenced schemas directly used by this schema. + public var referencedSchemaMap: + OrderedDictionary + { + [:] + } + + /// Collects all referenced schemas transitively. + /// - Returns: An ordered dictionary of all referenced schemas. + public func allReferencedSchemaMap() -> OrderedDictionary< + SchemaID, OpenAPISchemaRepresentable + > { + var results = OrderedDictionary() + var visited = Set() + collectReferencedSchemaMap(into: &results, visited: &visited) + return results + } + + fileprivate func collectReferencedSchemaMap( + into results: + inout OrderedDictionary, + visited: inout Set + ) { + for (id, schema) in referencedSchemaMap + where visited.insert(id).inserted { + results[id] = schema + if let schema = schema as? SchemaRepresentable { + schema.collectReferencedSchemaMap( + into: &results, + visited: &visited + ) + } + } + } +} diff --git a/Sources/FeatherOpenAPI/Schema/ArraySchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/ArraySchemaRepresentable.swift new file mode 100644 index 0000000..f257efe --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/ArraySchemaRepresentable.swift @@ -0,0 +1,57 @@ +// +// ArraySchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for arrays. +public protocol ArraySchemaRepresentable: + SchemaRepresentable +{ + /// The item schema for the array. + var items: SchemaRepresentable? { get } +} + +extension ArraySchemaRepresentable { + + /// Builds an array JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .array( + format: .generic, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + minItems: nil, + maxItems: nil, + uniqueItems: nil, + items: items?.openAPISchema(), + allowedValues: nil, + defaultValue: nil, + example: nil + ) + } + + /// Referenced schemas used by the array items. + public var referencedSchemaMap: + OrderedDictionary + { + var results: OrderedDictionary = + [:] + + for (key, value) in items?.referencedSchemaMap ?? [:] { + // if let ref = value as? SchemaReferenceRepresentable { + results[key] = value + // } + } + return results + } +} diff --git a/Sources/FeatherOpenAPI/Schema/BinarySchema.swift b/Sources/FeatherOpenAPI/Schema/BinarySchema.swift new file mode 100644 index 0000000..2f86748 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/BinarySchema.swift @@ -0,0 +1,23 @@ +// +// BinarySchema.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 24.. +// + +import OpenAPIKit30 + +struct BinarySchema: SchemaRepresentable { + + func openAPISchema() -> JSONSchema { + JSONSchema.string( + format: .binary + ) + } + + var referencedSchemaMap: + OrderedDictionary + { + [:] + } +} diff --git a/Sources/FeatherOpenAPI/Schema/BoolSchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/BoolSchemaRepresentable.swift new file mode 100644 index 0000000..d4976da --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/BoolSchemaRepresentable.swift @@ -0,0 +1,42 @@ +// +// BoolSchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for boolean values. +public protocol BoolSchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty +where + ExamplePropertyType == Bool, + DefaultValuePropertyType == Bool +{ + +} + +extension BoolSchemaRepresentable { + + /// Builds a boolean JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .boolean( + format: .generic, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + allowedValues: nil, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Schema/DoubleSchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/DoubleSchemaRepresentable.swift new file mode 100644 index 0000000..b4a6287 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/DoubleSchemaRepresentable.swift @@ -0,0 +1,47 @@ +// +// DoubleSchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for double values. +public protocol DoubleSchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty, + AllowedValuesProperty +where + ExamplePropertyType == Double, + DefaultValuePropertyType == Double, + AllowedValuesPropertyType == Double +{ + +} + +extension DoubleSchemaRepresentable { + + /// Builds a double JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .number( + format: .double, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + multipleOf: nil, + maximum: nil, + minimum: nil, + allowedValues: allowedValues?.map { .init($0) }, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Schema/FloatSchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/FloatSchemaRepresentable.swift new file mode 100644 index 0000000..7b641e7 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/FloatSchemaRepresentable.swift @@ -0,0 +1,47 @@ +// +// FloatSchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for float values. +public protocol FloatSchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty, + AllowedValuesProperty +where + ExamplePropertyType == Float, + DefaultValuePropertyType == Float, + AllowedValuesPropertyType == Float +{ + +} + +extension FloatSchemaRepresentable { + + /// Builds a float JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .number( + format: .float, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + multipleOf: nil, + maximum: nil, + minimum: nil, + allowedValues: allowedValues?.map { .init($0) }, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Schema/Int32SchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/Int32SchemaRepresentable.swift new file mode 100644 index 0000000..ce2d557 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/Int32SchemaRepresentable.swift @@ -0,0 +1,47 @@ +// +// Int32SchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for 32-bit integer values. +public protocol Int32SchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty, + AllowedValuesProperty +where + ExamplePropertyType == Int32, + DefaultValuePropertyType == Int32, + AllowedValuesPropertyType == Int32 +{ + +} + +extension Int32SchemaRepresentable { + + /// Builds an int32 JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .integer( + format: .int32, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + multipleOf: nil, + maximum: nil, + minimum: nil, + allowedValues: allowedValues?.map { .init($0) }, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Schema/Int64SchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/Int64SchemaRepresentable.swift new file mode 100644 index 0000000..43e15a4 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/Int64SchemaRepresentable.swift @@ -0,0 +1,47 @@ +// +// Int64SchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for 64-bit integer values. +public protocol Int64SchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty, + AllowedValuesProperty +where + ExamplePropertyType == Int64, + DefaultValuePropertyType == Int64, + AllowedValuesPropertyType == Int64 +{ + +} + +extension Int64SchemaRepresentable { + + /// Builds an int64 JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .integer( + format: .int64, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + multipleOf: nil, + maximum: nil, + minimum: nil, + allowedValues: allowedValues?.map { .init($0) }, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Schema/IntSchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/IntSchemaRepresentable.swift new file mode 100644 index 0000000..3030ef6 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/IntSchemaRepresentable.swift @@ -0,0 +1,47 @@ +// +// IntSchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for integer values. +public protocol IntSchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty, + AllowedValuesProperty +where + ExamplePropertyType == Int, + DefaultValuePropertyType == Int, + AllowedValuesPropertyType == Int +{ + +} + +extension IntSchemaRepresentable { + + /// Builds an integer JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .integer( + format: .unspecified, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + multipleOf: nil, + maximum: nil, + minimum: nil, + allowedValues: allowedValues?.map { .init($0) }, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/Schema/ObjectSchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/ObjectSchemaRepresentable.swift new file mode 100644 index 0000000..e159534 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/ObjectSchemaRepresentable.swift @@ -0,0 +1,56 @@ +// +// ObjectSchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for objects. +public protocol ObjectSchemaRepresentable: + SchemaRepresentable, + ExampleProperty +where + ExamplePropertyType == AnyCodable +{ + /// Map of property names to schemas. + var propertyMap: SchemaMap { get } +} + +extension ObjectSchemaRepresentable { + + /// Builds an object JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .object( + format: .generic, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + minProperties: nil, + maxProperties: nil, + properties: propertyMap.mapValues { $0.openAPISchema() }, + additionalProperties: nil, + allowedValues: nil, + defaultValue: nil, + example: example + ) + } + + /// Referenced schemas used by object properties. + public var referencedSchemaMap: + OrderedDictionary + { + var results = OrderedDictionary() + for (_, value) in propertyMap { + results.merge(value.referencedSchemaMap) + } + return results + } +} diff --git a/Sources/FeatherOpenAPI/Schema/StringSchemaRepresentable.swift b/Sources/FeatherOpenAPI/Schema/StringSchemaRepresentable.swift new file mode 100644 index 0000000..f0d9223 --- /dev/null +++ b/Sources/FeatherOpenAPI/Schema/StringSchemaRepresentable.swift @@ -0,0 +1,47 @@ +// +// StringSchemaRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Schema representation for string values. +public protocol StringSchemaRepresentable: + SchemaRepresentable, + ExampleProperty, + DefaultValueProperty, + AllowedValuesProperty +where + ExamplePropertyType == String, + DefaultValuePropertyType == String, + AllowedValuesPropertyType == String +{ + +} + +extension StringSchemaRepresentable { + + /// Builds a string JSON schema. + /// - Returns: The JSON schema. + public func openAPISchema() -> JSONSchema { + .string( + format: .generic, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + minLength: nil, + maxLength: nil, + pattern: nil, + allowedValues: allowedValues?.map { .init($0) }, + defaultValue: .init(defaultValue), + example: .init(example) + ) + } +} diff --git a/Sources/FeatherOpenAPI/SecurityRequirement/OpenAPISecurityRequirementRepresentable.swift b/Sources/FeatherOpenAPI/SecurityRequirement/OpenAPISecurityRequirementRepresentable.swift new file mode 100644 index 0000000..1e0ff1c --- /dev/null +++ b/Sources/FeatherOpenAPI/SecurityRequirement/OpenAPISecurityRequirementRepresentable.swift @@ -0,0 +1,26 @@ +// +// OpenAPISecurityRequirementRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI security requirement. +public protocol OpenAPISecurityRequirementRepresentable { + + // [JSONReference: [String]] + /// Returns the OpenAPI security requirement representation. + /// - Returns: The OpenAPI security requirement. + func openAPISecurityRequirement() -> OpenAPI.SecurityRequirement +} + +extension OpenAPI.SecurityRequirement: OpenAPISecurityRequirementRepresentable { + + /// Returns `self` as an OpenAPI security requirement. + /// - Returns: The current security requirement value. + public func openAPISecurityRequirement() -> OpenAPI.SecurityRequirement { + self + } +} diff --git a/Sources/FeatherOpenAPI/SecurityRequirement/SecurityRequirementRepresentable.swift b/Sources/FeatherOpenAPI/SecurityRequirement/SecurityRequirementRepresentable.swift new file mode 100644 index 0000000..962f7d9 --- /dev/null +++ b/Sources/FeatherOpenAPI/SecurityRequirement/SecurityRequirementRepresentable.swift @@ -0,0 +1,33 @@ +// +// SecurityRequirementRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes a security requirement for an operation or document. +public protocol SecurityRequirementRepresentable: + OpenAPISecurityRequirementRepresentable +{ + /// The referenced security scheme. + var security: any SecuritySchemeRepresentable { get } + /// The required scopes or requirements. + var requirements: [String] { get } +} + +extension SecurityRequirementRepresentable { + + /// Default requirements are empty. + public var requirements: [String] { [] } + + //[JSONReference: [String]] + /// Builds an OpenAPI security requirement. + /// - Returns: The OpenAPI security requirement. + public func openAPISecurityRequirement() -> OpenAPI.SecurityRequirement { + [ + .component(named: security.openAPIIdentifier): requirements + ] + } +} diff --git a/Sources/FeatherOpenAPI/SecurityScheme/OpenAPISecuritySchemeRepresentable.swift b/Sources/FeatherOpenAPI/SecurityScheme/OpenAPISecuritySchemeRepresentable.swift new file mode 100644 index 0000000..ce29e15 --- /dev/null +++ b/Sources/FeatherOpenAPI/SecurityScheme/OpenAPISecuritySchemeRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPISecuritySchemeRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI security scheme. +public protocol OpenAPISecuritySchemeRepresentable { + /// Returns the OpenAPI security scheme representation. + /// - Returns: The OpenAPI security scheme. + func openAPISecurityScheme() -> OpenAPI.SecurityScheme +} + +extension OpenAPI.SecurityScheme: OpenAPISecuritySchemeRepresentable { + + /// Returns `self` as an OpenAPI security scheme. + /// - Returns: The current security scheme value. + public func openAPISecurityScheme() -> OpenAPI.SecurityScheme { + self + } +} diff --git a/Sources/FeatherOpenAPI/SecurityScheme/SecuritySchemeRepresentable.swift b/Sources/FeatherOpenAPI/SecurityScheme/SecuritySchemeRepresentable.swift new file mode 100644 index 0000000..50481f9 --- /dev/null +++ b/Sources/FeatherOpenAPI/SecurityScheme/SecuritySchemeRepresentable.swift @@ -0,0 +1,32 @@ +// +// SecuritySchemeRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI security scheme. +public protocol SecuritySchemeRepresentable: + OpenAPISecuritySchemeRepresentable, + Identifiable, + DescriptionProperty, + VendorExtensionsProperty +{ + /// The security scheme type. + var type: OpenAPI.SecurityScheme.SecurityType { get } +} + +extension SecuritySchemeRepresentable { + + /// Builds an OpenAPI security scheme. + /// - Returns: The OpenAPI security scheme. + public func openAPISecurityScheme() -> OpenAPI.SecurityScheme { + .init( + type: type, + description: description, + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Server/OpenAPIServerRepresentable.swift b/Sources/FeatherOpenAPI/Server/OpenAPIServerRepresentable.swift new file mode 100644 index 0000000..e29b800 --- /dev/null +++ b/Sources/FeatherOpenAPI/Server/OpenAPIServerRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIServerRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI server. +public protocol OpenAPIServerRepresentable { + /// Returns the OpenAPI server representation. + /// - Returns: The OpenAPI server. + func openAPIServer() -> OpenAPI.Server +} + +extension OpenAPI.Server: OpenAPIServerRepresentable { + + /// Returns `self` as an OpenAPI server. + /// - Returns: The current server value. + public func openAPIServer() -> OpenAPI.Server { + self + } +} diff --git a/Sources/FeatherOpenAPI/Server/ServerRepresentable.swift b/Sources/FeatherOpenAPI/Server/ServerRepresentable.swift new file mode 100644 index 0000000..f7c108a --- /dev/null +++ b/Sources/FeatherOpenAPI/Server/ServerRepresentable.swift @@ -0,0 +1,42 @@ +// +// ServerRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI server object with defaults. +public protocol ServerRepresentable: + OpenAPIServerRepresentable, + DescriptionProperty, + VendorExtensionsProperty +{ + + /// Server URL template. + var url: LocationRepresentable { get } + + /// Server variable definitions. + var variables: VariableMap { get } + +} + +extension ServerRepresentable { + + /// Default server variables map. + public var variables: VariableMap { .init() } + + /// Builds an OpenAPI server object. + /// - Returns: The OpenAPI server. + public func openAPIServer() -> OpenAPI.Server { + .init( + url: url.openAPILocation(), + description: description, + variables: variables.mapValues { + $0.openAPIServerVariable() + }, + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Tag/OpenAPITagRepresentable.swift b/Sources/FeatherOpenAPI/Tag/OpenAPITagRepresentable.swift new file mode 100644 index 0000000..387c83f --- /dev/null +++ b/Sources/FeatherOpenAPI/Tag/OpenAPITagRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPITagRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI tag. +public protocol OpenAPITagRepresentable { + /// Returns the OpenAPI tag representation. + /// - Returns: The OpenAPI tag. + func openAPITag() -> OpenAPI.Tag +} + +extension OpenAPI.Tag: OpenAPITagRepresentable { + + /// Returns `self` as an OpenAPI tag. + /// - Returns: The current tag value. + public func openAPITag() -> OpenAPI.Tag { + self + } +} diff --git a/Sources/FeatherOpenAPI/Tag/TagRepresentable.swift b/Sources/FeatherOpenAPI/Tag/TagRepresentable.swift new file mode 100644 index 0000000..44530dc --- /dev/null +++ b/Sources/FeatherOpenAPI/Tag/TagRepresentable.swift @@ -0,0 +1,38 @@ +// +// TagRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI tag with defaults. +public protocol TagRepresentable: + OpenAPITagRepresentable, + Identifiable, + DescriptionProperty, + VendorExtensionsProperty +{ + /// Tag display name. + var name: String { get } + /// External documentation for the tag. + var externalDocs: ExternalDocsRepresentable? { get } +} + +extension TagRepresentable { + + /// Default external docs is `nil`. + public var externalDocs: ExternalDocsRepresentable? { nil } + + /// Builds an OpenAPI tag object. + /// - Returns: The OpenAPI tag. + public func openAPITag() -> OpenAPI.Tag { + .init( + name: name, + description: description, + externalDocs: externalDocs?.openAPIExternalDocs(), + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/Variable/OpenAPIVariableRepresentable.swift b/Sources/FeatherOpenAPI/Variable/OpenAPIVariableRepresentable.swift new file mode 100644 index 0000000..e279064 --- /dev/null +++ b/Sources/FeatherOpenAPI/Variable/OpenAPIVariableRepresentable.swift @@ -0,0 +1,24 @@ +// +// OpenAPIVariableRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// A type that can produce an OpenAPI server variable. +public protocol OpenAPIVariableRepresentable { + /// Returns the OpenAPI server variable representation. + /// - Returns: The OpenAPI server variable. + func openAPIServerVariable() -> OpenAPI.Server.Variable +} + +extension OpenAPI.Server.Variable: OpenAPIVariableRepresentable { + + /// Returns `self` as an OpenAPI server variable. + /// - Returns: The current server variable value. + public func openAPIServerVariable() -> OpenAPI.Server.Variable { + self + } +} diff --git a/Sources/FeatherOpenAPI/Variable/VariableMap.swift b/Sources/FeatherOpenAPI/Variable/VariableMap.swift new file mode 100644 index 0000000..865b640 --- /dev/null +++ b/Sources/FeatherOpenAPI/Variable/VariableMap.swift @@ -0,0 +1,14 @@ +// +// VariableMap.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Ordered map of server variable names to variable definitions. +public typealias VariableMap = OrderedDictionary< + String, + OpenAPIVariableRepresentable +> diff --git a/Sources/FeatherOpenAPI/Variable/VariableRepresentable.swift b/Sources/FeatherOpenAPI/Variable/VariableRepresentable.swift new file mode 100644 index 0000000..c5aa7a3 --- /dev/null +++ b/Sources/FeatherOpenAPI/Variable/VariableRepresentable.swift @@ -0,0 +1,34 @@ +// +// VariableRepresentable.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import OpenAPIKit30 + +/// Describes an OpenAPI server variable. +public protocol VariableRepresentable: + OpenAPIVariableRepresentable, + DescriptionProperty, + VendorExtensionsProperty +{ + /// Allowed values for the variable. + var `enum`: [String] { get } + /// Default value for the variable. + var `default`: String { get } +} + +extension VariableRepresentable { + + /// Builds an OpenAPI server variable. + /// - Returns: The OpenAPI server variable. + public func openAPIServerVariable() -> OpenAPI.Server.Variable { + .init( + enum: `enum`, + default: `default`, + description: description, + vendorExtensions: vendorExtensions + ) + } +} diff --git a/Sources/FeatherOpenAPI/_Properties/AllowedValuesProperty.swift b/Sources/FeatherOpenAPI/_Properties/AllowedValuesProperty.swift new file mode 100644 index 0000000..3b3d953 --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/AllowedValuesProperty.swift @@ -0,0 +1,22 @@ +// +// AllowedValuesProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Provides allowed values for schemas. +public protocol AllowedValuesProperty { + /// The associated allowed value type. + associatedtype AllowedValuesPropertyType = AnyCodable + + /// Allowed values for the schema. + var allowedValues: [AllowedValuesPropertyType]? { get } +} + +extension AllowedValuesProperty { + /// Default allowed values are `nil`. + public var allowedValues: [AllowedValuesPropertyType]? { nil } +} diff --git a/Sources/FeatherOpenAPI/_Properties/DefaultValueProperty.swift b/Sources/FeatherOpenAPI/_Properties/DefaultValueProperty.swift new file mode 100644 index 0000000..b56154f --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/DefaultValueProperty.swift @@ -0,0 +1,22 @@ +// +// DefaultValueProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Provides a default value for schemas. +public protocol DefaultValueProperty { + /// The associated default value type. + associatedtype DefaultValuePropertyType = AnyCodable + + /// The default value. + var defaultValue: DefaultValuePropertyType? { get } +} + +extension DefaultValueProperty { + /// Default default value is `nil`. + public var defaultValue: DefaultValuePropertyType? { nil } +} diff --git a/Sources/FeatherOpenAPI/_Properties/DeprecatedProperty.swift b/Sources/FeatherOpenAPI/_Properties/DeprecatedProperty.swift new file mode 100644 index 0000000..e14bd7f --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/DeprecatedProperty.swift @@ -0,0 +1,16 @@ +// +// DeprecatedProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public protocol DeprecatedProperty { + /// Indicates whether the item is deprecated. + var deprecated: Bool { get } +} + +extension DeprecatedProperty { + /// Default deprecated value is `false`. + public var deprecated: Bool { false } +} diff --git a/Sources/FeatherOpenAPI/_Properties/DescriptionProperty.swift b/Sources/FeatherOpenAPI/_Properties/DescriptionProperty.swift new file mode 100644 index 0000000..3e9a7a9 --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/DescriptionProperty.swift @@ -0,0 +1,16 @@ +// +// DescriptionProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public protocol DescriptionProperty { + /// Human-readable description. + var description: String? { get } +} + +extension DescriptionProperty { + /// Default description is `nil`. + public var description: String? { nil } +} diff --git a/Sources/FeatherOpenAPI/_Properties/ExampleProperty.swift b/Sources/FeatherOpenAPI/_Properties/ExampleProperty.swift new file mode 100644 index 0000000..517fcf2 --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/ExampleProperty.swift @@ -0,0 +1,22 @@ +// +// ExampleProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 + +/// Provides an example value for schemas. +public protocol ExampleProperty { + /// The associated example value type. + associatedtype ExamplePropertyType = AnyCodable + + /// The example value. + var example: ExamplePropertyType? { get } +} + +extension ExampleProperty { + /// Default example value is `nil`. + public var example: ExamplePropertyType? { nil } +} diff --git a/Sources/FeatherOpenAPI/_Properties/NullableProperty.swift b/Sources/FeatherOpenAPI/_Properties/NullableProperty.swift new file mode 100644 index 0000000..ca66936 --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/NullableProperty.swift @@ -0,0 +1,16 @@ +// +// NullableProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public protocol NullableProperty { + /// Indicates whether the schema value may be null. + var nullable: Bool? { get } +} + +extension NullableProperty { + /// Default nullable value is `nil`. + public var nullable: Bool? { nil } +} diff --git a/Sources/FeatherOpenAPI/_Properties/RequiredProperty.swift b/Sources/FeatherOpenAPI/_Properties/RequiredProperty.swift new file mode 100644 index 0000000..30ce25e --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/RequiredProperty.swift @@ -0,0 +1,16 @@ +// +// RequiredProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +public protocol RequiredProperty { + /// Indicates whether the item is required. + var required: Bool { get } +} + +extension RequiredProperty { + /// Default required value is `true`. + public var required: Bool { true } +} diff --git a/Sources/FeatherOpenAPI/_Properties/TitleProperty.swift b/Sources/FeatherOpenAPI/_Properties/TitleProperty.swift new file mode 100644 index 0000000..2609ef6 --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/TitleProperty.swift @@ -0,0 +1,16 @@ +// +// TitleProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +public protocol TitleProperty { + /// Short title. + var title: String? { get } +} + +extension TitleProperty { + /// Default title is `nil`. + public var title: String? { nil } +} diff --git a/Sources/FeatherOpenAPI/_Properties/VendorExtensionsProperty.swift b/Sources/FeatherOpenAPI/_Properties/VendorExtensionsProperty.swift new file mode 100644 index 0000000..66c0cf4 --- /dev/null +++ b/Sources/FeatherOpenAPI/_Properties/VendorExtensionsProperty.swift @@ -0,0 +1,19 @@ +// +// VendorExtensionsProperty.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 23.. +// + +import OpenAPIKit30 + +/// Provides vendor extension storage. +public protocol VendorExtensionsProperty { + /// Vendor extension values keyed by extension name. + var vendorExtensions: [String: AnyCodable] { get } +} + +extension VendorExtensionsProperty { + /// Default vendor extensions are empty. + public var vendorExtensions: [String: AnyCodable] { [:] } +} diff --git a/Sources/FeatherOpenAPIKit/Extensions/JSONSchema+Enumeration.swift b/Sources/FeatherOpenAPIKit/Extensions/JSONSchema+Enumeration.swift deleted file mode 100644 index 22e7ac4..0000000 --- a/Sources/FeatherOpenAPIKit/Extensions/JSONSchema+Enumeration.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 10/01/2024. -// - -import OpenAPIKit - -public extension JSONSchema { - - static func enumeration( - description: String, - allowedValues: [AnyCodable], - defaultValue: AnyCodable? = nil, - examples: [AnyCodable] = [] - ) -> JSONSchema { - .string( - format: .generic, - description: description, - allowedValues: allowedValues, - defaultValue: defaultValue, - examples: examples - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Extensions/JSONSchema+Text.swift b/Sources/FeatherOpenAPIKit/Extensions/JSONSchema+Text.swift deleted file mode 100644 index 28bfeb0..0000000 --- a/Sources/FeatherOpenAPIKit/Extensions/JSONSchema+Text.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public extension JSONSchema { - - static func text( - description: String, - examples: [AnyCodable] = [] - ) -> Self { - .string( - format: .generic, - description: description, - examples: examples - ) - } - -} diff --git a/Sources/FeatherOpenAPIKit/Extensions/OrderedDictionary+Composition.swift b/Sources/FeatherOpenAPIKit/Extensions/OrderedDictionary+Composition.swift deleted file mode 100644 index 20496d5..0000000 --- a/Sources/FeatherOpenAPIKit/Extensions/OrderedDictionary+Composition.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 10/01/2024. -// - -import OpenAPIKit - -public extension OrderedDictionary { - - static func + (lhs: Self, rhs: Self) -> Self { - var result = lhs - for key in rhs.keys { - result[key] = rhs[key] - } - return result - } -} diff --git a/Sources/FeatherOpenAPIKit/Extensions/String+LowercasedFirstLetter.swift b/Sources/FeatherOpenAPIKit/Extensions/String+LowercasedFirstLetter.swift deleted file mode 100644 index 121e2a8..0000000 --- a/Sources/FeatherOpenAPIKit/Extensions/String+LowercasedFirstLetter.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -extension String { - - func lowercasedFirstLetter() -> String { - guard !isEmpty else { - return self - } - return prefix(1).lowercased() + dropFirst() - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Component.swift b/Sources/FeatherOpenAPIKit/Interfaces/Component.swift deleted file mode 100644 index cb86666..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Component.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -public protocol Component { - - // NOTE: no support for examples yet in OpeAPIKit - // static var examples: [Example.Type] { get } - static var schemas: [Schema.Type] { get } - - static var parameters: [Parameter.Type] { get } - static var headers: [Header.Type] { get } - static var requestBodies: [RequestBody.Type] { get } - static var securitySchemes: [SecurityScheme.Type] { get } - - static var responses: [Response.Type] { get } - - static var tags: [Tag.Type] { get } - static var operations: [Operation.Type] { get } - static var pathItems: [PathItem.Type] { get } - - static func getComponentsOfType() -> [T] - static func getComponentsOfType(_: T.Type) -> [T] -} - -public extension Component { - static var schemas: [Schema.Type] { getComponentsOfType() } - static var parameters: [Parameter.Type] { getComponentsOfType() } - static var headers: [Header.Type] { getComponentsOfType() } - static var requestBodies: [RequestBody.Type] { getComponentsOfType() } - static var securitySchemes: [SecurityScheme.Type] { getComponentsOfType() } - static var responses: [Response.Type] { getComponentsOfType() } - static var tags: [Tag.Type] { getComponentsOfType() } - static var operations: [Operation.Type] { getComponentsOfType() } - static var pathItems: [PathItem.Type] { getComponentsOfType() } - - static func getComponentsOfType(_: T.Type) -> [T] { - getComponentsOfType() - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Document.swift b/Sources/FeatherOpenAPIKit/Interfaces/Document.swift deleted file mode 100644 index e997363..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Document.swift +++ /dev/null @@ -1,196 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit -import OpenAPIKitCore - -// https://spec.openapis.org/oas/latest.html -public protocol Document { - var components: [Component.Type] { get } - - func openAPIDocument() throws -> OpenAPI.Document - - func schemas() throws -> OpenAPI.ComponentDictionary - func parameters() throws -> OpenAPI.ComponentDictionary - func headers() throws -> OpenAPI.ComponentDictionary - func requestBodies() throws -> OpenAPI.ComponentDictionary - func securitySchemes() throws - -> OpenAPI.ComponentDictionary - func responses() throws -> OpenAPI.ComponentDictionary - func tags() throws -> [OpenAPI.Tag] - func paths() throws -> OpenAPI.PathItem.Map - - func composedDocument( - info: OpenAPI.Document.Info, - servers: [OpenAPI.Server] - ) throws -> OpenAPI.Document -} - -public struct ComposeDocumentError: Swift.Error { - public let message: String -} - -public extension Document { - - static private func filterIdentifiables( - lists: [[T]] - ) throws -> [T] { - var ret: [String: T] = [:] - - for list in lists { - for item in list { - let itemId = item as! any Identifiable.Type - - if ret[itemId.id] != nil { - if itemId.override == false { - throw ComposeDocumentError.init( - message: - "Feather OpenAPI item id is duplicated: '\(itemId.id)' (Did you forget to include override=true?)" - ) - } - } - else { - if itemId.override { - throw ComposeDocumentError.init( - message: - "Feather OpenAPI item '\(itemId.id)' is set as override but has no parent. (Are the component orders correct? Or are the IDs the same?)" - ) - } - } - - ret[itemId.id] = item - } - } - - return ret.sorted { $0.key < $1.key }.map { $0.value } - } - - func schemas() throws -> OpenAPI.ComponentDictionary { - return - try Self.filterIdentifiables( - lists: components.map { - $0.schemas - } - ) - .reduce(into: [:]) { into, item in - into[item.componentKey] = item.openAPISchema() - } - } - - func parameters() throws -> OpenAPI.ComponentDictionary { - return - try Self.filterIdentifiables( - lists: components.map { - $0.parameters - } - ) - .reduce(into: [:]) { into, item in - into[item.componentKey] = item.openAPIParameter() - } - } - - func headers() throws -> OpenAPI.ComponentDictionary { - return - try Self.filterIdentifiables( - lists: components.map { - $0.headers - } - ) - .reduce(into: [:]) { into, item in - into[item.componentKey] = item.openAPIHeader() - } - } - - func requestBodies() throws -> OpenAPI.ComponentDictionary - { - return - try Self.filterIdentifiables( - lists: components.map { - $0.requestBodies - } - ) - .reduce(into: [:]) { into, item in - into[item.componentKey] = item.openAPIRequestBody() - } - } - - func securitySchemes() throws - -> OpenAPI.ComponentDictionary - { - return - try Self.filterIdentifiables( - lists: components.map { - $0.securitySchemes - } - ) - .reduce(into: [:]) { into, item in - into[item.componentKey] = item.openAPISecurityScheme() - } - } - - func responses() throws -> OpenAPI.ComponentDictionary { - return - try Self.filterIdentifiables( - lists: components.map { - $0.responses - } - ) - .reduce(into: [:]) { into, item in - into[item.componentKey] = item.openAPIResponse() - } - } - - // MARK: - - - func tags() throws -> [OpenAPI.Tag] { - return - try Self.filterIdentifiables( - lists: components.map { - $0.tags - } - ) - .reduce(into: []) { into, item in - into.append(item.openAPITag()) - } - .sorted { lhs, rhs in - lhs.name < rhs.name - } - } - - func paths() throws -> OpenAPI.PathItem.Map { - return - try Self.filterIdentifiables( - lists: components.map { - $0.pathItems - } - ) - .sorted { $0.path.value < $1.path.value } - .reduce(into: [:]) { into, item in - into[item.openAPIPath] = .init(item.openAPIPathItem()) - } - } - - func composedDocument( - info: OpenAPI.Document.Info, - servers: [OpenAPI.Server] - ) throws -> OpenAPI.Document { - .init( - info: info, - servers: servers, - paths: try paths(), - components: .init( - schemas: try schemas(), - responses: try responses(), - parameters: try parameters(), - requestBodies: try requestBodies(), - headers: try headers(), - securitySchemes: try securitySchemes() - ), - tags: try tags() - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Header/Header.swift b/Sources/FeatherOpenAPIKit/Interfaces/Header/Header.swift deleted file mode 100644 index 5b1746e..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Header/Header.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPIHeader: Identifiable { - static func openAPIHeader() -> OpenAPI.Header -} - -public protocol Header: OpenAPIHeader { - static var name: String { get } - static var description: String { get } - static var schema: Schema.Type { get } -} - -public extension Header { - - static func reference() -> Either< - OpenAPI.Reference, OpenAPI.Header - > { - .reference(.component(named: id)) - } -} - -public extension Header { - - static var id: String { name } - - static func openAPIHeader() -> OpenAPI.Header { - .init( - schema: schema.reference(), - description: description - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Identifiable.swift b/Sources/FeatherOpenAPIKit/Interfaces/Identifiable.swift deleted file mode 100644 index 4cefb77..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Identifiable.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol Identifiable { - static var id: String { get } - static var override: Bool { get } -} - -public extension Identifiable { - - static var id: String { - var components = String(reflecting: self).split(separator: ".") - components.remove(at: 0) // remove namespace - components.remove(at: 2) // remove enum name - return components.joined(separator: "") - .replacingOccurrences(of: "GenericComponent", with: "Generic") - } - - static var override: Bool { false } -} - -public extension Identifiable { - static var componentKey: OpenAPI.ComponentKey { .init(stringLiteral: id) } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Operation/Operation.swift b/Sources/FeatherOpenAPIKit/Interfaces/Operation/Operation.swift deleted file mode 100644 index eeffe2a..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Operation/Operation.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPIOperation: Identifiable { - static func openAPIOperation() -> OpenAPI.Operation -} - -public protocol Operation: OpenAPIOperation { - static var operationId: String { get } - static var tag: Tag.Type { get } - static var summary: String { get } - static var description: String { get } - - static var parameters: [Parameter.Type] { get } - static var requestBody: RequestBody.Type? { get } - static var responses: [OperationResponse] { get } - static var security: [SecurityScheme.Type] { get } -} - -public extension Operation { - - static var operationId: String { - var components = String(reflecting: self) - .split(separator: ".") - .dropFirst() - .map(String.init) - - components.remove(at: 2) - if let last = components.popLast()?.lowercasedFirstLetter() { - components.insert(last, at: 0) - } - return components.joined(separator: "") - } - - static var parameters: [Parameter.Type] { [] } - static var requestBody: RequestBody.Type? { nil } - static var security: [SecurityScheme.Type] { [] } - - static func openAPIOperation() -> OpenAPI.Operation { - .init( - tags: tag.name, - summary: summary, - description: description, - operationId: operationId, - parameters: parameters.map { $0.reference() }, - requestBody: requestBody?.openAPIRequestBody(), - responses: responses.reduce(into: [:]) { - $0[.init(integerLiteral: $1.statusCode)] = $1.response - .reference() - }, - security: security.map { [$0.reference(): []] } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Operation/OperationResponse.swift b/Sources/FeatherOpenAPIKit/Interfaces/Operation/OperationResponse.swift deleted file mode 100644 index b196c5c..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Operation/OperationResponse.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -public struct OperationResponse { - - public let statusCode: Int - public let response: Response.Type - - public init( - _ statusCode: Int, - _ response: Response.Type - ) { - self.statusCode = statusCode - self.response = response - } -} - -public extension OperationResponse { - - static func ok(_ response: Response.Type) -> Self { - .init(200, response) - } - - static func found(_ response: Response.Type) -> Self { - .init(302, response) - } - - static func seeOther(_ response: Response.Type) -> Self { - .init(303, response) - } - - static func temporaryRedirect(_ response: Response.Type) -> Self { - .init(307, response) - } - - static func badRequest(_ response: Response.Type) -> Self { - .init(400, response) - } - - static func unauthorized(_ response: Response.Type) -> Self { - .init(401, response) - } - - static func forbidden(_ response: Response.Type) -> Self { - .init(403, response) - } - - static func notFound(_ response: Response.Type) -> Self { - .init(404, response) - } - - static func methodNotAllowed(_ response: Response.Type) -> Self { - .init(405, response) - } - - static func notAcceptable(_ response: Response.Type) -> Self { - .init(406, response) - } - - static func conflict(_ response: Response.Type) -> Self { - .init(409, response) - } - - static func gone(_ response: Response.Type) -> Self { - .init(410, response) - } - - static func unsupportedMediaType(_ response: Response.Type) -> Self { - .init(415, response) - } - - static func unprocessableContent(_ response: Response.Type) -> Self { - .init(422, response) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/HeaderParameter.swift b/Sources/FeatherOpenAPIKit/Interfaces/Parameter/HeaderParameter.swift deleted file mode 100644 index c5d69c5..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/HeaderParameter.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 15/02/2024. -// - -import OpenAPIKit - -public protocol HeaderParameter: Parameter {} - -public extension HeaderParameter { - - static var context: OpenAPI.Parameter.Context { - .header(required: Self.required) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/Parameter.swift b/Sources/FeatherOpenAPIKit/Interfaces/Parameter/Parameter.swift deleted file mode 100644 index d9ca974..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/Parameter.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPIParameter: Identifiable { - static func openAPIParameter() -> OpenAPI.Parameter -} - -public extension OpenAPIParameter { - - static func reference() -> Either< - OpenAPI.Reference, - OpenAPI.Parameter - > { - .reference(.component(named: id)) - } -} - -public protocol Parameter: OpenAPIParameter { - static var name: String { get } - static var context: OpenAPI.Parameter.Context { get } - static var schema: Schema.Type { get } - static var description: String { get } - static var required: Bool { get } -} - -public extension Parameter { - - static var required: Bool { true } - - static var path: Path { .parameter(name) } - - static func openAPIParameter() -> OpenAPI.Parameter { - .init( - name: name, - context: context, - schema: schema.reference(), - description: description - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/PathParameter.swift b/Sources/FeatherOpenAPIKit/Interfaces/Parameter/PathParameter.swift deleted file mode 100644 index de941aa..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/PathParameter.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol PathParameter: Parameter {} - -public extension PathParameter { - static var context: OpenAPI.Parameter.Context { .path } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/QueryParameter.swift b/Sources/FeatherOpenAPIKit/Interfaces/Parameter/QueryParameter.swift deleted file mode 100644 index 9f7b014..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Parameter/QueryParameter.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol QueryParameter: Parameter {} - -public extension QueryParameter { - - static var required: Bool { false } - - static var context: OpenAPI.Parameter.Context { - .query(required: Self.required) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/PathItem/Path.swift b/Sources/FeatherOpenAPIKit/Interfaces/PathItem/Path.swift deleted file mode 100644 index d96ee53..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/PathItem/Path.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -public struct Path: ExpressibleByStringLiteral { - - public let value: String - - public init(_ value: String) { - self.value = value - } - - public init(stringLiteral value: StringLiteralType) { - self.value = value - } - - public static func / (lhs: Self, rhs: Self) -> Self { - .init(lhs.value + "/" + rhs.value) - } - - public static func / (lhs: Self, rhs: String) -> Self { - lhs / Self(rhs) - } - - public static func / (lhs: String, rhs: Self) -> Self { - Self(lhs) / rhs - } -} - -public extension Path { - - static func parameter(_ param: String) -> Self { - .init("{" + param + "}") - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/PathItem/PathItem.swift b/Sources/FeatherOpenAPIKit/Interfaces/PathItem/PathItem.swift deleted file mode 100644 index 8654d16..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/PathItem/PathItem.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPIPathItem: Identifiable { - static var openAPIPath: OpenAPI.Path { get } - static func openAPIPathItem() -> OpenAPI.PathItem -} - -public protocol PathItem: OpenAPIPathItem { - - static var path: Path { get } - - static var summary: String? { get } - static var description: String? { get } - static var parameters: [Parameter.Type] { get } - static var get: Operation.Type? { get } - static var put: Operation.Type? { get } - static var post: Operation.Type? { get } - static var delete: Operation.Type? { get } - static var options: Operation.Type? { get } - static var head: Operation.Type? { get } - static var patch: Operation.Type? { get } - static var trace: Operation.Type? { get } -} - -public extension PathItem { - - static var openAPIPath: OpenAPI.Path { - .init(stringLiteral: path.value) - } - - static var summary: String? { nil } - static var description: String? { nil } - static var parameters: [Parameter.Type] { [] } - static var get: Operation.Type? { nil } - static var put: Operation.Type? { nil } - static var post: Operation.Type? { nil } - static var delete: Operation.Type? { nil } - static var options: Operation.Type? { nil } - static var head: Operation.Type? { nil } - static var patch: Operation.Type? { nil } - static var trace: Operation.Type? { nil } - - static func openAPIPathItem() -> OpenAPI.PathItem { - .init( - summary: summary, - description: description, - servers: nil, - parameters: parameters.map { $0.reference() }, - get: get?.openAPIOperation(), - put: put?.openAPIOperation(), - post: post?.openAPIOperation(), - delete: delete?.openAPIOperation(), - options: options?.openAPIOperation(), - head: head?.openAPIOperation(), - patch: patch?.openAPIOperation(), - trace: trace?.openAPIOperation(), - vendorExtensions: [:] - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/BinaryBody.swift b/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/BinaryBody.swift deleted file mode 100644 index 7d4cb2f..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/BinaryBody.swift +++ /dev/null @@ -1,26 +0,0 @@ -import OpenAPIKit - -public protocol BinaryBody: RequestBody { - static var contentType: OpenAPI.ContentType { get } - static var description: String { get } - static var required: Bool { get } -} - -extension BinaryBody { - - public static var required: Bool { true } - - public static func openAPIRequestBody() -> OpenAPI.Request { - .init( - description: description, - content: [ - contentType: .init( - schema: .string( - contentMediaType: .other("application/octet-stream") - ) - ) - ], - required: required - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/FormBody.swift b/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/FormBody.swift deleted file mode 100644 index 0829cf9..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/FormBody.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// File.swift -// -// Created by gerp83 on 28/08/2024 -// - -import OpenAPIKit - -public protocol FormBody: RequestBody { - static var description: String { get } - static var schema: Schema.Type { get } - static var required: Bool { get } -} - -public extension FormBody { - - static var required: Bool { true } - - static func openAPIRequestBody() -> OpenAPI.Request { - .init( - description: description, - content: [ - .form: schema.reference() - ], - required: required - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/JSONBody.swift b/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/JSONBody.swift deleted file mode 100644 index 04fe4cc..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/JSONBody.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol JSONBody: RequestBody { - static var description: String { get } - static var schema: Schema.Type { get } - static var required: Bool { get } -} - -public extension JSONBody { - - static var required: Bool { true } - - static func openAPIRequestBody() -> OpenAPI.Request { - .init( - description: description, - content: [ - .json: schema.reference() - ], - required: required - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/RequestBody.swift b/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/RequestBody.swift deleted file mode 100644 index e8662df..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/RequestBody/RequestBody.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPIRequestBody: Identifiable { - static func openAPIRequestBody() -> OpenAPI.Request -} - -public protocol RequestBody: OpenAPIRequestBody { - -} - -extension RequestBody {} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Response/BinaryResponse.swift b/Sources/FeatherOpenAPIKit/Interfaces/Response/BinaryResponse.swift deleted file mode 100644 index 0c4de85..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Response/BinaryResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -import OpenAPIKit - -public protocol BinaryResponse: Response {} - -public extension BinaryResponse { - static func openAPIResponse() -> OpenAPI.Response { - .init( - description: description, - headers: openAPIHeaderMap(), - content: openAPIContentMap() + [ - .any: .init( - schema: .string( - contentMediaType: .other("application/octet-stream") - ) - ) - ] - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Response/JSONResponse.swift b/Sources/FeatherOpenAPIKit/Interfaces/Response/JSONResponse.swift deleted file mode 100644 index 2e01680..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Response/JSONResponse.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol JSONResponse: Response { - - static var schema: Schema.Type { get } -} - -public extension JSONResponse { - - static var contents: [OpenAPI.ContentType: Schema.Type] { - [ - .json: schema - ] - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Response/Response.swift b/Sources/FeatherOpenAPIKit/Interfaces/Response/Response.swift deleted file mode 100644 index e3e5283..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Response/Response.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPIResponse: Identifiable { - static func openAPIResponse() -> OpenAPI.Response -} - -public extension OpenAPIResponse { - - static func reference() -> Either< - OpenAPI.Reference, - OpenAPI.Response - > { - .reference(.component(named: id)) - } -} - -public protocol Response: OpenAPIResponse { - static var description: String { get } - static var headers: [Header.Type] { get } - static var contents: [OpenAPI.ContentType: Schema.Type] { get } -} - -public extension Response { - - static var headers: [Header.Type] { [] } - - static var contents: - [OpenAPIKit.OpenAPI.ContentType: FeatherOpenAPIKit.Schema.Type] - { - [:] - } - - static func openAPIResponse() -> OpenAPI.Response { - .init( - description: description, - headers: openAPIHeaderMap(), - content: openAPIContentMap() - ) - } - - static func openAPIContentMap() -> OpenAPI.Content.Map { - var result: OpenAPI.Content.Map = [:] - for (key, content) in contents { - result[key] = content.reference() - } - return result - } - - static func openAPIHeaderMap() -> OpenAPI.Header.Map? { - guard !headers.isEmpty else { - return nil - } - var result: OpenAPI.Header.Map = [:] - for header in headers { - result[header.id] = header.reference() - } - return result - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/ArraySchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/ArraySchema.swift deleted file mode 100644 index 5b5bec6..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/ArraySchema.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol ArraySchema: Schema { - static var minItems: Int? { get } - static var maxItems: Int? { get } - static var items: Schema.Type { get } -} - -public extension ArraySchema { - static var minItems: Int? { 0 } - static var maxItems: Int? { 1000 } -} - -public extension ArraySchema { - - static func openAPISchema() -> JSONSchema { - .array( - description: description, - minItems: minItems, - maxItems: maxItems, - items: items.reference() - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/BooleanSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/BooleanSchema.swift deleted file mode 100644 index 10683d5..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/BooleanSchema.swift +++ /dev/null @@ -1,14 +0,0 @@ -import OpenAPIKit - -public protocol BooleanSchema: Schema { - static var defaultValue: Bool { get } -} - -extension BooleanSchema { - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .boolean( - description: description, - defaultValue: .init(defaultValue) - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/DateTimeSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/DateTimeSchema.swift deleted file mode 100644 index 7898efc..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/DateTimeSchema.swift +++ /dev/null @@ -1,17 +0,0 @@ -import OpenAPIKit - -public protocol DateTimeSchema: Schema { - static var examples: [String] { get } -} - -extension DateTimeSchema { - public static var examples: [String] { ["2023-04-04T09:20:15.000Z"] } - - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .string( - format: .dateTime, - description: description, - examples: examples.map { .init(stringLiteral: $0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/DoubleSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/DoubleSchema.swift deleted file mode 100644 index 827e10e..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/DoubleSchema.swift +++ /dev/null @@ -1,20 +0,0 @@ -import OpenAPIKit - -public protocol DoubleSchema: NumberSchema { - associatedtype T = Double -} - -extension DoubleSchema where T == Double { - - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .number( - format: .double, - required: true, - description: description, - maximum: maximum, - minimum: minimum, - defaultValue: defaultValue.map { .init($0) }, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/EmailSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/EmailSchema.swift deleted file mode 100644 index 87baa24..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/EmailSchema.swift +++ /dev/null @@ -1,16 +0,0 @@ -import OpenAPIKit - -public protocol EmailSchema: Schema { - static var examples: [String] { get } -} - -extension EmailSchema { - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .string( - format: .email, - required: true, - description: description, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/EnumSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/EnumSchema.swift deleted file mode 100644 index 5b0472f..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/EnumSchema.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol EnumSchema: Schema { - static var allowedValues: [String] { get } - static var defaultValue: String? { get } - static var examples: [String] { get } -} - -public extension EnumSchema { - static var defaultValue: String? { nil } - static var examples: [String] { [] } -} - -public extension EnumSchema { - - static func openAPISchema() -> JSONSchema { - var anyDefault: AnyCodable? = nil - if let defaultValue { - anyDefault = .init(stringLiteral: defaultValue) - } - return .enumeration( - description: description, - allowedValues: allowedValues.map { .init(stringLiteral: $0) }, - defaultValue: anyDefault, - examples: examples.map { .init(stringLiteral: $0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/FloatSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/FloatSchema.swift deleted file mode 100644 index 4de1a5e..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/FloatSchema.swift +++ /dev/null @@ -1,20 +0,0 @@ -import OpenAPIKit - -public protocol FloatSchema: NumberSchema { - associatedtype T = Float -} - -extension FloatSchema where T == Float { - - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .number( - format: .float, - required: true, - description: description, - maximum: maximum.map { (Double($0.0), exclusive: $0.1) }, - minimum: minimum.map { (Double($0.0), exclusive: $0.1) }, - defaultValue: defaultValue.map { .init($0) }, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/IDSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/IDSchema.swift deleted file mode 100644 index ec6521e..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/IDSchema.swift +++ /dev/null @@ -1,16 +0,0 @@ -public protocol IDSchema: TextSchema { - -} - -extension IDSchema { - - public static var examples: [String] { - [ - "P6p5WCctPKqhYKtGMUoTQ" - // "9lRYd11kppK1Nd6M0QMJh", - // "iIPAqgGFZRkyYmjU76YfG", - // "Mgzt9YAQUpjzNxYkf_xz2", - // "slIgHPBDHvXa4D6NQJ2on", - ] - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/Int32Schema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/Int32Schema.swift deleted file mode 100644 index 7b22b08..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/Int32Schema.swift +++ /dev/null @@ -1,23 +0,0 @@ -import OpenAPIKit - -public protocol Int32Schema: NumberSchema { - associatedtype T = Int32 -} - -extension Int32Schema where T == Int32 { - - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .integer( - format: .int32, - required: true, - description: description, - - //TODO: add Int64 cast after openapi kit fixed - maximum: maximum.map { (Int($0.0), exclusive: $0.exclusive) }, - minimum: maximum.map { (Int($0.0), exclusive: $0.exclusive) }, - - defaultValue: defaultValue.map { .init($0) }, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/Int64Schema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/Int64Schema.swift deleted file mode 100644 index 6bad6e1..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/Int64Schema.swift +++ /dev/null @@ -1,22 +0,0 @@ -import OpenAPIKit - -public protocol Int64Schema: NumberSchema { - //TODO: use int64, remove Int cast after openapi kit fixed - associatedtype T = Int -} - -//TODO: use int64, remove Int cast after openapi kit fixed -extension Int64Schema where T == Int { - - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .integer( - format: .int64, - required: true, - description: description, - maximum: maximum, - minimum: minimum, - defaultValue: defaultValue.map { .init($0) }, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/IntSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/IntSchema.swift deleted file mode 100644 index 217466a..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/IntSchema.swift +++ /dev/null @@ -1,20 +0,0 @@ -import OpenAPIKit - -public protocol IntSchema: NumberSchema { - associatedtype T = Int -} - -extension IntSchema where T == Int { - - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .integer( - format: .unspecified, - required: true, - description: description, - maximum: maximum, - minimum: minimum, - defaultValue: defaultValue.map { .init(integerLiteral: $0) }, - examples: examples.map { .init(integerLiteral: $0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/NanoIDSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/NanoIDSchema.swift deleted file mode 100644 index b156518..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/NanoIDSchema.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 16/03/2024. -// - -import OpenAPIKit - -public protocol NanoIDSchema: Schema { - static var examples: [String] { get } -} - -public extension NanoIDSchema { - - static var examples: [String] { - [ - "xHVX15b8z_wQDPH93uVp5", - "n4a9MyIfbqED76LPz4EaL", - "9eo5rQfE4I8ldgk5JLvZW", - "ZtmPFlB6FFU16suLM-LVf", - "mGUz4JpOVWYwpN2pwjYk9", - ] - } - - static func openAPISchema() -> JSONSchema { - .string( - format: .generic, - description: description, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/NumberSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/NumberSchema.swift deleted file mode 100644 index 25b9d71..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/NumberSchema.swift +++ /dev/null @@ -1,37 +0,0 @@ -public protocol NumberSchema: Schema { - associatedtype T - - static var defaultValue: T? { get } - static var examples: [T] { get } - - static var minimumValue: T? { get } - static var minimumExclusive: Bool { get } - static var minimum: (T, exclusive: Bool)? { get } - - static var maximumValue: T? { get } - static var maximumExclusive: Bool { get } - static var maximum: (T, exclusive: Bool)? { get } -} - -extension NumberSchema { - - public static var minimumValue: T? { nil } - public static var minimumExclusive: Bool { false } - public static var minimum: (T, exclusive: Bool)? { - if let value = minimumValue { - return (value, exclusive: minimumExclusive) - } - return nil - } - public static var maximumValue: T? { nil } - public static var maximumExclusive: Bool { false } - public static var maximum: (T, exclusive: Bool)? { - if let value = maximumValue { - return (value, exclusive: maximumExclusive) - } - return nil - } - - public static var defaultValue: T? { nil } - public static var examples: [T] { [] } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/ObjectSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/ObjectSchema.swift deleted file mode 100644 index da5415e..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/ObjectSchema.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol ObjectSchema: Schema { - static var properties: [ObjectSchemaProperty] { get } -} - -public extension ObjectSchema { - - static func openAPISchema() -> JSONSchema { - .object( - description: description, - properties: properties.reduce(into: [:]) { - $0[$1.name] = $1.schema.reference(required: $1.required) - } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/ObjectSchemaProperty.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/ObjectSchemaProperty.swift deleted file mode 100644 index 9839b29..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/ObjectSchemaProperty.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public struct ObjectSchemaProperty { - - public let name: String - public let schema: Schema.Type - public let required: Bool - - public init( - _ name: String, - _ schema: Schema.Type, - required: Bool = true - ) { - self.name = name - self.schema = schema - self.required = required - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/PasswordSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/PasswordSchema.swift deleted file mode 100644 index ebf45b2..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/PasswordSchema.swift +++ /dev/null @@ -1,16 +0,0 @@ -import OpenAPIKit - -public protocol PasswordSchema: Schema { - static var examples: [String] { get } -} - -extension PasswordSchema { - public static func openAPISchema() -> OpenAPIKit.JSONSchema { - .string( - format: .password, - required: true, - description: description, - examples: examples.map { .init($0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/Schema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/Schema.swift deleted file mode 100644 index 97f75da..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/Schema.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPISchema: Identifiable { - static func openAPISchema() -> JSONSchema -} - -public extension OpenAPISchema { - - static func reference(required: Bool = true) -> JSONSchema { - .reference(.component(named: id), required: required) - } - - static func reference() -> OpenAPI.Content { - .init(schemaReference: .component(named: id)) - } -} - -public protocol Schema: OpenAPISchema { - static var description: String { get } -} - -extension Schema { - - public static var description: String { - let desc = String(describing: self) - if desc.hasSuffix("Schema") { - return desc.dropLast("Schema".count) + " description" - } - return desc + " description" - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/TextSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/TextSchema.swift deleted file mode 100644 index 5e80269..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/TextSchema.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit - -public protocol TextSchema: Schema { - static var examples: [String] { get } -} - -public extension TextSchema { - - static func openAPISchema() -> OpenAPIKit.JSONSchema { - .text( - description: description, - examples: examples.map { .init(stringLiteral: $0) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Schema/UUIDSchema.swift b/Sources/FeatherOpenAPIKit/Interfaces/Schema/UUIDSchema.swift deleted file mode 100644 index ef1a19a..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Schema/UUIDSchema.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import OpenAPIKit -import Foundation - -public protocol UUIDSchema: Schema { - static var examples: [UUID] { get } -} - -public extension UUIDSchema { - - static var examples: [UUID] { - [ - .init(uuidString: "F257448D-73F6-4D6F-BB8A-8D756A622F70")!, - .init(uuidString: "84FB7C06-838A-4712-9E2A-C76294670C04")!, - .init(uuidString: "BA4D8D41-BF9F-4122-8546-AB82A98BE28F")!, - .init(uuidString: "519749BB-1210-4479-9B9E-292FC76444C7")!, - .init(uuidString: "E0A42968-28EE-4D1F-BE18-13D526458686")!, - ] - } - - static func openAPISchema() -> JSONSchema { - .string( - format: .uuid, - description: description, - examples: examples.map { .init($0.uuidString) } - ) - } -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/SecurityScheme/SecurityScheme.swift b/Sources/FeatherOpenAPIKit/Interfaces/SecurityScheme/SecurityScheme.swift deleted file mode 100644 index 4f073bc..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/SecurityScheme/SecurityScheme.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPISecurityScheme: Identifiable { - static func openAPISecurityScheme() -> OpenAPI.SecurityScheme -} - -public extension OpenAPISecurityScheme { - - static func securityRequirement() -> [OpenAPI.SecurityRequirement] { - [ - [ - reference(): [] - ] - ] - } - - static func reference() -> JSONReference { - .component(named: id) - } -} - -public protocol SecurityScheme: OpenAPISecurityScheme { - -} diff --git a/Sources/FeatherOpenAPIKit/Interfaces/Tag/Tag.swift b/Sources/FeatherOpenAPIKit/Interfaces/Tag/Tag.swift deleted file mode 100644 index 9ba2b03..0000000 --- a/Sources/FeatherOpenAPIKit/Interfaces/Tag/Tag.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import OpenAPIKit - -public protocol OpenAPITag: Identifiable { - static func openAPITag() -> OpenAPI.Tag -} - -public protocol Tag: OpenAPITag { - static var name: String { get } - static var description: String { get } -} - -public extension Tag { - - static var name: String { id } - static var description: String { "" } - - static func openAPITag() -> OpenAPI.Tag { - .init(name: name, description: description) - } -} diff --git a/Sources/FeatherOpenAPIKitMacros/FeatherOpenAPIKitMacros.swift b/Sources/FeatherOpenAPIKitMacros/FeatherOpenAPIKitMacros.swift deleted file mode 100644 index 65af380..0000000 --- a/Sources/FeatherOpenAPIKitMacros/FeatherOpenAPIKitMacros.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 26/01/2024. -// - -@attached(peer, names: arbitrary) -public macro ComponentCollection() = - #externalMacro( - module: "FeatherOpenAPIKitMacrosKit", - type: "ComponentCollectionMacro" - ) diff --git a/Sources/FeatherOpenAPIKitMacrosKit/FeatherOpenAPIKitMacrosKit.swift b/Sources/FeatherOpenAPIKitMacrosKit/FeatherOpenAPIKitMacrosKit.swift deleted file mode 100644 index 02c3774..0000000 --- a/Sources/FeatherOpenAPIKitMacrosKit/FeatherOpenAPIKitMacrosKit.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 26/01/2024. -// - -import SwiftCompilerPlugin -import SwiftSyntaxMacros - -@main -struct FeatherOpenAPIKitMacrosKit: CompilerPlugin { - - let providingMacros: [Macro.Type] = [ - ComponentCollectionMacro.self - ] -} diff --git a/Sources/FeatherOpenAPIKitMacrosKit/Macros/ComponentCollectionMacro.swift b/Sources/FeatherOpenAPIKitMacrosKit/Macros/ComponentCollectionMacro.swift deleted file mode 100644 index b6acb72..0000000 --- a/Sources/FeatherOpenAPIKitMacrosKit/Macros/ComponentCollectionMacro.swift +++ /dev/null @@ -1,100 +0,0 @@ -import SwiftCompilerPlugin -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros -import Foundation - -enum CustomError: Error { case message(String) } - -public struct ComponentCollectionMacro: PeerMacro { - private static func getTypeAndName(_ groupType: String) throws -> ( - variable: String, type: String - ) { - switch groupType { - case "Schemas": - return ("schemas", "Schema") - case "Parameters": - return ("parameters", "Parameter") - case "Headers": - return ("headers", "Header") - case "RequestBodies": - return ("requestBodies", "RequestBody") - case "SecuritySchemes": - return ("securitySchemes", "SecurityScheme") - case "Responses": - return ("responses", "Response") - case "Tags": - return ("tags", "Tag") - case "Operations": - return ("operations", "Operation") - case "PathItems": - return ("pathItems", "PathItem") - default: - throw CustomError.message("Invalid enum name: \(groupType)") - } - } - - private static func collectEnumTypes( - _ parentNodeName: String, - _ node: MemberBlockSyntax - ) -> [String] { - var ret: [String] = [] - - for member in node.members { - if let enumDecl = member.decl.as(EnumDeclSyntax.self) { - let enumName = parentNodeName + "." + enumDecl.name.text - - ret += collectEnumTypes(enumName, enumDecl.memberBlock) - - if let _ = enumDecl.inheritanceClause { - ret.append(enumName + ".self") - } - } - else if let structDecl = member.decl.as(StructDeclSyntax.self) { - ret += collectEnumTypes( - parentNodeName + "." + structDecl.name.text, - structDecl.memberBlock - ) - } - else if let classDecl = member.decl.as(ClassDeclSyntax.self) { - ret += collectEnumTypes( - parentNodeName + "." + classDecl.name.text, - classDecl.memberBlock - ) - } - } - - return ret - } - - public static func expansion( - of node: SwiftSyntax.AttributeSyntax, - providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, - in context: some SwiftSyntaxMacros.MacroExpansionContext - ) throws -> [SwiftSyntax.DeclSyntax] { - - guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { - throw CustomError.message( - "This macro can only be applied to an enum." - ) - } - - let groupType = enumDecl.name.text - let nameAndType = try getTypeAndName(groupType) - let collectedMemberTypes = collectEnumTypes( - groupType, - enumDecl.memberBlock - ) - .joined(separator: ",\n") - - let extended = DeclSyntax( - """ - public static let \(raw: nameAndType.variable) : [\(raw: nameAndType.type).Type] = [ - \(raw: collectedMemberTypes) - ] - """ - ) - - return [extended] - } -} diff --git a/Sources/feather-openapi-generator/feather-openapi-generator.swift b/Sources/feather-openapi-generator/feather-openapi-generator.swift deleted file mode 100644 index 3cda871..0000000 --- a/Sources/feather-openapi-generator/feather-openapi-generator.swift +++ /dev/null @@ -1,171 +0,0 @@ -import Foundation -import SwiftParser -import SwiftSyntax - -enum BuilderError: Error { - case wrongArgumentsNumber - case invalidOutputFormat - case errorReadingContent(String) - case errorAccessContent(String) -} - -@main -struct _Tool { - static func main() async throws { - print("FeatherOpenAPIGenerator is running...") - - guard CommandLine.arguments.count >= 3 else { - throw BuilderError.wrongArgumentsNumber - } - - let input = URL(fileURLWithPath: CommandLine.arguments[1]) - let output = URL(fileURLWithPath: CommandLine.arguments[2]) - let target = - CommandLine.arguments.count > 3 ? CommandLine.arguments[3] : "" - - let typeList = try collectTypes(input.path) - let collectedTypes = typeList.joined(separator: ",\n ") - - let dateString = DateFormatter.localizedString( - from: Date(), - dateStyle: .medium, - timeStyle: .medium - ) - - let code = - """ - //generated on: \(dateString) - \(target == "FeatherOpenAPIKit" ? "" : "import FeatherOpenAPIKit") - - extension Component { - - public static func getComponentsOfType() -> [T] { - let prefixName = String(reflecting: self) + "." - return [ - \(collectedTypes) - ].compactMap { $0 as? T }.filter { - String(reflecting: $0).hasPrefix(prefixName) - } - } - } - """ - - print("Generated code path: \(output.path)") - - guard let data = code.data(using: .utf8) else { - throw BuilderError.invalidOutputFormat - } - - try data.write(to: output, options: .atomic) - - print("FeatherOpenAPIGenerator finished.") - } - - private static func getFullName(_ node: TypeSyntax) -> String { - node.trimmedDescription - } - - private static func getNodeName( - _ parentNodeName: String, - _ nodeName: String - ) -> String { - if parentNodeName.isEmpty { - return nodeName - } - return parentNodeName + "." + nodeName - } - - private static func collectTypes( - _ parentNodeName: String, - _ node: SyntaxProtocol - ) -> [String] { - var ret: [String] = [] - - var members: MemberBlockItemListSyntax? - var nodeName: String = "" - - if let extensionDecl = node.as(ExtensionDeclSyntax.self) { - nodeName = getNodeName( - parentNodeName, - getFullName(extensionDecl.extendedType) - ) - members = extensionDecl.memberBlock.members - } - else if let enumDecl = node.as(EnumDeclSyntax.self) { - nodeName = getNodeName(parentNodeName, enumDecl.name.text) - members = enumDecl.memberBlock.members - ret.append(nodeName + ".self") - } - else if let structDecl = node.as(StructDeclSyntax.self) { - nodeName = getNodeName(parentNodeName, structDecl.name.text) - members = structDecl.memberBlock.members - } - else if let classDecl = node.as(ClassDeclSyntax.self) { - nodeName = getNodeName(parentNodeName, classDecl.name.text) - members = classDecl.memberBlock.members - } - - if let _ = members { - for member in members! { - let list = collectTypes(nodeName, member.decl) - ret += list - } - } - - return ret - } - - private static func collectTypes(_ root: SourceFileSyntax) -> [String] { - var ret: [String] = [] - - for st in root.statements { - let list = collectTypes("", st.item) - ret += list - } - - return ret - } - - private static func collectTypes(_ dirPath: String) throws -> [String] { - var ret: [String] = [] - - let fileManager = FileManager.default - let folderURL = URL(fileURLWithPath: dirPath) - - do { - let fileURLs = try fileManager.contentsOfDirectory( - at: folderURL, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - - for fileURL in fileURLs { - if fileURL.hasDirectoryPath { - let list = try collectTypes(fileURL.path) - ret += list - } - else if fileURL.pathExtension == "swift" { - do { - let fileContent = try String(contentsOf: fileURL) - let list = collectTypes( - Parser.parse(source: fileContent) - ) - ret += list - } - catch { - throw BuilderError.errorReadingContent( - "Error reading content of \(fileURL.relativePath): \(error)" - ) - } - } - } - } - catch { - throw BuilderError.errorAccessContent( - "Error accessing contents of the folder: \(error)" - ) - } - - return ret - } -} diff --git a/Tests/FeatherOpenAPIKitMacrosKitTests/FeatherOpenAPIKitMacrosTests.swift b/Tests/FeatherOpenAPIKitMacrosKitTests/FeatherOpenAPIKitMacrosTests.swift deleted file mode 100644 index e0c50fb..0000000 --- a/Tests/FeatherOpenAPIKitMacrosKitTests/FeatherOpenAPIKitMacrosTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport -import XCTest -import FeatherOpenAPIKitMacrosKit - -final class FeatherOpenAPIKitMacrosTests: XCTestCase { - - let testMacros: [String: Macro.Type] = [ - "ComponentCollection": ComponentCollectionMacro.self - ] - - func testProtoMacro() { - //TODO: make test - assertMacroExpansion( - """ - import FeatherOpenAPIKit - import OpenAPIKit - - extension Example.Model { - @ComponentCollection - enum Schemas { - enum empty { - } - - enum Id: UUIDSchema { - static let description = "Unique example model identifier" - } - - enum Key { - enum InsiderKey: TextSchema { - enum InsiderKeyLevel3: TextSchema { - enum InsiderKeyLevel4 { - enum InsiderKeyLevel5: TextSchema { - } - } - } - static let description = "Key of the example model" - static let examples = [ - "my-example-key", - ] - } - - static let description = "Key of the example model" - static let examples = [ - "my-example-key", - ] - } - } - } - """, - expandedSource: """ - import FeatherOpenAPIKit - import OpenAPIKit - - extension Example.Model { - enum Schemas { - enum empty { - } - - enum Id: UUIDSchema { - static let description = "Unique example model identifier" - } - - enum Key { - enum InsiderKey: TextSchema { - enum InsiderKeyLevel3: TextSchema { - enum InsiderKeyLevel4 { - enum InsiderKeyLevel5: TextSchema { - } - } - } - static let description = "Key of the example model" - static let examples = [ - "my-example-key", - ] - } - - static let description = "Key of the example model" - static let examples = [ - "my-example-key", - ] - } - } - - public static let schemas : [Schema.Type] = [ - Schemas.Id.self, - Schemas.Key.InsiderKey.InsiderKeyLevel3.InsiderKeyLevel4.InsiderKeyLevel5.self, - Schemas.Key.InsiderKey.InsiderKeyLevel3.self, - Schemas.Key.InsiderKey.self - ] - } - """, - macros: testMacros - ) - } -} diff --git a/Tests/FeatherOpenAPIKitMacrosTests/FeatherOpenAPIKitMacrosTests.swift b/Tests/FeatherOpenAPIKitMacrosTests/FeatherOpenAPIKitMacrosTests.swift deleted file mode 100644 index 21716ac..0000000 --- a/Tests/FeatherOpenAPIKitMacrosTests/FeatherOpenAPIKitMacrosTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 26/01/2024. -// - -import XCTest -import FeatherOpenAPIKit -import FeatherOpenAPIKitMacros - -final class FeatherOpenAPIKitMacrosTests: XCTestCase { - - func testExample() { - enum TopLevel { - @ComponentCollection - enum Schemas { - enum Foo { - enum Bar: TextSchema { - enum empty { - } - - enum Baz: TextSchema { - static let description = "" - static let examples = ["baz"] - } - static let description = "" - static let examples = ["bar"] - } - } - } - } - - XCTAssert(TopLevel.schemas.count == 2) - // wtf??? why does not work this test properly? (when I change it for 3 it will not produce error but it should do) - //XCTAssertEqual(TopLevel.schemas.count, 3) - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Example.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Example.swift deleted file mode 100644 index d023f26..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Example.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -enum Example {} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Headers.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Headers.swift deleted file mode 100644 index caaa4b1..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Headers.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let headers: [Header.Type] = [ - Headers.CustomResponseHeader.self - ] - - enum Headers { - - enum CustomResponseHeader: Header { - static let name = "X-Custom-Response-Header" - static let description: String = "My custom response header" - static var schema: Schema.Type = Schemas.CustomHeader.self - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Operations.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Operations.swift deleted file mode 100644 index 3dffe9e..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Operations.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let operations: [Operation.Type] = [ - Operations.Get.self, - Operations.Create.self, - ] - - enum Operations { - - enum Get: Operation { - static let tag: Tag.Type = Tags.Main.self - static let summary = "Detail example" - static let description = "Detail example detail" - static var parameters: [Parameter.Type] = [ - // Parameters.Id.self, - Parameters.CustomRequestHeader.self - ] - static let responses: [OperationResponse] = [ - .init(200, Responses.Detail.self) - ] - } - - enum Create: Operation { - static let tag: Tag.Type = Tags.Main.self - static let summary = "Create example" - static let description = "Create example detail" - static var requestBody: RequestBody.Type? = RequestBodies.Create - .self - static let responses: [OperationResponse] = [ - .init(200, Responses.Detail.self) - ] - static let security: [SecurityScheme.Type] = [ - SecuritySchemes.BearerToken.self - ] - } - - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Parameters.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Parameters.swift deleted file mode 100644 index dfd2c58..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Parameters.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let parameters: [Parameter.Type] = [ - Parameters.Id.self, - Parameters.CustomRequestHeader.self, - ] - - enum Parameters { - - enum Id: PathParameter { - static let name = "id" - static let description = "Example parameter" - static let schema: Schema.Type = Schemas.Id.self - } - - enum CustomRequestHeader: HeaderParameter { - static let name = "CustomRequestHeader" - static let description = "Example request header parameter" - static let schema: Schema.Type = Schemas.CustomHeader.self - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+PathItems.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+PathItems.swift deleted file mode 100644 index 2e61c74..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+PathItems.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let pathItems: [PathItem.Type] = [ - PathItems.Main.self, - PathItems.Identified.self, - ] - - enum PathItems { - - enum Main: PathItem { - static let path: Path = "/example/models" - - static let post: Operation.Type? = Operations.Create.self - } - - enum Identified: PathItem { - static let path: Path = Main.path / Parameters.Id.path - static let parameters: [Parameter.Type] = [ - Parameters.Id.self - ] - - static let get: Operation.Type? = Operations.Get.self - } - - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+RequestBodies.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+RequestBodies.swift deleted file mode 100644 index 7b8c0fc..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+RequestBodies.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let requestBodies: [RequestBody.Type] = [ - RequestBodies.Create.self - ] - - enum RequestBodies { - - enum Create: JSONBody { - static let description = "Create example" - static let schema: Schema.Type = Schemas.Create.self - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Responses.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Responses.swift deleted file mode 100644 index 6c54167..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Responses.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit -import OpenAPIKit - -extension Example.Model { - - static let responses: [Response.Type] = [ - Responses.Detail.self - ] - - enum Responses { - - enum Custom: Response { - static let description = "Example" - static var contents: [OpenAPI.ContentType: Schema.Type] = [ - .xml: Schemas.Detail.self - ] - } - - enum Detail: JSONResponse { - static let description = "Example" - static var headers: [Header.Type] = [ - Headers.CustomResponseHeader.self - ] - static let schema: Schema.Type = Schemas.Detail.self - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Schemas.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Schemas.swift deleted file mode 100644 index a6da844..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Schemas.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let schemas: [Schema.Type] = [ - Schemas.Id.self, - Schemas.Key.self, - Schemas.Create.self, - Schemas.Patch.self, - Schemas.Detail.self, - Schemas.List.self, - Schemas.List.Item.self, - Schemas.CustomHeader.self, - Schemas.PatchOverride.self, - ] - - enum Schemas { - - enum Id: UUIDSchema { - static let description = "Unique example model identifier" - } - - enum CustomHeader: TextSchema { - static let description = "Custom header" - static let examples = [ - "my-example-key" - ] - } - - enum Key: TextSchema { - static let description = "Key of the example model" - static let examples = [ - "my-example-key" - ] - } - - enum Create: ObjectSchema { - static let description = "example model create object" - static let properties: [ObjectSchemaProperty] = [ - .init("key", Key.self) - ] - } - - enum Detail: ObjectSchema { - static let description = "example model detail object" - - static let properties: [ObjectSchemaProperty] = [ - .init("id", Id.self), - .init("key", Key.self), - ] - } - - enum Patch: ObjectSchema { - static let description = "example model detail object" - - static let properties: [ObjectSchemaProperty] = [ - .init("key", Key.self, required: false) - ] - } - - enum List: ArraySchema { - - enum Item: ObjectSchema { - static let description = "example model detail object" - - static let properties: [ObjectSchemaProperty] = [ - .init("id", Id.self), - .init("key", Key.self), - ] - } - - static let description = "Lorem ipsum dolor sit amet" - static let items: Schema.Type = Item.self - } - - enum PatchOverride: ObjectSchema { - static let id = Patch.id - static let override = true - - static let description = "overridden" - - static let properties: [ObjectSchemaProperty] = [ - .init("key", Key.self, required: false) - ] - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+SecuritySchemes.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+SecuritySchemes.swift deleted file mode 100644 index 27e40c2..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+SecuritySchemes.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit -import OpenAPIKit - -// shared operation security -extension Operation { - - static func bearerToken() -> [OpenAPI.SecurityRequirement] { - Example.Model.SecuritySchemes.BearerToken.securityRequirement() - } -} - -extension Example.Model { - - static let securitySchemes: [SecurityScheme.Type] = [ - SecuritySchemes.BearerToken.self - ] - - enum SecuritySchemes { - - enum BearerToken: SecurityScheme { - - static func openAPISecurityScheme() -> OpenAPI.SecurityScheme { - .init( - type: .http( - scheme: "bearer", - bearerFormat: "token" - ), - description: "Authorization header using a Bearer token" - ) - } - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Tags.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Tags.swift deleted file mode 100644 index bf75f33..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel+Tags.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension Example.Model { - - static let tags: [Tag.Type] = [ - Tags.Main.self - ] - - enum Tags { - - enum Main: Tag { - static let name = "Model" - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel.swift deleted file mode 100644 index aa8f8a1..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/Example/Model/ExampleModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import FeatherOpenAPIKit - -extension Example { - - enum Model: Component {} -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/ExampleDuplicatedItem.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/ExampleDuplicatedItem.swift deleted file mode 100644 index 5e23ba9..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/ExampleDuplicatedItem.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -enum ExampleDuplicatedItem {} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/Model/ExampleDuplicatedItemModel+Schemas.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/Model/ExampleDuplicatedItemModel+Schemas.swift deleted file mode 100644 index 9751b50..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/Model/ExampleDuplicatedItemModel+Schemas.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension ExampleDuplicatedItem.Model { - - static let schemas: [Schema.Type] = [ - Schemas.Id.self, - Schemas.Key.self, - Schemas.KeySecond.self, - ] - - enum Schemas { - - enum Id: UUIDSchema { - static let description = "Unique example model identifier" - } - - enum Key: TextSchema { - static let description = "Key of the example model" - static let examples = [ - "my-example-key" - ] - } - - enum KeySecond: TextSchema { - static let id = Key.id - - static let description = "Key of the example model" - static let examples = [ - "my-example-key" - ] - } - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/Model/ExampleDuplicatedItemModel.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/Model/ExampleDuplicatedItemModel.swift deleted file mode 100644 index 5582609..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleDuplicatedItem/Model/ExampleDuplicatedItemModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import FeatherOpenAPIKit - -extension ExampleDuplicatedItem { - - enum Model: Component {} -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/ExampleMissingParentItem.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/ExampleMissingParentItem.swift deleted file mode 100644 index 2cec5f7..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/ExampleMissingParentItem.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -enum ExampleMissingParentItem {} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/Model/ExampleMissingParentItemModel+Schemas.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/Model/ExampleMissingParentItemModel+Schemas.swift deleted file mode 100644 index dc80984..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/Model/ExampleMissingParentItemModel+Schemas.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit - -extension ExampleMissingParentItem.Model { - - static let schemas: [Schema.Type] = [ - Schemas.Id.self, - Schemas.Key.self, - ] - - enum Schemas { - - enum Id: UUIDSchema { - static let description = "Unique example model identifier" - } - - enum Key: TextSchema { - static let override = true - static let description = "Key of the example model" - static let examples = [ - "my-example-key" - ] - } - - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/Model/ExampleMissingParentItemModel.swift b/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/Model/ExampleMissingParentItemModel.swift deleted file mode 100644 index 1bc4f14..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/Components/ExampleMissingParentItem/Model/ExampleMissingParentItemModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. -// - -import FeatherOpenAPIKit - -extension ExampleMissingParentItem { - - enum Model: Component {} -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/ExampleDocument.swift b/Tests/FeatherOpenAPIKitTests/Example/ExampleDocument.swift deleted file mode 100644 index 85d45dc..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/ExampleDocument.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit -import OpenAPIKit - -struct ExampleDocument: Document { - - let components: [Component.Type] - - init() { - self.components = [ - Example.Model.self - ] - } - - func openAPIDocument() throws -> OpenAPI.Document { - try composedDocument( - info: .init( - title: "Example", - description: """ - Example API description - """, - contact: .init( - name: "Binary Birds", - url: .init(string: "https://binarybirds.com")!, - email: "info@binarybirds.com" - ), - version: "1.0.0" - ), - servers: [ - .init( - url: .init(string: "http://localhost:8080")!, - description: "dev" - ) - ] - ) - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/ExampleDuplicatedItemDocument.swift b/Tests/FeatherOpenAPIKitTests/Example/ExampleDuplicatedItemDocument.swift deleted file mode 100644 index 001c8ec..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/ExampleDuplicatedItemDocument.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit -import OpenAPIKit - -struct ExampleDuplicatedItemDocument: Document { - - let components: [Component.Type] - - init() { - self.components = [ - ExampleDuplicatedItem.Model.self - ] - } - - func openAPIDocument() throws -> OpenAPI.Document { - try composedDocument( - info: .init( - title: "ExampleDuplicatedItem", - description: """ - Example API description - """, - contact: .init( - name: "Binary Birds", - url: .init(string: "https://binarybirds.com")!, - email: "info@binarybirds.com" - ), - version: "1.0.0" - ), - servers: [ - .init( - url: .init(string: "http://localhost:8080")!, - description: "dev" - ) - ] - ) - } -} diff --git a/Tests/FeatherOpenAPIKitTests/Example/ExampleMissingParentItemDocument.swift b/Tests/FeatherOpenAPIKitTests/Example/ExampleMissingParentItemDocument.swift deleted file mode 100644 index 1ede020..0000000 --- a/Tests/FeatherOpenAPIKitTests/Example/ExampleMissingParentItemDocument.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import FeatherOpenAPIKit -import OpenAPIKit - -struct ExampleMissingParentItemItemDocument: Document { - - let components: [Component.Type] - - init() { - self.components = [ - ExampleMissingParentItem.Model.self - ] - } - - func openAPIDocument() throws -> OpenAPI.Document { - try composedDocument( - info: .init( - title: "ExampleMissingParentItem", - description: """ - Example API description - """, - contact: .init( - name: "Binary Birds", - url: .init(string: "https://binarybirds.com")!, - email: "info@binarybirds.com" - ), - version: "1.0.0" - ), - servers: [ - .init( - url: .init(string: "http://localhost:8080")!, - description: "dev" - ) - ] - ) - } -} diff --git a/Tests/FeatherOpenAPIKitTests/FeatherOpenAPIKitTests.swift b/Tests/FeatherOpenAPIKitTests/FeatherOpenAPIKitTests.swift deleted file mode 100644 index 955a83c..0000000 --- a/Tests/FeatherOpenAPIKitTests/FeatherOpenAPIKitTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// File.swift -// -// -// Created by Tibor Bodecs on 20/01/2024. -// - -import Foundation -import OpenAPIKit -import OpenAPIKitCore -import Yams -import XCTest - -@testable import FeatherOpenAPIKit - -final class FeatherOpenAPIKitTests: XCTestCase { - - func testRender() throws { - - let document = ExampleDocument() - - XCTAssert( - try document.schemas() - .contains { - $0.key.rawValue == "ExampleModelPatch" - && $0.value.description == "overridden" - } - ) - - let encoder = YAMLEncoder() - let openAPIDocument = try document.openAPIDocument() - do { - _ = try openAPIDocument.locallyDereferenced() - } - catch { - return XCTFail("\(error)") - } - - _ = try encoder.encode(openAPIDocument) - } - - func testSchemaDescription() throws { - - struct IDSchema: NanoIDSchema {} - struct Foo: NanoIDSchema {} - - XCTAssertEqual(IDSchema.description, "ID description") - XCTAssertEqual(Foo.description, "Foo description") - } - - func testDuplicatedItem() throws { - - let document = ExampleDuplicatedItemDocument() - var errorMessage: String = "none" - - do { - let _ = try document.openAPIDocument() - } - catch let error as ComposeDocumentError { - errorMessage = error.message - } - - XCTAssertEqual( - errorMessage, - "Feather OpenAPI item id is duplicated: 'ExampleDuplicatedItemModelKey' (Did you forget to include override=true?)" - ) - } - - func testMissingParentItem() throws { - - let document = ExampleMissingParentItemItemDocument() - var errorMessage: String = "none" - - do { - let _ = try document.openAPIDocument() - } - catch let error as ComposeDocumentError { - errorMessage = error.message - } - - XCTAssertEqual( - errorMessage, - "Feather OpenAPI item 'ExampleMissingParentItemModelKey' is set as override but has no parent. (Are the component orders correct? Or are the IDs the same?)" - ) - } - -} diff --git a/Tests/FeatherOpenAPITests/Example/Example.swift b/Tests/FeatherOpenAPITests/Example/Example.swift new file mode 100644 index 0000000..11e9bbc --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/Example.swift @@ -0,0 +1,7 @@ +// +// Example.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +enum Example {} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDocument.swift b/Tests/FeatherOpenAPITests/Example/ExampleDocument.swift new file mode 100644 index 0000000..5de9829 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleDocument.swift @@ -0,0 +1,58 @@ +// +// ExampleDocument.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +struct ExampleLocation: LocationRepresentable { + let location: String +} + +struct ExampleContact: ContactRepresentable { + var name: String? { "Binary Birds" } + var url: LocationRepresentable? { + ExampleLocation(location: "https://binarybirds.com") + } + var email: String? { "info@binarybirds.com" } +} + +struct ExampleInfo: InfoRepresentable { + var title: String { "Example" } + var description: String? { + """ + Example API description + """ + } + var contact: OpenAPIContactRepresentable? { ExampleContact() } + var version: String { "1.0.0" } +} + +struct ExampleServer: ServerRepresentable { + var url: LocationRepresentable { + ExampleLocation(location: "http://localhost:8080") + } + var description: String? { "dev" } +} + +struct ExamplePathCollection: PathCollectionRepresentable { + var pathMap: PathMap { + [ + "/example/models": Example.Model.MainPathItem(), + "/example/models/{id}": Example.Model.IdentifiedPathItem(), + ] + } +} + +struct ExampleDocument: DocumentRepresentable { + + let collection = ExamplePathCollection() + + var info: OpenAPIInfoRepresentable { ExampleInfo() } + var servers: [OpenAPIServerRepresentable] { [ExampleServer()] } + + var paths: PathMap { collection.pathMap } + var components: OpenAPIComponentsRepresentable { collection.components } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItem.swift b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItem.swift new file mode 100644 index 0000000..786f0fd --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItem.swift @@ -0,0 +1,7 @@ +// +// ExampleDuplicatedItem.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +enum ExampleDuplicatedItem {} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemDocument.swift b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemDocument.swift new file mode 100644 index 0000000..d470039 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemDocument.swift @@ -0,0 +1,46 @@ +// +// ExampleDuplicatedItemDocument.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +struct ExampleDuplicatedItemInfo: InfoRepresentable { + var title: String { "ExampleDuplicatedItem" } + var description: String? { + """ + Example API description + """ + } + var contact: OpenAPIContactRepresentable? { ExampleContact() } + var version: String { "1.0.0" } +} + +struct ExampleDuplicatedItemServer: ServerRepresentable { + var url: LocationRepresentable { + ExampleLocation(location: "http://localhost:8080") + } + var description: String? { "dev" } +} + +struct ExampleDuplicatedItemDocument: DocumentRepresentable { + var info: OpenAPIInfoRepresentable { ExampleDuplicatedItemInfo() } + var servers: [OpenAPIServerRepresentable] { + [ExampleDuplicatedItemServer()] + } + var paths: PathMap { [:] } + var components: OpenAPIComponentsRepresentable { + let idSchema = ExampleDuplicatedItem.Model.IdSchema() + let keySchema = ExampleDuplicatedItem.Model.KeySchema() + let keySecondSchema = ExampleDuplicatedItem.Model.KeySecondSchema() + return Components( + schemas: [ + SchemaID(idSchema.openAPIIdentifier): idSchema, + SchemaID(keySchema.openAPIIdentifier): keySchema, + SchemaID(keySecondSchema.openAPIIdentifier): keySecondSchema, + ] + ) + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel+Schemas.swift b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel+Schemas.swift new file mode 100644 index 0000000..dc750f8 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel+Schemas.swift @@ -0,0 +1,25 @@ +// +// ExampleDuplicatedItemModel+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension ExampleDuplicatedItem.Model { + + struct IdSchema: StringSchemaRepresentable { + var description: String? { "Unique example model identifier" } + } + + struct KeySchema: StringSchemaRepresentable { + var description: String? { "Key of the example model" } + var example: String? { "my-example-key" } + } + + struct KeySecondSchema: StringSchemaRepresentable { + var openAPIIdentifier: String { KeySchema().openAPIIdentifier } + var description: String? { "Key of the example model" } + var example: String? { "my-example-key" } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel.swift b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel.swift new file mode 100644 index 0000000..9d6c829 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel.swift @@ -0,0 +1,11 @@ +// +// ExampleDuplicatedItemModel.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension ExampleDuplicatedItem { + enum Model {} +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItem.swift b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItem.swift new file mode 100644 index 0000000..06cc657 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItem.swift @@ -0,0 +1,7 @@ +// +// ExampleMissingParentItem.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +enum ExampleMissingParentItem {} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemDocument.swift b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemDocument.swift new file mode 100644 index 0000000..d2218ce --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemDocument.swift @@ -0,0 +1,44 @@ +// +// ExampleMissingParentItemDocument.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +struct ExampleMissingParentItemInfo: InfoRepresentable { + var title: String { "ExampleMissingParentItem" } + var description: String? { + """ + Example API description + """ + } + var contact: OpenAPIContactRepresentable? { ExampleContact() } + var version: String { "1.0.0" } +} + +struct ExampleMissingParentItemServer: ServerRepresentable { + var url: LocationRepresentable { + ExampleLocation(location: "http://localhost:8080") + } + var description: String? { "dev" } +} + +struct ExampleMissingParentItemItemDocument: DocumentRepresentable { + var info: OpenAPIInfoRepresentable { ExampleMissingParentItemInfo() } + var servers: [OpenAPIServerRepresentable] { + [ExampleMissingParentItemServer()] + } + var paths: PathMap { [:] } + var components: OpenAPIComponentsRepresentable { + let idSchema = ExampleMissingParentItem.Model.IdSchema() + let keySchema = ExampleMissingParentItem.Model.KeySchema() + return Components( + schemas: [ + SchemaID(idSchema.openAPIIdentifier): idSchema, + SchemaID(keySchema.openAPIIdentifier): keySchema, + ] + ) + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel+Schemas.swift b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel+Schemas.swift new file mode 100644 index 0000000..8eb3fcd --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel+Schemas.swift @@ -0,0 +1,19 @@ +// +// ExampleMissingParentItemModel+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension ExampleMissingParentItem.Model { + + struct IdSchema: StringSchemaRepresentable { + var description: String? { "Unique example model identifier" } + } + + struct KeySchema: StringSchemaRepresentable { + var description: String? { "Key of the example model" } + var example: String? { "my-example-key" } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel.swift b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel.swift new file mode 100644 index 0000000..ef57733 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel.swift @@ -0,0 +1,11 @@ +// +// ExampleMissingParentItemModel.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension ExampleMissingParentItem { + enum Model {} +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Headers.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+Headers.swift new file mode 100644 index 0000000..e819bbe --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+Headers.swift @@ -0,0 +1,17 @@ +// +// ExampleModel+Headers.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension Example.Model { + + struct CustomResponseHeader: HeaderRepresentable { + var description: String? { "My custom response header" } + var schema: any OpenAPISchemaRepresentable { + CustomHeaderSchema().reference() + } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Operations.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+Operations.swift new file mode 100644 index 0000000..6e5f17a --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+Operations.swift @@ -0,0 +1,42 @@ +// +// ExampleModel+Operations.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Example.Model { + + struct GetOperation: OperationRepresentable { + var tags: [TagRepresentable] { [ModelTag()] } + var summary: String? { "Detail example" } + var description: String? { "Detail example detail" } + var parameters: [ParameterRepresentable] { + [ + IdParameter().reference(), + CustomRequestHeaderParameter().reference(), + ] + } + var responseMap: ResponseMap { + [ + 200: DetailResponse().reference() + ] + } + } + + struct CreateOperation: OperationRepresentable { + var tags: [TagRepresentable] { [ModelTag()] } + var summary: String? { "Create example" } + var description: String? { "Create example detail" } + var requestBody: RequestBodyRepresentable? { + CreateRequestBody().reference() + } + var responseMap: ResponseMap { + [ + 200: DetailResponse().reference() + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Parameters.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+Parameters.swift new file mode 100644 index 0000000..0424378 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+Parameters.swift @@ -0,0 +1,26 @@ +// +// ExampleModel+Parameters.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension Example.Model { + + struct IdParameter: PathParameterRepresentable { + var name: String { "id" } + var description: String? { "Example parameter" } + var schema: any OpenAPISchemaRepresentable { + IdSchema().reference() + } + } + + struct CustomRequestHeaderParameter: HeaderParameterRepresentable { + var name: String { "CustomRequestHeader" } + var description: String? { "Example request header parameter" } + var schema: any OpenAPISchemaRepresentable { + CustomHeaderSchema().reference() + } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+PathItems.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+PathItems.swift new file mode 100644 index 0000000..5f90a47 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+PathItems.swift @@ -0,0 +1,18 @@ +// +// ExampleModel+PathItems.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension Example.Model { + + struct MainPathItem: PathItemRepresentable { + var post: OperationRepresentable? { CreateOperation() } + } + + struct IdentifiedPathItem: PathItemRepresentable { + var get: OperationRepresentable? { GetOperation() } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+RequestBodies.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+RequestBodies.swift new file mode 100644 index 0000000..4754b08 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+RequestBodies.swift @@ -0,0 +1,15 @@ +// +// ExampleModel+RequestBodies.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension Example.Model { + + struct CreateRequestBody: JSONRequestBodyRepresentable { + var description: String? { "Create example" } + var schema: SchemaReference { CreateSchema().reference() } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Responses.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+Responses.swift new file mode 100644 index 0000000..d2130ed --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+Responses.swift @@ -0,0 +1,30 @@ +// +// ExampleModel+Responses.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Example.Model { + + struct CustomResponse: ResponseRepresentable { + var description: String { "Example" } + var contentMap: ContentMap { + [ + .xml: Content(DetailSchema().reference()) + ] + } + } + + struct DetailResponse: JSONResponseRepresentable { + var description: String { "Example" } + var schema: SchemaReference { DetailSchema().reference() } + var headerMap: HeaderMap { + [ + "X-Custom-Response-Header": CustomResponseHeader().reference() + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Schemas.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+Schemas.swift new file mode 100644 index 0000000..e937fe8 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+Schemas.swift @@ -0,0 +1,77 @@ +// +// ExampleModel+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Example.Model { + + struct IdSchema: StringSchemaRepresentable { + var description: String? { "Unique example model identifier" } + } + + struct CustomHeaderSchema: StringSchemaRepresentable { + var description: String? { "Custom header" } + var example: String? { "my-example-key" } + } + + struct KeySchema: StringSchemaRepresentable { + var description: String? { "Key of the example model" } + var example: String? { "my-example-key" } + } + + struct CreateSchema: ObjectSchemaRepresentable { + var description: String? { "example model create object" } + var propertyMap: SchemaMap { + [ + "key": KeySchema().reference() + ] + } + } + + struct DetailSchema: ObjectSchemaRepresentable { + var description: String? { "example model detail object" } + var propertyMap: SchemaMap { + [ + "id": IdSchema().reference(), + "key": KeySchema().reference(), + ] + } + } + + struct PatchSchema: ObjectSchemaRepresentable { + var description: String? { "example model detail object" } + var propertyMap: SchemaMap { + [ + "key": KeySchema().reference() + ] + } + } + + struct ListItemSchema: ObjectSchemaRepresentable { + var description: String? { "example model detail object" } + var propertyMap: SchemaMap { + [ + "id": IdSchema().reference(), + "key": KeySchema().reference(), + ] + } + } + + struct ListSchema: ArraySchemaRepresentable { + var description: String? { "Lorem ipsum dolor sit amet" } + var items: SchemaRepresentable? { ListItemSchema() } + } + + struct PatchOverrideSchema: ObjectSchemaRepresentable { + var description: String? { "overridden" } + var propertyMap: SchemaMap { + [ + "key": KeySchema().reference() + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+SecuritySchemes.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+SecuritySchemes.swift new file mode 100644 index 0000000..610ebc0 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+SecuritySchemes.swift @@ -0,0 +1,23 @@ +// +// ExampleModel+SecuritySchemes.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Example.Model { + + struct BearerTokenSecurityScheme: SecuritySchemeRepresentable { + var type: OpenAPI.SecurityScheme.SecurityType { + .http( + scheme: "bearer", + bearerFormat: "token" + ) + } + var description: String? { + "Authorization header using a Bearer token" + } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Tags.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel+Tags.swift new file mode 100644 index 0000000..89b0478 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel+Tags.swift @@ -0,0 +1,14 @@ +// +// ExampleModel+Tags.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension Example.Model { + + struct ModelTag: TagRepresentable { + var name: String { "Model" } + } +} diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel.swift b/Tests/FeatherOpenAPITests/Example/ExampleModel.swift new file mode 100644 index 0000000..934d6ba --- /dev/null +++ b/Tests/FeatherOpenAPITests/Example/ExampleModel.swift @@ -0,0 +1,11 @@ +// +// ExampleModel.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import FeatherOpenAPI + +extension Example { + enum Model {} +} diff --git a/Tests/FeatherOpenAPITests/ExampleTestSuite.swift b/Tests/FeatherOpenAPITests/ExampleTestSuite.swift new file mode 100644 index 0000000..8ca5449 --- /dev/null +++ b/Tests/FeatherOpenAPITests/ExampleTestSuite.swift @@ -0,0 +1,59 @@ +// +// ExampleTestSuite.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import OpenAPIKit +import OpenAPIKit30 +import OpenAPIKitCompat +import Testing +import Yams + +@testable import FeatherOpenAPI + +@Suite +struct ExampleTestSuite { + + @Test + func render() throws { + + let document = ExampleDocument() + + let openAPIdoc = document.openAPIDocument() + _ = try openAPIdoc.locallyDereferenced().resolved() + + let encoder = YAMLEncoder() + let result = try encoder.encode(openAPIdoc) + print("---- 3.0 ----") + print(result) + } + + @Test + func duplicatedItem() throws { + + let document = ExampleDuplicatedItemDocument() + + let openAPIdoc = document.openAPIDocument() + _ = try openAPIdoc.locallyDereferenced().resolved() + + let encoder = YAMLEncoder() + let result = try encoder.encode(openAPIdoc) + print("---- 3.0 ----") + print(result) + } + + @Test + func missingParentItem() throws { + + let document = ExampleMissingParentItemItemDocument() + + let openAPIdoc = document.openAPIDocument() + _ = try openAPIdoc.locallyDereferenced().resolved() + + let encoder = YAMLEncoder() + let result = try encoder.encode(openAPIdoc) + print("---- 3.0 ----") + print(result) + } +} diff --git a/Tests/FeatherOpenAPIKitTests/PathComponentTests.swift b/Tests/FeatherOpenAPITests/PathComponentTests.swift similarity index 54% rename from Tests/FeatherOpenAPIKitTests/PathComponentTests.swift rename to Tests/FeatherOpenAPITests/PathComponentTests.swift index 9bbc103..cb96c28 100644 --- a/Tests/FeatherOpenAPIKitTests/PathComponentTests.swift +++ b/Tests/FeatherOpenAPITests/PathComponentTests.swift @@ -1,22 +1,20 @@ // -// File.swift -// -// -// Created by Tibor Bodecs on 25/01/2024. +// PathComponentTests.swift +// feather-openapi // +// Created by Tibor Bödecs on 2026. 01. 22.. -import Foundation -import XCTest +import Testing -@testable import FeatherOpenAPIKit +@testable import FeatherOpenAPI -fileprivate extension Path { +extension Path { - static func star(_ param: String) -> Path { + fileprivate static func star(_ param: String) -> Path { Path("*" + param + "*") } - static func superstar() -> Path { + fileprivate static func superstar() -> Path { Path("********") } } @@ -26,82 +24,95 @@ private struct ParameterDummy { let name: String } -fileprivate extension Path { +extension Path { - static func parameter(_ param: ParameterDummy) -> Path { + fileprivate static func parameter(_ param: ParameterDummy) -> Path { parameter(param.name) } } -final class PathComponentTests: XCTestCase { +@Suite +struct PathComponentTests { - func testDoesNotCompile() { + @Test + func doesNotCompile() { // let test = "foo" / "part_second" / "part_third" } - func testStringLiteralInit() { + @Test + func stringLiteralInit() { let test: Path = .init("foo") - XCTAssertEqual(test.value, "foo") + #expect(test.value == "foo") } - func testExplicitPathComponent() { + @Test + func explicitPathComponent() { let test = Path("foo") / Path("part_second") / Path("part_third") - XCTAssertEqual(test.value, "foo/part_second/part_third") + #expect(test.value == "foo/part_second/part_third") } - func testExplicitPathComponentOp() { + @Test + func explicitPathComponentOp() { let test = Path("foo") / Path("part_second") / Path("part_third") - XCTAssertEqual(test.value, "foo/part_second/part_third") + #expect(test.value == "foo/part_second/part_third") } - func testPureStringLiteralOp() { + @Test + func pureStringLiteralOp() { let test: Path = .init("foo") / "part_second" - XCTAssertEqual(test.value, "foo/part_second") + #expect(test.value == "foo/part_second") } - func testDecorInit() { + @Test + func decorInit() { let test: Path = .parameter("param") - XCTAssertEqual(test.value, "{param}") + #expect(test.value == "{param}") } - func testDecorOp() { + @Test + func decorOp() { let test: Path = .init("foo") / "part_second" / .parameter("param") / .star("star") - XCTAssertEqual(test.value, "foo/part_second/{param}/*star*") + #expect(test.value == "foo/part_second/{param}/*star*") } - func testStringInit() { + @Test + func stringInit() { let strTest = "string" let test: Path = .init(strTest) - XCTAssertEqual(test.value, "string") + #expect(test.value == "string") } - func testStringOp_1() { + @Test + func stringOp_1() { let strTest = "string" let test: Path = Path("foo") / strTest - XCTAssertEqual(test.value, "foo/string") + #expect(test.value == "foo/string") } - func testStringOp_2() { + @Test + func stringOp_2() { let strTest = "string" let test: Path = strTest / Path("foo") - XCTAssertEqual(test.value, "string/foo") + #expect(test.value == "string/foo") } - func testParameterOp() { + @Test + func parameterOp() { let paramDummy = ParameterDummy(name: "param") let test: Path = .init("foo") / "part_second" / .parameter(paramDummy) // also works with explicit name: //let test: PathComponent = .init("foo") / "part_second" / .parameter(paramDummy.name) - XCTAssertEqual(test.value, "foo/part_second/{param}") + #expect(test.value == "foo/part_second/{param}") } - func testUltimate() { + @Test + func ultimate() { // utimate op test //TODO: may replace by freestanding macros like that: // #let_path_comp("example") @@ -112,6 +123,6 @@ final class PathComponentTests: XCTestCase { let test = example / model / .superstar() / .parameter(paramVariable) - XCTAssertEqual(test.value, "example/model/********/{variable-id}") + #expect(test.value == "example/model/********/{variable-id}") } } diff --git a/Tests/FeatherOpenAPITests/Petstore/ApiResponse/ApiResponse+Schemas.swift b/Tests/FeatherOpenAPITests/Petstore/ApiResponse/ApiResponse+Schemas.swift new file mode 100644 index 0000000..96448cc --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/ApiResponse/ApiResponse+Schemas.swift @@ -0,0 +1,35 @@ +// +// ApiResponse+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.ApiResponse { + + struct CodeSchema: Int32SchemaRepresentable { + var required: Bool { false } + } + + struct ResponseTypeSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct MessageSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct ApiResponseSchema: ObjectSchemaRepresentable { + var openAPIIdentifier: String { "ApiResponse" } + var propertyMap: SchemaMap { + [ + "code": CodeSchema(), + "type": ResponseTypeSchema(), + "message": MessageSchema(), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/ApiResponse/ApiResponse.swift b/Tests/FeatherOpenAPITests/Petstore/ApiResponse/ApiResponse.swift new file mode 100644 index 0000000..1939df3 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/ApiResponse/ApiResponse.swift @@ -0,0 +1,12 @@ +// +// ApiResponse.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore { + enum ApiResponse {} +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Category/Category+Schemas.swift b/Tests/FeatherOpenAPITests/Petstore/Category/Category+Schemas.swift new file mode 100644 index 0000000..9ced790 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Category/Category+Schemas.swift @@ -0,0 +1,32 @@ +// +// Category+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Category { + + struct IdSchema: Int64SchemaRepresentable { + var example: Int64? { 1 } + var required: Bool { false } + } + + struct NameSchema: StringSchemaRepresentable { + var example: String? { "Dogs" } + var required: Bool { false } + } + + struct CategorySchema: ObjectSchemaRepresentable { + var openAPIIdentifier: String { "Category" } + var propertyMap: SchemaMap { + [ + "id": IdSchema(), + "name": NameSchema(), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Category/Category.swift b/Tests/FeatherOpenAPITests/Petstore/Category/Category.swift new file mode 100644 index 0000000..91849fc --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Category/Category.swift @@ -0,0 +1,12 @@ +// +// Category.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore { + enum Category {} +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Operations.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Operations.swift new file mode 100644 index 0000000..d5bcc17 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Operations.swift @@ -0,0 +1,231 @@ +// +// Pet+Operations.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Pet { + + struct UpdateOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Update an existing pet." } + var description: String? { "Update an existing pet by Id." } + var operationId: String? { "updatePet" } + var requestBody: RequestBodyRepresentable? { + UpdateRequestBody() + } + var responseMap: ResponseMap { + [ + 200: PetResponse(description: "Successful operation"), + 400: EmptyResponse(description: "Invalid ID supplied"), + 404: EmptyResponse(description: "Pet not found"), + 422: EmptyResponse(description: "Validation exception"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } + + struct AddOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Add a new pet to the store." } + var description: String? { "Add a new pet to the store." } + var operationId: String? { "addPet" } + var requestBody: RequestBodyRepresentable? { + AddRequestBody() + } + var responseMap: ResponseMap { + [ + 200: PetResponse(description: "Successful operation"), + 400: EmptyResponse(description: "Invalid input"), + 422: EmptyResponse(description: "Validation exception"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } + + struct FindByStatusOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Finds Pets by status." } + var description: String? { + "Multiple status values can be provided with comma separated strings." + } + var operationId: String? { "findPetsByStatus" } + var parameters: [ParameterRepresentable] { + [ + StatusQueryParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: PetListResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid status value"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } + + struct FindByTagsOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Finds Pets by tags." } + var description: String? { + "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing." + } + var operationId: String? { "findPetsByTags" } + var parameters: [ParameterRepresentable] { + [ + TagsQueryParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: PetListResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid tag value"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } + + struct GetByIdOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Find pet by ID." } + var description: String? { "Returns a single pet." } + var operationId: String? { "getPetById" } + var parameters: [ParameterRepresentable] { + [ + IdParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: PetResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid ID supplied"), + 404: EmptyResponse(description: "Pet not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + ApiKeySecurityRequirement(), + PetstoreAuthSecurityRequirement(), + ] + } + } + + struct UpdateWithFormOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { + "Updates a pet in the store with form data." + } + var description: String? { + "Updates a pet resource based on the form data." + } + var operationId: String? { "updatePetWithForm" } + var parameters: [ParameterRepresentable] { + [ + UpdateIdParameter(), + NameQueryParameter(), + StatusUpdateQueryParameter(), + ] + } + var responseMap: ResponseMap { + [ + 200: PetResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid input"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } + + struct DeleteOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Deletes a pet." } + var description: String? { "Delete a pet." } + var operationId: String? { "deletePet" } + var parameters: [ParameterRepresentable] { + [ + ApiKeyHeaderParameter(), + DeleteIdParameter(), + ] + } + var responseMap: ResponseMap { + [ + 200: EmptyResponse(description: "Pet deleted"), + 400: EmptyResponse(description: "Invalid pet value"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } + + struct UploadImageOperation: OperationRepresentable { + var tags: [TagRepresentable] { [PetTag()] } + var summary: String? { "Uploads an image." } + var description: String? { "Upload image of the pet." } + var operationId: String? { "uploadFile" } + var parameters: [ParameterRepresentable] { + [ + UploadIdParameter(), + AdditionalMetadataQueryParameter(), + ] + } + var requestBody: RequestBodyRepresentable? { + UploadImageRequestBody() + } + var responseMap: ResponseMap { + [ + 200: ApiResponseJSONResponse( + description: "successful operation" + ), + 400: EmptyResponse(description: "No file uploaded"), + 404: EmptyResponse(description: "Pet not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + PetstoreAuthSecurityRequirement() + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Parameters.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Parameters.swift new file mode 100644 index 0000000..1e26068 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Parameters.swift @@ -0,0 +1,99 @@ +// +// Pet+Parameters.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.Pet { + + struct IdParameter: PathParameterRepresentable { + var name: String { "petId" } + var description: String? { "ID of pet to return" } + var schema: any OpenAPISchemaRepresentable { + IdSchema() + } + } + + struct UpdateIdParameter: PathParameterRepresentable { + var name: String { "petId" } + var description: String? { "ID of pet that needs to be updated" } + var schema: any OpenAPISchemaRepresentable { + IdSchema() + } + } + + struct DeleteIdParameter: PathParameterRepresentable { + var name: String { "petId" } + var description: String? { "Pet id to delete" } + var schema: any OpenAPISchemaRepresentable { + IdSchema() + } + } + + struct UploadIdParameter: PathParameterRepresentable { + var name: String { "petId" } + var description: String? { "ID of pet to update" } + var schema: any OpenAPISchemaRepresentable { + IdSchema() + } + } + + struct StatusQueryParameter: QueryParameterRepresentable { + var name: String { "status" } + var description: String? { + "Status values that need to be considered for filter" + } + var required: Bool { true } + var schema: any OpenAPISchemaRepresentable { + StatusQuerySchema() + } + } + + struct TagsQueryParameter: QueryParameterRepresentable { + var name: String { "tags" } + var description: String? { "Tags to filter by" } + var required: Bool { true } + var schema: any OpenAPISchemaRepresentable { + TagsQuerySchema() + } + } + + struct NameQueryParameter: QueryParameterRepresentable { + var name: String { "name" } + var description: String? { "Name of pet that needs to be updated" } + var required: Bool { false } + var schema: any OpenAPISchemaRepresentable { + UpdateNameSchema() + } + } + + struct StatusUpdateQueryParameter: QueryParameterRepresentable { + var name: String { "status" } + var description: String? { "Status of pet that needs to be updated" } + var required: Bool { false } + var schema: any OpenAPISchemaRepresentable { + UpdateStatusSchema() + } + } + + struct ApiKeyHeaderParameter: HeaderParameterRepresentable { + var name: String { "api_key" } + var description: String? { "" } + var required: Bool { false } + var schema: any OpenAPISchemaRepresentable { + ApiKeySchema() + } + } + + struct AdditionalMetadataQueryParameter: QueryParameterRepresentable { + var name: String { "additionalMetadata" } + var description: String? { "Additional Metadata" } + var required: Bool { false } + var schema: any OpenAPISchemaRepresentable { + AdditionalMetadataSchema() + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+PathItems.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+PathItems.swift new file mode 100644 index 0000000..07dcc69 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+PathItems.swift @@ -0,0 +1,34 @@ +// +// Pet+PathItems.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.Pet { + + struct MainPathItem: PathItemRepresentable { + var put: OperationRepresentable? { UpdateOperation() } + var post: OperationRepresentable? { AddOperation() } + } + + struct FindByStatusPathItem: PathItemRepresentable { + var get: OperationRepresentable? { FindByStatusOperation() } + } + + struct FindByTagsPathItem: PathItemRepresentable { + var get: OperationRepresentable? { FindByTagsOperation() } + } + + struct IdentifiedPathItem: PathItemRepresentable { + var get: OperationRepresentable? { GetByIdOperation() } + var post: OperationRepresentable? { UpdateWithFormOperation() } + var delete: OperationRepresentable? { DeleteOperation() } + } + + struct UploadImagePathItem: PathItemRepresentable { + var post: OperationRepresentable? { UploadImageOperation() } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+RequestBodies.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+RequestBodies.swift new file mode 100644 index 0000000..9bf8732 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+RequestBodies.swift @@ -0,0 +1,54 @@ +// +// Pet+RequestBodies.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Pet { + + struct UpdateRequestBody: RequestBodyRepresentable { + var description: String? { "Update an existent pet in the store" } + var required: Bool { true } + var contentMap: ContentMap { + [ + .json: Content(PetSchema().reference()), + .xml: Content(PetSchema().reference()), + .form: Content(PetSchema().reference()), + ] + } + } + + struct AddRequestBody: RequestBodyRepresentable { + var description: String? { "Create a new pet in the store" } + var required: Bool { true } + var contentMap: ContentMap { + [ + .json: Content(PetSchema().reference()), + .xml: Content(PetSchema().reference()), + .form: Content(PetSchema().reference()), + ] + } + } + + struct UploadImageRequestBody: BinaryRequestBodyRepresentable { + var required: Bool { false } + } + + struct PetComponentRequestBody: RequestBodyRepresentable { + var openAPIIdentifier: String { "Pet" } + var description: String? { + "Pet object that needs to be added to the store" + } + var required: Bool { false } + var contentMap: ContentMap { + [ + .json: Content(PetSchema().reference()), + .xml: Content(PetSchema().reference()), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Responses.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Responses.swift new file mode 100644 index 0000000..85ea59f --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Responses.swift @@ -0,0 +1,48 @@ +// +// Pet+Responses.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Pet { + + struct PetResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content(PetSchema().reference()), + .xml: Content(PetSchema().reference()), + ] + } + } + + struct PetListResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content(PetListSchema()), + .xml: Content(PetListSchema()), + ] + } + } + + struct ApiResponseJSONResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content( + Petstore.ApiResponse.ApiResponseSchema().reference() + ) + ] + } + } + + struct EmptyResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { [:] } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Schemas.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Schemas.swift new file mode 100644 index 0000000..027f202 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Schemas.swift @@ -0,0 +1,98 @@ +// +// Pet+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Pet { + + struct IdSchema: Int64SchemaRepresentable { + var example: Int64? { 10 } + var required: Bool { false } + } + + struct NameSchema: StringSchemaRepresentable { + var example: String? { "doggie" } + } + + struct PhotoUrlItemSchema: StringSchemaRepresentable { + } + + struct PhotoUrlsSchema: ArraySchemaRepresentable { + var items: SchemaRepresentable? { PhotoUrlItemSchema() } + } + + struct TagsSchema: ArraySchemaRepresentable { + var required: Bool { false } + var items: SchemaRepresentable? { Petstore.Tag.TagSchema().reference() } + } + + struct StatusSchema: StringSchemaRepresentable { + var description: String? { "pet status in the store" } + var allowedValues: [String]? { + [ + "available", + "pending", + "sold", + ] + } + var required: Bool { false } + } + + struct PetSchema: ObjectSchemaRepresentable { + var openAPIIdentifier: String { "Pet" } + var propertyMap: SchemaMap { + [ + "id": IdSchema(), + "name": NameSchema(), + "category": Petstore.Category.CategorySchema() + .reference(required: false), + "photoUrls": PhotoUrlsSchema(), + "tags": TagsSchema(), + "status": StatusSchema(), + ] + } + } + + struct PetListSchema: ArraySchemaRepresentable { + var items: SchemaRepresentable? { PetSchema().reference() } + } + + struct StatusQuerySchema: StringSchemaRepresentable { + var defaultValue: String? { "available" } + var allowedValues: [String]? { + [ + "available", + "pending", + "sold", + ] + } + } + + struct TagsQueryItemSchema: StringSchemaRepresentable { + } + + struct TagsQuerySchema: ArraySchemaRepresentable { + var items: SchemaRepresentable? { TagsQueryItemSchema() } + } + + struct UpdateNameSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct UpdateStatusSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct AdditionalMetadataSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct ApiKeySchema: StringSchemaRepresentable { + var required: Bool { false } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Tags.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Tags.swift new file mode 100644 index 0000000..c6fa2eb --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet+Tags.swift @@ -0,0 +1,26 @@ +// +// Pet+Tags.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.Pet { + + struct PetTag: TagRepresentable { + var name: String { "pet" } + var description: String? { "Everything about your Pets" } + var externalDocs: ExternalDocsRepresentable? { + PetTagExternalDocs() + } + } + + struct PetTagExternalDocs: ExternalDocsRepresentable { + var description: String? { "Find out more" } + var url: LocationRepresentable { + PetstoreLocation(location: "https://swagger.io") + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Pet/Pet.swift b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet.swift new file mode 100644 index 0000000..63d77ea --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Pet/Pet.swift @@ -0,0 +1,12 @@ +// +// Pet.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore { + enum Pet {} +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Petstore+Security.swift b/Tests/FeatherOpenAPITests/Petstore/Petstore+Security.swift new file mode 100644 index 0000000..ab95a42 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Petstore+Security.swift @@ -0,0 +1,62 @@ +// +// Petstore+Security.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct PetstoreAuthSecurityScheme: SecuritySchemeRepresentable { + + var type: OpenAPI.SecurityScheme.SecurityType { + .oauth2( + flows: .init( + implicit: .init( + authorizationUrl: URL( + string: "https://petstore3.swagger.io/oauth/authorize" + )!, + refreshUrl: nil, + scopes: [ + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + ] + ) + ) + ) + } +} + +struct ApiKeySecurityScheme: SecuritySchemeRepresentable { + var type: OpenAPI.SecurityScheme.SecurityType { + .apiKey( + name: "api_key", + location: .header + ) + } +} + +struct PetstoreAuthSecurityRequirement: SecurityRequirementRepresentable { + var security: any SecuritySchemeRepresentable { + PetstoreAuthSecurityScheme() + } + var requirements: [String] { + [ + "write:pets", + "read:pets", + ] + } +} + +struct ApiKeySecurityRequirement: SecurityRequirementRepresentable { + var security: any SecuritySchemeRepresentable { + ApiKeySecurityScheme() + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Petstore.swift b/Tests/FeatherOpenAPITests/Petstore/Petstore.swift new file mode 100644 index 0000000..324ee94 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Petstore.swift @@ -0,0 +1,159 @@ +// +// Petstore.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +enum Petstore {} + +struct PetstoreLocation: LocationRepresentable { + let location: String +} + +struct PetstoreContact: ContactRepresentable { + var email: String? { "apiteam@swagger.io" } +} + +struct PetstoreLicense: LicenseRepresentable { + var name: String { "Apache 2.0" } + var url: LocationRepresentable? { + PetstoreLocation( + location: "https://www.apache.org/licenses/LICENSE-2.0.html" + ) + } +} + +struct PetstoreInfo: InfoRepresentable { + var title: String { "Swagger Petstore - OpenAPI 3.0" } + var description: String? { + """ + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + """ + } + var termsOfService: LocationRepresentable? { + PetstoreLocation(location: "https://swagger.io/terms/") + } + var contact: OpenAPIContactRepresentable? { PetstoreContact() } + var license: OpenAPILicenseRepresentable? { PetstoreLicense() } + var version: String { "1.0.27" } +} + +struct PetstoreServer: ServerRepresentable { + var url: LocationRepresentable { PetstoreLocation(location: "/api/v3") } +} + +struct PetstoreExternalDocs: ExternalDocsRepresentable { + var description: String? { "Find out more about Swagger" } + var url: LocationRepresentable { + PetstoreLocation(location: "https://swagger.io") + } +} + +struct PetstorePathCollection: PathCollectionRepresentable { + var pathMap: PathMap { + [ + "/pet": Petstore.Pet.MainPathItem(), + "/pet/findByStatus": Petstore.Pet.FindByStatusPathItem(), + "/pet/findByTags": Petstore.Pet.FindByTagsPathItem(), + "/pet/{petId}": Petstore.Pet.IdentifiedPathItem(), + "/pet/{petId}/uploadImage": Petstore.Pet.UploadImagePathItem(), + "/store/inventory": Petstore.Store.InventoryPathItem(), + "/store/order": Petstore.Store.OrderPathItem(), + "/store/order/{orderId}": Petstore.Store.OrderIdentifiedPathItem(), + "/user": Petstore.User.MainPathItem(), + "/user/createWithList": Petstore.User.CreateWithListPathItem(), + "/user/login": Petstore.User.LoginPathItem(), + "/user/logout": Petstore.User.LogoutPathItem(), + "/user/{username}": Petstore.User.IdentifiedPathItem(), + ] + } +} + +struct PetstoreComponents: ComponentsRepresentable { + let base: FeatherOpenAPI.Components + + var schemas: OrderedDictionary { + base.schemas + } + + var parameters: + OrderedDictionary + { + base.parameters + } + + var examples: OrderedDictionary { + base.examples + } + + var responses: OrderedDictionary { + base.responses + } + + var requestBodies: + OrderedDictionary< + RequestBodyID, OpenAPIRequestBodyRepresentable + > + { + var results = base.requestBodies + + let petRequestBody = Petstore.Pet.PetComponentRequestBody() + results[.init(petRequestBody.openAPIIdentifier)] = petRequestBody + + let userArrayRequestBody = + Petstore.User.UserArrayComponentRequestBody() + results[.init(userArrayRequestBody.openAPIIdentifier)] = + userArrayRequestBody + + return results + } + + var headers: OrderedDictionary { + base.headers + } + + var securityRequirements: [SecurityRequirementRepresentable] { + base.securityRequirements + } + + var links: OrderedDictionary { + base.links + } +} + +struct PetstoreDocument: DocumentRepresentable { + let collection = PetstorePathCollection() + + var info: OpenAPIInfoRepresentable { PetstoreInfo() } + var servers: [OpenAPIServerRepresentable] { [PetstoreServer()] } + + var externalDocs: ExternalDocsRepresentable? { PetstoreExternalDocs() } + + var paths: PathMap { collection.pathMap } + var components: OpenAPIComponentsRepresentable { + PetstoreComponents(base: collection.components) + } + + var referencedTags: [OpenAPITagRepresentable] { + [ + Petstore.Pet.PetTag(), + Petstore.Store.StoreTag(), + Petstore.User.UserTag(), + ] + } + + var referencedSecurityRequirements: [SecurityRequirementRepresentable] { + [] + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+Operations.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Operations.swift new file mode 100644 index 0000000..e8fc46a --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Operations.swift @@ -0,0 +1,98 @@ +// +// Store+Operations.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Store { + + struct InventoryOperation: OperationRepresentable { + var tags: [TagRepresentable] { [StoreTag()] } + var summary: String? { "Returns pet inventories by status." } + var description: String? { + "Returns a map of status codes to quantities." + } + var operationId: String? { "getInventory" } + var responseMap: ResponseMap { + [ + 200: InventoryResponse(description: "successful operation"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + var security: [any SecurityRequirementRepresentable]? { + [ + ApiKeySecurityRequirement() + ] + } + } + + struct PlaceOrderOperation: OperationRepresentable { + var tags: [TagRepresentable] { [StoreTag()] } + var summary: String? { "Place an order for a pet." } + var description: String? { "Place a new order in the store." } + var operationId: String? { "placeOrder" } + var requestBody: RequestBodyRepresentable? { + PlaceOrderRequestBody() + } + var responseMap: ResponseMap { + [ + 200: OrderJSONResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid input"), + 422: EmptyResponse(description: "Validation exception"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct GetOrderOperation: OperationRepresentable { + var tags: [TagRepresentable] { [StoreTag()] } + var summary: String? { "Find purchase order by ID." } + var description: String? { + "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions." + } + var operationId: String? { "getOrderById" } + var parameters: [ParameterRepresentable] { + [ + OrderIdParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: OrderResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid ID supplied"), + 404: EmptyResponse(description: "Order not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct DeleteOrderOperation: OperationRepresentable { + var tags: [TagRepresentable] { [StoreTag()] } + var summary: String? { "Delete purchase order by identifier." } + var description: String? { + "For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors." + } + var operationId: String? { "deleteOrder" } + var parameters: [ParameterRepresentable] { + [ + OrderIdDeleteParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: EmptyResponse(description: "order deleted"), + 400: EmptyResponse(description: "Invalid ID supplied"), + 404: EmptyResponse(description: "Order not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+Parameters.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Parameters.swift new file mode 100644 index 0000000..aad03d0 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Parameters.swift @@ -0,0 +1,29 @@ +// +// Store+Parameters.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.Store { + + struct OrderIdParameter: PathParameterRepresentable { + var name: String { "orderId" } + var description: String? { "ID of order that needs to be fetched" } + var schema: any OpenAPISchemaRepresentable { + OrderIdSchema() + } + } + + struct OrderIdDeleteParameter: PathParameterRepresentable { + var name: String { "orderId" } + var description: String? { + "ID of the order that needs to be deleted" + } + var schema: any OpenAPISchemaRepresentable { + OrderIdSchema() + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+PathItems.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+PathItems.swift new file mode 100644 index 0000000..e5ca791 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+PathItems.swift @@ -0,0 +1,24 @@ +// +// Store+PathItems.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.Store { + + struct InventoryPathItem: PathItemRepresentable { + var get: OperationRepresentable? { InventoryOperation() } + } + + struct OrderPathItem: PathItemRepresentable { + var post: OperationRepresentable? { PlaceOrderOperation() } + } + + struct OrderIdentifiedPathItem: PathItemRepresentable { + var get: OperationRepresentable? { GetOrderOperation() } + var delete: OperationRepresentable? { DeleteOrderOperation() } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+RequestBodies.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+RequestBodies.swift new file mode 100644 index 0000000..3daa84b --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+RequestBodies.swift @@ -0,0 +1,23 @@ +// +// Store+RequestBodies.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Store { + + struct PlaceOrderRequestBody: RequestBodyRepresentable { + var required: Bool { false } + var contentMap: ContentMap { + [ + .json: Content(OrderSchema().reference()), + .xml: Content(OrderSchema().reference()), + .form: Content(OrderSchema().reference()), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+Responses.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Responses.swift new file mode 100644 index 0000000..37bcf59 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Responses.swift @@ -0,0 +1,45 @@ +// +// Store+Responses.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Store { + + struct OrderResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content(OrderSchema().reference()), + .xml: Content(OrderSchema().reference()), + ] + } + } + + struct OrderJSONResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content(OrderSchema().reference()) + ] + } + } + + struct InventoryResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content(InventorySchema()) + ] + } + } + + struct EmptyResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { [:] } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+Schemas.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Schemas.swift new file mode 100644 index 0000000..3b5fb7f --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Schemas.swift @@ -0,0 +1,109 @@ +// +// Store+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Store { + + struct OrderIdSchema: Int64SchemaRepresentable { + var example: Int64? { 10 } + var required: Bool { false } + } + + struct OrderPetIdSchema: Int64SchemaRepresentable { + var example: Int64? { 198772 } + var required: Bool { false } + } + + struct OrderQuantitySchema: Int32SchemaRepresentable { + var example: Int32? { 7 } + var required: Bool { false } + } + + struct OrderShipDateSchema: SchemaRepresentable { + var required: Bool { false } + func openAPISchema() -> JSONSchema { + .string( + format: .dateTime, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + minLength: nil, + maxLength: nil, + pattern: nil, + allowedValues: nil, + defaultValue: nil, + example: nil + ) + } + } + + struct OrderStatusSchema: StringSchemaRepresentable { + var description: String? { "Order Status" } + var allowedValues: [String]? { + [ + "placed", + "approved", + "delivered", + ] + } + var example: String? { "approved" } + var required: Bool { false } + } + + struct OrderCompleteSchema: BoolSchemaRepresentable { + var required: Bool { false } + } + + struct OrderSchema: ObjectSchemaRepresentable { + var openAPIIdentifier: String { "Order" } + var propertyMap: SchemaMap { + [ + "id": OrderIdSchema(), + "petId": OrderPetIdSchema(), + "quantity": OrderQuantitySchema(), + "shipDate": OrderShipDateSchema(), + "status": OrderStatusSchema(), + "complete": OrderCompleteSchema(), + ] + } + } + + struct InventorySchema: SchemaRepresentable { + func openAPISchema() -> JSONSchema { + .object( + format: .generic, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + minProperties: nil, + maxProperties: nil, + properties: [:], + additionalProperties: .schema( + InventoryQuantitySchema().openAPISchema() + ), + allowedValues: nil, + defaultValue: nil, + example: nil + ) + } + } + + struct InventoryQuantitySchema: Int32SchemaRepresentable { + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store+Tags.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Tags.swift new file mode 100644 index 0000000..15c7eb8 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store+Tags.swift @@ -0,0 +1,26 @@ +// +// Store+Tags.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.Store { + + struct StoreTag: TagRepresentable { + var name: String { "store" } + var description: String? { "Access to Petstore orders" } + var externalDocs: ExternalDocsRepresentable? { + StoreTagExternalDocs() + } + } + + struct StoreTagExternalDocs: ExternalDocsRepresentable { + var description: String? { "Find out more about our store" } + var url: LocationRepresentable { + PetstoreLocation(location: "https://swagger.io") + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Store/Store.swift b/Tests/FeatherOpenAPITests/Petstore/Store/Store.swift new file mode 100644 index 0000000..38c8164 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Store/Store.swift @@ -0,0 +1,12 @@ +// +// Store.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore { + enum Store {} +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Tag/Tag+Schemas.swift b/Tests/FeatherOpenAPITests/Petstore/Tag/Tag+Schemas.swift new file mode 100644 index 0000000..1b176ba --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Tag/Tag+Schemas.swift @@ -0,0 +1,30 @@ +// +// Tag+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.Tag { + + struct IdSchema: Int64SchemaRepresentable { + var required: Bool { false } + } + + struct NameSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct TagSchema: ObjectSchemaRepresentable { + var openAPIIdentifier: String { "Tag" } + var propertyMap: SchemaMap { + [ + "id": IdSchema(), + "name": NameSchema(), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/Tag/Tag.swift b/Tests/FeatherOpenAPITests/Petstore/Tag/Tag.swift new file mode 100644 index 0000000..2432e9c --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/Tag/Tag.swift @@ -0,0 +1,12 @@ +// +// Tag.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore { + enum Tag {} +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+Headers.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+Headers.swift new file mode 100644 index 0000000..f8d4f9c --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+Headers.swift @@ -0,0 +1,51 @@ +// +// User+Headers.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.User { + + struct RateLimitHeader: HeaderRepresentable { + var description: String? { "calls per hour allowed by the user" } + var schema: any OpenAPISchemaRepresentable { + RateLimitSchema() + } + } + + struct ExpiresAfterHeader: HeaderRepresentable { + var description: String? { "date in UTC when token expires" } + var schema: any OpenAPISchemaRepresentable { + ExpiresAfterSchema() + } + } + + struct RateLimitSchema: Int32SchemaRepresentable { + } + + struct ExpiresAfterSchema: SchemaRepresentable { + func openAPISchema() -> JSONSchema { + .string( + format: .dateTime, + required: `required`, + nullable: nullable, + permissions: nil, + deprecated: deprecated, + title: title, + description: description, + discriminator: nil, + externalDocs: nil, + minLength: nil, + maxLength: nil, + pattern: nil, + allowedValues: nil, + defaultValue: nil, + example: nil + ) + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+Operations.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+Operations.swift new file mode 100644 index 0000000..5f57f1e --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+Operations.swift @@ -0,0 +1,153 @@ +// +// User+Operations.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.User { + + struct CreateOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Create user." } + var description: String? { + "This can only be done by the logged in user." + } + var operationId: String? { "createUser" } + var requestBody: RequestBodyRepresentable? { CreateRequestBody() } + var responseMap: ResponseMap { + [ + 200: UserResponse(description: "successful operation"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct CreateWithListOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Creates list of users with given input array." } + var description: String? { + "Creates list of users with given input array." + } + var operationId: String? { "createUsersWithListInput" } + var requestBody: RequestBodyRepresentable? { + CreateWithListRequestBody() + } + var responseMap: ResponseMap { + [ + 200: UserResponse(description: "Successful operation"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct LoginOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Logs user into the system." } + var description: String? { "Log into the system." } + var operationId: String? { "loginUser" } + var parameters: [ParameterRepresentable] { + [ + LoginUsernameParameter(), + LoginPasswordParameter(), + ] + } + var responseMap: ResponseMap { + [ + 200: LoginResponse(), + 400: EmptyResponse( + description: "Invalid username/password supplied" + ), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct LogoutOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Logs out current logged in user session." } + var description: String? { "Log user out of the system." } + var operationId: String? { "logoutUser" } + var responseMap: ResponseMap { + [ + 200: EmptyResponse(description: "successful operation"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct GetByNameOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Get user by user name." } + var description: String? { "Get user detail based on username." } + var operationId: String? { "getUserByName" } + var parameters: [ParameterRepresentable] { + [ + UsernameParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: UserResponse(description: "successful operation"), + 400: EmptyResponse(description: "Invalid username supplied"), + 404: EmptyResponse(description: "User not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct UpdateOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Update user resource." } + var description: String? { + "This can only be done by the logged in user." + } + var operationId: String? { "updateUser" } + var parameters: [ParameterRepresentable] { + [ + UpdateUsernameParameter() + ] + } + var requestBody: RequestBodyRepresentable? { UpdateRequestBody() } + var responseMap: ResponseMap { + [ + 200: EmptyResponse(description: "successful operation"), + 400: EmptyResponse(description: "bad request"), + 404: EmptyResponse(description: "user not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } + + struct DeleteOperation: OperationRepresentable { + var tags: [TagRepresentable] { [UserTag()] } + var summary: String? { "Delete user resource." } + var description: String? { + "This can only be done by the logged in user." + } + var operationId: String? { "deleteUser" } + var parameters: [ParameterRepresentable] { + [ + DeleteUsernameParameter() + ] + } + var responseMap: ResponseMap { + [ + 200: EmptyResponse(description: "User deleted"), + 400: EmptyResponse(description: "Invalid username supplied"), + 404: EmptyResponse(description: "User not found"), + .default: + EmptyResponse(description: "Unexpected error"), + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+Parameters.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+Parameters.swift new file mode 100644 index 0000000..314e929 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+Parameters.swift @@ -0,0 +1,55 @@ +// +// User+Parameters.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.User { + + struct LoginUsernameParameter: QueryParameterRepresentable { + var name: String { "username" } + var description: String? { "The user name for login" } + var required: Bool { false } + var schema: any OpenAPISchemaRepresentable { + LoginUsernameSchema() + } + } + + struct LoginPasswordParameter: QueryParameterRepresentable { + var name: String { "password" } + var description: String? { "The password for login in clear text" } + var required: Bool { false } + var schema: any OpenAPISchemaRepresentable { + LoginPasswordSchema() + } + } + + struct UsernameParameter: PathParameterRepresentable { + var name: String { "username" } + var description: String? { + "The name that needs to be fetched. Use user1 for testing" + } + var schema: any OpenAPISchemaRepresentable { + UsernameSchema() + } + } + + struct UpdateUsernameParameter: PathParameterRepresentable { + var name: String { "username" } + var description: String? { "name that need to be deleted" } + var schema: any OpenAPISchemaRepresentable { + UsernameSchema() + } + } + + struct DeleteUsernameParameter: PathParameterRepresentable { + var name: String { "username" } + var description: String? { "The name that needs to be deleted" } + var schema: any OpenAPISchemaRepresentable { + UsernameSchema() + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+PathItems.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+PathItems.swift new file mode 100644 index 0000000..6a50c75 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+PathItems.swift @@ -0,0 +1,33 @@ +// +// User+PathItems.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.User { + + struct MainPathItem: PathItemRepresentable { + var post: OperationRepresentable? { CreateOperation() } + } + + struct CreateWithListPathItem: PathItemRepresentable { + var post: OperationRepresentable? { CreateWithListOperation() } + } + + struct LoginPathItem: PathItemRepresentable { + var get: OperationRepresentable? { LoginOperation() } + } + + struct LogoutPathItem: PathItemRepresentable { + var get: OperationRepresentable? { LogoutOperation() } + } + + struct IdentifiedPathItem: PathItemRepresentable { + var get: OperationRepresentable? { GetByNameOperation() } + var put: OperationRepresentable? { UpdateOperation() } + var delete: OperationRepresentable? { DeleteOperation() } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+RequestBodies.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+RequestBodies.swift new file mode 100644 index 0000000..a2f7dd0 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+RequestBodies.swift @@ -0,0 +1,56 @@ +// +// User+RequestBodies.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.User { + + struct CreateRequestBody: RequestBodyRepresentable { + var description: String? { "Created user object" } + var required: Bool { false } + var contentMap: ContentMap { + [ + .json: Content(UserSchema().reference()), + .xml: Content(UserSchema().reference()), + .form: Content(UserSchema().reference()), + ] + } + } + + struct UpdateRequestBody: RequestBodyRepresentable { + var description: String? { "Update an existent user in the store" } + var required: Bool { false } + var contentMap: ContentMap { + [ + .json: Content(UserSchema().reference()), + .xml: Content(UserSchema().reference()), + .form: Content(UserSchema().reference()), + ] + } + } + + struct CreateWithListRequestBody: RequestBodyRepresentable { + var required: Bool { false } + var contentMap: ContentMap { + [ + .json: Content(UserArraySchema()) + ] + } + } + + struct UserArrayComponentRequestBody: RequestBodyRepresentable { + var openAPIIdentifier: String { "UserArray" } + var description: String? { "List of user object" } + var required: Bool { false } + var contentMap: ContentMap { + [ + .json: Content(UserArraySchema()) + ] + } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+Responses.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+Responses.swift new file mode 100644 index 0000000..3fd2951 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+Responses.swift @@ -0,0 +1,46 @@ +// +// User+Responses.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.User { + + struct UserResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { + [ + .json: Content(UserSchema().reference()), + .xml: Content(UserSchema().reference()), + ] + } + } + + struct LoginResponse: ResponseRepresentable { + var description: String { "successful operation" } + var headerMap: HeaderMap { + [ + "X-Rate-Limit": RateLimitHeader(), + "X-Expires-After": ExpiresAfterHeader(), + ] + } + var contentMap: ContentMap { + [ + .xml: Content(LoginResponseSchema()), + .json: Content(LoginResponseSchema()), + ] + } + } + + struct LoginResponseSchema: StringSchemaRepresentable { + } + + struct EmptyResponse: ResponseRepresentable { + let description: String + var contentMap: ContentMap { [:] } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+Schemas.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+Schemas.swift new file mode 100644 index 0000000..4e26dd6 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+Schemas.swift @@ -0,0 +1,84 @@ +// +// User+Schemas.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +extension Petstore.User { + + struct UserIdSchema: Int64SchemaRepresentable { + var example: Int64? { 10 } + var required: Bool { false } + } + + struct UserNameSchema: StringSchemaRepresentable { + var example: String? { "theUser" } + var required: Bool { false } + } + + struct UserFirstNameSchema: StringSchemaRepresentable { + var example: String? { "John" } + var required: Bool { false } + } + + struct UserLastNameSchema: StringSchemaRepresentable { + var example: String? { "James" } + var required: Bool { false } + } + + struct UserEmailSchema: StringSchemaRepresentable { + var example: String? { "john@email.com" } + var required: Bool { false } + } + + struct UserPasswordSchema: StringSchemaRepresentable { + var example: String? { "12345" } + var required: Bool { false } + } + + struct UserPhoneSchema: StringSchemaRepresentable { + var example: String? { "12345" } + var required: Bool { false } + } + + struct UserStatusSchema: Int32SchemaRepresentable { + var description: String? { "User Status" } + var example: Int32? { 1 } + var required: Bool { false } + } + + struct UserSchema: ObjectSchemaRepresentable { + var openAPIIdentifier: String { "User" } + var propertyMap: SchemaMap { + [ + "id": UserIdSchema(), + "username": UserNameSchema(), + "firstName": UserFirstNameSchema(), + "lastName": UserLastNameSchema(), + "email": UserEmailSchema(), + "password": UserPasswordSchema(), + "phone": UserPhoneSchema(), + "userStatus": UserStatusSchema(), + ] + } + } + + struct UsernameSchema: StringSchemaRepresentable { + } + + struct LoginUsernameSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct LoginPasswordSchema: StringSchemaRepresentable { + var required: Bool { false } + } + + struct UserArraySchema: ArraySchemaRepresentable { + var items: SchemaRepresentable? { UserSchema().reference() } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User+Tags.swift b/Tests/FeatherOpenAPITests/Petstore/User/User+Tags.swift new file mode 100644 index 0000000..9d89fe5 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User+Tags.swift @@ -0,0 +1,16 @@ +// +// User+Tags.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore.User { + + struct UserTag: TagRepresentable { + var name: String { "user" } + var description: String? { "Operations about user" } + } +} diff --git a/Tests/FeatherOpenAPITests/Petstore/User/User.swift b/Tests/FeatherOpenAPITests/Petstore/User/User.swift new file mode 100644 index 0000000..110334a --- /dev/null +++ b/Tests/FeatherOpenAPITests/Petstore/User/User.swift @@ -0,0 +1,12 @@ +// +// User.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import FeatherOpenAPI + +extension Petstore { + enum User {} +} diff --git a/Tests/FeatherOpenAPITests/PetstoreTestSuite.swift b/Tests/FeatherOpenAPITests/PetstoreTestSuite.swift new file mode 100644 index 0000000..374495d --- /dev/null +++ b/Tests/FeatherOpenAPITests/PetstoreTestSuite.swift @@ -0,0 +1,40 @@ +// +// PetstoreTestSuite.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 22.. +// + +import OpenAPIKit30 +import Testing +import Yams + +@testable import FeatherOpenAPI + +@Suite +struct PetstoreTestSuite { + + @Test + func render() throws { + + let document = PetstoreDocument() + + let encoder = YAMLEncoder() + let openAPIdoc = document.openAPIDocument() + + do { + _ = + try openAPIdoc + .locallyDereferenced() + .resolved() + } + catch { + Issue.record("\(error)") + return + } + + let result = try encoder.encode(openAPIdoc) + print("---- 3.0 ----") + print(result) + } +} diff --git a/Tests/FeatherOpenAPITests/Todo/TestObjects.swift b/Tests/FeatherOpenAPITests/Todo/TestObjects.swift new file mode 100644 index 0000000..6eaf712 --- /dev/null +++ b/Tests/FeatherOpenAPITests/Todo/TestObjects.swift @@ -0,0 +1,191 @@ +// +// TestObjects.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 21.. +// + +import FeatherOpenAPI +import OpenAPIKit30 + +struct MyPathCollection: PathCollectionRepresentable { + + var pathMap: PathMap { + [ + "todos": TodoPathItems() + // "laci": LaciPathItems(), + ] + } +} + +struct MyInfo: InfoRepresentable { + var title: String { "foo" } + var version: String { "1.0.0" } +} + +struct MyDocument: DocumentRepresentable { + + var info: OpenAPIInfoRepresentable + + var servers: [any OpenAPIServerRepresentable] { + [ + TestServer() + ] + } + + var paths: PathMap + var components: OpenAPIComponentsRepresentable + + init( + info: OpenAPIInfoRepresentable, + paths: PathMap, + components: OpenAPIComponentsRepresentable + ) { + self.info = info + self.paths = paths + self.components = components + } +} + +extension String: LocationRepresentable { + public var location: String { self } +} + +struct TestServer: ServerRepresentable { + var url: any LocationRepresentable { "http://127.0.0.1:8080/" } +} + +struct TodoIDField: IntSchemaRepresentable { + var example: Int? { 1 } +} + +struct TodoTitleField: StringSchemaRepresentable { + var example: String? = "Buy milk" +} + +struct TodoIsCompleteField: BoolSchemaRepresentable { + +} + +struct TodoDetailObject: ObjectSchemaRepresentable { + + var propertyMap: SchemaMap { + [ + "id": TodoIDField().reference(), + "title": TodoTitleField(), + "isComplete": TodoIsCompleteField(), + // "unsafe": UnsafeSchemaReference("asdf"), + ] + } + +} + +struct TodoCreateRequestBody: RequestBodyRepresentable { + + var contentMap: ContentMap { + [ + .json: Content(TodoDetailObject().reference()) + ] + } +} + +struct TodoCreateResponse: JSONResponseRepresentable { + var description: String = "Todo response" + var schema = TodoDetailObject().reference() + + var headerMap: HeaderMap { + [ + "x-custom-header": CustomHeader().reference() + ] + } +} + +struct TodoIdParameter: ParameterRepresentable { + + var name: String { "todoId" } + var context: OpenAPIKit30.OpenAPI.Parameter.Context { + .path + } + var schema: any OpenAPISchemaRepresentable { + TodoIDField().reference() + } +} + +struct CustomHeader: HeaderRepresentable { + var schema: any OpenAPISchemaRepresentable { + TodoIDField().reference() + } +} + +struct TodoTag: TagRepresentable { + var name: String = "Todos" + var description: String? = "This is the todo tag." +} + +struct TodoCreateOperation: OperationRepresentable { + + var tags: [TagRepresentable] { + [ + TodoTag() + ] + } + + var parameters: [ParameterRepresentable] { + [ + TodoIdParameter().reference() + ] + } + + var requestBody: RequestBodyRepresentable? { + TodoCreateRequestBody().reference() + } + + var responseMap: ResponseMap { + [ + 200: TodoCreateResponse().reference() + ] + } + + var security: [any SecurityRequirementRepresentable]? { + [ + OAuthSecurityRequirement(), + APIKeySecurityRequirement(), + ] + } + + var servers: [any ServerRepresentable]? { + [ + TestServer() + ] + } +} + +struct TodoPathItems: PathItemRepresentable { + var post: OperationRepresentable? = TodoCreateOperation() +} + +struct OAuthSecurityScheme: SecuritySchemeRepresentable { + + var type: OpenAPIKit30.OpenAPI.SecurityScheme.SecurityType = .oauth2( + flows: .init() + ) +} + +struct OAuthSecurityRequirement: SecurityRequirementRepresentable { + + var security: any SecuritySchemeRepresentable = OAuthSecurityScheme() + var requirements: [String] = ["read"] +} + +struct APIKeySecurityScheme: SecuritySchemeRepresentable { + + var type: OpenAPIKit30.OpenAPI.SecurityScheme.SecurityType = .apiKey( + name: "test", + location: .header + ) +} + +struct APIKeySecurityRequirement: SecurityRequirementRepresentable { + + var security: any SecuritySchemeRepresentable = APIKeySecurityScheme() +} diff --git a/Tests/FeatherOpenAPITests/TodoTestSuite.swift b/Tests/FeatherOpenAPITests/TodoTestSuite.swift new file mode 100644 index 0000000..82d679a --- /dev/null +++ b/Tests/FeatherOpenAPITests/TodoTestSuite.swift @@ -0,0 +1,54 @@ +// +// TodoTestSuite.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 01. 25.. + +import OpenAPIKit +import OpenAPIKit30 +import OpenAPIKitCompat +import Testing +import Yams + +@testable import FeatherOpenAPI + +@Suite +struct TodoTestSuite { + + @Test + func example() throws { + + let collection = MyPathCollection() + // collection.components.schemas.register(id: "", TodoFieldId()) + + let document = MyDocument( + info: MyInfo(), + paths: collection.pathMap, + components: collection.components, + ) + + let openAPIdoc = document.openAPIDocument() + + let encoder = YAMLEncoder() + + _ = + try openAPIdoc + .locallyDereferenced() + .resolved() + + let result = try encoder.encode(openAPIdoc) + print("---- 3.0 ----") + print(result) + + // let doc31 = openAPIdoc.convert(to: .v3_1_0) + // let result31 = try encoder.encode(doc31) + // print("---- 3.1 ----") + // print(result31) + // + // let doc32 = openAPIdoc.convert(to: .v3_2_0) + // let result32 = try encoder.encode(doc32) + // print("---- 3.2 ----") + // print(result32) + + } +} diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile new file mode 100644 index 0000000..73be175 --- /dev/null +++ b/docker/tests/Dockerfile @@ -0,0 +1,11 @@ +FROM swift:6.1 + +WORKDIR /app + +COPY . ./ + +RUN swift package resolve +RUN swift package clean +RUN swift package update + +CMD ["swift", "test", "--parallel", "--enable-code-coverage"] diff --git a/scripts/check-broken-symlinks.sh b/scripts/check-broken-symlinks.sh deleted file mode 100755 index 3d7e919..0000000 --- a/scripts/check-broken-symlinks.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - -log "Checking for broken symlinks..." -NUM_BROKEN_SYMLINKS=0 -while read -r -d '' file; do - if ! test -e "${REPO_ROOT}/${file}"; then - error "Broken symlink: ${file}" - ((NUM_BROKEN_SYMLINKS++)) - fi -done < <(git -C "${REPO_ROOT}" ls-files -z) - -if [ "${NUM_BROKEN_SYMLINKS}" -gt 0 ]; then - fatal "❌ Found ${NUM_BROKEN_SYMLINKS} symlinks." -fi - -log "✅ Found 0 symlinks." diff --git a/scripts/check-local-swift-dependencies.sh b/scripts/check-local-swift-dependencies.sh deleted file mode 100755 index 0cccd11..0000000 --- a/scripts/check-local-swift-dependencies.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - -read -ra PATHS_TO_CHECK <<< "$( \ - git -C "${REPO_ROOT}" ls-files -z \ - "Package.swift" \ - | xargs -0 \ -)" - -for FILE_PATH in "${PATHS_TO_CHECK[@]}"; do -echo $FILE_PATH - if [[ $(grep ".package(path:" "${FILE_PATH}"|wc -l) -ne 0 ]] ; then - fatal "❌ The '${FILE_PATH}' file contains local Swift package reference(s)." - fi -done - -log "✅ Found 0 local Swift package dependency references." diff --git a/scripts/check-unacceptable-language.sh b/scripts/check-unacceptable-language.sh deleted file mode 100755 index 3d93707..0000000 --- a/scripts/check-unacceptable-language.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" -UNACCEPTABLE_LANGUAGE_PATTERNS_PATH="${CURRENT_SCRIPT_DIR}/unacceptable-language.txt" - -log "Checking for unacceptable language..." -PATHS_WITH_UNACCEPTABLE_LANGUAGE=$(git -C "${REPO_ROOT}" grep \ - -l -F -w \ - -f "${UNACCEPTABLE_LANGUAGE_PATTERNS_PATH}" \ - -- \ - ":(exclude)${UNACCEPTABLE_LANGUAGE_PATTERNS_PATH}" \ -) || true | /usr/bin/paste -s -d " " - - -if [ -n "${PATHS_WITH_UNACCEPTABLE_LANGUAGE}" ]; then - fatal "❌ Found unacceptable language in files: ${PATHS_WITH_UNACCEPTABLE_LANGUAGE}." -fi - -log "✅ Found no unacceptable language." diff --git a/scripts/install-swift-format.sh b/scripts/install-swift-format.sh deleted file mode 100755 index 3fb3333..0000000 --- a/scripts/install-swift-format.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -# https://github.com/apple/swift-format - -VERSION="509.0.0" - -curl -L -o "${VERSION}.tar.gz" "https://github.com/apple/swift-format/archive/refs/tags/${VERSION}.tar.gz" -tar -xf "${VERSION}.tar.gz" -cd "swift-format-${VERSION}" -swift build -c release -install .build/release/swift-format /usr/local/bin/swift-format -cd .. -rm -f "${VERSION}.tar.gz" -rm -rf "swift-format-${VERSION}" diff --git a/scripts/run-checks.sh b/scripts/run-checks.sh deleted file mode 100755 index b42e0f9..0000000 --- a/scripts/run-checks.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -NUM_CHECKS_FAILED=0 - -FIX_FORMAT="" -for arg in "$@"; do - if [ "$arg" == "--fix" ]; then - FIX_FORMAT="--fix" - fi -done - -SCRIPT_PATHS=( - "${CURRENT_SCRIPT_DIR}/check-broken-symlinks.sh" - "${CURRENT_SCRIPT_DIR}/check-unacceptable-language.sh" - "${CURRENT_SCRIPT_DIR}/check-local-swift-dependencies.sh" -) - -for SCRIPT_PATH in "${SCRIPT_PATHS[@]}"; do - log "Running ${SCRIPT_PATH}..." - if ! bash "${SCRIPT_PATH}"; then - ((NUM_CHECKS_FAILED+=1)) - fi -done - -log "Running swift-format..." -bash "${CURRENT_SCRIPT_DIR}"/run-swift-format.sh $FIX_FORMAT > /dev/null -FORMAT_EXIT_CODE=$? -if [ $FORMAT_EXIT_CODE -ne 0 ]; then - ((NUM_CHECKS_FAILED+=1)) -fi - -if [ "${NUM_CHECKS_FAILED}" -gt 0 ]; then - fatal "❌ ${NUM_CHECKS_FAILED} check(s) failed." -fi - -log "✅ All check(s) passed." diff --git a/scripts/run-chmod.sh b/scripts/run-chmod.sh deleted file mode 100755 index 783e89d..0000000 --- a/scripts/run-chmod.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" -chmod -R oug+x "${REPO_ROOT}/scripts/" \ No newline at end of file diff --git a/scripts/run-swift-format.sh b/scripts/run-swift-format.sh deleted file mode 100755 index f6fd10d..0000000 --- a/scripts/run-swift-format.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { printf -- "** %s\n" "$*" >&2; } -error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } - -CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(git -C "${CURRENT_SCRIPT_DIR}" rev-parse --show-toplevel)" - -FORMAT_COMMAND=(lint --strict) -for arg in "$@"; do - if [ "$arg" == "--fix" ]; then - FORMAT_COMMAND=(format --in-place) - fi -done - -SWIFTFORMAT_BIN=${SWIFTFORMAT_BIN:-$(command -v swift-format)} || fatal "❌ SWIFTFORMAT_BIN unset and no swift-format on PATH" - -git -C "${REPO_ROOT}" ls-files -z '*.swift' \ - | grep -z -v \ - -e 'Sources/CoreOpenAPIRuntimeKit/Types.swift' \ - -e 'Package.swift' \ - | xargs -0 "${SWIFTFORMAT_BIN}" "${FORMAT_COMMAND[@]}" --parallel \ - && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? - -if [ "${SWIFT_FORMAT_RC}" -ne 0 ]; then - fatal "❌ Running swift-format produced errors. - - To fix, run the following command: - - % ./scripts/run-swift-format.sh --fix - " - exit "${SWIFT_FORMAT_RC}" -fi - -log "✅ Ran swift-format with no errors." diff --git a/scripts/unacceptable-language.txt b/scripts/unacceptable-language.txt deleted file mode 100755 index 6ac4a98..0000000 --- a/scripts/unacceptable-language.txt +++ /dev/null @@ -1,15 +0,0 @@ -blacklist -whitelist -slave -master -sane -sanity -insane -insanity -kill -killed -killing -hang -hung -hanged -hanging