├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .swiftformat ├── LICENSE ├── Package.resolved ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources ├── SwiftOpenAPI │ ├── Common │ │ ├── AnyRange.swift │ │ ├── Bool++.swift │ │ ├── Codable++.swift │ │ ├── CodingKeys.swift │ │ ├── Collection++.swift │ │ ├── Decimal++.swift │ │ ├── ExpressibleBy.swift │ │ ├── OrderedDictionary │ │ │ ├── OrderedDictionary.swift │ │ │ └── StringConvertibleHintProvider.swift │ │ └── String++.swift │ ├── Encoders │ │ ├── AnyValueEncoder.swift │ │ ├── CheckAllKeysDecoder.swift │ │ ├── DateEncodingFormat.swift │ │ ├── HeadersEncoder.swift │ │ ├── KeyEncodingStrategy.swift │ │ ├── ParametersEncoder.swift │ │ ├── Ref.swift │ │ ├── SchemeEncoder.swift │ │ └── TypeRevision │ │ │ ├── CodableTypes.swift │ │ │ ├── TypeRevision.swift │ │ │ ├── TypeRevisionDecoder.swift │ │ │ └── TypeRevisionEncoder.swift │ ├── Objects │ │ ├── AnyValue.swift │ │ ├── CallbackObject.swift │ │ ├── ComponentsObject.swift │ │ ├── CompositeType.swift │ │ ├── ContactObject.swift │ │ ├── ContentObject.swift │ │ ├── EncodingObject.swift │ │ ├── ExampleObject.swift │ │ ├── ExternalDocumentationObject.swift │ │ ├── HeaderObject.swift │ │ ├── InfoObject.swift │ │ ├── JSONSchema │ │ │ ├── AdditionalProperties.swift │ │ │ ├── DataFormat.swift │ │ │ ├── DataType.swift │ │ │ ├── DiscriminatorObject.swift │ │ │ ├── ExpressibleBySchemaObject.swift │ │ │ ├── SchemaContexts.swift │ │ │ ├── SchemaObjec+deprecated.swift │ │ │ ├── SchemaObject++.swift │ │ │ └── SchemaObject.swift │ │ ├── LicenseObject.swift │ │ ├── LinkObject.swift │ │ ├── MediaType.swift │ │ ├── MediaTypeObject.swift │ │ ├── OAuthFlowObject.swift │ │ ├── OAuthFlowsObject.swift │ │ ├── OpenAPIObject.swift │ │ ├── OperationObject.swift │ │ ├── ParameterObject.swift │ │ ├── Path.swift │ │ ├── PathItemObject.swift │ │ ├── PathsObject.swift │ │ ├── ReferenceObject.swift │ │ ├── ReferenceOr.swift │ │ ├── RequestBodyObject.swift │ │ ├── ResponseObject.swift │ │ ├── ResponsesObject.swift │ │ ├── RuntimeExpression.swift │ │ ├── RuntimeExpressionOr.swift │ │ ├── SecurityRequirementObject.swift │ │ ├── SecuritySchemeObject.swift │ │ ├── ServerObject.swift │ │ ├── ServerVariableObject.swift │ │ ├── SpecificationExtendable.swift │ │ ├── TagObject.swift │ │ ├── Version.swift │ │ └── XMLObject.swift │ ├── OpenAPIDescriptable.swift │ ├── OpenAPIDescription.swift │ ├── OpenAPIType.swift │ └── refactor.swift └── SwiftOpenAPIMacros │ ├── OpenAPIDescriptionMacro.swift │ ├── StringError.swift │ └── SyntaxExt.swift └── Tests ├── SwiftOpenAPIMacrosTests └── SwiftOpenAPIMacrosTests.swift └── SwiftOpenAPITests ├── AnyValueTests ├── AnyValueTests.swift └── TestHelpers.swift ├── ArrayDecodingTests.swift ├── Mocks.swift ├── Mocks ├── PetsSwagger.swift └── pets-swagger.json └── SwiftOpenAPITests.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dankinsoid 4 | open_collective: voidilov-daniil 5 | ko_fi: dankinsoid 6 | custom: ["https://paypal.me/voidilovuae"] 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - uses: swift-actions/setup-swift@v1 18 | with: 19 | swift-version: "5.9" 20 | - name: Get swift version 21 | run: swift --version # Swift 5.9 22 | - uses: actions/checkout@v4 23 | - name: Build 24 | run: swift build -v 25 | - name: Run tests 26 | run: swift test -v 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swiftpm 2 | Package.resolved~main 3 | Package.resolved~main_0 4 | .build 5 | .idea 6 | **/.DS_Store 7 | .aider* 8 | .env 9 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --indent tab 2 | --ifdef no-indent 3 | --swiftversion 5.7 4 | --exclude Pods,**/Templates 5 | --disable preferKeyPath 6 | --disable wrapConditionalBodies 7 | --disable sortDeclarations 8 | --disable blankLinesAtStartOfScope 9 | --disable opaqueGenericParameters 10 | --redundanttype inferred 11 | --header "" 12 | --enable organizeDeclarations 13 | --organizetypes markcategories 14 | --extensionacl on-extension 15 | --stripunusedargs unnamed-only 16 | --enable genericExtensions 17 | --enable typeSugar 18 | --enable docComments 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 dankinsoid 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swiftlang/swift-syntax.git", 7 | "state" : { 8 | "revision" : "0687f71944021d616d34d922343dcef086855920", 9 | "version" : "600.0.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftOpenAPI", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | ], 14 | products: [ 15 | .library(name: "SwiftOpenAPI", targets: ["SwiftOpenAPI"]), 16 | ], 17 | dependencies: [ 18 | ], 19 | targets: [ 20 | .target(name: "SwiftOpenAPI", dependencies: []), 21 | .testTarget( 22 | name: "SwiftOpenAPITests", 23 | dependencies: [ 24 | "SwiftOpenAPI" 25 | ], 26 | exclude: ["Mocks/"] 27 | ), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let package = Package( 8 | name: "SwiftOpenAPI", 9 | platforms: [ 10 | .macOS(.v10_15), 11 | .iOS(.v13), 12 | .tvOS(.v13), 13 | .watchOS(.v6), 14 | ], 15 | products: [ 16 | .library(name: "SwiftOpenAPI", targets: ["SwiftOpenAPI"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0") 20 | ], 21 | targets: [ 22 | .target( 23 | name: "SwiftOpenAPI", 24 | dependencies: [ 25 | "SwiftOpenAPIMacros" 26 | ] 27 | ), 28 | .testTarget( 29 | name: "SwiftOpenAPITests", 30 | dependencies: [ 31 | "SwiftOpenAPI" 32 | ], 33 | exclude: ["Mocks/"] 34 | ), 35 | .macro( 36 | name: "SwiftOpenAPIMacros", 37 | dependencies: [ 38 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 39 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 40 | ] 41 | ), 42 | .testTarget( 43 | name: "SwiftOpenAPIMacrosTests", 44 | dependencies: [ 45 | "SwiftOpenAPI", 46 | "SwiftOpenAPIMacros", 47 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 48 | ] 49 | ), 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftOpenAPI 2 | 3 | [![build](https://github.com/dankinsoid/SwiftOpenAPI/actions/workflows/test.yml/badge.svg)](https://github.com/dankinsoid/SwiftOpenAPI/actions/workflows/test.yml) 4 | [![Version](https://img.shields.io/cocoapods/v/SwiftOpenAPI.svg?style=flat)](https://cocoapods.org/pods/SwiftOpenAPI) 5 | [![License](https://img.shields.io/cocoapods/l/SwiftOpenAPI.svg?style=flat)](https://cocoapods.org/pods/SwiftOpenAPI) 6 | [![Platform](https://img.shields.io/cocoapods/p/SwiftOpenAPI.svg?style=flat)](https://cocoapods.org/pods/SwiftOpenAPI) 7 | 8 | 9 | ## Description 10 | SwiftOpenAPI is a Swift library which can generate output compatible with [OpenAPI](https://swagger.io/specification/) version 3.1.0. You can describe your API using `OpenAPIObject` type.\ 11 | The main accent in the library is on simplifying the syntax: the active use of literals (array, dictionary, string etc) and static methods greatly simplifies writing and reading `OpenAPI` docs in `Swift`. 12 | 13 | ## Short example 14 | ```swift 15 | try OpenAPIObject( 16 | openapi: "3.0.11", 17 | info: InfoObject( 18 | title: "Example API", 19 | version: "0.1.0" 20 | ), 21 | servers: [ 22 | "https://example-server.com", 23 | "https://example-server-test.com" 24 | ], 25 | paths: [ 26 | "services": .get( 27 | summary: "Get services", 28 | OperationObject(description: "Get services") 29 | ), 30 | "login": .post( 31 | OperationObject( 32 | description: "login", 33 | requestBody: .ref(components: \.requestBodies, "LoginRequest"), 34 | responses: [ 35 | .ok: .ref(components: \.responses, "LoginResponse"), 36 | .unauthorized: .ref(components: \.responses, "ErrorResponse") 37 | ] 38 | ) 39 | ), 40 | "/services/{serviceID}": [ 41 | .get: OperationObject(description: "Get service"), 42 | .delete: OperationObject(description: "Delete service") 43 | ], 44 | "/services": .ref(components: \.pathItems, "T") 45 | ], 46 | components: ComponentsObject( 47 | schemas: [ 48 | "LoginBody": [ 49 | "username": .string, 50 | "password": .string 51 | ], 52 | "LoginResponse": .value(.encode(LoginResponse.example)) 53 | ], 54 | examples: [ 55 | "LoginBody": [ 56 | "username": "SomeUser", 57 | "password": "12345678" 58 | ], 59 | "LoginResponse": .value( 60 | ExampleObject(value: .encode(LoginResponse.example)) 61 | ) 62 | ], 63 | requestBodies: [ 64 | "LoginRequest": .value( 65 | RequestBodyObject( 66 | content: [ 67 | .application(.json): MediaTypeObject( 68 | schema: .ref(components: \.schemas, "LoginBody") 69 | ) 70 | ], 71 | required: nil 72 | ) 73 | ) 74 | ] 75 | ) 76 | ) 77 | ``` 78 | ## Pets store example 79 | [PetsSwagger.swift](Tests/SwiftOpenAPITests/Mocks/PetsSwagger.swift) demonstrates syntaxis well 80 | 81 | ## Creating schemas and parameters for `Codable` types 82 | There is a possibility to create `SchemeObject`, `[ParameterObject]`, `AnyValue` and `[String: HeaderObject]` instances from `Codable` types. It's possible to use `SchemeObject.decode/encode`, `[ParameterObject].decode/encode`, `[String: HeaderObject].decode/encode` and `AnyValue.encode` methods for it. 83 | ```swift 84 | let loginBodySchemeFromType: SchemeObject = try .decode(LoginBody.self) 85 | let loginBodySchemeFromInstance: SchemeObject = try .encode(LoginBody.example) 86 | let loginBodyExample = try ExampleObject(value: .encode(LoginBody.example)) 87 | ``` 88 | You can customize the encoding/decoding result by implementing `OpenAPIDescriptable` and `OpenAPIType` protocols. 89 | 1. `OpenAPIDescriptable` protocol allows you to provide a custom description for the type and its properties. `@OpenAPIAutoDescriptable` macro implements this protocol with your comments. 90 | ```swift 91 | import SwiftOpenAPI 92 | 93 | @OpenAPIDescriptable 94 | /// Login request body. 95 | struct LoginBody: Codable { 96 | 97 | /// Username string. 98 | let username: String 99 | /// Password string. Encoded. 100 | let password: String 101 | } 102 | ``` 103 | Manually: 104 | ```swift 105 | struct LoginBody: Codable, OpenAPIDescriptable { 106 | 107 | let username: String 108 | let password: String 109 | 110 | static var openAPIDescription: OpenAPIDescriptionType? { 111 | OpenAPIDescription("Login request body.") 112 | .add(for: .username, "Username string.") 113 | .add(for: .password, "Password string. Encoded.") 114 | } 115 | } 116 | ``` 117 | 2. `OpenAPIType` protocol allows you to provide a custom schema for the type. 118 | ```swift 119 | struct Color: Codable, OpenAPIType { 120 | 121 | static var openAPISchema: SchemaObject { 122 | .string(format: "hex", description: "Color in hex format") 123 | } 124 | } 125 | ``` 126 | 127 | ## Specification extensions 128 | While the OpenAPI Specification tries to accommodate most use cases, [additional data](https://swagger.io/specification/#specification-extensions) can be added to extend the specification at certain points.\ 129 | ```swift 130 | var api = OpenAPIObject(...) 131 | api.specificationExtensions = ["x-some-extension": "some value"] 132 | // or 133 | api.specificationExtensions = try? SpecificationExtensions(from: someEncodable) 134 | ``` 135 | It was a bit tricky challenge to implement additional dynamic properties for any codable struct. The solution is to use `SpecificationExtendable` protocol in combination with `WithSpecExtensions` property wrapper. 136 | There is two ways to decode/encode `SpecificationExtendable` types with additional properties: 137 | 1. Use `SpecificationExtendable.json`, `SpecificationExtendable.Type.from(json:)` methods. 138 | ```swift 139 | let schema = try SchemaObject.from(json: jsonData) 140 | let jsonData = try schema.json() 141 | ``` 142 | 2. If you cannot use custom decoding methods, you can use `WithSpecExtensions` wrapper. 143 | ```swift 144 | let api = try WithSpecExtensions(wrappedValue: OpenAPIObject(...)) 145 | let jsonData = try JSONEncoder().encode(api) 146 | ``` 147 | 148 | ## TODO 149 | - `URI` type instead of `String` 150 | - `refactor` method on `OpenAPIObject` (?) 151 | - Extend `RuntimeExpression` type 152 | - `DataEncodingFormat` 153 | 154 | ## Installation 155 | 156 | 1. [Swift Package Manager](https://github.com/apple/swift-package-manager) 157 | 158 | Create a `Package.swift` file. 159 | ```swift 160 | // swift-tools-version:5.9 161 | import PackageDescription 162 | 163 | let package = Package( 164 | name: "SomeProject", 165 | dependencies: [ 166 | .package(url: "https://github.com/dankinsoid/SwiftOpenAPI.git", from: "2.23.0") 167 | ], 168 | targets: [ 169 | .target(name: "SomeProject", dependencies: ["SwiftOpenAPI"]) 170 | ] 171 | ) 172 | ``` 173 | ```ruby 174 | $ swift build 175 | ``` 176 | 177 | ## Related projects 178 | - [VaporToOpenAPI](https://github.com/dankinsoid/VaporToOpenAPI.git) 179 | 180 | ## Author 181 | 182 | dankinsoid, voidilov@gmail.com 183 | 184 | ## License 185 | 186 | SwiftOpenAPI is available under the MIT license. See the LICENSE file for more info. 187 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/AnyRange.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Range expression that supports all range operators 4 | public struct AnyRange: RangeExpression { 5 | 6 | public let lowerBound: Bound? 7 | public let upperBound: Bound? 8 | public let include: Set 9 | 10 | public init(lowerBound: Bound?, upperBound: Bound?, include: Set) { 11 | self.lowerBound = lowerBound.map { lowerBound in 12 | upperBound.map { min(lowerBound, $0) } ?? lowerBound 13 | } 14 | self.upperBound = upperBound.map { upperBound in 15 | lowerBound.map { min(upperBound, $0) } ?? upperBound 16 | } 17 | self.include = include 18 | } 19 | 20 | public subscript(_ edge: RangeEdge) -> Bound? { 21 | switch edge { 22 | case .lower: return lowerBound 23 | case .upper: return upperBound 24 | } 25 | } 26 | 27 | public func relative(to collection: C) -> Range where Bound == C.Index { 28 | switch (lowerBound, upperBound, include.contains(.lower), include.contains(.upper)) { 29 | case (.some(let lower), let .some(upper), true, true): 30 | return (lower ... upper).relative(to: collection) 31 | case (.some(let lower), let .some(upper), false, true): 32 | return upLowerBound((lower ... upper).relative(to: collection), in: collection) 33 | case (.some(let lower), let .some(upper), true, false): 34 | return (lower ..< upper).relative(to: collection) 35 | case (.some(let lower), let .some(upper), false, false): 36 | return upLowerBound((lower ..< upper).relative(to: collection), in: collection) 37 | case (.some(let lower), .none, true, _): 38 | return (lower...).relative(to: collection) 39 | case (.some(let lower), .none, false, _): 40 | return upLowerBound((lower...).relative(to: collection), in: collection) 41 | case (.none, let .some(upper), _, true): 42 | return (...upper).relative(to: collection) 43 | case (.none, let .some(upper), _, false): 44 | return (.. Bool { 51 | let isUpper: Bool 52 | if let lowerBound { 53 | isUpper = include.contains(.lower) ? element >= lowerBound : element > lowerBound 54 | } else { 55 | isUpper = true 56 | } 57 | let isLower: Bool 58 | if let upperBound { 59 | isLower = include.contains(.upper) ? element <= upperBound : element < upperBound 60 | } else { 61 | isLower = true 62 | } 63 | return isLower && isUpper 64 | } 65 | 66 | public static var any: AnyRange { 67 | AnyRange(lowerBound: nil, upperBound: nil, include: []) 68 | } 69 | 70 | private func upLowerBound(_ range: Range, in collection: C) -> Range where Bound == C.Index { 71 | guard range.upperBound > range.lowerBound else { 72 | return range 73 | } 74 | return collection.index(after: range.lowerBound) ..< range.upperBound 75 | } 76 | } 77 | 78 | @_disfavoredOverload 79 | public func ... (_ lhs: Bound, _ rhs: Bound) -> AnyRange { 80 | AnyRange(lowerBound: lhs, upperBound: rhs, include: [.lower, .upper]) 81 | } 82 | 83 | @_disfavoredOverload 84 | public func ..< (_ lhs: Bound, _ rhs: Bound) -> AnyRange { 85 | AnyRange(lowerBound: lhs, upperBound: rhs, include: [.lower]) 86 | } 87 | 88 | @_disfavoredOverload 89 | public postfix func ... (_ lhs: Bound) -> AnyRange { 90 | AnyRange(lowerBound: lhs, upperBound: nil, include: [.lower]) 91 | } 92 | 93 | @_disfavoredOverload 94 | public prefix func ..< (_ rhs: Bound) -> AnyRange { 95 | AnyRange(lowerBound: nil, upperBound: rhs, include: []) 96 | } 97 | 98 | @_disfavoredOverload 99 | public prefix func ... (_ rhs: Bound) -> AnyRange { 100 | AnyRange(lowerBound: nil, upperBound: rhs, include: [.upper]) 101 | } 102 | 103 | public enum RangeEdge: Hashable { 104 | 105 | case lower, upper 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/Bool++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bool { 4 | 5 | var nilIfFalse: Bool? { 6 | self ? self : nil 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/Codable++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Decoder { 4 | 5 | func decodeDictionary(of type: [Key: Value].Type) throws -> [Key: Value] { 6 | let container = try container(keyedBy: StringKey.self) 7 | let pairs = try container.allKeys.filter { !$0.stringValue.hasPrefix("x-") }.compactMap { 8 | try ($0.value, container.decode(Value.self, forKey: $0)) 9 | } 10 | return Dictionary(pairs) { _, second in 11 | second 12 | } 13 | } 14 | } 15 | 16 | extension Encoder { 17 | 18 | func encodeDictionary(_ dict: [Key: some Encodable]) throws { 19 | var container = container(keyedBy: StringKey.self) 20 | for (key, value) in dict { 21 | try container.encode(value, forKey: StringKey(key)) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/CodingKeys.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct IntKey: CodingKey { 4 | 5 | var intValue: Int? 6 | var stringValue: String { 7 | "\(intValue ?? 0)" 8 | } 9 | 10 | init(intValue: Int) { 11 | self.intValue = intValue 12 | } 13 | 14 | init(stringValue: String) { 15 | intValue = Int(stringValue) 16 | } 17 | } 18 | 19 | struct StringKey: CodingKey { 20 | 21 | var stringValue: String { value.description } 22 | var intValue: Int? { nil } 23 | var value: Value 24 | 25 | init?(stringValue: String) { 26 | guard let value = Value(stringValue) else { 27 | return nil 28 | } 29 | self.value = value 30 | } 31 | 32 | init?(intValue _: Int) { 33 | nil 34 | } 35 | 36 | init(_ value: Value) { 37 | self.value = value 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/Collection++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Collection { 4 | 5 | var nilIfEmpty: Self? { 6 | isEmpty ? nil : self 7 | } 8 | } 9 | 10 | extension OrderedDictionary { 11 | 12 | func mapKeys(_ map: (Key) -> T) -> OrderedDictionary { 13 | OrderedDictionary(self.map { (map($0.key), $0.value) }) { _, new in 14 | new 15 | } 16 | } 17 | 18 | func mapKeys(_ map: (Key) -> T, values: (Value) -> V) -> OrderedDictionary { 19 | OrderedDictionary(self.map { (map($0.key), values($0.value)) }) { _, new in 20 | new 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/Decimal++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Decimal { 4 | 5 | var double: Double { 6 | (self as NSDecimalNumber).doubleValue 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/ExpressibleBy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ExpressibleByArray: ExpressibleByArrayLiteral { 4 | init(arrayElements elements: [ArrayLiteralElement]) 5 | } 6 | 7 | public extension ExpressibleByArrayLiteral where Self: ExpressibleByArray { 8 | 9 | init(arrayLiteral elements: ArrayLiteralElement...) { 10 | self.init(arrayElements: elements) 11 | } 12 | } 13 | 14 | public protocol MutableDictionary { 15 | associatedtype Key: Hashable 16 | associatedtype Value 17 | 18 | subscript(_: Key) -> Value? { get set } 19 | } 20 | 21 | extension Dictionary: MutableDictionary {} 22 | 23 | public protocol ExpressibleByDictionary: ExpressibleByDictionaryLiteral, MutableDictionary { 24 | init(dictionaryElements elements: [(Key, Value)]) 25 | } 26 | 27 | public extension ExpressibleByDictionaryLiteral where Self: ExpressibleByDictionary { 28 | 29 | init(dictionaryLiteral elements: (Key, Value)...) { 30 | self.init(dictionaryElements: elements) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/OrderedDictionary/StringConvertibleHintProvider.swift: -------------------------------------------------------------------------------- 1 | internal protocol StringConvertibleHintProvider { 2 | /// Get a `String` describing why the given value cannot 3 | /// be used to create this type. Returns `nil` if there are no 4 | /// problems with the provided value. 5 | /// 6 | /// The idea is for this function to augment the use of `init?(rawValue:)` 7 | /// or `init?(_ description:)` by providing a hint as to why initialization 8 | /// from a `String` value will fail. 9 | static func problem(with proposedString: String) -> String? 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Common/String++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | static func typeName(_ type: Any.Type) -> String { 6 | String(reflecting: type) 7 | .components(separatedBy: ["<", ",", " ", ">", ":", "[", "]", "?"]) 8 | .lazy 9 | .flatMap { 10 | var result = $0.components(separatedBy: ["."]) 11 | if result.count > 1 { 12 | result.removeFirst() 13 | } 14 | return result 15 | } 16 | .flatMap { 17 | $0.components(separatedBy: .alphanumerics.inverted) 18 | } 19 | .joined() 20 | } 21 | 22 | func toCamelCase(separator: String = "_") -> String { 23 | var result = "" 24 | 25 | for word in components(separatedBy: separator) { 26 | if result.isEmpty { 27 | // keep the first word in lowercase 28 | result += word.lowercased() 29 | } else { 30 | // capitalize the first character of the remaining words 31 | result += word.capitalized 32 | } 33 | } 34 | return result 35 | } 36 | 37 | func toSnakeCase(separator: String = "_") -> String { 38 | var result = "" 39 | 40 | for character in self { 41 | if character.isUppercase { 42 | result += separator + character.lowercased() 43 | } else { 44 | result += String(character) 45 | } 46 | } 47 | 48 | return result 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/CheckAllKeysDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class CheckAllKeysDecoder: Decoder { 4 | 5 | var codingPath: [CodingKey] = [] 6 | var userInfo: [CodingUserInfoKey: Any] = [:] 7 | var isAdditional = false 8 | 9 | func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { 10 | KeyedDecodingContainer( 11 | CheckAllKeysDecodingContainer(isAdditional: Ref(self, \.isAdditional)) 12 | ) 13 | } 14 | 15 | func unkeyedContainer() throws -> UnkeyedDecodingContainer { throw AnyError() } 16 | func singleValueContainer() throws -> SingleValueDecodingContainer { throw AnyError() } 17 | } 18 | 19 | private struct CheckAllKeysDecodingContainer: KeyedDecodingContainerProtocol { 20 | 21 | var allKeys: [Key] { 22 | isAdditional = true 23 | return [] 24 | } 25 | 26 | @Ref var isAdditional: Bool 27 | 28 | var codingPath: [CodingKey] { [] } 29 | func contains(_: Key) -> Bool { false } 30 | func decodeNil(forKey _: Key) throws -> Bool { throw AnyError() } 31 | func decode(_: Bool.Type, forKey _: Key) throws -> Bool { throw AnyError() } 32 | func decode(_: String.Type, forKey _: Key) throws -> String { throw AnyError() } 33 | func decode(_: Double.Type, forKey _: Key) throws -> Double { throw AnyError() } 34 | func decode(_: Float.Type, forKey _: Key) throws -> Float { throw AnyError() } 35 | func decode(_: Int.Type, forKey _: Key) throws -> Int { throw AnyError() } 36 | func decode(_: Int8.Type, forKey _: Key) throws -> Int8 { throw AnyError() } 37 | func decode(_: Int16.Type, forKey _: Key) throws -> Int16 { throw AnyError() } 38 | func decode(_: Int32.Type, forKey _: Key) throws -> Int32 { throw AnyError() } 39 | func decode(_: Int64.Type, forKey _: Key) throws -> Int64 { throw AnyError() } 40 | func decode(_: UInt.Type, forKey _: Key) throws -> UInt { throw AnyError() } 41 | func decode(_: UInt8.Type, forKey _: Key) throws -> UInt8 { throw AnyError() } 42 | func decode(_: UInt16.Type, forKey _: Key) throws -> UInt16 { throw AnyError() } 43 | func decode(_: UInt32.Type, forKey _: Key) throws -> UInt32 { throw AnyError() } 44 | func decode(_: UInt64.Type, forKey _: Key) throws -> UInt64 { throw AnyError() } 45 | func decode(_: T.Type, forKey _: Key) throws -> T where T: Decodable { throw AnyError() } 46 | func nestedContainer(keyedBy _: NestedKey.Type, forKey _: Key) throws -> KeyedDecodingContainer { throw AnyError() } 47 | func nestedUnkeyedContainer(forKey _: Key) throws -> UnkeyedDecodingContainer { throw AnyError() } 48 | func superDecoder() throws -> Decoder { throw AnyError() } 49 | func superDecoder(forKey _: Key) throws -> Decoder { throw AnyError() } 50 | } 51 | 52 | private struct AnyError: Error {} 53 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/DateEncodingFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DateEncodingFormat { 4 | 5 | public let schema: SchemaObject 6 | public let encode: (Date, inout SingleValueEncodingContainer) throws -> Void 7 | } 8 | 9 | public extension DateEncodingFormat { 10 | 11 | static var `default`: DateEncodingFormat = .dateTime 12 | 13 | /// full-date notation as defined by RFC 3339, section 5.6, for example, 2017-07-21 14 | static var date: DateEncodingFormat { 15 | DateEncodingFormat(schema: .string(format: .date)) { date, encoder in 16 | try encoder.encode(DateEncodingFormat.date(date)) 17 | } 18 | } 19 | 20 | /// the date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z 21 | static var dateTime: DateEncodingFormat { 22 | DateEncodingFormat(schema: .string(format: .dateTime)) { date, encoder in 23 | try encoder.encode(DateEncodingFormat.dateTime(date)) 24 | } 25 | } 26 | 27 | /// the interval between the date value and 00:00:00 UTC on 1 January 1970. 28 | static var timestamp: DateEncodingFormat { 29 | DateEncodingFormat(schema: .number(format: "timestamp")) { date, encoder in 30 | try encoder.encode(date.timeIntervalSince1970) 31 | } 32 | } 33 | 34 | static func custom(_ format: String) -> DateEncodingFormat { 35 | DateEncodingFormat(schema: .string(format: DataFormat(format))) { date, encoder in 36 | dateFormatter.dateFormat = format 37 | try encoder.encode(dateFormatter.string(from: date)) 38 | } 39 | } 40 | 41 | static func custom( 42 | _ schema: SchemaObject, 43 | encode: @escaping (Date, inout SingleValueEncodingContainer) throws -> Void 44 | ) -> DateEncodingFormat { 45 | DateEncodingFormat(schema: schema, encode: encode) 46 | } 47 | 48 | static func custom( 49 | _ dataFormat: DataFormat, 50 | formatter: DateFormatter 51 | ) -> DateEncodingFormat { 52 | DateEncodingFormat(schema: .string(format: dataFormat)) { date, encoder in 53 | try encoder.encode(formatter.string(from: date)) 54 | } 55 | } 56 | } 57 | 58 | extension DateEncodingFormat { 59 | 60 | static func dateTime(_ date: Date) -> String { 61 | isoFormatter.string(from: date) 62 | } 63 | 64 | static func date(_ date: Date) -> String { 65 | dateFormatter.dateFormat = "yyyy-MM-dd" 66 | return dateFormatter.string(from: date) 67 | } 68 | } 69 | 70 | private let isoFormatter = ISO8601DateFormatter() 71 | private let dateFormatter = DateFormatter() 72 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/HeadersEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct HeadersEncoder { 4 | 5 | var dateFormat: DateEncodingFormat 6 | var keyEncodingStrategy: KeyEncodingStrategy 7 | 8 | @discardableResult 9 | func encode( 10 | _ value: Encodable, 11 | schemas: inout ComponentsMap 12 | ) throws -> OrderedDictionary { 13 | try parse( 14 | value: TypeRevision().describeType(of: value), 15 | type: type(of: value), 16 | into: &schemas 17 | ) 18 | } 19 | 20 | @discardableResult 21 | func decode( 22 | _ type: Decodable.Type, 23 | schemas: inout ComponentsMap 24 | ) throws -> OrderedDictionary { 25 | try parse( 26 | value: TypeRevision().describe(type: type), 27 | type: type, 28 | into: &schemas 29 | ) 30 | } 31 | 32 | private func parse( 33 | value: TypeInfo, 34 | type: Any.Type, 35 | into schemas: inout ComponentsMap 36 | ) throws -> OrderedDictionary { 37 | switch type { 38 | case is URL.Type: 39 | throw InvalidType() 40 | 41 | default: 42 | switch value.container { 43 | case .single, .unkeyed, .recursive: 44 | throw InvalidType() 45 | 46 | case let .keyed(keyedInfo): 47 | return try keyedInfo.fields 48 | .mapKeys(keyEncodingStrategy.encode) 49 | .mapValues { 50 | try HeaderObject( 51 | required: !$0.isOptional, 52 | schema: SchemeEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 53 | .parse(value: $0, type: $0.type, into: &schemas), 54 | example: $0.container.anyValue 55 | ) 56 | } 57 | .with(description: (type as? OpenAPIDescriptable.Type)?.openAPIDescription) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/KeyEncodingStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct KeyEncodingStrategy { 4 | 5 | public let encode: (String) -> String 6 | } 7 | 8 | public extension KeyEncodingStrategy { 9 | 10 | static var `default`: KeyEncodingStrategy = .useDefaultKeys 11 | 12 | static var useDefaultKeys: KeyEncodingStrategy = .custom { $0 } 13 | 14 | static func custom(_ encode: @escaping (String) -> String) -> KeyEncodingStrategy { 15 | KeyEncodingStrategy(encode: encode) 16 | } 17 | 18 | static var convertToSnakeCase: KeyEncodingStrategy { 19 | .convertToSnakeCase(separator: "_") 20 | } 21 | 22 | static func convertToSnakeCase(separator: String) -> KeyEncodingStrategy { 23 | .custom { 24 | $0.toSnakeCase(separator: separator) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/ParametersEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ParametersEncoder { 4 | 5 | var location: ParameterObject.Location 6 | var dateFormat: DateEncodingFormat 7 | var keyEncodingStrategy: KeyEncodingStrategy 8 | 9 | @discardableResult 10 | func encode( 11 | _ value: Encodable, 12 | schemas: inout ComponentsMap 13 | ) throws -> [ParameterObject] { 14 | try parse( 15 | value: TypeRevision().describeType(of: value), 16 | type: type(of: value), 17 | into: &schemas 18 | ) 19 | } 20 | 21 | @discardableResult 22 | func decode( 23 | _ type: Decodable.Type, 24 | schemas: inout ComponentsMap 25 | ) throws -> [ParameterObject] { 26 | try parse( 27 | value: TypeRevision().describe(type: type), 28 | type: type, 29 | into: &schemas 30 | ) 31 | } 32 | 33 | private func parse( 34 | value: TypeInfo, 35 | type: Any.Type, 36 | into schemas: inout ComponentsMap 37 | ) throws -> [ParameterObject] { 38 | switch type { 39 | case is URL.Type: 40 | throw InvalidType() 41 | 42 | default: 43 | switch value.container { 44 | case .single, .unkeyed, .recursive: 45 | throw InvalidType() 46 | 47 | case let .keyed(keyedInfo): 48 | return try keyedInfo.fields.map { 49 | try ParameterObject( 50 | name: keyEncodingStrategy.encode($0.key), 51 | in: location, 52 | required: !$0.value.isOptional, 53 | schema: SchemeEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 54 | .parse(value: $0.value, type: $0.value.type, into: &schemas), 55 | example: $0.value.container.anyValue 56 | ) 57 | } 58 | .with(description: (type as? OpenAPIDescriptable.Type)?.openAPIDescription) 59 | } 60 | } 61 | } 62 | } 63 | 64 | struct InvalidType: Error {} 65 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/Ref.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | struct Ref { 5 | 6 | let get: () -> Value 7 | let set: (Value) -> Void 8 | 9 | var wrappedValue: Value { 10 | get { get() } 11 | nonmutating set { set(newValue) } 12 | } 13 | 14 | var projectedValue: Ref { 15 | get { self } 16 | set { self = newValue } 17 | } 18 | } 19 | 20 | extension Ref { 21 | 22 | static func constant(_ value: Value) -> Ref { 23 | self.init { 24 | value 25 | } set: { _ in 26 | } 27 | } 28 | 29 | init(_ value: T, _ keyPath: ReferenceWritableKeyPath) { 30 | self.init { 31 | value[keyPath: keyPath] 32 | } set: { newValue in 33 | value[keyPath: keyPath] = newValue 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/SchemeEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SchemeEncoder { 4 | 5 | var extractReferences = true 6 | var dateFormat: DateEncodingFormat 7 | var keyEncodingStrategy: KeyEncodingStrategy 8 | 9 | @discardableResult 10 | func encode( 11 | _ value: Encodable, 12 | into schemas: inout ComponentsMap 13 | ) throws -> ReferenceOr { 14 | try parse( 15 | value: TypeRevision().describeType(of: value), 16 | type: Swift.type(of: value), 17 | into: &schemas 18 | ) 19 | } 20 | 21 | @discardableResult 22 | func decode( 23 | _ type: Decodable.Type, 24 | into schemas: inout ComponentsMap 25 | ) throws -> ReferenceOr { 26 | try parse( 27 | value: TypeRevision().describe(type: type), 28 | type: type, 29 | into: &schemas 30 | ) 31 | } 32 | 33 | func parse( 34 | value: TypeInfo, 35 | type: Any.Type, 36 | into schemas: inout ComponentsMap 37 | ) throws -> ReferenceOr { 38 | var needReparse = false 39 | var cache: [ObjectIdentifier: ReferenceOr] = [:] 40 | var result = try parse( 41 | value: value, 42 | type: type, 43 | into: &schemas, 44 | cache: &cache, 45 | needReparse: &needReparse 46 | ) 47 | if needReparse { 48 | result = try parse( 49 | value: value, 50 | type: type, 51 | into: &schemas, 52 | cache: &cache, 53 | needReparse: &needReparse 54 | ) 55 | } 56 | return result 57 | } 58 | 59 | func parse( 60 | value: TypeInfo, 61 | type: Any.Type, 62 | into schemas: inout ComponentsMap, 63 | cache: inout [ObjectIdentifier: ReferenceOr], 64 | needReparse: inout Bool 65 | ) throws -> ReferenceOr { 66 | let name = String.typeName(type) 67 | var result: ReferenceOr 68 | let typeInfo = value 69 | let typeID = ObjectIdentifier(typeInfo.type) 70 | 71 | switch type { 72 | case is Date.Type: 73 | result = .value(dateFormat.schema) 74 | 75 | case let openAPI as OpenAPIType.Type: 76 | result = .value(openAPI.openAPISchema) 77 | 78 | default: 79 | switch typeInfo.container { 80 | case let .single(codableValues): 81 | let (dataType, format) = try parse(value: codableValues) 82 | var schema: SchemaObject 83 | if let iterable = type as? any CaseIterable.Type { 84 | let allCases = iterable.allCases as any Collection 85 | let namesValues = allCases.map { ("\($0)", caseValue(for: $0)) } 86 | schema = .enum(of: dataType, cases: namesValues.map { .string($0.1) }) 87 | if namesValues.contains(where: !=) { 88 | schema.description = namesValues.map { "• \($0.1) → \($0.0)" }.joined(separator: "\n") 89 | } 90 | } else { 91 | schema = SchemaObject(format: format, context: SchemaContexts(dataType)) 92 | } 93 | result = .value(schema) 94 | 95 | case let .keyed(keyedInfo): 96 | switch keyedInfo.isFixed { 97 | case true: 98 | let schema = try SchemaObject.object( 99 | properties: keyedInfo.fields.mapKeys { 100 | keyEncodingStrategy.encode($0) 101 | }.mapValues { 102 | try parse(value: $0, type: $0.type, into: &schemas, cache: &cache, needReparse: &needReparse) 103 | }, 104 | required: Set(keyedInfo.fields.unorderedHash.filter { !$0.value.isOptional }.keys) 105 | ) 106 | result = .value(schema) 107 | 108 | case false: 109 | let schema = try SchemaObject.dictionary( 110 | of: (keyedInfo.fields.first?.value).map { 111 | try parse(value: $0, type: $0.type, into: &schemas, cache: &cache, needReparse: &needReparse) 112 | } ?? .any 113 | ) 114 | result = .value(schema) 115 | } 116 | 117 | case let .unkeyed(itemInfo): 118 | let schema = try SchemaObject.array( 119 | of: parse(value: itemInfo, type: itemInfo.type, into: &schemas, cache: &cache, needReparse: &needReparse) 120 | ) 121 | result = .value(schema) 122 | 123 | case .recursive: 124 | needReparse = true 125 | if let cached = cache[typeID] { 126 | result = cached 127 | } else { 128 | result = .ref(components: \.schemas, name) 129 | } 130 | } 131 | } 132 | 133 | if let descriptable = type as? OpenAPIDescriptable.Type { 134 | result = result.with(description: descriptable.openAPIDescription) 135 | } 136 | 137 | if extractReferences, result.isReferenceable { 138 | result.object?.nullable = nil 139 | schemas[name] = result 140 | cache[typeID] = .ref(components: \.schemas, name) 141 | return .ref(components: \.schemas, name) 142 | } else { 143 | if typeInfo.isOptional, result.object?.enum == nil { 144 | result.object?.nullable = true 145 | } 146 | cache[typeID] = result 147 | return result 148 | } 149 | } 150 | 151 | private func parse( 152 | value: CodableValues 153 | ) throws -> (PrimitiveDataType, DataFormat?) { 154 | switch value { 155 | case .int8, .int16, .int32, .uint8, .uint16, .uint32: 156 | return (.integer, .int32) 157 | case .int, .int64, .uint, .uint64: 158 | return (.integer, .int64) 159 | case .double: 160 | return (.number, .double) 161 | case .float: 162 | return (.number, .float) 163 | case .bool: 164 | return (.boolean, nil) 165 | case .string, .null: 166 | return (.string, nil) 167 | } 168 | } 169 | 170 | private func caseValue(for value: Any) -> String { 171 | if let anyRaw = value as? any RawRepresentable { 172 | return "\(anyRaw.rawValue)" 173 | } else { 174 | return "\(value)" 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/TypeRevision/CodableTypes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct TypeInfo { 4 | 5 | static let any = TypeInfo(type: Any.self, container: .single(.null)) 6 | 7 | var type: Any.Type 8 | var isOptional = false 9 | var container: CodableContainerValue 10 | } 11 | 12 | struct KeyedInfo { 13 | 14 | var fields: OrderedDictionary = [:] 15 | var isFixed = true 16 | 17 | subscript(_ key: String) -> TypeInfo { 18 | get { fields[key] ?? .any } 19 | set { fields[key] = newValue } 20 | } 21 | } 22 | 23 | indirect enum CodableContainerValue { 24 | 25 | case single(CodableValues) 26 | case keyed(KeyedInfo) 27 | case unkeyed(TypeInfo) 28 | case recursive 29 | 30 | var keyed: KeyedInfo { 31 | get { 32 | if case let .keyed(info) = self { 33 | return info 34 | } 35 | return KeyedInfo() 36 | } 37 | set { 38 | self = .keyed(newValue) 39 | } 40 | } 41 | 42 | var unkeyed: TypeInfo? { 43 | get { 44 | if case let .unkeyed(value) = self { 45 | return value 46 | } 47 | return nil 48 | } 49 | set { 50 | if let newValue { 51 | self = .unkeyed(newValue) 52 | } 53 | } 54 | } 55 | 56 | var single: CodableValues? { 57 | get { 58 | if case let .single(value) = self { 59 | return value 60 | } 61 | return nil 62 | } 63 | set { 64 | if let newValue { 65 | self = .single(newValue) 66 | } 67 | } 68 | } 69 | } 70 | 71 | enum CodableValues: Equatable { 72 | 73 | case int(Int?) 74 | case int8(Int8?) 75 | case int16(Int16?) 76 | case int32(Int32?) 77 | case int64(Int64?) 78 | case uint(UInt?) 79 | case uint8(UInt8?) 80 | case uint16(UInt16?) 81 | case uint32(UInt32?) 82 | case uint64(UInt64?) 83 | case double(Double?) 84 | case float(Float?) 85 | case bool(Bool?) 86 | case string(String?) 87 | case null 88 | 89 | var type: Any.Type { 90 | switch self { 91 | case .int: return Int.self 92 | case .int8: return Int8.self 93 | case .int16: return Int16.self 94 | case .int32: return Int32.self 95 | case .int64: return Int64.self 96 | case .uint: return UInt.self 97 | case .uint8: return UInt8.self 98 | case .uint16: return UInt16.self 99 | case .uint32: return UInt32.self 100 | case .uint64: return UInt64.self 101 | case .double: return Double.self 102 | case .float: return Float.self 103 | case .bool: return Bool.self 104 | case .string: return String.self 105 | case .null: return Any.self 106 | } 107 | } 108 | } 109 | 110 | extension CodableContainerValue { 111 | 112 | var anyValue: AnyValue? { 113 | switch self { 114 | case let .single(codableValues): 115 | switch codableValues { 116 | case let .int(value): return value.map { .int($0) } 117 | case let .int8(value): return value.map { .int(Int($0)) } 118 | case let .int16(value): return value.map { .int(Int($0)) } 119 | case let .int32(value): return value.map { .int(Int($0)) } 120 | case let .int64(value): return value.map { .int(Int($0)) } 121 | case let .uint(value): return value.map { .int(Int($0)) } 122 | case let .uint8(value): return value.map { .int(Int($0)) } 123 | case let .uint16(value): return value.map { .int(Int($0)) } 124 | case let .uint32(value): return value.map { .int(Int($0)) } 125 | case let .uint64(value): return value.map { .int(Int($0)) } 126 | case let .double(value): return value.map { .double($0) } 127 | case let .float(value): return value.map { .double(Double($0)) } 128 | case let .bool(value): return value.map { .bool($0) } 129 | case let .string(value): return value.map { .string($0) } 130 | case .null: return .null 131 | } 132 | 133 | case let .keyed(keyedInfo): 134 | return .object(keyedInfo.fields.unorderedHash.compactMapValues(\.container.anyValue)) 135 | 136 | case let .unkeyed(typeInfo): 137 | return typeInfo.container.anyValue.map { .array([$0]) } 138 | 139 | case .recursive: 140 | return nil 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Encoders/TypeRevision/TypeRevision.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct TypeRevision { 4 | 5 | let customDescription: (Any.Type, Any?) -> CodableContainerValue? 6 | 7 | init(custom: @escaping (Any.Type, Any?) -> CodableContainerValue?) { 8 | customDescription = custom 9 | } 10 | 11 | init() { 12 | self.init { _, _ in nil } 13 | } 14 | 15 | func describeType(of value: Encodable) throws -> TypeInfo { 16 | let encoder = TypeRevisionEncoder(context: self) 17 | try encoder.encode(value, type: type(of: value)) 18 | return encoder.result 19 | } 20 | 21 | func describe(type: Decodable.Type) throws -> TypeInfo { 22 | let decoder = TypeRevisionDecoder(context: self) 23 | try decoder.decode(type) 24 | return decoder.result 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/AnyValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @dynamicMemberLookup 4 | public enum AnyValue: Codable, Equatable { 5 | 6 | case string(String) 7 | case bool(Bool) 8 | case int(Int) 9 | case double(Double) 10 | case object([String: AnyValue]) 11 | case array([AnyValue]) 12 | case null 13 | 14 | public init(from decoder: Decoder) throws { 15 | let container = try decoder.singleValueContainer() 16 | 17 | if container.decodeNil() { 18 | self = .null 19 | } else if let bool = try? container.decode(Bool.self) { 20 | self = .bool(bool) 21 | } else if let int = try? container.decode(Int.self) { 22 | self = .int(int) 23 | } else if let double = try? container.decode(Double.self) { 24 | self = .double(double) 25 | } else if let string = try? container.decode(String.self) { 26 | self = .string(string) 27 | } else if let array = try? container.decode([AnyValue].self) { 28 | self = .array(array) 29 | } else if let dictionary = try? container.decode([String: AnyValue].self) { 30 | self = .object(dictionary) 31 | } else { 32 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyValue value cannot be decoded") 33 | } 34 | } 35 | 36 | public func encode(to encoder: Encoder) throws { 37 | switch self { 38 | case let .string(value): try value.encode(to: encoder) 39 | case let .bool(value): try value.encode(to: encoder) 40 | case let .int(value): try value.encode(to: encoder) 41 | case let .double(value): try value.encode(to: encoder) 42 | case let .object(value): try value.encode(to: encoder) 43 | case let .array(value): try value.encode(to: encoder) 44 | case .null: 45 | var container = encoder.singleValueContainer() 46 | try container.encodeNil() 47 | } 48 | } 49 | 50 | public subscript(_ key: String) -> AnyValue? { 51 | get { 52 | switch self { 53 | case let .object(value): return value[key] 54 | default: return nil 55 | } 56 | } 57 | set { 58 | switch self { 59 | case var .object(value): 60 | value[key] = newValue 61 | self = .object(value) 62 | default: 63 | break 64 | } 65 | } 66 | } 67 | 68 | public subscript(dynamicMember key: String) -> AnyValue? { 69 | get { self[key] } 70 | set { self[key] = newValue } 71 | } 72 | 73 | public subscript(_ index: Int) -> AnyValue? { 74 | switch self { 75 | case let .array(value): 76 | return value.indices.contains(index) ? value[index] : nil 77 | default: 78 | return nil 79 | } 80 | } 81 | 82 | public var dataType: DataType? { 83 | switch self { 84 | case .string: return .string 85 | case .bool: return .boolean 86 | case .int: return .integer 87 | case .double: return .number 88 | case .object: return .object 89 | case .array: return .array 90 | case .null: return nil 91 | } 92 | } 93 | } 94 | 95 | extension AnyValue: ExpressibleByDictionary { 96 | 97 | public typealias Key = String 98 | public typealias Value = AnyValue 99 | 100 | public init(dictionaryElements elements: [(String, AnyValue)]) { 101 | self = .object( 102 | Dictionary(elements) { _, s in s } 103 | ) 104 | } 105 | } 106 | 107 | extension AnyValue: ExpressibleByStringInterpolation { 108 | 109 | public init(stringLiteral value: String) { 110 | self = .string(value) 111 | } 112 | 113 | public init(stringInterpolation value: String.StringInterpolation) { 114 | self = .string(String(stringInterpolation: value)) 115 | } 116 | } 117 | 118 | extension AnyValue: ExpressibleByArray { 119 | 120 | public typealias ArrayLiteralElement = AnyValue 121 | 122 | public init(arrayElements array: [AnyValue]) { 123 | self = .array(array) 124 | } 125 | } 126 | 127 | extension AnyValue: ExpressibleByBooleanLiteral { 128 | 129 | public init(booleanLiteral value: Bool) { 130 | self = .bool(value) 131 | } 132 | } 133 | 134 | extension AnyValue: ExpressibleByIntegerLiteral { 135 | 136 | public init(integerLiteral value: Int) { 137 | self = .int(value) 138 | } 139 | } 140 | 141 | extension AnyValue: ExpressibleByFloatLiteral { 142 | 143 | public init(floatLiteral value: Double) { 144 | self = .double(value) 145 | } 146 | } 147 | 148 | extension AnyValue: ExpressibleByNilLiteral { 149 | 150 | public init(nilLiteral: ()) { 151 | self = .null 152 | } 153 | } 154 | 155 | extension AnyValue: LosslessStringConvertible { 156 | 157 | public var description: String { 158 | switch self { 159 | case let .string(string): 160 | return string.description 161 | case let .bool(bool): 162 | return bool.description 163 | case let .int(int): 164 | return int.description 165 | case let .double(double): 166 | return double.description 167 | case let .object(dictionary): 168 | return dictionary.description 169 | case let .array(array): 170 | return array.description 171 | case .null: 172 | return "nil" 173 | } 174 | } 175 | 176 | public init(_ description: String) { 177 | switch description { 178 | case "nil": 179 | self = .null 180 | default: 181 | if let int = Int(description) { 182 | self = .int(int) 183 | } else if let bool = Bool(description) { 184 | self = .bool(bool) 185 | } else if let double = Double(description) { 186 | self = .double(double) 187 | } else { 188 | self = .string(description) 189 | } 190 | } 191 | } 192 | } 193 | 194 | public extension AnyValue { 195 | 196 | static func encode(_ value: Encodable, dateFormat: DateEncodingFormat = .default) throws -> AnyValue { 197 | let encoder = AnyValueEncoder(dateFormat: dateFormat) 198 | return try encoder.encode(value) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/CallbackObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct CallbackObject: Codable, Equatable, SpecificationExtendable, ExpressibleByDictionary { 4 | 5 | public typealias Key = RuntimeExpression 6 | public typealias Value = ReferenceOr 7 | 8 | public var value: [Key: Value] 9 | public var specificationExtensions: SpecificationExtensions? = nil 10 | 11 | public init(_ value: [Key: Value] = [:]) { 12 | self.value = value 13 | } 14 | 15 | public init(dictionaryElements elements: [(Key, Value)]) { 16 | self.init( 17 | Dictionary(elements) { _, second in 18 | second 19 | } 20 | ) 21 | } 22 | 23 | public init(from decoder: Decoder) throws { 24 | value = try decoder.decodeDictionary(of: [Key: Value].self) 25 | } 26 | 27 | public func encode(to encoder: Encoder) throws { 28 | try encoder.encodeDictionary(value) 29 | } 30 | 31 | public subscript(_ key: Key) -> Value? { 32 | get { value[key] } 33 | set { value[key] = newValue } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ComponentsObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias ComponentsMap = OrderedDictionary> 4 | 5 | /// Holds a set of reusable objects for different aspects of the OAS. All objects defined within the components object will have no effect on the API unless they are explicitly referenced from properties outside the components object. 6 | public struct ComponentsObject: Codable, Equatable, SpecificationExtendable { 7 | 8 | /// An object to hold reusable Schema Objects. 9 | public var schemas: ComponentsMap? 10 | 11 | /// An object to hold reusable Response Objects. 12 | public var responses: ComponentsMap? 13 | 14 | /// An object to hold reusable Parameter Objects. 15 | public var parameters: ComponentsMap? 16 | 17 | /// An object to hold reusable Example Objects. 18 | public var examples: ComponentsMap? 19 | 20 | /// An object to hold reusable Request Body Objects. 21 | public var requestBodies: ComponentsMap? 22 | 23 | /// An object to hold reusable Header Objects. 24 | public var headers: ComponentsMap? 25 | 26 | /// An object to hold reusable Security Scheme Objecvar ts. 27 | public var securitySchemes: ComponentsMap? 28 | 29 | /// An object to hold reusable Link Objects. 30 | public var links: ComponentsMap? 31 | 32 | /// An object to hold reusable Callback Objects. 33 | public var callbacks: ComponentsMap? 34 | 35 | /// An object to hold reusable Path Item Object. 36 | public var pathItems: ComponentsMap? 37 | 38 | public var specificationExtensions: SpecificationExtensions? = nil 39 | 40 | public init( 41 | schemas: ComponentsMap? = nil, 42 | responses: ComponentsMap? = nil, 43 | parameters: ComponentsMap? = nil, 44 | examples: ComponentsMap? = nil, 45 | requestBodies: ComponentsMap? = nil, 46 | headers: ComponentsMap? = nil, 47 | securitySchemes: ComponentsMap? = nil, 48 | links: ComponentsMap? = nil, 49 | callbacks: ComponentsMap? = nil, 50 | pathItems: ComponentsMap? = nil, 51 | specificationExtensions: SpecificationExtensions = [:] 52 | ) { 53 | self.schemas = schemas 54 | self.responses = responses 55 | self.parameters = parameters 56 | self.examples = examples 57 | self.requestBodies = requestBodies 58 | self.headers = headers 59 | self.securitySchemes = securitySchemes 60 | self.links = links 61 | self.callbacks = callbacks 62 | self.pathItems = pathItems 63 | self.specificationExtensions = specificationExtensions 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/CompositeType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum CompositeType: String, CodingKey { 4 | 5 | case oneOf, allOf, anyOf, not 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ContactObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Contact information for the exposed API. 4 | public struct ContactObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// The identifying name of the contact person/organization. 7 | public var name: String? 8 | 9 | /// The URL pointing to the contact information. 10 | public var url: URL? 11 | 12 | /// The email address of the contact person/organization. This MUST be in the form of an email address. 13 | public var email: String? 14 | 15 | public var specificationExtensions: SpecificationExtensions? = nil 16 | 17 | public init( 18 | name: String? = nil, 19 | url: URL? = nil, 20 | email: String? = nil 21 | ) { 22 | self.name = name 23 | self.url = url 24 | self.email = email 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ContentObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ContentObject: Codable, Equatable, SpecificationExtendable, ExpressibleByDictionary { 4 | 5 | public typealias Key = MediaType 6 | public typealias Value = MediaTypeObject 7 | 8 | public var value: [Key: Value] 9 | public var specificationExtensions: SpecificationExtensions? = nil 10 | 11 | public init(_ value: [Key: Value] = [:]) { 12 | self.value = value 13 | } 14 | 15 | public init(dictionaryElements elements: [(Key, Value)]) { 16 | self.init( 17 | Dictionary(elements) { _, second in 18 | second 19 | } 20 | ) 21 | } 22 | 23 | public subscript(_ key: Key) -> Value? { 24 | get { value[key] } 25 | set { value[key] = newValue } 26 | } 27 | 28 | public init(from decoder: Decoder) throws { 29 | value = try decoder.decodeDictionary(of: [Key: Value].self) 30 | } 31 | 32 | public func encode(to encoder: Encoder) throws { 33 | try encoder.encodeDictionary(value) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/EncodingObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct EncodingObject: Codable, Equatable, SpecificationExtendable { 4 | 5 | /// The Content-Type for encoding a specific property. Default value depends on the property type: for object - application/json; for array – the default is defined based on the inner type; for all other cases the default is application/octet-stream. The value can be a specific media type (e.g. application/json), a wildcard media type (e.g. image/*), or a comma-separated list of the two types. 6 | public var contentType: MediaType? 7 | 8 | /// A map allowing additional information to be provided as headers, for example Content-Disposition. Content-Type is described separately and SHALL be ignored in this section. This property SHALL be ignored if the request body media type is not a multipart. 9 | public var headers: ComponentsMap? 10 | 11 | /// Describes how a specific property value will be serialized depending on its type. See Parameter Object for details on the style property. The behavior follows the same values as query parameters, including default values. This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored. 12 | public var style: String? 13 | 14 | /// When this is true, property values of type array or object generate separate parameters for each value of the array, or key-value-pair of the map. For other types of properties this property has no effect. When style is form, the default value is true. For all other styles, the default value is false. This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored. 15 | public var explode: Bool? 16 | 17 | /// Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding. The default value is false. This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored. 18 | public var allowReserved: Bool? 19 | 20 | public var specificationExtensions: SpecificationExtensions? = nil 21 | 22 | public init(contentType: MediaType? = nil, headers: ComponentsMap? = nil, style: String? = nil, explode: Bool? = nil, allowReserved: Bool? = nil) { 23 | self.contentType = contentType 24 | self.headers = headers 25 | self.style = style 26 | self.explode = explode 27 | self.allowReserved = allowReserved 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ExampleObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ExampleObject: Codable, Equatable, SpecificationExtendable { 4 | 5 | /// Short description for the example. 6 | public var summary: String? 7 | 8 | /// Long description for the example. CommonMark syntax MAY be used for rich text representation. 9 | public var description: String? 10 | 11 | /// Embedded literal example. The value field and externalValue field are mutually exclusive. To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary. 12 | public var value: AnyValue? 13 | 14 | /// A URI that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. The value field and externalValue field are mutually exclusive. See the rules for resolving Relative References. 15 | public var externalValue: String? 16 | 17 | public var specificationExtensions: SpecificationExtensions? = nil 18 | 19 | public init(summary: String? = nil, description: String? = nil, value: AnyValue? = nil, externalValue: String? = nil) { 20 | self.summary = summary 21 | self.description = description 22 | self.value = value 23 | self.externalValue = externalValue 24 | } 25 | } 26 | 27 | extension ExampleObject: ExpressibleByDictionary { 28 | 29 | public typealias Key = String 30 | public typealias Value = AnyValue 31 | 32 | public subscript(_ key: String) -> AnyValue? { 33 | get { 34 | value?[key] 35 | } 36 | set { 37 | value?[key] = newValue 38 | } 39 | } 40 | 41 | public init(dictionaryElements elements: [(String, AnyValue)]) { 42 | self.init(value: .object(Dictionary(elements) { _, s in s })) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ExternalDocumentationObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Allows referencing an external resource for extended documentation. 4 | public struct ExternalDocumentationObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | public var specificationExtensions: SpecificationExtensions? = nil 7 | 8 | /// A description of the target documentation. CommonMark syntax MAY be used for rich text representation. 9 | public var description: String? 10 | 11 | /// The URL for the target documentation. 12 | public var url: URL? 13 | 14 | public init(description: String? = nil, url: URL?) { 15 | self.description = description 16 | self.url = url 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/HeaderObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct HeaderObject: Codable, Equatable, SpecificationExtendable { 4 | 5 | /// A brief description of the parameter. This could contain examples of use. CommonMark syntax MAY be used for rich text representation. 6 | public var description: String? 7 | 8 | /// Determines whether this parameter is mandatory. If the parameter location is "path", this property is REQUIRED and its value MUST be true. Otherwise, the property MAY be included and its default value is false. 9 | public var required: Bool? 10 | 11 | /// Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is false. 12 | public var deprecated: Bool? 13 | 14 | /// Sets the ability to pass empty-valued parameters. This is valid only for query parameters and allows sending a parameter with an empty value. Default value is false. If style is used, and if behavior is n/a (cannot be serialized), the value of allowEmptyValue SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision. 15 | public var allowEmptyValue: Bool? 16 | 17 | /// Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of in): for query - form; for path - simple; for header - simple; for cookie - form. 18 | public var style: ParameterObject.Style? 19 | 20 | /// When this is true, parameter values of type array or object generate separate parameters for each value of the array or key-value pair of the map. For other types of parameters this property has no effect. When style is form, the default value is true. For all other styles, the default value is false. 21 | public var explode: Bool? 22 | 23 | /// Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding. This property only applies to parameters with an in value of query. The default value is false. 24 | public var allowReserved: Bool? 25 | 26 | /// The schema defining the type used for the parameter. 27 | public var schema: ReferenceOr? 28 | 29 | /// Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema that contains an example, the example value SHALL override the example provided by the schema. To represent public var examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary. 30 | public var example: AnyValue? 31 | 32 | /// Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema that contains an example, the examples value SHALL override the example provided by the schema. 33 | public var examples: ComponentsMap? 34 | 35 | /// A map containing the representations for the parameter. The key is the media type and the value describes it. The map MUST only contain one entry. 36 | public var content: ContentObject? 37 | 38 | public var specificationExtensions: SpecificationExtensions? = nil 39 | 40 | public init(description: String? = nil, required: Bool? = nil, deprecated: Bool? = nil, allowEmptyValue: Bool? = nil, style: ParameterObject.Style? = nil, explode: Bool? = nil, allowReserved: Bool? = nil, schema: ReferenceOr? = nil, example: AnyValue? = nil, examples: ComponentsMap? = nil, content: ContentObject? = nil) { 41 | self.description = description 42 | self.required = required 43 | self.deprecated = deprecated 44 | self.allowEmptyValue = allowEmptyValue 45 | self.style = style 46 | self.explode = explode 47 | self.allowReserved = allowReserved 48 | self.schema = schema 49 | self.example = example 50 | self.examples = examples 51 | self.content = content 52 | } 53 | } 54 | 55 | public extension ComponentsMap { 56 | 57 | static func encode( 58 | _ value: Encodable, 59 | dateFormat: DateEncodingFormat = .default, 60 | keyEncodingStrategy: KeyEncodingStrategy = .default, 61 | schemas: inout ComponentsMap 62 | ) throws -> ComponentsMap { 63 | try HeadersEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 64 | .encode(value, schemas: &schemas) 65 | .mapValues { 66 | .value($0) 67 | } 68 | } 69 | 70 | static func decode( 71 | _ type: Decodable.Type, 72 | dateFormat: DateEncodingFormat = .default, 73 | keyEncodingStrategy: KeyEncodingStrategy = .default, 74 | schemas: inout ComponentsMap 75 | ) throws -> ComponentsMap { 76 | try HeadersEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 77 | .decode(type, schemas: &schemas) 78 | .mapValues { 79 | .value($0) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/InfoObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The object provides metadata about the API. The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience. 4 | public struct InfoObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// The title of the API. 7 | public var title: String 8 | 9 | /// A short summary of the API. 10 | public var summary: String? 11 | 12 | /// A description of the API. CommonMark syntax MAY be used for rich text representation. 13 | public var description: String? 14 | 15 | /// A URL to the Terms of Service for the API. 16 | public var termsOfService: URL? 17 | 18 | /// The contact information for the exposed API. 19 | public var contact: ContactObject? 20 | 21 | /// The license information for the exposed API. 22 | public var license: LicenseObject? 23 | 24 | /// The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version). 25 | public var version: Version 26 | 27 | public var specificationExtensions: SpecificationExtensions? = nil 28 | 29 | public init(title: String, summary: String? = nil, description: String? = nil, termsOfService: URL? = nil, contact: ContactObject? = nil, license: LicenseObject? = nil, version: Version) { 30 | self.title = title 31 | self.summary = summary 32 | self.description = description 33 | self.termsOfService = termsOfService 34 | self.contact = contact 35 | self.license = license 36 | self.version = version 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/AdditionalProperties.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum AdditionalProperties: Codable, Equatable, ExpressibleByBooleanLiteral, ExpressibleBySchemaObject, ExpressibleByReferenceOr { 4 | 5 | case boolean(Bool) 6 | case schema(ReferenceOr) 7 | 8 | public init(schemaObject: SchemaObject) { 9 | self = .schema(.value(schemaObject)) 10 | } 11 | 12 | public init(referenceOr: ReferenceOr) { 13 | self = .schema(referenceOr) 14 | } 15 | 16 | public init(booleanLiteral value: Bool) { 17 | self = .boolean(value) 18 | } 19 | 20 | public init(from decoder: Decoder) throws { 21 | do { 22 | self = try .boolean(Bool(from: decoder)) 23 | } catch { 24 | self = try .schema(ReferenceOr(from: decoder)) 25 | } 26 | } 27 | 28 | public func encode(to encoder: Encoder) throws { 29 | switch self { 30 | case let .boolean(value): 31 | try value.encode(to: encoder) 32 | case let .schema(schema): 33 | try schema.encode(to: encoder) 34 | } 35 | } 36 | 37 | public var asSchemaObject: SchemaObject? { 38 | get { 39 | switch self { 40 | case let .schema(object): return object.object 41 | default: return nil 42 | } 43 | } 44 | set { 45 | if let newValue { 46 | self = .schema(.value(newValue)) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/DataFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DataFormat: RawRepresentable, ExpressibleByStringLiteral, LosslessStringConvertible, Hashable, Codable { 4 | 5 | public var rawValue: String 6 | public var description: String { rawValue } 7 | 8 | public init(_ description: String) { 9 | rawValue = description 10 | } 11 | 12 | public init(rawValue: String) { 13 | self.init(rawValue) 14 | } 15 | 16 | public init(stringLiteral value: String) { 17 | self.init(value) 18 | } 19 | 20 | public init(from decoder: Decoder) throws { 21 | try self.init(String(from: decoder)) 22 | } 23 | 24 | public func encode(to encoder: Encoder) throws { 25 | try rawValue.encode(to: encoder) 26 | } 27 | } 28 | 29 | public extension DataFormat { 30 | 31 | static var email: DataFormat = "email" 32 | static var uuid: DataFormat = "uuid" 33 | static var uri: DataFormat = "uri" 34 | static var hostname: DataFormat = "hostname" 35 | static var ipv4: DataFormat = "ipv4" 36 | static var ipv6: DataFormat = "ipv6" 37 | static var int64: DataFormat = "int64" 38 | static var int32: DataFormat = "int32" 39 | static var double: DataFormat = "double" 40 | static var float: DataFormat = "float" 41 | static var decimal: DataFormat = "decimal" 42 | 43 | /// full-date notation as defined by RFC 3339, section 5.6, for example, 2017-07-21 44 | static var date: DataFormat = "date" 45 | 46 | /// the date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z 47 | static var dateTime: DataFormat = "date-time" 48 | 49 | /// a hint to UIs to mask the input 50 | static var password: DataFormat = "password" 51 | 52 | /// base64-encoded characters, for example, U3dhZ2dlciByb2Nrcw== 53 | static var byte: DataFormat = "byte" 54 | 55 | /// binary data, used to describe files 56 | static var binary: DataFormat = "binary" 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/DataType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum DataType: String, Codable { 4 | 5 | case string 6 | case number 7 | case integer 8 | case boolean 9 | case array 10 | case object 11 | 12 | public var asPrimitive: PrimitiveDataType? { 13 | switch self { 14 | case .string: return .string 15 | case .number: return .number 16 | case .integer: return .integer 17 | case .boolean: return .boolean 18 | case .array, .object: return nil 19 | } 20 | } 21 | } 22 | 23 | public enum PrimitiveDataType: String, Codable { 24 | 25 | case string 26 | case number 27 | case integer 28 | case boolean 29 | 30 | public var asDataType: DataType { 31 | switch self { 32 | case .string: return .string 33 | case .number: return .number 34 | case .integer: return .integer 35 | case .boolean: return .boolean 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/DiscriminatorObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// When request bodies or response payloads may be one of a number of different schemas, a discriminator object can be used to aid in serialization, deserialization, and validation. The discriminator is a specific object in a schema which is used to inform the consumer of the document of an alternative schema based on the value associated with it. 4 | /// 5 | /// When using the discriminator, inline schemas will not be considered. 6 | public struct DiscriminatorObject: Codable, Equatable, SpecificationExtendable { 7 | 8 | /// The name of the property in the payload that will hold the discriminator value. 9 | public var propertyName: String 10 | 11 | /// An object to hold mappings between payload values and schema names or references. 12 | public var mapping: [String: String]? 13 | 14 | public var specificationExtensions: SpecificationExtensions? = nil 15 | 16 | public init(propertyName: String, mapping: [String: String]? = nil) { 17 | self.propertyName = propertyName 18 | self.mapping = mapping 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/ExpressibleBySchemaObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ExpressibleBySchemaObject { 4 | 5 | init(schemaObject: SchemaObject) 6 | var asSchemaObject: SchemaObject? { get set } 7 | } 8 | 9 | extension SchemaObject: ExpressibleBySchemaObject { 10 | 11 | public init(schemaObject: SchemaObject) { 12 | self = schemaObject 13 | } 14 | 15 | public var asSchemaObject: SchemaObject? { 16 | get { self } 17 | set { 18 | if let newValue { 19 | self = newValue 20 | } 21 | } 22 | } 23 | } 24 | 25 | extension ReferenceOr: ExpressibleBySchemaObject { 26 | 27 | public init(schemaObject: SchemaObject) { 28 | self = .value(schemaObject) 29 | } 30 | 31 | public var asSchemaObject: SchemaObject? { 32 | get { object } 33 | set { object = newValue } 34 | } 35 | } 36 | 37 | public extension ExpressibleBySchemaObject { 38 | 39 | func with(_ keyPath: WritableKeyPath, _ value: T) -> Self { 40 | var result = self 41 | result.asSchemaObject?[keyPath: keyPath] = value 42 | return result 43 | } 44 | 45 | static func one( 46 | of types: ReferenceOr..., 47 | discriminator: DiscriminatorObject? = nil 48 | ) -> Self { 49 | Self( 50 | schemaObject: SchemaObject( 51 | context: .composition(.one(of: types, discriminator: discriminator)) 52 | ) 53 | ) 54 | } 55 | 56 | static func all( 57 | of types: ReferenceOr..., 58 | discriminator: DiscriminatorObject? = nil 59 | ) -> Self { 60 | Self( 61 | schemaObject: SchemaObject( 62 | context: .composition(.all(of: types, discriminator: discriminator)) 63 | ) 64 | ) 65 | } 66 | 67 | static func any( 68 | of types: ReferenceOr..., 69 | discriminator: DiscriminatorObject? = nil 70 | ) -> Self { 71 | Self( 72 | schemaObject: SchemaObject( 73 | context: .composition(.any(of: types, discriminator: discriminator)) 74 | ) 75 | ) 76 | } 77 | 78 | static func not( 79 | a type: ReferenceOr 80 | ) -> Self { 81 | Self( 82 | schemaObject: SchemaObject( 83 | context: .composition(.not(a: type)) 84 | ) 85 | ) 86 | } 87 | 88 | static var any: Self { 89 | Self(schemaObject: SchemaObject()) 90 | } 91 | 92 | static func object( 93 | properties: ComponentsMap, 94 | required: Set = [], 95 | size: AnyRange = .any 96 | ) -> Self { 97 | Self( 98 | schemaObject: SchemaObject( 99 | context: .object( 100 | ObjectContext( 101 | properties: properties, 102 | required: required, 103 | size: size 104 | ) 105 | ) 106 | ) 107 | ) 108 | } 109 | 110 | static func dictionary( 111 | of additionalProperties: ReferenceOr, 112 | minProperties: Int? = nil, 113 | maxProperties: Int? = nil, 114 | size: AnyRange = .any 115 | ) -> Self { 116 | Self( 117 | schemaObject: SchemaObject( 118 | context: .object( 119 | ObjectContext( 120 | properties: nil, 121 | additionalProperties: .schema(additionalProperties), 122 | size: size 123 | ) 124 | ) 125 | ) 126 | ) 127 | } 128 | 129 | static func array( 130 | of type: ReferenceOr, 131 | size: AnyRange = .any, 132 | uniqueItems: Bool? = nil 133 | ) -> Self { 134 | Self( 135 | schemaObject: SchemaObject( 136 | context: .array( 137 | ArrayContext( 138 | items: type, 139 | size: size, 140 | uniqueItems: uniqueItems 141 | ) 142 | ) 143 | ) 144 | ) 145 | } 146 | 147 | static func `enum`( 148 | of type: PrimitiveDataType = .string, 149 | cases: [AnyValue] 150 | ) -> Self { 151 | Self( 152 | schemaObject: SchemaObject( 153 | enum: cases, 154 | context: SchemaContexts(type) 155 | ) 156 | ) 157 | } 158 | 159 | static func `enum`( 160 | _ type: T.Type, 161 | example: T? = nil 162 | ) -> Self where T: CaseIterable, T.AllCases.Element: RawRepresentable, T.AllCases.Element.RawValue == String { 163 | .enum(cases: type.allCases.map { .string($0.rawValue) }) 164 | .with(\.example, example.map { .string($0.rawValue) }) 165 | } 166 | 167 | static func `enum`( 168 | _ type: T.Type, 169 | example: T? = nil 170 | ) -> Self where T: CaseIterable, T.AllCases.Element: RawRepresentable, T.AllCases.Element.RawValue == Int { 171 | .enum(cases: type.allCases.map { .int($0.rawValue) }) 172 | .with(\.example, example.map { .int($0.rawValue) }) 173 | .with(\.description, type.allCases.map { "• \($0.rawValue) → \($0)" }.joined(separator: "\n")) 174 | } 175 | 176 | static func `enum`( 177 | _ type: T.Type, 178 | example: T? = nil 179 | ) -> Self where T: CaseIterable, T.AllCases.Element: RawRepresentable, T.AllCases.Element.RawValue == Double { 180 | .enum(cases: type.allCases.map { .double($0.rawValue) }) 181 | .with(\.example, example.map { .double($0.rawValue) }) 182 | .with(\.description, type.allCases.map { "• \($0.rawValue) → \($0)" }.joined(separator: "\n")) 183 | } 184 | 185 | static var string: Self { 186 | .string() 187 | } 188 | 189 | static func string( 190 | format: DataFormat? = nil, 191 | pattern: String? = nil, 192 | size: AnyRange = .any, 193 | example: String? = nil 194 | ) -> Self { 195 | Self( 196 | schemaObject: SchemaObject( 197 | format: format, 198 | example: example.map { .string($0) }, 199 | context: .string( 200 | StringContext( 201 | pattern: pattern, 202 | size: size 203 | ) 204 | ) 205 | ) 206 | ) 207 | } 208 | 209 | static func number( 210 | format: DataFormat? = nil, 211 | range: AnyRange = .any, 212 | multipleOf: Double? = nil, 213 | example: Double? = nil 214 | ) -> Self { 215 | Self( 216 | schemaObject: SchemaObject( 217 | format: format, 218 | example: example.map { .double($0) }, 219 | context: .number( 220 | NumberContext( 221 | range: range, 222 | multipleOf: multipleOf 223 | ) 224 | ) 225 | ) 226 | ) 227 | } 228 | 229 | static var number: Self { .number() } 230 | 231 | static func integer( 232 | format: DataFormat? = .int64, 233 | range: AnyRange = .any, 234 | multipleOf: Int? = nil, 235 | example: Int? = nil 236 | ) -> Self { 237 | Self( 238 | schemaObject: SchemaObject( 239 | format: format, 240 | example: example.map { .int($0) }, 241 | context: .integer( 242 | NumberContext( 243 | range: range, 244 | multipleOf: multipleOf 245 | ) 246 | ) 247 | ) 248 | ) 249 | } 250 | 251 | static var integer: Self { .integer(format: .int64) } 252 | 253 | static func float( 254 | range: AnyRange = .any, 255 | multipleOf: Float? = nil, 256 | example: Float? = nil 257 | ) -> Self { 258 | Self( 259 | schemaObject: SchemaObject( 260 | format: .float, 261 | example: example.map { .double(Double($0)) }, 262 | context: .number( 263 | NumberContext( 264 | range: AnyRange( 265 | lowerBound: range.lowerBound.map { Double($0) }, 266 | upperBound: range.upperBound.map { Double($0) }, 267 | include: range.include 268 | ), 269 | multipleOf: multipleOf.map { Double($0) } 270 | ) 271 | ) 272 | ) 273 | ) 274 | } 275 | 276 | static var float: Self { .float() } 277 | 278 | static func decimal( 279 | format: DataFormat? = .decimal, 280 | range: AnyRange = .any, 281 | multipleOf: Decimal? = nil, 282 | example: Decimal? = nil 283 | ) -> Self { 284 | Self( 285 | schemaObject: SchemaObject( 286 | format: format, 287 | example: example.map { .double($0.double) }, 288 | context: .number( 289 | NumberContext( 290 | range: AnyRange( 291 | lowerBound: range.lowerBound.map(\.double), 292 | upperBound: range.upperBound.map(\.double), 293 | include: range.include 294 | ), 295 | multipleOf: multipleOf.map(\.double) 296 | ) 297 | ) 298 | ) 299 | ) 300 | } 301 | 302 | static var decimal: Self { .decimal() } 303 | 304 | static func double( 305 | range: AnyRange = .any, 306 | multipleOf: Double? = nil, 307 | example: Double? = nil 308 | ) -> Self { 309 | .number( 310 | format: .double, 311 | range: range, 312 | multipleOf: multipleOf, 313 | example: example 314 | ) 315 | } 316 | 317 | static var double: Self { .double() } 318 | 319 | static func boolean( 320 | example: Bool? = nil 321 | ) -> Self { 322 | Self( 323 | schemaObject: SchemaObject( 324 | example: example.map { .bool($0) }, 325 | context: .boolean 326 | ) 327 | ) 328 | } 329 | 330 | static var boolean: Self { .boolean() } 331 | 332 | static func uuid( 333 | example: UUID? = nil 334 | ) -> Self { 335 | .string(format: .uuid, example: example?.uuidString) 336 | } 337 | 338 | static var uuid: Self { .uuid() } 339 | 340 | static func uri( 341 | example: UUID? = nil 342 | ) -> Self { 343 | .string(format: .uri, example: example?.uuidString) 344 | } 345 | 346 | static var uri: Self { .uri() } 347 | 348 | static func date( 349 | example: Date? = nil 350 | ) -> Self { 351 | .string(format: .date, example: example.map { DateEncodingFormat.date($0) }) 352 | } 353 | 354 | static var date: Self { .date() } 355 | 356 | static func dateTime( 357 | example: Date? = nil 358 | ) -> Self { 359 | .string(format: .date, example: example.map { DateEncodingFormat.dateTime($0) }) 360 | } 361 | 362 | static var dateTime: Self { .dateTime() } 363 | } 364 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/SchemaContexts.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public indirect enum SchemaContexts: Equatable { 4 | 5 | case object(ObjectContext) 6 | case array(ArrayContext) 7 | case integer(NumberContext) 8 | case number(NumberContext) 9 | case string(StringContext) 10 | case boolean(BooleanContext) 11 | case composition(CompositionContext) 12 | 13 | public var type: DataType? { 14 | switch self { 15 | case .object: return .object 16 | case .array: return .array 17 | case .integer: return .integer 18 | case .number: return .number 19 | case .string: return .string 20 | case .boolean: return .boolean 21 | case .composition: return nil 22 | } 23 | } 24 | 25 | public static var integer: SchemaContexts { 26 | .integer(NumberContext()) 27 | } 28 | 29 | public static var number: SchemaContexts { 30 | .number(NumberContext()) 31 | } 32 | 33 | public static var string: SchemaContexts { 34 | .string(StringContext()) 35 | } 36 | 37 | public static var boolean: SchemaContexts { 38 | .boolean(BooleanContext()) 39 | } 40 | 41 | public init(_ type: PrimitiveDataType) { 42 | switch type { 43 | case .string: 44 | self = .string(StringContext()) 45 | case .number: 46 | self = .number(NumberContext()) 47 | case .integer: 48 | self = .integer(NumberContext()) 49 | case .boolean: 50 | self = .boolean(BooleanContext()) 51 | } 52 | } 53 | } 54 | 55 | public struct ArrayContext: Codable, Equatable { 56 | 57 | public var items: ReferenceOr 58 | private var minItems: Int? 59 | private var maxItems: Int? 60 | public var uniqueItems: Bool? 61 | 62 | public var size: AnyRange { 63 | get { 64 | AnyRange( 65 | lowerBound: minItems, 66 | upperBound: maxItems, 67 | include: [.lower, .upper] 68 | ) 69 | } 70 | set { 71 | (minItems, maxItems) = ObjectContext.values(from: newValue) 72 | } 73 | } 74 | 75 | public init( 76 | items: ReferenceOr, 77 | size: AnyRange = .any, 78 | uniqueItems: Bool? = nil 79 | ) { 80 | self.items = items 81 | (minItems, maxItems) = ObjectContext.values(from: size) 82 | self.uniqueItems = uniqueItems 83 | } 84 | } 85 | 86 | public struct BooleanContext: Codable, Equatable { 87 | 88 | public init() {} 89 | } 90 | 91 | public struct NumberContext: Codable, Equatable { 92 | 93 | private var minimum: Number? 94 | private var maximum: Number? 95 | private var exclusiveMinimum: Number? 96 | private var exclusiveMaximum: Number? 97 | public var multipleOf: Number? 98 | 99 | public var range: AnyRange { 100 | get { 101 | AnyRange( 102 | lowerBound: minimum ?? exclusiveMaximum, 103 | upperBound: maximum ?? exclusiveMaximum, 104 | include: { 105 | var result: Set = [] 106 | if minimum != nil { result.insert(.lower) } 107 | if maximum != nil { result.insert(.upper) } 108 | return result 109 | }() 110 | ) 111 | } 112 | set { 113 | (minimum, maximum, exclusiveMinimum, exclusiveMaximum) = Self.values(from: newValue) 114 | } 115 | } 116 | 117 | public init( 118 | range: AnyRange = .any, 119 | multipleOf: Number? = nil 120 | ) { 121 | self.multipleOf = multipleOf 122 | (minimum, maximum, exclusiveMinimum, exclusiveMaximum) = Self.values(from: range) 123 | } 124 | 125 | private static func values(from range: AnyRange) -> ( 126 | minimum: Number?, 127 | maximum: Number?, 128 | exclusiveMinimum: Number?, 129 | exclusiveMaximum: Number? 130 | ) { 131 | ( 132 | range.include.contains(.lower) ? range.lowerBound : nil, 133 | range.include.contains(.upper) ? range.upperBound : nil, 134 | !range.include.contains(.lower) ? range.lowerBound : nil, 135 | !range.include.contains(.upper) ? range.upperBound : nil 136 | ) 137 | } 138 | } 139 | 140 | public struct ObjectContext: Codable, Equatable { 141 | 142 | public var properties: ComponentsMap? 143 | public var additionalProperties: AdditionalProperties? 144 | public var required: Set? 145 | private var minProperties: Int? 146 | private var maxProperties: Int? 147 | 148 | public var size: AnyRange { 149 | get { 150 | AnyRange( 151 | lowerBound: minProperties, 152 | upperBound: maxProperties, 153 | include: [.lower, .upper] 154 | ) 155 | } 156 | set { 157 | (minProperties, maxProperties) = Self.values(from: newValue) 158 | } 159 | } 160 | 161 | public init( 162 | properties: ComponentsMap? = nil, 163 | additionalProperties: AdditionalProperties? = nil, 164 | required: Set? = nil, 165 | size: AnyRange = .any 166 | ) { 167 | self.properties = properties?.nilIfEmpty 168 | self.additionalProperties = additionalProperties 169 | self.required = required?.nilIfEmpty 170 | (minProperties, maxProperties) = Self.values(from: size) 171 | } 172 | 173 | public func encode(to encoder: Encoder) throws { 174 | var container = encoder.container(keyedBy: CodingKeys.self) 175 | try container.encodeIfPresent(properties?.nilIfEmpty, forKey: .properties) 176 | try container.encodeIfPresent(additionalProperties, forKey: .additionalProperties) 177 | try container.encodeIfPresent(required?.nilIfEmpty?.sorted(), forKey: .required) 178 | try container.encodeIfPresent(minProperties, forKey: .minProperties) 179 | try container.encodeIfPresent(maxProperties, forKey: .maxProperties) 180 | } 181 | 182 | static func values(from range: AnyRange) -> ( 183 | minimum: Int?, 184 | maximum: Int? 185 | ) { 186 | ( 187 | range.lowerBound.map { range.include.contains(.lower) ? $0 : $0 + 1 }, 188 | range.upperBound.map { range.include.contains(.upper) ? $0 : $0 - 1 } 189 | ) 190 | } 191 | } 192 | 193 | public struct StringContext: Codable, Equatable { 194 | 195 | public var pattern: String? 196 | private var minLength: Int? 197 | private var maxLength: Int? 198 | 199 | public var size: AnyRange { 200 | get { 201 | AnyRange( 202 | lowerBound: minLength, 203 | upperBound: maxLength, 204 | include: [.lower, .upper] 205 | ) 206 | } 207 | set { 208 | (minLength, maxLength) = ObjectContext.values(from: newValue) 209 | } 210 | } 211 | 212 | public init( 213 | pattern: String? = nil, 214 | size: AnyRange = .any 215 | ) { 216 | self.pattern = pattern 217 | (minLength, maxLength) = ObjectContext.values(from: size) 218 | } 219 | } 220 | 221 | public struct CompositionContext: Codable, Equatable { 222 | 223 | public var allOf: [ReferenceOr]? 224 | public var oneOf: [ReferenceOr]? 225 | public var anyOf: [ReferenceOr]? 226 | public var not: ReferenceOr? 227 | public var discriminator: DiscriminatorObject? 228 | 229 | public static func all(of schemas: [ReferenceOr], discriminator: DiscriminatorObject? = nil) -> CompositionContext { 230 | CompositionContext(allOf: schemas, discriminator: discriminator) 231 | } 232 | 233 | public static func one(of schemas: [ReferenceOr], discriminator: DiscriminatorObject? = nil) -> CompositionContext { 234 | CompositionContext(oneOf: schemas, discriminator: discriminator) 235 | } 236 | 237 | public static func any(of schemas: [ReferenceOr], discriminator: DiscriminatorObject? = nil) -> CompositionContext { 238 | CompositionContext(anyOf: schemas, discriminator: discriminator) 239 | } 240 | 241 | public static func not(a schema: ReferenceOr) -> CompositionContext { 242 | CompositionContext(not: schema) 243 | } 244 | 245 | static let invalid = CompositionContext() 246 | } 247 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/SchemaObject++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension SchemaObject: ExpressibleByDictionary { 4 | 5 | public typealias Key = String 6 | public typealias Value = ReferenceOr 7 | 8 | public init(dictionaryElements elements: [(String, ReferenceOr)]) { 9 | self = .object( 10 | properties: OrderedDictionary(elements) { _, s in s } 11 | ) 12 | } 13 | 14 | public subscript(key: String) -> ReferenceOr? { 15 | get { 16 | if case let .object(context) = context { 17 | return context.properties?[key] 18 | } 19 | return nil 20 | } 21 | set { 22 | if case var .object(objectContext) = context { 23 | objectContext.properties?[key] = newValue 24 | context = .object(objectContext) 25 | } 26 | } 27 | } 28 | } 29 | 30 | extension SchemaObject { 31 | 32 | var isReferenceable: Bool { 33 | switch context { 34 | case .none, .array, .string, .number, .integer, .boolean: 35 | return self.enum?.isEmpty == false 36 | case .composition: 37 | return true 38 | case let .object(context): 39 | switch context.additionalProperties { 40 | case .schema: 41 | return false 42 | case let .boolean(value): 43 | return !value 44 | case .none: 45 | return true 46 | } 47 | } 48 | } 49 | } 50 | 51 | extension ReferenceOr { 52 | 53 | var isReferenceable: Bool { 54 | switch self { 55 | case let .value(object): 56 | return object.isReferenceable 57 | case .ref: 58 | return false 59 | } 60 | } 61 | } 62 | 63 | public extension SchemaObject { 64 | 65 | static func encode( 66 | _ value: Encodable, 67 | dateFormat: DateEncodingFormat = .default, 68 | keyEncodingStrategy: KeyEncodingStrategy = .default, 69 | into schemas: inout ComponentsMap 70 | ) throws { 71 | let encoder = SchemeEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 72 | try encoder.encode(value, into: &schemas) 73 | } 74 | 75 | static func decode( 76 | _ type: Decodable.Type, 77 | dateFormat: DateEncodingFormat = .default, 78 | keyEncodingStrategy: KeyEncodingStrategy = .default, 79 | into schemas: inout ComponentsMap 80 | ) throws { 81 | let encoder = SchemeEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 82 | try encoder.decode(type, into: &schemas) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/JSONSchema/SchemaObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The Schema Object allows the definition of input and output data types. These types can be objects, but also primitives and arrays. This object is a superset of the JSON Schema Specification Draft 2020-12. 4 | /// 5 | /// For more information about the properties, see JSON Schema Core and JSON Schema Validation. 6 | /// 7 | /// Unless stated otherwise, the property definitions follow those of JSON Schema and do not add any additional semantics. Where JSON Schema indicates that behavior is defined by the application (e.g. for annotations), OAS also defers the definition of semantics to the application consuming the OpenAPI document. 8 | public struct SchemaObject: Equatable, Codable, SpecificationExtendable { 9 | 10 | public var title: String? 11 | public var description: String? 12 | public var deprecated: Bool? 13 | public var format: DataFormat? 14 | public var xml: XMLObject? 15 | public var externalDocs: ExternalDocumentationObject? 16 | 17 | public var `default`: AnyValue? 18 | public var `enum`: [AnyValue]? { 19 | didSet { 20 | checkNullableEnum() 21 | } 22 | } 23 | 24 | public var readOnly: Bool? 25 | public var writeOnly: Bool? 26 | 27 | public var specificationExtensions: SpecificationExtensions? 28 | public var context: SchemaContexts? 29 | 30 | public var nullable: Bool? { 31 | didSet { 32 | checkNullableEnum() 33 | } 34 | } 35 | 36 | public var example: AnyValue? { 37 | didSet { 38 | if example != nil { 39 | examples = nil 40 | } 41 | } 42 | } 43 | 44 | public var examples: ComponentsMap? { 45 | didSet { 46 | if examples != nil { 47 | example = nil 48 | } 49 | } 50 | } 51 | 52 | public init( 53 | title: String? = nil, 54 | description: String? = nil, 55 | deprecated: Bool? = nil, 56 | format: DataFormat? = nil, 57 | xml: XMLObject? = nil, 58 | externalDocs: ExternalDocumentationObject? = nil, 59 | default: AnyValue? = nil, 60 | enum: [AnyValue]? = nil, 61 | nullable: Bool? = nil, 62 | readOnly: Bool? = nil, 63 | writeOnly: Bool? = nil, 64 | specificationExtensions: SpecificationExtensions? = nil, 65 | example: AnyValue? = nil, 66 | examples: ComponentsMap? = nil, 67 | context: SchemaContexts? = nil 68 | ) { 69 | self.title = title 70 | self.description = description 71 | self.deprecated = deprecated 72 | self.xml = xml 73 | self.format = format 74 | self.externalDocs = externalDocs 75 | self.default = `default` 76 | self.enum = `enum` 77 | self.nullable = nullable 78 | self.readOnly = readOnly 79 | self.writeOnly = writeOnly 80 | self.specificationExtensions = specificationExtensions 81 | self.context = context 82 | self.example = examples == nil ? example : nil 83 | self.examples = examples 84 | } 85 | 86 | public init(from decoder: Decoder) throws { 87 | specificationExtensions = try SpecificationExtensions(from: decoder) 88 | 89 | let container = try decoder.container(keyedBy: CodingKeys.self) 90 | 91 | title = try container.decodeIfPresent(String.self, forKey: .title) 92 | description = try container.decodeIfPresent(String.self, forKey: .description) 93 | xml = try container.decodeIfPresent(XMLObject.self, forKey: .xml) 94 | externalDocs = try container.decodeIfPresent(ExternalDocumentationObject.self, forKey: .externalDocs) 95 | format = try container.decodeIfPresent(DataFormat.self, forKey: .format) 96 | example = try container.decodeIfPresent(AnyValue.self, forKey: .example) 97 | examples = try container.decodeIfPresent(ComponentsMap.self, forKey: .examples) 98 | deprecated = try container.decodeIfPresent(Bool.self, forKey: .deprecated) 99 | `default` = try container.decodeIfPresent(AnyValue.self, forKey: .default) 100 | `enum` = try container.decodeIfPresent([AnyValue].self, forKey: .enum) 101 | nullable = try container.decodeIfPresent(Bool.self, forKey: .nullable) 102 | readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) 103 | writeOnly = try container.decodeIfPresent(Bool.self, forKey: .writeOnly) 104 | 105 | let type = try container.decodeIfPresent(DataType.self, forKey: .type) 106 | switch type { 107 | case .array: 108 | context = try .array(ArrayContext(from: decoder)) 109 | case .object: 110 | context = try .object(ObjectContext(from: decoder)) 111 | case .boolean: 112 | context = try .boolean(BooleanContext(from: decoder)) 113 | case .integer: 114 | context = try .integer(NumberContext(from: decoder)) 115 | case .number: 116 | context = try .number(NumberContext(from: decoder)) 117 | case .string: 118 | context = try .string(StringContext(from: decoder)) 119 | case .none: 120 | let composition = try CompositionContext(from: decoder) 121 | if composition != .invalid { 122 | context = .composition(composition) 123 | } 124 | } 125 | } 126 | 127 | public func encode(to encoder: Encoder) throws { 128 | try specificationExtensions?.encode(to: encoder) 129 | 130 | var container = encoder.container(keyedBy: CodingKeys.self) 131 | try container.encodeIfPresent(context?.type, forKey: .type) 132 | try container.encodeIfPresent(title, forKey: .title) 133 | try container.encodeIfPresent(description, forKey: .description) 134 | try container.encodeIfPresent(format, forKey: .format) 135 | try container.encodeIfPresent(xml, forKey: .xml) 136 | try container.encodeIfPresent(externalDocs, forKey: .externalDocs) 137 | try container.encodeIfPresent(examples?.nilIfEmpty, forKey: .examples) 138 | try container.encodeIfPresent(example, forKey: .example) 139 | try container.encodeIfPresent(`default`, forKey: .default) 140 | try container.encodeIfPresent(deprecated?.nilIfFalse, forKey: .deprecated) 141 | try container.encodeIfPresent(`enum`, forKey: .enum) 142 | try container.encodeIfPresent(nullable?.nilIfFalse, forKey: .nullable) 143 | try container.encodeIfPresent(readOnly?.nilIfFalse, forKey: .readOnly) 144 | try container.encodeIfPresent(writeOnly?.nilIfFalse, forKey: .writeOnly) 145 | 146 | switch context { 147 | case .none: 148 | break 149 | case let .array(context): 150 | try context.encode(to: encoder) 151 | case let .object(context): 152 | try context.encode(to: encoder) 153 | case let .boolean(context): 154 | try context.encode(to: encoder) 155 | case let .integer(context): 156 | try context.encode(to: encoder) 157 | case let .number(context): 158 | try context.encode(to: encoder) 159 | case let .string(context): 160 | try context.encode(to: encoder) 161 | case let .composition(context): 162 | try context.encode(to: encoder) 163 | } 164 | } 165 | 166 | public enum CodingKeys: String, CodingKey { 167 | 168 | case type 169 | case title 170 | case description 171 | case format 172 | case examples 173 | case deprecated 174 | case xml 175 | case externalDocs 176 | case example 177 | case `default` 178 | case `enum` 179 | case nullable 180 | case readOnly 181 | case writeOnly 182 | } 183 | } 184 | 185 | private extension SchemaObject { 186 | 187 | mutating func checkNullableEnum() { 188 | let nullValue: AnyValue = .null 189 | if nullable == true { 190 | if var allValues = `enum`, !allValues.contains(nullValue) { 191 | allValues.append(nullValue) 192 | self.enum = allValues 193 | } 194 | } else if var allValues = `enum`, let i = allValues.firstIndex(of: nullValue) { 195 | allValues.remove(at: i) 196 | self.enum = allValues 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/LicenseObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// License information for the exposed API. 4 | public struct LicenseObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// The license name used for the API. 7 | public var name: String 8 | 9 | /// An SPDX license expression for the API. The identifier field is mutually exclusive of the url field. 10 | public var identifier: String? 11 | 12 | /// A URL to the license used for the API. This MUST be in the form of a URL. The url field is mutually exclusive of the identifier field. 13 | public var url: URL? 14 | 15 | public var specificationExtensions: SpecificationExtensions? = nil 16 | 17 | public init(name: String, identifier: String? = nil, url: URL? = nil) { 18 | self.name = name 19 | self.identifier = identifier 20 | self.url = url 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/LinkObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The Link object represents a possible design-time link for a response. The presence of a link does not guarantee the caller's ability to successfully invoke it, rather it provides a known relationship and traversal mechanism between responses and other operations. 4 | /// 5 | /// Unlike dynamic links (i.e. links provided in the response payload), the OAS linking mechanism does not require link information in the runtime response. 6 | /// 7 | /// For computing links, and providing instructions to execute them, a runtime expression is used for accessing values in an operation and using them as parameters while invoking the linked operation. 8 | public struct LinkObject: Equatable, Codable, SpecificationExtendable { 9 | 10 | /// A relative or absolute URI reference to an OAS operation. This field is mutually exclusive of the operationId field, and MUST point to an Operation Object. Relative operationRef values MAY be used to locate an existing Operation Object in the OpenAPI definition. See the rules for resolving Relative References. 11 | public var operationRef: String? 12 | 13 | /// The name of an existing, resolvable OAS operation, as defined with a unique operationId. This field is mutually exclusive of the operationRef field. 14 | public var operationId: String? 15 | 16 | /// A map representing parameters to pass to an operation as specified with operationId or identified via operationRef. The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation. The parameter name can be qualified using the parameter location [{in}.]{name} for operations that use the same parameter name in different locations (e.g. path.id). 17 | public var parameters: [String: RuntimeExpressionOr]? 18 | 19 | /// A literal value or {expression} to use as a request body when calling the target operation. 20 | public var requestBody: RuntimeExpressionOr? 21 | 22 | /// A description of the link. CommonMark syntax MAY be used for rich text representation. 23 | public var description: String? 24 | 25 | /// A server object to be used by the target operation. 26 | public var server: ServerObject? 27 | 28 | public var specificationExtensions: SpecificationExtensions? = nil 29 | 30 | public init(operationRef: String? = nil, operationId: String? = nil, parameters: [String: RuntimeExpressionOr]? = nil, requestBody: RuntimeExpressionOr? = nil, description: String? = nil, server: ServerObject? = nil) { 31 | self.operationRef = operationRef 32 | self.operationId = operationId 33 | self.parameters = parameters 34 | self.requestBody = requestBody 35 | self.description = description 36 | self.server = server 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/MediaType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MediaType: Codable, Hashable, RawRepresentable, ExpressibleByStringLiteral, LosslessStringConvertible { 4 | 5 | public typealias RawValue = String 6 | public typealias StringLiteralType = String 7 | 8 | public var rawValue: String { 9 | get { 10 | (["\(type)/\(subtype)"] + parameters.map { "\($0.key)=\($0.value)" }.sorted()) 11 | .joined(separator: ";") 12 | } 13 | set { 14 | self = MediaType(rawValue: newValue) 15 | } 16 | } 17 | 18 | public var description: String { rawValue } 19 | 20 | public var type: String 21 | public var subtype: String 22 | public var parameters: [String: String] 23 | 24 | public init( 25 | _ type: String, 26 | _ subtype: String, 27 | parameters: [String: String] = [:] 28 | ) { 29 | self.type = type 30 | self.subtype = subtype 31 | self.parameters = parameters 32 | } 33 | 34 | public init(rawValue: String) { 35 | var type = "" 36 | var index = rawValue.startIndex 37 | while index < rawValue.endIndex, rawValue[index] != "/" { 38 | type.append(rawValue[index]) 39 | index = rawValue.index(after: index) 40 | } 41 | if index < rawValue.endIndex { 42 | index = rawValue.index(after: index) 43 | } 44 | 45 | var subtype = "" 46 | while index < rawValue.endIndex, rawValue[index] != ";" { 47 | subtype.append(rawValue[index]) 48 | index = rawValue.index(after: index) 49 | } 50 | 51 | var parameters: [String: String] = [:] 52 | while index < rawValue.endIndex { 53 | index = rawValue.index(after: index) 54 | guard index < rawValue.endIndex else { break } 55 | var key = "" 56 | while index < rawValue.endIndex, rawValue[index] != ";" { 57 | key.append(rawValue[index]) 58 | index = rawValue.index(after: index) 59 | } 60 | var value = "" 61 | while index < rawValue.endIndex, rawValue[index] != ";" { 62 | value.append(rawValue[index]) 63 | index = rawValue.index(after: index) 64 | } 65 | parameters[key] = value.trimmingCharacters(in: [" ", "\""]) 66 | } 67 | self.init( 68 | type.isEmpty ? "*" : type, 69 | subtype.isEmpty ? "*" : subtype, 70 | parameters: parameters 71 | ) 72 | } 73 | 74 | public init(stringLiteral value: String) { 75 | self.init(rawValue: value) 76 | } 77 | 78 | public init(_ stringValue: String) { 79 | self.init(rawValue: stringValue) 80 | } 81 | 82 | public init(from decoder: Decoder) throws { 83 | try self.init(rawValue: String(from: decoder)) 84 | } 85 | 86 | public func encode(to encoder: Encoder) throws { 87 | try rawValue.encode(to: encoder) 88 | } 89 | } 90 | 91 | public extension MediaType { 92 | 93 | struct Application: RawRepresentable, ExpressibleByStringLiteral { 94 | public var rawValue: String 95 | 96 | public init(rawValue: RawValue) { 97 | self.rawValue = rawValue 98 | } 99 | 100 | public init(stringLiteral value: String) { 101 | self.init(rawValue: value) 102 | } 103 | 104 | public static let json: Application = "json" 105 | public static let schemaJson: Application = "schema+json" 106 | public static let schemaInstanceJson: Application = "schema-instance+json" 107 | public static let xml: Application = "xml" 108 | public static let octetStream: Application = "octet-stream" 109 | public static let urlEncoded: Application = "x-www-form-urlencoded" 110 | } 111 | 112 | static func application(_ subtype: Application) -> MediaType { 113 | MediaType("application", subtype.rawValue) 114 | } 115 | 116 | struct Text: RawRepresentable, ExpressibleByStringLiteral { 117 | public var rawValue: String 118 | 119 | public init(rawValue: RawValue) { 120 | self.rawValue = rawValue 121 | } 122 | 123 | public init(stringLiteral value: String) { 124 | self.init(rawValue: value) 125 | } 126 | 127 | public static let plain: Text = "plain" 128 | public static let html: Text = "html" 129 | } 130 | 131 | static func text(_ subtype: Text, charset: String? = nil) -> MediaType { 132 | MediaType( 133 | "text", 134 | subtype.rawValue, 135 | parameters: charset.map { ["charset": $0] } ?? [:] 136 | ) 137 | } 138 | 139 | struct Multipart: RawRepresentable, ExpressibleByStringLiteral { 140 | public var rawValue: String 141 | 142 | public init(rawValue: RawValue) { 143 | self.rawValue = rawValue 144 | } 145 | 146 | public init(stringLiteral value: String) { 147 | self.init(rawValue: value) 148 | } 149 | 150 | public static let formData: Multipart = "form-data" 151 | public static let byteranges: Multipart = "byteranges" 152 | } 153 | 154 | static func multipart(_ subtype: Multipart) -> MediaType { 155 | MediaType("multipart", subtype.rawValue) 156 | } 157 | 158 | static let any = MediaType("*", "*") 159 | } 160 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/MediaTypeObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Each Media Type Object provides schema and examples for the media type identified by its key. 4 | public struct MediaTypeObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// The schema defining the content of the request, response, or parameter. 7 | public var schema: ReferenceOr? 8 | 9 | /// Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The `example` field is mutually exclusive of the `examples` field. Furthermore, if referencing a `schema` which contains an example, the `example` value SHALL override the example provided by the schema. 10 | public var example: AnyValue? 11 | 12 | /// Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The `examples` field is mutually exclusive of the `example` field. Furthermore, if referencing a `schema` which contains an example, the `examples` value SHALL override the example provided by the schema. 13 | public var examples: ComponentsMap? 14 | 15 | /// A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded. 16 | public var encoding: [String: EncodingObject]? 17 | 18 | public var specificationExtensions: SpecificationExtensions? = nil 19 | 20 | public init( 21 | schema: ReferenceOr? = nil, 22 | example: AnyValue? = nil, 23 | encoding: [String: EncodingObject]? = nil 24 | ) { 25 | self.schema = schema 26 | self.example = example 27 | self.encoding = encoding 28 | } 29 | 30 | public init( 31 | schema: ReferenceOr? = nil, 32 | examples: ComponentsMap, 33 | encoding: [String: EncodingObject]? = nil 34 | ) { 35 | self.schema = schema 36 | self.examples = examples 37 | self.encoding = encoding 38 | } 39 | } 40 | 41 | extension MediaTypeObject: ExpressibleByReferenceOr { 42 | 43 | public init(referenceOr: ReferenceOr) { 44 | self.init(schema: referenceOr) 45 | } 46 | } 47 | 48 | extension MediaTypeObject: ExpressibleBySchemaObject { 49 | 50 | public init(schemaObject: SchemaObject) { 51 | self.init(schema: .value(schemaObject)) 52 | } 53 | 54 | public var asSchemaObject: SchemaObject? { 55 | get { schema?.object } 56 | set { schema?.object = newValue } 57 | } 58 | } 59 | 60 | public extension MediaTypeObject { 61 | 62 | static func encode( 63 | _ value: Encodable, 64 | schemas: inout ComponentsMap 65 | ) throws -> MediaTypeObject { 66 | try MediaTypeObject( 67 | schema: .encodeSchema(value, into: &schemas), 68 | example: .encode(value) 69 | ) 70 | } 71 | 72 | static func encode( 73 | _ value: Encodable, 74 | schemas: inout ComponentsMap, 75 | examples: inout ComponentsMap 76 | ) throws -> MediaTypeObject { 77 | try MediaTypeObject( 78 | schema: .encodeSchema(value, into: &schemas), 79 | examples: [.typeName(type(of: value)): .ref(example: value, into: &examples)] 80 | ) 81 | } 82 | 83 | static func decode( 84 | _ type: Decodable.Type, 85 | schemas: inout ComponentsMap 86 | ) throws -> MediaTypeObject { 87 | try MediaTypeObject( 88 | schema: .decodeSchema(type, into: &schemas), 89 | examples: [:] 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/OAuthFlowObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct OAuthFlowObject: Codable, Equatable, SpecificationExtendable { 4 | 5 | /// The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. 6 | public var authorizationUrl: String? 7 | 8 | /// The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. 9 | public var tokenUrl: String? 10 | 11 | /// The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. 12 | public var refreshUrl: String? 13 | 14 | /// The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty. 15 | public var scopes: [String: String]? 16 | 17 | public var specificationExtensions: SpecificationExtensions? = nil 18 | 19 | public init(authorizationUrl: String? = nil, tokenUrl: String? = nil, refreshUrl: String? = nil, scopes: [String: String]? = nil) { 20 | self.authorizationUrl = authorizationUrl 21 | self.tokenUrl = tokenUrl 22 | self.refreshUrl = refreshUrl 23 | self.scopes = scopes 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/OAuthFlowsObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Allows configuration of the supported OAuth Flows. 4 | public struct OAuthFlowsObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// Configuration for the OAuth Implicit flow 7 | public var implicit: OAuthFlowObject? 8 | 9 | /// Configuration for the OAuth Resource Owner Password flow 10 | public var password: OAuthFlowObject? 11 | 12 | /// Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0. 13 | public var clientCredentials: OAuthFlowObject? 14 | 15 | /// Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0. 16 | public var authorizationCode: OAuthFlowObject? 17 | 18 | public var specificationExtensions: SpecificationExtensions? = nil 19 | 20 | public init(implicit: OAuthFlowObject? = nil, password: OAuthFlowObject? = nil, clientCredentials: OAuthFlowObject? = nil, authorizationCode: OAuthFlowObject? = nil) { 21 | self.implicit = implicit 22 | self.password = password 23 | self.clientCredentials = clientCredentials 24 | self.authorizationCode = authorizationCode 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/OpenAPIObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// This is the root object of the OpenAPI document. 4 | public struct OpenAPIObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// Version number of the OpenAPI Specification that the OpenAPI document uses. The `openapi` field SHOULD be used by tooling to interpret the OpenAPI document. This is not related to the API info.version string. 7 | public var openapi: Version 8 | 9 | /// Provides metadata about the API. The metadata MAY be used by tooling as required. 10 | public var info: InfoObject 11 | 12 | /// The default value for the $schema keyword within ```SchemaObjects``` contained within this OAS document. 13 | public var jsonSchemaDialect: URL? 14 | 15 | /// An array of ```ServerObject```, which provide connectivity information to a target server. If the servers property is not provided, or is an empty array, the default value would be a ```ServerObject``` with a url value of /. 16 | public var servers: [ServerObject]? 17 | 18 | /// The available paths and operations for the API. 19 | public var paths: PathsObject? 20 | 21 | /// The incoming webhooks that MAY be received as part of this API and that the API consumer MAY choose to implement. Closely related to the callbacks feature, this section describes requests initiated other than by an API call, for example by an out of band registration. The key name is a unique string to refer to each webhook, while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider and the expected responses. An [example](https://swagger.io/specification/examples/v3.1/webhook-example.yaml) is available. 22 | public var webhooks: [String: ReferenceOr]? 23 | 24 | /// An element to hold various schemas for the document. 25 | public var components: ComponentsObject? 26 | 27 | /// A declaration of which security mechanisms can be used across the API. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. Individual operations can override this definition. To make security optional, an empty security requirement ({}) can be included in the array. 28 | public var security: [SecurityRequirementObject]? 29 | 30 | /// A list of tags used by the document with additional metadata. The order of the tags can be used to reflect on their order by the parsing tools. Not all tags that are used by the ```OperationObject``` must be declared. The tags that are not declared MAY be organized randomly or based on the tools' logic. Each tag name in the list MUST be unique. 31 | public var tags: [TagObject]? 32 | 33 | /// Additional external documentation. 34 | public var externalDocs: ExternalDocumentationObject? 35 | 36 | public var specificationExtensions: SpecificationExtensions? = nil 37 | 38 | public init( 39 | openapi: Version = Version(3, 0, 1), 40 | info: InfoObject, 41 | jsonSchemaDialect: URL? = nil, 42 | servers: [ServerObject]? = nil, 43 | paths: PathsObject? = nil, 44 | webhooks: [String: ReferenceOr]? = nil, 45 | components: ComponentsObject? = nil, 46 | security: [SecurityRequirementObject]? = nil, 47 | tags: [TagObject]? = nil, 48 | externalDocs: ExternalDocumentationObject? = nil 49 | ) { 50 | self.openapi = openapi 51 | self.info = info 52 | self.jsonSchemaDialect = jsonSchemaDialect 53 | self.servers = servers 54 | self.paths = paths 55 | self.webhooks = webhooks 56 | self.components = components 57 | self.security = security 58 | self.tags = tags 59 | self.externalDocs = externalDocs 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/OperationObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Describes a single API operation on a path. 4 | public struct OperationObject: Equatable, Codable, SpecificationExtendable { 5 | 6 | /// A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier. 7 | public var tags: [String]? 8 | 9 | /// A short summary of what the operation does. 10 | public var summary: String? 11 | 12 | /// A verbose explanation of the operation behavior. CommonMark syntax MAY be used for rich text representation. 13 | public var description: String 14 | 15 | /// Additional external documentation for this operation. 16 | public var externalDocs: ExternalDocumentationObject? 17 | 18 | /// Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions. 19 | public var operationId: String? 20 | 21 | /// A list of parameters that are applicable for this operation. If a parameter is already defined at the Path Item, the new definition will override it but can never remove it. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and location. The list can use the Reference Object to link to parameters that are defined at the OpenAPI Object's components/parameters. 22 | public var parameters: ParametersList? 23 | 24 | /// The request body applicable for this operation. The requestBody is fully supported in HTTP methods where the HTTP 1.1 specification RFC7231 has explicitly defined semantics for request bodies. In other cases where the HTTP spec is vague (such as GET, HEAD and DELETE), requestBody is permitted but does not have well-defined semantics and SHOULD be avoided if possible. 25 | public var requestBody: ReferenceOr? 26 | 27 | /// The list of possible responses as they are returned from executing this operation. 28 | public var responses: ResponsesObject? 29 | 30 | /// A map of possible out-of band callbacks related to the parent operation. The key is a unique identifier for the Callback Object. Each value in the map is a Callback Object that describes a request that may be initiated by the API provider and the expected responses. 31 | public var callbacks: [String: ReferenceOr]? 32 | 33 | /// Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is false. 34 | public var deprecated: Bool? 35 | 36 | /// A declaration of which security mechanisms can be used for this operation. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. To make security optional, an empty security requirement ({}) can be included in the array. This definition overrides any declared top-level security. To remove a top-level security declaration, an empty array can be used. 37 | public var security: [SecurityRequirementObject]? 38 | 39 | /// An alternative server array to service this operation. If an alternative server object is specified at the ```PathItemObject``` or Root level, it will be overridden by this value. 40 | public var servers: [ServerObject]? 41 | 42 | public var specificationExtensions: SpecificationExtensions? = nil 43 | 44 | public init(tags: [String]? = nil, summary: String? = nil, description: String, externalDocs: ExternalDocumentationObject? = nil, operationId: String? = nil, parameters: ParametersList? = nil, requestBody: ReferenceOr? = nil, responses: ResponsesObject? = nil, callbacks: [String: ReferenceOr]? = nil, deprecated: Bool? = nil, security: [SecurityRequirementObject]? = nil, servers: [ServerObject]? = nil) { 45 | self.tags = tags 46 | self.summary = summary 47 | self.description = description 48 | self.externalDocs = externalDocs 49 | self.operationId = operationId 50 | self.parameters = parameters 51 | self.requestBody = requestBody 52 | self.responses = responses 53 | self.callbacks = callbacks 54 | self.deprecated = deprecated 55 | self.security = security 56 | self.servers = servers 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ParameterObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Describes a single operation parameter. 4 | /// 5 | /// A unique parameter is defined by a combination of a name and location. 6 | /// 7 | /// Parameter Locations 8 | /// There are four possible parameter locations specified by the in field: 9 | /// 10 | /// - `path` - Used together with Path Templating, where the parameter value is actually part of the operation's URL. This does not include the host or base path of the API. For example, in /items/{itemId}, the path parameter is itemId. 11 | /// - `query` - ParametersList that are appended to the URL. For example, in /items?id=###, the query parameter is id. 12 | /// - `header` - Custom headers that are expected as part of the request. Note that RFC7230 states header names are case insensitive. 13 | /// - `cookie` - Used to pass a specific cookie value to the API. 14 | public struct ParameterObject: Codable, Equatable, SpecificationExtendable { 15 | 16 | /// The name of the parameter. Parameter names are case sensitive. 17 | /// - If in is "path", the name field MUST correspond to a template expression occurring within the path field in the Paths Object. See Path Templating for further information. 18 | /// - If in is "header" and the name field is "Accept", "Content-Type" or "Authorization", the parameter definition SHALL be ignored. 19 | /// - For all other cases, the name corresponds to the parameter name used by the in property. 20 | public var name: String 21 | 22 | /// The location of the parameter. Possible values are "query", "header", "path" or "cookie". 23 | public var `in`: Location 24 | 25 | /// A brief description of the parameter. This could contain examples of use. CommonMark syntax MAY be used for rich text representation. 26 | public var description: String? 27 | 28 | /// Determines whether this parameter is mandatory. If the parameter location is "path", this property is REQUIRED and its value MUST be true. Otherwise, the property MAY be included and its default value is false. 29 | public var required: Bool? 30 | 31 | /// Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is false. 32 | public var deprecated: Bool? 33 | 34 | /// Sets the ability to pass empty-valued parameters. This is valid only for query parameters and allows sending a parameter with an empty value. Default value is false. If style is used, and if behavior is n/a (cannot be serialized), the value of allowEmptyValue SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision. 35 | public var allowEmptyValue: Bool? 36 | 37 | /// Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of in): for query - form; for path - simple; for header - simple; for cookie - form. 38 | public var style: Style? 39 | 40 | /// When this is true, parameter values of type array or object generate separate parameters for each value of the array or key-value pair of the map. For other types of parameters this property has no effect. When style is form, the default value is true. For all other styles, the default value is false. 41 | public var explode: Bool? 42 | 43 | /// Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding. This property only applies to parameters with an in value of query. The default value is false. 44 | public var allowReserved: Bool? 45 | 46 | /// The schema defining the type used for the parameter. 47 | public var schema: ReferenceOr? 48 | 49 | /// Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema that contains an example, the example value SHALL override the example provided by the schema. To represent public var examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary. 50 | public var example: AnyValue? 51 | 52 | /// Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema that contains an example, the examples value SHALL override the example provided by the schema. 53 | public var examples: ComponentsMap? 54 | 55 | /// A map containing the representations for the parameter. The key is the media type and the value describes it. The map MUST only contain one entry. 56 | public var content: ContentObject? 57 | 58 | public var specificationExtensions: SpecificationExtensions? = nil 59 | 60 | public init(name: String, in location: ParameterObject.Location, description: String? = nil, required: Bool? = nil, deprecated: Bool? = nil, allowEmptyValue: Bool? = nil, style: ParameterObject.Style? = nil, explode: Bool? = nil, allowReserved: Bool? = nil, schema: ReferenceOr? = nil, example: AnyValue? = nil, examples: ComponentsMap? = nil, content: ContentObject? = nil) { 61 | self.name = name 62 | self.in = location 63 | self.description = description 64 | self.required = location == .path ? true : required 65 | self.deprecated = deprecated 66 | self.allowEmptyValue = allowEmptyValue 67 | self.style = style 68 | self.explode = explode 69 | self.allowReserved = allowReserved 70 | self.schema = schema 71 | self.example = example 72 | self.examples = examples 73 | self.content = content 74 | } 75 | } 76 | 77 | public extension ParameterObject { 78 | 79 | enum Location: String, Codable, Equatable { 80 | case query, header, path, cookie 81 | } 82 | 83 | enum Style: String, Codable, Equatable { 84 | case matrix 85 | case label 86 | case form 87 | case simple 88 | case spaceDelimited 89 | case pipeDelimited 90 | case deepObject 91 | } 92 | } 93 | 94 | public typealias ParametersList = [ReferenceOr] 95 | 96 | public extension ParametersList { 97 | 98 | static func encode( 99 | _ value: Encodable, 100 | in location: ParameterObject.Location, 101 | dateFormat: DateEncodingFormat = .default, 102 | keyEncodingStrategy: KeyEncodingStrategy = .default, 103 | schemas: inout ComponentsMap 104 | ) throws -> ParametersList { 105 | try ParametersEncoder(location: location, dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 106 | .encode(value, schemas: &schemas) 107 | .map { 108 | .value($0) 109 | } 110 | } 111 | 112 | static func decode( 113 | _ type: Decodable.Type, 114 | in location: ParameterObject.Location, 115 | dateFormat: DateEncodingFormat = .default, 116 | keyEncodingStrategy: KeyEncodingStrategy = .default, 117 | schemas: inout ComponentsMap 118 | ) throws -> ParametersList { 119 | try ParametersEncoder(location: location, dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 120 | .decode(type, schemas: &schemas) 121 | .map { 122 | .value($0) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/Path.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Path: Hashable, ExpressibleByStringInterpolation, LosslessStringConvertible, ExpressibleByArray { 4 | 5 | public typealias ArrayLiteralElement = PathElement 6 | 7 | public var path: [PathElement] 8 | public var stringValue: String { 9 | "/" + path.map(\.string).joined(separator: "/") 10 | } 11 | 12 | public var description: String { 13 | stringValue 14 | } 15 | 16 | public init(_ stringValue: String) { 17 | path = stringValue 18 | .components(separatedBy: ["/"]) 19 | .lazy 20 | .filter { !$0.isEmpty } 21 | .map { 22 | PathElement($0) 23 | } 24 | } 25 | 26 | public init(_ path: [PathElement]) { 27 | self.path = path 28 | } 29 | 30 | public init(stringLiteral value: String) { 31 | self.init(value) 32 | } 33 | 34 | public init(arrayElements elements: [PathElement]) { 35 | self.init(elements) 36 | } 37 | 38 | public init(stringInterpolation value: DefaultStringInterpolation) { 39 | self.init(String(stringInterpolation: value)) 40 | } 41 | } 42 | 43 | public enum PathElement: Hashable, ExpressibleByStringLiteral { 44 | 45 | case constant(String) 46 | case variable(String) 47 | 48 | public var string: String { 49 | switch self { 50 | case let .constant(string): 51 | return string 52 | case let .variable(string): 53 | return "{\(string)}" 54 | } 55 | } 56 | 57 | public init(stringLiteral value: String) { 58 | self.init(value) 59 | } 60 | 61 | public init(_ string: String) { 62 | if string.hasPrefix("{") || string.hasSuffix("}") { 63 | self = .variable(string.trimmingCharacters(in: ["{", "}"])) 64 | } else { 65 | self = .constant(string) 66 | } 67 | } 68 | } 69 | 70 | public extension PathElement { 71 | 72 | static let components: PathElement = "components" 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/PathsObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PathsObject: Codable, Equatable, SpecificationExtendable, ExpressibleByDictionary { 4 | 5 | public typealias Key = Path 6 | public typealias Value = ReferenceOr 7 | 8 | public var value: [Key: Value] 9 | public var specificationExtensions: SpecificationExtensions? = nil 10 | 11 | public init(_ value: [Key: Value] = [:]) { 12 | self.value = value 13 | } 14 | 15 | public init(dictionaryElements elements: [(Key, Value)]) { 16 | self.init( 17 | Dictionary(elements) { _, second in 18 | second 19 | } 20 | ) 21 | } 22 | 23 | public init(from decoder: Decoder) throws { 24 | value = try decoder.decodeDictionary(of: [Key: Value].self) 25 | } 26 | 27 | public func encode(to encoder: Encoder) throws { 28 | try encoder.encodeDictionary(value) 29 | } 30 | 31 | public subscript(_ key: Key) -> Value? { 32 | get { value[key] } 33 | set { value[key] = newValue } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ReferenceObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A simple object to allow referencing other components in the OpenAPI document, internally and externally. 4 | /// 5 | /// The $ref string value contains a URI RFC3986, which identifies the location of the value being referenced. 6 | /// 7 | /// See the rules for resolving Relative References. 8 | public struct ReferenceObject: Codable, Equatable { 9 | 10 | /// The reference identifier. This MUST be in the form of a URI. 11 | public var ref: String 12 | 13 | /// A short summary which by default SHOULD override that of the referenced component. If the referenced object-type does not allow a summary field, then this field has no effect. 14 | public var summary: String? 15 | 16 | /// A description which by default SHOULD override that of the referenced component. CommonMark syntax MAY be used for rich text representation. If the referenced object-type does not allow a description field, then this field has no effect. 17 | public var description: String? 18 | 19 | public enum CodingKeys: String, CodingKey { 20 | 21 | case ref = "$ref" 22 | case summary 23 | case description 24 | } 25 | 26 | public init(ref: String, summary: String? = nil, description: String? = nil) { 27 | self.ref = ref 28 | self.summary = summary 29 | self.description = description 30 | } 31 | } 32 | 33 | extension ReferenceObject: ExpressibleByStringInterpolation { 34 | 35 | public init(stringLiteral value: String) { 36 | self.init(ref: value) 37 | } 38 | 39 | public init(stringInterpolation value: DefaultStringInterpolation) { 40 | self.init(ref: String(stringInterpolation: value)) 41 | } 42 | } 43 | 44 | public protocol ReferenceObjectExpressible { 45 | 46 | init(referenceObject: ReferenceObject) 47 | } 48 | 49 | extension ReferenceOr: ReferenceObjectExpressible { 50 | 51 | public init(referenceObject: ReferenceObject) { 52 | self = .ref(referenceObject) 53 | } 54 | } 55 | 56 | extension ReferenceObject: ReferenceObjectExpressible { 57 | 58 | public init(referenceObject: ReferenceObject) { 59 | self = referenceObject 60 | } 61 | } 62 | 63 | public extension ReferenceObjectExpressible { 64 | 65 | /// file#/type 66 | static func ref(to type: String, file: String? = nil) -> Self { 67 | Self( 68 | referenceObject: ReferenceObject( 69 | ref: "\(file ?? "")#/\(type)" 70 | ) 71 | ) 72 | } 73 | 74 | /// file#/type 75 | static func ref(to type: (some Any).Type, file: String? = nil) -> Self { 76 | .ref(to: .typeName(type), file: file) 77 | } 78 | 79 | /// #/components/path/type 80 | static func ref(components path: String, _ name: String) -> Self { 81 | Self(referenceObject: "#/components/\(path)/\(name)") 82 | } 83 | 84 | /// #/components/path/type 85 | static func ref(components path: String, _ type: Any.Type) -> Self { 86 | .ref(components: path, .typeName(type)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ReferenceOr.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ReferenceOr { 4 | 5 | case value(Object) 6 | case ref(ReferenceObject) 7 | 8 | public var ref: ReferenceObject? { 9 | get { 10 | if case let .ref(referenceObject) = self { 11 | return referenceObject 12 | } 13 | return nil 14 | } 15 | set { 16 | if let newValue { 17 | self = .ref(newValue) 18 | } 19 | } 20 | } 21 | 22 | public var object: Object? { 23 | get { 24 | if case let .value(value) = self { 25 | return value 26 | } 27 | return nil 28 | } 29 | set { 30 | if let newValue { 31 | self = .value(newValue) 32 | } 33 | } 34 | } 35 | } 36 | 37 | extension ReferenceOr: Equatable where Object: Equatable {} 38 | 39 | extension ReferenceOr: Encodable where Object: Encodable { 40 | 41 | public func encode(to encoder: Encoder) throws { 42 | switch self { 43 | case let .value(object): 44 | try object.encode(to: encoder) 45 | case let .ref(referenceObject): 46 | try referenceObject.encode(to: encoder) 47 | } 48 | } 49 | } 50 | 51 | extension ReferenceOr: Decodable where Object: Decodable { 52 | 53 | public init(from decoder: Decoder) throws { 54 | do { 55 | self = try .ref(ReferenceObject(from: decoder)) 56 | } catch { 57 | self = try .value(Object(from: decoder)) 58 | } 59 | } 60 | } 61 | 62 | extension ReferenceOr: ExpressibleByUnicodeScalarLiteral where Object: ExpressibleByUnicodeScalarLiteral { 63 | 64 | public init(unicodeScalarLiteral value: Object.UnicodeScalarLiteralType) { 65 | self = .value(Object(unicodeScalarLiteral: value)) 66 | } 67 | } 68 | 69 | extension ReferenceOr: ExpressibleByExtendedGraphemeClusterLiteral where Object: ExpressibleByExtendedGraphemeClusterLiteral { 70 | 71 | public init(extendedGraphemeClusterLiteral value: Object.ExtendedGraphemeClusterLiteralType) { 72 | self = .value(Object(extendedGraphemeClusterLiteral: value)) 73 | } 74 | } 75 | 76 | extension ReferenceOr: ExpressibleByStringLiteral where Object: ExpressibleByStringLiteral { 77 | 78 | public init(stringLiteral value: Object.StringLiteralType) { 79 | self = .value(Object(stringLiteral: value)) 80 | } 81 | } 82 | 83 | extension ReferenceOr: ExpressibleByFloatLiteral where Object: ExpressibleByFloatLiteral { 84 | 85 | public init(floatLiteral value: Object.FloatLiteralType) { 86 | self = .value(Object(floatLiteral: value)) 87 | } 88 | } 89 | 90 | extension ReferenceOr: ExpressibleByIntegerLiteral where Object: ExpressibleByIntegerLiteral { 91 | 92 | public init(integerLiteral value: Object.IntegerLiteralType) { 93 | self = .value(Object(integerLiteral: value)) 94 | } 95 | } 96 | 97 | extension ReferenceOr: ExpressibleByBooleanLiteral where Object: ExpressibleByBooleanLiteral { 98 | 99 | public init(booleanLiteral value: Object.BooleanLiteralType) { 100 | self = .value(Object(booleanLiteral: value)) 101 | } 102 | } 103 | 104 | extension ReferenceOr: ExpressibleByStringInterpolation where Object: ExpressibleByStringInterpolation { 105 | 106 | public init(stringInterpolation value: Object.StringInterpolation) { 107 | self = .value(Object(stringInterpolation: value)) 108 | } 109 | } 110 | 111 | extension ReferenceOr: ExpressibleByArrayLiteral where Object: ExpressibleByArray { 112 | 113 | public init(arrayLiteral elements: Object.ArrayLiteralElement...) { 114 | self = .value(Object(arrayElements: elements)) 115 | } 116 | } 117 | 118 | extension ReferenceOr: ExpressibleByArray where Object: ExpressibleByArray { 119 | 120 | public init(arrayElements elements: [Object.ArrayLiteralElement]) { 121 | self = .value(Object(arrayElements: elements)) 122 | } 123 | } 124 | 125 | extension ReferenceOr: ExpressibleByDictionaryLiteral where Object: ExpressibleByDictionary { 126 | 127 | public init(dictionaryLiteral elements: (Object.Key, Object.Value)...) { 128 | self = .value(Object(dictionaryElements: elements)) 129 | } 130 | } 131 | 132 | extension ReferenceOr: MutableDictionary where Object: MutableDictionary { 133 | 134 | public typealias Key = Object.Key 135 | public typealias Value = Object.Value 136 | 137 | public subscript(key: Object.Key) -> Object.Value? { 138 | get { 139 | if case let .value(object) = self { 140 | return object[key] 141 | } 142 | return nil 143 | } 144 | set { 145 | if case var .value(object) = self { 146 | object[key] = newValue 147 | self = .value(object) 148 | } 149 | } 150 | } 151 | } 152 | 153 | extension ReferenceOr: ExpressibleByDictionary where Object: ExpressibleByDictionary { 154 | 155 | public init(dictionaryElements elements: [(Object.Key, Object.Value)]) { 156 | self = .value(Object(dictionaryElements: elements)) 157 | } 158 | } 159 | 160 | public protocol ExpressibleByReferenceOr: ReferenceObjectExpressible { 161 | 162 | associatedtype Object 163 | 164 | init(referenceOr: ReferenceOr) 165 | } 166 | 167 | public extension ReferenceObjectExpressible where Self: ExpressibleByReferenceOr { 168 | 169 | init(referenceObject: ReferenceObject) { 170 | self.init(referenceOr: .ref(referenceObject)) 171 | } 172 | } 173 | 174 | extension ReferenceOr: ExpressibleByReferenceOr { 175 | 176 | public init(referenceOr: ReferenceOr) { 177 | self = referenceOr 178 | } 179 | } 180 | 181 | public extension ExpressibleByReferenceOr { 182 | 183 | static func ref(components keyPath: WritableKeyPath?>, _ name: String) -> Self { 184 | let path: String 185 | if let name = names[keyPath] { 186 | path = name 187 | } else { 188 | var object = ComponentsObject() 189 | object[keyPath: keyPath] = [:] 190 | let anyValue = try? AnyValue.encode(object) 191 | switch anyValue { 192 | case let .object(dictionary): 193 | path = dictionary.keys.first ?? "schemas" 194 | default: 195 | path = "schemas" 196 | } 197 | names[keyPath] = path 198 | } 199 | return .ref(components: path, name) 200 | } 201 | 202 | static func ref(components keyPath: WritableKeyPath?>, _ type: Any.Type) -> Self { 203 | .ref(components: keyPath, .typeName(type)) 204 | } 205 | } 206 | 207 | public extension ExpressibleByReferenceOr { 208 | 209 | static func ref(schema: Encodable, dateFormat: DateEncodingFormat = .default, into schemas: inout ComponentsMap) -> Self { 210 | _ = try? encodeSchema(schema, dateFormat: dateFormat, into: &schemas) 211 | return .ref(components: \.schemas, .typeName(type(of: schema))) 212 | } 213 | 214 | static func encodeSchema( 215 | _ value: Encodable, 216 | dateFormat: DateEncodingFormat = .default, 217 | keyEncodingStrategy: KeyEncodingStrategy = .default, 218 | into schemas: inout ComponentsMap 219 | ) throws -> Self { 220 | let encoder = SchemeEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 221 | return try Self(referenceOr: encoder.encode(value, into: &schemas)) 222 | } 223 | 224 | static func decodeSchema( 225 | _ type: Decodable.Type, 226 | dateFormat: DateEncodingFormat = .default, 227 | keyEncodingStrategy: KeyEncodingStrategy = .default, 228 | into schemas: inout ComponentsMap 229 | ) throws -> Self { 230 | let decoder = SchemeEncoder(dateFormat: dateFormat, keyEncodingStrategy: keyEncodingStrategy) 231 | return try Self(referenceOr: decoder.decode(type, into: &schemas)) 232 | } 233 | } 234 | 235 | public extension ExpressibleByReferenceOr { 236 | 237 | static func ref(example value: Encodable, dateFormat: DateEncodingFormat = .default, into examples: inout ComponentsMap) throws -> Self { 238 | let encoder = AnyValueEncoder(dateFormat: dateFormat) 239 | let example = try encoder.encode(value) 240 | let typeName = String.typeName(type(of: value)) 241 | var name = typeName 242 | var i = 0 243 | while let current = examples[name]?.object?.value, current != example { 244 | i += 1 245 | name = "\(typeName)\(i)" 246 | } 247 | examples[name] = .value(ExampleObject(value: example)) 248 | return .ref(components: \.examples, name) 249 | } 250 | } 251 | 252 | private var names: [PartialKeyPath: String] = [ 253 | \.schemas: "schemas", 254 | \.parameters: "parameters", 255 | \.responses: "responses", 256 | \.requestBodies: "requestBodies", 257 | \.pathItems: "pathItems", 258 | \.examples: "examples", 259 | \.headers: "headers", 260 | \.links: "links", 261 | \.callbacks: "callbacks", 262 | \.securitySchemes: "securitySchemes", 263 | ] 264 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/RequestBodyObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Describes a single request body. 4 | public struct RequestBodyObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// A brief description of the request body. This could contain examples of use. ```CommonMark syntax``` MAY be used for rich text representation. 7 | public var description: String? 8 | 9 | /// The content of the request body. The key is a media type or media type range and the value describes it. For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* 10 | public var content: ContentObject 11 | 12 | /// Determines if the request body is required in the request. Defaults to false. 13 | public var required: Bool? 14 | 15 | public var specificationExtensions: SpecificationExtensions? = nil 16 | 17 | public init( 18 | description: String? = nil, 19 | content: ContentObject, 20 | required: Bool? = nil 21 | ) { 22 | self.description = description 23 | self.content = content 24 | self.required = required 25 | } 26 | } 27 | 28 | extension RequestBodyObject: ExpressibleByDictionary { 29 | 30 | public typealias Key = ContentObject.Key 31 | public typealias Value = ContentObject.Value 32 | 33 | public subscript(key: ContentObject.Key) -> ContentObject.Value? { 34 | get { content[key] } 35 | set { content[key] = newValue } 36 | } 37 | 38 | public init(dictionaryElements elements: [(ContentObject.Key, ContentObject.Value)]) { 39 | self.init(content: ContentObject(dictionaryElements: elements)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ResponseObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Describes a single response from an API Operation, including design-time, static links to operations based on the response. 4 | public struct ResponseObject: Codable, Equatable, SpecificationExtendable, ExpressibleByStringLiteral { 5 | 6 | /// A description of the response. CommonMark syntax MAY be used for rich text representation. 7 | public var description: String 8 | 9 | /// Maps a header name to its definition. RFC7230 states header names are case insensitive. If a response header is defined with the name "Content-Type", it SHALL be ignored. 10 | public var headers: ComponentsMap? 11 | 12 | /// A map containing descriptions of potential response payloads. The key is a media type or media type range and the value describes it. For responses that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* 13 | public var content: ContentObject? 14 | 15 | /// A map of operations links that can be followed from the response. The key of the map is a short name for the link, following the naming constraints of the names for Component Objects. 16 | public var links: ComponentsMap? 17 | 18 | public var specificationExtensions: SpecificationExtensions? = nil 19 | 20 | public init( 21 | description: String, 22 | headers: ComponentsMap? = nil, 23 | content: ContentObject? = nil, 24 | links: ComponentsMap? = nil 25 | ) { 26 | self.description = description 27 | self.headers = headers 28 | self.content = content 29 | self.links = links 30 | } 31 | 32 | public init(stringLiteral value: String) { 33 | self.init(description: value) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ResponsesObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ResponsesObject: Codable, Equatable, SpecificationExtendable, ExpressibleByDictionary { 4 | 5 | public typealias Value = ReferenceOr 6 | 7 | public var value: [Key: Value] 8 | public var specificationExtensions: SpecificationExtensions? = nil 9 | 10 | public init(_ value: [Key: Value] = [:]) { 11 | self.value = value 12 | } 13 | 14 | public init(dictionaryElements elements: [(Key, Value)]) { 15 | self.init( 16 | Dictionary(elements) { _, second in 17 | second 18 | } 19 | ) 20 | } 21 | 22 | public init(from decoder: Decoder) throws { 23 | value = try decoder.decodeDictionary(of: [Key: Value].self) 24 | } 25 | 26 | public func encode(to encoder: Encoder) throws { 27 | try encoder.encodeDictionary(value) 28 | } 29 | 30 | public subscript(_ key: Key) -> Value? { 31 | get { value[key] } 32 | set { value[key] = newValue } 33 | } 34 | } 35 | 36 | public extension ResponsesObject { 37 | 38 | enum Key: Hashable, Codable, Equatable, RawRepresentable, CodingKey, ExpressibleByIntegerLiteral, LosslessStringConvertible { 39 | 40 | case code(Int) 41 | case `default` 42 | 43 | public var rawValue: String { 44 | switch self { 45 | case let .code(int): 46 | return "\(int)" 47 | case .default: 48 | return Self.defaultRawValue 49 | } 50 | } 51 | 52 | public var stringValue: String { rawValue } 53 | public var description: String { rawValue } 54 | 55 | public var intValue: Int? { 56 | if case let .code(int) = self { 57 | return int 58 | } 59 | return nil 60 | } 61 | 62 | public init?(rawValue: String) { 63 | if rawValue == Self.defaultRawValue { 64 | self = .default 65 | } else if let code = Int(rawValue) { 66 | self = .code(code) 67 | } else { 68 | return nil 69 | } 70 | } 71 | 72 | public init?(intValue: Int) { 73 | self = .code(intValue) 74 | } 75 | 76 | public init(integerLiteral value: Int) { 77 | self = .code(value) 78 | } 79 | 80 | public init?(stringValue: String) { 81 | self.init(rawValue: stringValue) 82 | } 83 | 84 | public init?(_ description: String) { 85 | self.init(rawValue: description) 86 | } 87 | 88 | public init(from decoder: Decoder) throws { 89 | let rawValue = try String(from: decoder) 90 | guard let key = Self(rawValue: rawValue) else { 91 | throw DecodingError.dataCorrupted( 92 | DecodingError.Context( 93 | codingPath: decoder.codingPath, 94 | debugDescription: "Invalid responses field, expected '\(Self.defaultRawValue)' or status code, \(rawValue) found" 95 | ) 96 | ) 97 | } 98 | self = key 99 | } 100 | 101 | public func encode(to encoder: Encoder) throws { 102 | try rawValue.encode(to: encoder) 103 | } 104 | 105 | private static let defaultRawValue = "default" 106 | } 107 | } 108 | 109 | public extension ResponsesObject.Key { 110 | 111 | static let `continue`: ResponsesObject.Key = .code(100) 112 | static let switchingProtocols: ResponsesObject.Key = .code(101) 113 | static let processing: ResponsesObject.Key = .code(102) 114 | 115 | static let ok: ResponsesObject.Key = .code(200) 116 | static let created: ResponsesObject.Key = .code(201) 117 | static let accepted: ResponsesObject.Key = .code(202) 118 | static let nonAuthoritativeInformation: ResponsesObject.Key = .code(203) 119 | static let noContent: ResponsesObject.Key = .code(204) 120 | static let resetContent: ResponsesObject.Key = .code(205) 121 | static let partialContent: ResponsesObject.Key = .code(206) 122 | static let multiStatus: ResponsesObject.Key = .code(207) 123 | static let alreadyReported: ResponsesObject.Key = .code(208) 124 | static let imUsed: ResponsesObject.Key = .code(226) 125 | 126 | static let multipleChoices: ResponsesObject.Key = .code(300) 127 | static let movedPermanently: ResponsesObject.Key = .code(301) 128 | static let found: ResponsesObject.Key = .code(302) 129 | static let seeOther: ResponsesObject.Key = .code(303) 130 | static let notModified: ResponsesObject.Key = .code(304) 131 | static let useProxy: ResponsesObject.Key = .code(305) 132 | static let temporaryRedirect: ResponsesObject.Key = .code(307) 133 | static let permanentRedirect: ResponsesObject.Key = .code(308) 134 | 135 | static let badRequest: ResponsesObject.Key = .code(400) 136 | static let unauthorized: ResponsesObject.Key = .code(401) 137 | static let paymentRequired: ResponsesObject.Key = .code(402) 138 | static let forbidden: ResponsesObject.Key = .code(403) 139 | static let notFound: ResponsesObject.Key = .code(404) 140 | static let methodNotAllowed: ResponsesObject.Key = .code(405) 141 | static let notAcceptable: ResponsesObject.Key = .code(406) 142 | static let proxyAuthenticationRequired: ResponsesObject.Key = .code(407) 143 | static let requestTimeout: ResponsesObject.Key = .code(408) 144 | static let conflict: ResponsesObject.Key = .code(409) 145 | static let gone: ResponsesObject.Key = .code(410) 146 | static let lengthRequired: ResponsesObject.Key = .code(411) 147 | static let preconditionFailed: ResponsesObject.Key = .code(412) 148 | static let payloadTooLarge: ResponsesObject.Key = .code(413) 149 | static let uriTooLong: ResponsesObject.Key = .code(414) 150 | static let unsupportedMediaType: ResponsesObject.Key = .code(415) 151 | static let rangeNotSatisfiable: ResponsesObject.Key = .code(416) 152 | static let expectationFailed: ResponsesObject.Key = .code(417) 153 | static let imATeapot: ResponsesObject.Key = .code(418) 154 | static let misdirectedRequest: ResponsesObject.Key = .code(421) 155 | static let unprocessableEntity: ResponsesObject.Key = .code(422) 156 | static let locked: ResponsesObject.Key = .code(423) 157 | static let failedDependency: ResponsesObject.Key = .code(424) 158 | static let upgradeRequired: ResponsesObject.Key = .code(426) 159 | static let preconditionRequired: ResponsesObject.Key = .code(428) 160 | static let tooManyRequests: ResponsesObject.Key = .code(429) 161 | static let requestHeaderFieldsTooLarge: ResponsesObject.Key = .code(431) 162 | static let unavailableForLegalReasons: ResponsesObject.Key = .code(451) 163 | 164 | static let internalServerError: ResponsesObject.Key = .code(500) 165 | static let notImplemented: ResponsesObject.Key = .code(501) 166 | static let badGateway: ResponsesObject.Key = .code(502) 167 | static let serviceUnavailable: ResponsesObject.Key = .code(503) 168 | static let gatewayTimeout: ResponsesObject.Key = .code(504) 169 | static let httpVersionNotSupported: ResponsesObject.Key = .code(505) 170 | static let variantAlsoNegotiates: ResponsesObject.Key = .code(506) 171 | static let insufficientStorage: ResponsesObject.Key = .code(507) 172 | static let loopDetected: ResponsesObject.Key = .code(508) 173 | static let notExtended: ResponsesObject.Key = .code(510) 174 | static let networkAuthenticationRequired: ResponsesObject.Key = .code(511) 175 | } 176 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/RuntimeExpression.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct RuntimeExpression: Codable, Hashable, ExpressibleByStringInterpolation, RawRepresentable, LosslessStringConvertible { 4 | 5 | public var rawValue: String 6 | 7 | public var surrounded: String { 8 | "{\(rawValue)}" 9 | } 10 | 11 | public var description: String { surrounded } 12 | 13 | public init?(rawValue: String) { 14 | self.init(rawValue) 15 | } 16 | 17 | public init(stringLiteral value: String) { 18 | rawValue = value.trimmingCharacters(in: ["{", "}"]) 19 | } 20 | 21 | public init(stringInterpolation value: DefaultStringInterpolation) { 22 | self.init(stringLiteral: String(stringInterpolation: value)) 23 | } 24 | 25 | public init?(_ stringValue: String) { 26 | guard stringValue.hasPrefix("{"), stringValue.hasSuffix("}") else { 27 | return nil 28 | } 29 | self.init(stringLiteral: stringValue) 30 | } 31 | 32 | public init(from decoder: Decoder) throws { 33 | try self.init(stringLiteral: String(from: decoder)) 34 | } 35 | 36 | public func encode(to encoder: Encoder) throws { 37 | try rawValue.encode(to: encoder) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/RuntimeExpressionOr.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum RuntimeExpressionOr: Codable, Equatable { 4 | 5 | case expression(RuntimeExpression) 6 | case value(Value) 7 | 8 | public init(from decoder: Decoder) throws { 9 | do { 10 | let string = try String(from: decoder) 11 | guard let expression = RuntimeExpression(string) else { 12 | throw DecodingError.dataCorrupted( 13 | DecodingError.Context( 14 | codingPath: decoder.codingPath, 15 | debugDescription: "Invalid expression string \(string)" 16 | ) 17 | ) 18 | } 19 | self = .expression(expression) 20 | } catch { 21 | self = try .value(Value(from: decoder)) 22 | } 23 | } 24 | 25 | public func encode(to encoder: Encoder) throws { 26 | switch self { 27 | case let .expression(runtimeExpression): 28 | try runtimeExpression.encode(to: encoder) 29 | case let .value(value): 30 | try value.encode(to: encoder) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/SecurityRequirementObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Lists the required security schemas to execute this operation. The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. 4 | /// 5 | /// Security Requirement Objects that contain multiple schemas require that all schemas MUST be satisfied for a request to be authorized. 6 | /// This enables support for scenarios where multiple query parameters or HTTP headers are required to convey security information. 7 | /// 8 | /// When a list of Security Requirement Objects is defined on the OpenAPI Object or Operation Object, only one of the Security Requirement Objects in the list needs to be satisfied to authorize the request. 9 | public struct SecurityRequirementObject: Codable, Equatable, SpecificationExtendable, ExpressibleByStringLiteral { 10 | 11 | /// Each name MUST correspond to a security scheme which is declared in the Security Schemes under the ```ComponentsObject```. If the security scheme is of type "oauth2" or "openIdConnect", then the value is a list of scope names required for the execution, and the list MAY be empty if authorization does not require a specified scope. For other security scheme types, the array MAY contain a list of role names which are required for the execution, but are not otherwise defined or exchanged in-band. 12 | public var name: String 13 | public var values: [String] 14 | public var specificationExtensions: SpecificationExtensions? = nil 15 | 16 | public init(from decoder: Decoder) throws { 17 | let dictionary = try [String: [String]](from: decoder) 18 | guard dictionary.count == 1 else { 19 | throw DecodingError.dataCorrupted( 20 | DecodingError.Context( 21 | codingPath: decoder.codingPath, 22 | debugDescription: "Invalid SecurityRequirementObject \(dictionary)" 23 | ) 24 | ) 25 | } 26 | self.init(dictionary[dictionary.startIndex].key, dictionary[dictionary.startIndex].value) 27 | } 28 | 29 | public init( 30 | _ name: String, 31 | _ values: [String] = [] 32 | ) { 33 | self.name = name 34 | self.values = values 35 | } 36 | 37 | public init(stringLiteral value: String) { 38 | self.init(value) 39 | } 40 | 41 | public func encode(to encoder: Encoder) throws { 42 | try [name: values].encode(to: encoder) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ServerObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An object representing a Server. 4 | public struct ServerObject: Codable, Equatable, SpecificationExtendable, ExpressibleByStringLiteral { 5 | 6 | /// A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}. 7 | public var url: String 8 | 9 | /// An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation. 10 | public var description: String? 11 | 12 | /// A map between a variable name and its value. The value is used for substitution in the server's URL template. 13 | public var variables: [String: ServerVariableObject]? 14 | 15 | public var specificationExtensions: SpecificationExtensions? = nil 16 | 17 | public init(url: String, description: String? = nil, variables: [String: ServerVariableObject]? = nil) { 18 | self.url = url 19 | self.description = description 20 | self.variables = variables 21 | } 22 | 23 | public init(stringLiteral value: String) { 24 | self.init(url: value) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/ServerVariableObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An object representing a Server Variable for server URL template substitution. 4 | public struct ServerVariableObject: Codable, Equatable, SpecificationExtendable { 5 | 6 | /// An enumeration of string values to be used if the substitution options are from a limited set. The array MUST NOT be empty. 7 | public var `enum`: [String] 8 | 9 | /// The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Object's treatment of default values, because in those cases parameter values are optional. If the enum is defined, the value MUST exist in the enum's values. 10 | public var `default`: String 11 | 12 | /// An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. 13 | public var description: String? 14 | 15 | public var specificationExtensions: SpecificationExtensions? = nil 16 | 17 | public init(enum: [String], default: String, description: String? = nil) { 18 | 19 | self.enum = `enum` 20 | self.default = `default` 21 | self.description = description 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/SpecificationExtendable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points. 4 | /// 5 | /// The extensions properties are implemented as patterned fields that are always prefixed by "x-". 6 | public protocol SpecificationExtendable { 7 | 8 | var specificationExtensions: SpecificationExtensions? { get set } 9 | } 10 | 11 | public struct SpecificationExtensions: Codable, ExpressibleByDictionary, Equatable { 12 | 13 | public typealias Value = AnyValue 14 | 15 | public var fields: [Key: AnyValue] 16 | 17 | public subscript(_ key: Key) -> AnyValue? { 18 | get { fields[key] } 19 | set { fields[key] = newValue } 20 | } 21 | 22 | public init(dictionaryElements elements: [(Key, AnyValue)]) { 23 | fields = Dictionary(elements) { _, s in s } 24 | } 25 | 26 | public init(fields: [Key: AnyValue]) { 27 | self.fields = fields 28 | } 29 | 30 | public init(from decoder: Decoder) throws { 31 | fields = [:] 32 | let container = try decoder.container(keyedBy: StringKey.self) 33 | for key in container.allKeys { 34 | fields[key.value] = try container.decodeIfPresent(AnyValue.self, forKey: key) 35 | } 36 | } 37 | 38 | public init( 39 | from value: some Encodable, 40 | encoder: JSONEncoder = JSONEncoder(), 41 | decoder: JSONDecoder = JSONDecoder() 42 | ) throws { 43 | encoder.keyEncodingStrategy = .specificationExtension 44 | decoder.keyDecodingStrategy = .useDefaultKeys 45 | let data = try encoder.encode(value) 46 | self = try decoder.decode(SpecificationExtensions.self, from: data) 47 | } 48 | 49 | public func encode(to encoder: Encoder) throws { 50 | var container = encoder.container(keyedBy: StringKey.self) 51 | for (key, value) in fields { 52 | try container.encode(value, forKey: StringKey(key)) 53 | } 54 | } 55 | 56 | public struct Key: LosslessStringConvertible, Codable, ExpressibleByStringLiteral, Hashable { 57 | 58 | public let name: String 59 | public var description: String { name } 60 | 61 | public init?(_ description: String) { 62 | guard description.hasPrefix("x-") else { 63 | return nil 64 | } 65 | name = description 66 | } 67 | 68 | public init(stringLiteral value: String) { 69 | if let result = Self(value) { 70 | self = result 71 | } else { 72 | name = "x-\(value)" 73 | } 74 | } 75 | 76 | public init(from decoder: Decoder) throws { 77 | try self.init(stringLiteral: String(from: decoder)) 78 | } 79 | 80 | public func encode(to encoder: Encoder) throws { 81 | try description.encode(to: encoder) 82 | } 83 | } 84 | } 85 | 86 | @propertyWrapper 87 | public struct WithSpecExtensions { 88 | 89 | public var wrappedValue: Wrapped 90 | public var projectedValue: SpecificationExtensions { 91 | get { wrappedValue.specificationExtensions ?? [:] } 92 | set { wrappedValue.specificationExtensions = newValue } 93 | } 94 | 95 | public init(wrappedValue: Wrapped) { 96 | self.wrappedValue = wrappedValue 97 | } 98 | } 99 | 100 | public extension WithSpecExtensions { 101 | 102 | init() where T? == Wrapped { 103 | self.init(wrappedValue: nil) 104 | } 105 | } 106 | 107 | extension WithSpecExtensions: Decodable where Wrapped: Decodable { 108 | 109 | public init(from decoder: Decoder) throws { 110 | var wrapped = try Wrapped(from: decoder) 111 | wrapped.specificationExtensions = try? SpecificationExtensions(from: decoder) 112 | self.init(wrappedValue: wrapped) 113 | } 114 | } 115 | 116 | extension WithSpecExtensions: Encodable where Wrapped: Encodable { 117 | 118 | public func encode(to encoder: Encoder) throws { 119 | try projectedValue.encode(to: encoder) 120 | var wrapped = wrappedValue 121 | wrapped.specificationExtensions = nil 122 | try wrapped.encode(to: encoder) 123 | } 124 | } 125 | 126 | extension WithSpecExtensions: Equatable where Wrapped: Equatable {} 127 | 128 | extension Optional: SpecificationExtendable where Wrapped: SpecificationExtendable { 129 | 130 | public var specificationExtensions: SpecificationExtensions? { 131 | get { self?.specificationExtensions } 132 | set { self?.specificationExtensions = newValue } 133 | } 134 | } 135 | 136 | public extension JSONDecoder.KeyDecodingStrategy { 137 | 138 | static var specificationExtension: JSONDecoder.KeyDecodingStrategy { 139 | .custom { codingPath in 140 | guard let last = codingPath.last else { 141 | return StringKey("") 142 | } 143 | 144 | let string = last.stringValue.replacingOccurrences(of: "_", with: "-").toCamelCase(separator: "-") 145 | return StringKey(SpecificationExtensions.Key(stringLiteral: string)) 146 | } 147 | } 148 | } 149 | 150 | public extension JSONEncoder.KeyEncodingStrategy { 151 | 152 | static var specificationExtension: JSONEncoder.KeyEncodingStrategy { 153 | .custom { codingPath in 154 | guard let last = codingPath.last else { 155 | return StringKey("") 156 | } 157 | 158 | let string = last.stringValue.toSnakeCase(separator: "-").replacingOccurrences(of: "_", with: "-") 159 | return StringKey(SpecificationExtensions.Key(stringLiteral: string)) 160 | } 161 | } 162 | } 163 | 164 | public extension KeyedDecodingContainer { 165 | 166 | func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? { 167 | try decodeIfPresent(WithSpecExtensions.self, forKey: key)?.wrappedValue 168 | } 169 | 170 | func decode(_ type: T.Type, forKey key: Key) throws -> T { 171 | try decode(WithSpecExtensions.self, forKey: key).wrappedValue 172 | } 173 | } 174 | 175 | public extension UnkeyedDecodingContainer { 176 | 177 | mutating func decode(_: T.Type) throws -> T { 178 | try decode(WithSpecExtensions.self).wrappedValue 179 | } 180 | 181 | mutating func decodeIfPresent(_: T.Type) throws -> T? { 182 | try decodeIfPresent(WithSpecExtensions.self)?.wrappedValue 183 | } 184 | } 185 | 186 | public extension KeyedEncodingContainer { 187 | 188 | mutating func encodeIfPresent(_ value: (some SpecificationExtendable & Encodable)?, forKey key: K) throws { 189 | try encodeIfPresent(value.map { WithSpecExtensions(wrappedValue: $0) }, forKey: key) 190 | } 191 | 192 | mutating func encode(_ value: some SpecificationExtendable & Encodable, forKey key: K) throws { 193 | try encode(WithSpecExtensions(wrappedValue: value), forKey: key) 194 | } 195 | } 196 | 197 | public extension UnkeyedEncodingContainer { 198 | 199 | mutating func encode(_ value: some SpecificationExtendable & Encodable) throws { 200 | try encode(WithSpecExtensions(wrappedValue: value)) 201 | } 202 | 203 | mutating func encode(contentsOf sequence: T) throws where T: Sequence, T.Element: Encodable, T.Element: SpecificationExtendable { 204 | try encode(contentsOf: sequence.map { WithSpecExtensions(wrappedValue: $0) }) 205 | } 206 | } 207 | 208 | public extension SingleValueEncodingContainer { 209 | 210 | mutating func encode(_ value: some Encodable & SpecificationExtendable) throws { 211 | try encode(WithSpecExtensions(wrappedValue: value)) 212 | } 213 | } 214 | 215 | public extension SingleValueDecodingContainer { 216 | 217 | func decode(_: T.Type) throws -> T { 218 | try decode(WithSpecExtensions.self).wrappedValue 219 | } 220 | } 221 | 222 | public extension SpecificationExtendable where Self: Encodable { 223 | 224 | func json(encoder: JSONEncoder = JSONEncoder()) throws -> Data { 225 | encoder.outputFormatting.insert(.sortedKeys) 226 | return try encoder.encode(WithSpecExtensions(wrappedValue: self)) 227 | } 228 | } 229 | 230 | public extension SpecificationExtendable where Self: Decodable { 231 | 232 | init(json: Data, decoder: JSONDecoder = JSONDecoder()) throws { 233 | self = try decoder.decode(WithSpecExtensions.self, from: json).wrappedValue 234 | } 235 | } 236 | 237 | public extension SpecificationExtendable { 238 | 239 | func extended(with extensions: SpecificationExtensions) -> Self { 240 | var result = self 241 | result.specificationExtensions = SpecificationExtensions( 242 | fields: (result.specificationExtensions ?? [:]).fields 243 | .merging(extensions.fields) { _, new in 244 | new 245 | } 246 | ) 247 | return result 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/TagObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Adds metadata to a single tag that is used by the ```OperationObject```. It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. 4 | public struct TagObject: Codable, Equatable, SpecificationExtendable, Identifiable { 5 | 6 | /// The name of the tag 7 | public var name: String 8 | 9 | public var id: String { name } 10 | 11 | /// A description for the tag. CommonMark syntax MAY be used for rich text representation. 12 | public var description: String? 13 | 14 | /// Additional external documentation for this tag. 15 | public var externalDocs: ExternalDocumentationObject? 16 | 17 | public var specificationExtensions: SpecificationExtensions? = nil 18 | 19 | public init(name: String, description: String? = nil, externalDocs: ExternalDocumentationObject? = nil) { 20 | self.name = name 21 | self.description = description 22 | self.externalDocs = externalDocs 23 | } 24 | } 25 | 26 | extension TagObject: ExpressibleByStringLiteral { 27 | 28 | public init(stringLiteral value: String) { 29 | self.init(name: value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/Version.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Version: Codable, Comparable, LosslessStringConvertible, ExpressibleByStringInterpolation { 4 | 5 | public static let zero = Version(0, 0, 0) 6 | 7 | /// version when you make incompatible API changes 8 | public var major: UInt 9 | 10 | /// version when you add functionality in a backwards compatible manner 11 | public var minor: UInt 12 | 13 | /// version when you make backwards compatible bug fixes 14 | public var patch: UInt 15 | 16 | /// The pre-release identifier according to the semantic versioning standard, such as -beta.1. 17 | public var prereleaseIdentifiers: [String] 18 | 19 | /// The build metadata of this version according to the semantic versioning standard, such as a commit hash. 20 | public var buildMetadataIdentifiers: [String] 21 | 22 | public init( 23 | _ major: UInt, 24 | _ minor: UInt, 25 | _ patch: UInt, 26 | prereleaseIdentifiers: [String] = [], 27 | buildMetadataIdentifiers: [String] = [] 28 | ) { 29 | self.major = major 30 | self.minor = minor 31 | self.patch = patch 32 | self.prereleaseIdentifiers = prereleaseIdentifiers 33 | self.buildMetadataIdentifiers = buildMetadataIdentifiers 34 | } 35 | 36 | public init?(_ string: String) { 37 | let prereleaseStartIndex = string.firstIndex(of: "-") 38 | let metadataStartIndex = string.firstIndex(of: "+") 39 | 40 | let requiredEndIndex = prereleaseStartIndex ?? metadataStartIndex ?? string.endIndex 41 | let requiredCharacters = string.prefix(upTo: requiredEndIndex) 42 | let requiredComponents = requiredCharacters 43 | .split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false) 44 | .compactMap { UInt($0) } 45 | 46 | guard requiredComponents.count == 3 else { return nil } 47 | 48 | let major = requiredComponents[0] 49 | let minor = requiredComponents[1] 50 | let patch = requiredComponents[2] 51 | 52 | func identifiers(start: String.Index?, end: String.Index) -> [String] { 53 | guard let start else { return [] } 54 | let identifiers = string[string.index(after: start) ..< end] 55 | return identifiers.split(separator: ".").map(String.init(_:)) 56 | } 57 | 58 | let prereleaseIdentifiers = identifiers( 59 | start: prereleaseStartIndex, 60 | end: metadataStartIndex ?? string.endIndex 61 | ) 62 | let buildMetadataIdentifiers = identifiers( 63 | start: metadataStartIndex, 64 | end: string.endIndex 65 | ) 66 | self.init( 67 | major, 68 | minor, 69 | patch, 70 | prereleaseIdentifiers: prereleaseIdentifiers, 71 | buildMetadataIdentifiers: buildMetadataIdentifiers 72 | ) 73 | } 74 | 75 | public init(stringLiteral value: String) { 76 | self = Version(value) ?? .zero 77 | } 78 | 79 | public init(stringInterpolation: StringInterpolation) { 80 | self = Version(stringInterpolation.string) ?? .zero 81 | } 82 | 83 | public init(from decoder: Decoder) throws { 84 | let string = try String(from: decoder) 85 | guard let version = Version(string) else { 86 | throw DecodingError.dataCorrupted( 87 | DecodingError.Context( 88 | codingPath: decoder.codingPath, 89 | debugDescription: "Invalid version string \(string)" 90 | ) 91 | ) 92 | } 93 | self = version 94 | } 95 | 96 | public var description: String { 97 | var base = "\(major).\(minor).\(patch)" 98 | if !prereleaseIdentifiers.isEmpty { 99 | base += "-" + prereleaseIdentifiers.joined(separator: ".") 100 | } 101 | if !buildMetadataIdentifiers.isEmpty { 102 | base += "+" + buildMetadataIdentifiers.joined(separator: ".") 103 | } 104 | return base 105 | } 106 | 107 | public func encode(to encoder: Encoder) throws { 108 | try description.encode(to: encoder) 109 | } 110 | 111 | public static func < (lhs: Version, rhs: Version) -> Bool { 112 | let lhsComparators = [lhs.major, lhs.minor, lhs.patch] 113 | let rhsComparators = [rhs.major, rhs.minor, rhs.patch] 114 | 115 | if lhsComparators != rhsComparators { 116 | return lhsComparators.lexicographicallyPrecedes(rhsComparators) 117 | } 118 | 119 | guard lhs.prereleaseIdentifiers.count > 0 else { 120 | return false // Non-prerelease lhs >= potentially prerelease rhs 121 | } 122 | 123 | guard rhs.prereleaseIdentifiers.count > 0 else { 124 | return true // Prerelease lhs < non-prerelease rhs 125 | } 126 | 127 | let zippedIdentifiers = zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) 128 | for (lhsPrereleaseIdentifier, rhsPrereleaseIdentifier) in zippedIdentifiers { 129 | if lhsPrereleaseIdentifier == rhsPrereleaseIdentifier { 130 | continue 131 | } 132 | 133 | let typedLhsIdentifier: Any = Int(lhsPrereleaseIdentifier) ?? lhsPrereleaseIdentifier 134 | let typedRhsIdentifier: Any = Int(rhsPrereleaseIdentifier) ?? rhsPrereleaseIdentifier 135 | 136 | switch (typedLhsIdentifier, typedRhsIdentifier) { 137 | case let (int1 as Int, int2 as Int): return int1 < int2 138 | case let (string1 as String, string2 as String): return string1 < string2 139 | case (is Int, is String): return true // Int prereleases < String prereleases 140 | case (is String, is Int): return false 141 | default: return false 142 | } 143 | } 144 | 145 | return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count 146 | } 147 | 148 | public struct StringInterpolation: StringInterpolationProtocol { 149 | 150 | var string: String 151 | 152 | public init(literalCapacity _: Int, interpolationCount _: Int) { 153 | string = "" 154 | } 155 | 156 | public mutating func appendLiteral(_ literal: String) { 157 | string += literal 158 | } 159 | 160 | public mutating func appendInterpolation(_ value: some BinaryInteger) { 161 | string += "\(Int(value))" 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/Objects/XMLObject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A metadata object that allows for more fine-tuned XML model definitions. 4 | /// 5 | /// When using arrays, XML element names are not inferred (for singular/plural forms) and the name property SHOULD be used to add that information. See examples for expected behavior. 6 | public struct XMLObject: Codable, Equatable, SpecificationExtendable { 7 | 8 | /// Replaces the name of the element/attribute used for the described schema property. When defined within items, it will affect the name of the individual XML elements within the list. When defined alongside type being array (outside the items), it will affect the wrapping element and only if wrapped is true. If wrapped is false, it will be ignored. 9 | public var name: String? 10 | 11 | /// The URI of the namespace definition. 12 | public var namespace: URL? 13 | 14 | /// The prefix to be used for the name. 15 | public var prefix: String? 16 | 17 | /// Declares whether the property definition translates to an attribute instead of an element. Default value is false. 18 | /// wrapped boolean MAY be used only for an array definition. Signifies whether the array is wrapped (for example, ) or unwrapped (). Default value is false. The definition takes effect only when defined alongside type being array (outside the items). 19 | public var attribute: Bool? 20 | 21 | public var specificationExtensions: SpecificationExtensions? = nil 22 | 23 | public init(name: String? = nil, namespace: URL? = nil, prefix: String? = nil, attribute: Bool? = nil) { 24 | self.name = name 25 | self.namespace = namespace 26 | self.prefix = prefix 27 | self.attribute = attribute 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/OpenAPIDescriptable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol OpenAPIDescriptable { 4 | 5 | static var openAPIDescription: OpenAPIDescriptionType? { get } 6 | } 7 | 8 | public extension OpenAPIDescriptable { 9 | 10 | static var openAPIDescription: OpenAPIDescriptionType? { 11 | nil 12 | } 13 | } 14 | 15 | #if swift(>=5.9) 16 | /// `OpenAPIAutoDescriptable`: An automatic implementation macro for the `OpenAPIDescriptable` protocol. 17 | /// 18 | /// This macro facilitates the automatic implementation of the `OpenAPIDescriptable` protocol 19 | /// for any Swift type, utilizing both standard comments (`//`) and documentation comments (`///`) 20 | /// for the type and its stored properties. It's particularly useful for generating comprehensive 21 | /// OpenAPI documentation directly from your source code. 22 | /// 23 | /// - Parameters: 24 | /// - codingKeys: The Bool value indicating whether to use a `CodingKeys` enum for properties names. 25 | /// When `true`, the property names are extracted from the `CodingKeys` enum, only stored properties are collected. 26 | /// When `false`, the property names are used directly, all properties are collected including computed, lazy and with attributes. 27 | /// Defaults to `true`. 28 | /// - docCommentsOnly: The Bool value indicating whether to use only documentation comments (`///` and `/**`). Defaults to `false`. 29 | /// - includeAttributes: The Bool value indicating whether to include properties with attributes. Defaults to `false`. The property is ignored when `codingKeys` is `false`. 30 | /// 31 | /// Features: 32 | /// - Automatically extracts and synthesizes descriptions from comments on types and stored properties. 33 | /// - Simplifies the process of conforming to `OpenAPIDescriptable` by generating necessary implementation details. 34 | /// 35 | /// - Warning: By default this macro does not process properties with attributes, as it's currently not feasible 36 | /// to distinguish between stored and computed properties in such cases. 37 | /// You can override this behavior by setting the `includeAttributes` parameter to `true` or `codingKeys` to `false`. 38 | /// 39 | /// Example: 40 | /// ```swift 41 | /// /// Description of MyType. 42 | /// @OpenAPIAutoDescriptable 43 | /// struct MyType: Codable { 44 | /// 45 | /// /// Description of myProperty. 46 | /// var myProperty: String 47 | /// } 48 | /// ``` 49 | /// 50 | /// The `OpenAPIAutoDescriptable` significantly reduces the need for boilerplate code in 51 | /// API documentation, streamlining the process of maintaining up-to-date and accurate OpenAPI docs. 52 | @attached(member, conformances: OpenAPIDescriptable, names: arbitrary) 53 | @attached(extension, conformances: OpenAPIDescriptable, names: arbitrary) 54 | public macro OpenAPIDescriptable( 55 | codingKeys: Bool = true, 56 | docCommentsOnly: Bool = false, 57 | includeAttributes: Bool = false 58 | ) = #externalMacro( 59 | module: "SwiftOpenAPIMacros", 60 | type: "OpenAPIDescriptionMacro" 61 | ) 62 | 63 | @available(*, deprecated, renamed: "OpenAPIDescriptable") 64 | @attached(member, conformances: OpenAPIDescriptable, names: arbitrary) 65 | @attached(extension, conformances: OpenAPIDescriptable, names: arbitrary) 66 | public macro OpenAPIAutoDescriptable( 67 | codingKeys: Bool = true, 68 | docCommentsOnly: Bool = false, 69 | includeAttributes: Bool = false 70 | ) = #externalMacro( 71 | module: "SwiftOpenAPIMacros", 72 | type: "OpenAPIDescriptionMacro" 73 | ) 74 | #endif 75 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/OpenAPIDescription.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol OpenAPIDescriptionType { 4 | 5 | var openAPISchemeDescription: String? { get } 6 | var schemePropertyDescriptions: [String: String] { get } 7 | } 8 | 9 | extension String: OpenAPIDescriptionType { 10 | 11 | public var openAPISchemeDescription: String? { self } 12 | public var schemePropertyDescriptions: [String: String] { [:] } 13 | } 14 | 15 | extension OpenAPIDescriptionType { 16 | 17 | public var asStringOpenAPIDescription: OpenAPIDescription { 18 | OpenAPIDescription( 19 | openAPISchemeDescription: openAPISchemeDescription, 20 | schemePropertyDescriptions: schemePropertyDescriptions 21 | ) 22 | } 23 | } 24 | 25 | public struct OpenAPIDescription: OpenAPIDescriptionType, ExpressibleByStringInterpolation, Equatable { 26 | 27 | public var openAPISchemeDescription: String? 28 | public var schemePropertyDescriptions: [String: String] = [:] 29 | 30 | public init(_ openAPISchemeDescription: String? = nil) { 31 | self.openAPISchemeDescription = openAPISchemeDescription 32 | } 33 | 34 | init( 35 | openAPISchemeDescription: String?, 36 | schemePropertyDescriptions: [String: String] 37 | ) { 38 | self.openAPISchemeDescription = openAPISchemeDescription 39 | self.schemePropertyDescriptions = schemePropertyDescriptions 40 | } 41 | 42 | public init(stringLiteral value: String) { 43 | openAPISchemeDescription = value 44 | } 45 | 46 | public init(stringInterpolation: DefaultStringInterpolation) { 47 | openAPISchemeDescription = String(stringInterpolation: stringInterpolation) 48 | } 49 | } 50 | 51 | extension OpenAPIDescription where Key: CodingKey { 52 | 53 | public func add(for key: Key, _ description: String) -> OpenAPIDescription { 54 | var result = self 55 | result.schemePropertyDescriptions[key.stringValue] = description 56 | return result 57 | } 58 | } 59 | 60 | extension OpenAPIDescription where Key == String { 61 | 62 | public func add(for key: Key, _ description: String) -> OpenAPIDescription { 63 | var result = self 64 | result.schemePropertyDescriptions[key] = description 65 | return result 66 | } 67 | } 68 | 69 | extension OpenAPIDescription where Key: RawRepresentable, Key.RawValue == String { 70 | 71 | @_disfavoredOverload 72 | public func add(for key: Key, _ description: String) -> OpenAPIDescription { 73 | var result = self 74 | result.schemePropertyDescriptions[key.rawValue] = description 75 | return result 76 | } 77 | } 78 | 79 | extension SchemaObject { 80 | 81 | func with(description: OpenAPIDescriptionType?) -> SchemaObject { 82 | guard let description else { return self } 83 | var result = self 84 | result.description = description.openAPISchemeDescription ?? result.description 85 | switch result.context { 86 | case var .object(context): 87 | for (key, value) in description.schemePropertyDescriptions { 88 | if case var .value(scheme) = context.properties?[key] { 89 | scheme.description = value 90 | context.properties?[key] = .value(scheme) 91 | } 92 | } 93 | result.context = .object(context) 94 | default: 95 | break 96 | } 97 | return result 98 | } 99 | } 100 | 101 | extension ReferenceOr { 102 | 103 | func with(description: OpenAPIDescriptionType?) -> ReferenceOr { 104 | guard let description else { return self } 105 | switch self { 106 | case let .value(scheme): 107 | return .value(scheme.with(description: description)) 108 | default: 109 | return self 110 | } 111 | } 112 | } 113 | 114 | extension [ParameterObject] { 115 | 116 | func with(description: OpenAPIDescriptionType?) -> [ParameterObject] { 117 | guard let description else { return self } 118 | var result = self 119 | for (key, value) in description.schemePropertyDescriptions { 120 | guard let i = result.firstIndex(where: { $0.name == key }) else { continue } 121 | result[i].description = value 122 | } 123 | return result 124 | } 125 | } 126 | 127 | extension OrderedDictionary { 128 | 129 | func with(description: OpenAPIDescriptionType?) -> OrderedDictionary { 130 | guard let description else { return self } 131 | var result = self 132 | for (key, value) in description.schemePropertyDescriptions { 133 | result[key]?.description = value 134 | } 135 | return result 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/OpenAPIType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol OpenAPIType: OpenAPIDescriptable { 4 | 5 | static var openAPISchema: SchemaObject { get } 6 | } 7 | 8 | extension String: OpenAPIType { 9 | 10 | public static var openAPISchema: SchemaObject { .string } 11 | } 12 | 13 | extension StaticString: OpenAPIType { 14 | 15 | public static var openAPISchema: SchemaObject { .string } 16 | } 17 | 18 | extension Int: OpenAPIType { 19 | 20 | public static var openAPISchema: SchemaObject { .integer(format: .int64) } 21 | } 22 | 23 | extension Int8: OpenAPIType { 24 | 25 | public static var openAPISchema: SchemaObject { .integer(format: .int32, range: range) } 26 | } 27 | 28 | extension Int16: OpenAPIType { 29 | 30 | public static var openAPISchema: SchemaObject { .integer(format: .int32, range: range) } 31 | } 32 | 33 | extension Int32: OpenAPIType { 34 | 35 | public static var openAPISchema: SchemaObject { .integer(format: .int32) } 36 | } 37 | 38 | extension Int64: OpenAPIType { 39 | 40 | public static var openAPISchema: SchemaObject { .integer(format: .int64) } 41 | } 42 | 43 | extension UInt: OpenAPIType { 44 | 45 | public static var openAPISchema: SchemaObject { .integer(format: .int64, range: 0...) } 46 | } 47 | 48 | extension UInt8: OpenAPIType { 49 | 50 | public static var openAPISchema: SchemaObject { .integer(format: .int32, range: range) } 51 | } 52 | 53 | extension UInt16: OpenAPIType { 54 | 55 | public static var openAPISchema: SchemaObject { .integer(format: .int32, range: range) } 56 | } 57 | 58 | extension UInt32: OpenAPIType { 59 | 60 | public static var openAPISchema: SchemaObject { .integer(format: .int32, range: 0...) } 61 | } 62 | 63 | extension UInt64: OpenAPIType { 64 | 65 | public static var openAPISchema: SchemaObject { .integer(format: .int64, range: 0...) } 66 | } 67 | 68 | extension Double: OpenAPIType { 69 | 70 | public static var openAPISchema: SchemaObject { .double } 71 | } 72 | 73 | extension Float: OpenAPIType { 74 | 75 | public static var openAPISchema: SchemaObject { .float } 76 | } 77 | 78 | extension Decimal: OpenAPIType { 79 | 80 | public static var openAPISchema: SchemaObject { .decimal } 81 | } 82 | 83 | extension Date: OpenAPIType { 84 | 85 | public static var openAPISchema: SchemaObject { .string(format: .dateTime) } 86 | } 87 | 88 | extension Data: OpenAPIType { 89 | 90 | public static var openAPISchema: SchemaObject { .string(format: .byte) } 91 | } 92 | 93 | extension UUID: OpenAPIType { 94 | 95 | public static var openAPISchema: SchemaObject { .string(format: .uuid) } 96 | } 97 | 98 | extension URL: OpenAPIType { 99 | 100 | public static var openAPISchema: SchemaObject { .string(format: .uri) } 101 | } 102 | 103 | extension Optional: OpenAPIDescriptable where Wrapped: OpenAPIDescriptable { 104 | 105 | public static var openAPIDescription: OpenAPIDescriptionType? { 106 | Wrapped.openAPIDescription 107 | } 108 | } 109 | 110 | extension Optional: OpenAPIType where Wrapped: OpenAPIType { 111 | 112 | public static var openAPISchema: SchemaObject { Wrapped.openAPISchema.with(\.nullable, true) } 113 | } 114 | 115 | extension Dictionary: OpenAPIDescriptable where Key == String, Value: OpenAPIType {} 116 | 117 | extension Dictionary: OpenAPIType where Key == String, Value: OpenAPIType { 118 | 119 | public static var openAPISchema: SchemaObject { 120 | .dictionary(of: .value(Value.openAPISchema)) 121 | } 122 | } 123 | 124 | extension Array: OpenAPIDescriptable where Element: OpenAPIDescriptable {} 125 | 126 | extension Array: OpenAPIType where Element: OpenAPIType { 127 | 128 | public static var openAPISchema: SchemaObject { 129 | .array(of: .value(Element.openAPISchema)) 130 | } 131 | } 132 | 133 | extension Set: OpenAPIDescriptable where Element: OpenAPIDescriptable {} 134 | 135 | extension Set: OpenAPIType where Element: OpenAPIType { 136 | 137 | public static var openAPISchema: SchemaObject { 138 | .array(of: .value(Element.openAPISchema), uniqueItems: true) 139 | } 140 | } 141 | 142 | private extension FixedWidthInteger { 143 | 144 | static var range: AnyRange { 145 | Int(min) ... Int(max) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPI/refactor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // public extension OpenAPIObject { 4 | 5 | // 6 | // mutating func refactor() { 7 | // let base = MutatingRef(\OpenAPIObject.self) 8 | //// refactor( 9 | //// refs: [ 10 | //// MutatingRef(<#T##keyPath: WritableKeyPath##WritableKeyPath#>) 11 | //// ], 12 | //// component: \.schemas 13 | //// ) 14 | //// refactor( 15 | //// refs: [ 16 | //// MutatingRef() 17 | //// ], 18 | //// component: \.responses 19 | //// ) 20 | // 21 | // let ref = base.paths.object.parameters 22 | // 23 | // let t = (\OpenAPIObject.paths?.value)(\.keys, { \.[$0] }) 24 | // 25 | // } 26 | // } 27 | // 28 | // private extension OpenAPIObject { 29 | 30 | // 31 | // mutating func refactor( 32 | // refs: [MutatingRef>], 33 | // component: WritableKeyPath?> 34 | // ) { 35 | // 36 | // } 37 | // } 38 | // 39 | // @dynamicMemberLookup 40 | // struct MutatingRef { 41 | 42 | // 43 | // let get: (Base) -> Value? 44 | // let set: (inout Base, Value) -> Void 45 | // 46 | // init(get: @escaping (Base) -> Value?, set: @escaping (inout Base, Value) -> Void) { 47 | // self.get = get 48 | // self.set = set 49 | // } 50 | // 51 | // init(_ keyPath: WritableKeyPath) { 52 | // self.init { base in 53 | // base[keyPath: keyPath] 54 | // } set: { base, value in 55 | // base[keyPath: keyPath] = value 56 | // } 57 | // } 58 | // 59 | // init(_ keyPath: WritableKeyPath) { 60 | // self.init { base in 61 | // base[keyPath: keyPath] 62 | // } set: { base, value in 63 | // base[keyPath: keyPath] = value 64 | // } 65 | // } 66 | // 67 | // subscript(dynamicMember keyPath: WritableKeyPath) -> MutatingRef { 68 | // MutatingRef { base in 69 | // get(base)?[keyPath: keyPath] 70 | // } set: { base, t in 71 | // guard var value = get(base) else { return } 72 | // value[keyPath: keyPath] = t 73 | // set(&base, value) 74 | // } 75 | // } 76 | // 77 | // subscript(dynamicMember keyPath: WritableKeyPath) -> MutatingRef { 78 | // MutatingRef { base in 79 | // get(base)?[keyPath: keyPath] 80 | // } set: { base, t in 81 | // guard var value = get(base) else { return } 82 | // value[keyPath: keyPath] = t 83 | // set(&base, value) 84 | // } 85 | // } 86 | // } 87 | // 88 | // extension MutatingRef where Value: MutableCollection { 89 | 90 | // 91 | // subscript(dynamicMember keyPath: WritableKeyPath) -> MutatingRef { 92 | // MutatingRef { base in 93 | // get(base)?.map { 94 | // $0[keyPath: keyPath] 95 | // } 96 | // } set: { base, t in 97 | // guard var value = get(base) else { return } 98 | // zip(value.indices, t.indices).forEach { 99 | // value[$0.0][keyPath: keyPath] = t[$0.1] 100 | // } 101 | // set(&base, value) 102 | // } 103 | // } 104 | // } 105 | // 106 | // extension MutatingRef where Value: MutableDictionary { 107 | 108 | // 109 | // subscript(dynamicMember keyPath: WritableKeyPath) -> MutatingRef { 110 | // } 111 | // 112 | // subscript(dynamicMember keyPath: WritableKeyPath) -> MutatingRef { 113 | // MutatingRef { base in 114 | // [:] 115 | //// get(base)?.mapValues { 116 | //// $0[keyPath: keyPath] 117 | //// } 118 | // } set: { base, t in 119 | //// guard var value = get(base) else { return } 120 | //// zip(value.indices, t.indices).forEach { 121 | //// value[$0.0][keyPath: keyPath] = t[$0.1] 122 | //// } 123 | //// set(&base, value) 124 | // } 125 | // } 126 | // } 127 | // 128 | // extension MutatingRef where Value: MutableCollection { 129 | 130 | // } 131 | // 132 | // extension KeyPath { 133 | 134 | // 135 | // func callAsFunction( 136 | // _ keys: KeyPath>, 137 | // _ value: (T) -> WritableKeyPath 138 | // ) where A? == Value { 139 | // 140 | // } 141 | // } 142 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPIMacros/OpenAPIDescriptionMacro.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftCompilerPlugin) 2 | import SwiftCompilerPlugin 3 | import SwiftDiagnostics 4 | import SwiftOperators 5 | import SwiftSyntax 6 | import SwiftSyntaxBuilder 7 | import SwiftSyntaxMacroExpansion 8 | import SwiftSyntaxMacros 9 | 10 | @main 11 | struct OpenAPIDescriptionPlugin: CompilerPlugin { 12 | 13 | let providingMacros: [Macro.Type] = [ 14 | OpenAPIDescriptionMacro.self 15 | ] 16 | } 17 | 18 | public struct OpenAPIDescriptionMacro: ExtensionMacro, MemberMacro { 19 | 20 | public static func expansion( 21 | of node: AttributeSyntax, 22 | providingMembersOf declaration: some DeclGroupSyntax, 23 | in context: some MacroExpansionContext 24 | ) throws -> [DeclSyntax] { 25 | try _expansion(of: node, providingMembersOf: declaration, in: context) 26 | } 27 | 28 | public static func expansion( 29 | of node: AttributeSyntax, 30 | attachedTo declaration: some DeclGroupSyntax, 31 | providingExtensionsOf type: some TypeSyntaxProtocol, 32 | conformingTo protocols: [TypeSyntax], 33 | in context: some MacroExpansionContext 34 | ) throws -> [ExtensionDeclSyntax] { 35 | [openAPIDescriptableExtension(for: type)] 36 | } 37 | } 38 | 39 | private func _expansion( 40 | of node: AttributeSyntax, 41 | providingMembersOf declaration: some DeclGroupSyntax, 42 | in context: some MacroExpansionContext 43 | ) throws -> [DeclSyntax] { 44 | var onlyDocComments = false 45 | var type: DescriptionType = .CodingKeys 46 | var includeAttributes = false 47 | node.arguments?.as(LabeledExprListSyntax.self)?.forEach { 48 | switch $0.label?.text { 49 | case "codingKeys": 50 | if $0.bool == false { 51 | type = .String 52 | } 53 | case "docCommentsOnly": 54 | if $0.bool == true { 55 | onlyDocComments = true 56 | } 57 | case "includeAttributes": 58 | if $0.bool == true { 59 | includeAttributes = true 60 | } 61 | default: 62 | break 63 | } 64 | } 65 | let typeDoc = declaration 66 | .children(viewMode: .all) 67 | .compactMap { $0.documentation(onlyDocComment: onlyDocComments) } 68 | .first? 69 | .wrapped 70 | let varDocs = declaration.memberBlock.members.compactMap { member -> (String, String)? in 71 | guard 72 | let variable = member.decl.as(VariableDeclSyntax.self), 73 | includeAttributes || type == .String || variable.attributes.isEmpty, 74 | type == .CodingKeys && !variable.modifiers.contains(where: \.name.isStaticOrLazy) 75 | || type == .String && !variable.modifiers.contains(where: \.name.isStatic), 76 | let doc = variable.documentation(onlyDocComment: false) 77 | else { 78 | return nil 79 | } 80 | var name: String? 81 | for binding in variable.bindings { 82 | guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else { 83 | continue 84 | } 85 | name = identifier.identifier.text 86 | if let closure = binding.accessorBlock, type == .CodingKeys { 87 | guard 88 | let list = closure.accessors.as(AccessorDeclListSyntax.self), 89 | list.contains(where: \.accessorSpecifier.isWillSetOrDidSet) 90 | else { 91 | return nil 92 | } 93 | } 94 | } 95 | return name.map { ($0, doc) } 96 | } 97 | let varDocsModifiers = varDocs.map { 98 | "\n .add(for: \(type.wrap(name: $0.0)), \($0.1.wrapped))" 99 | } 100 | .joined() 101 | 102 | let openAPIDescription: DeclSyntax = 103 | """ 104 | 105 | public static var openAPIDescription: (any OpenAPIDescriptionType)? { 106 | OpenAPIDescription<\(raw: type.rawValue)>(\(raw: typeDoc ?? ""))\(raw: varDocsModifiers) 107 | } 108 | """ 109 | return [openAPIDescription] 110 | } 111 | 112 | private enum DescriptionType: String { 113 | 114 | case CodingKeys 115 | case String 116 | 117 | func wrap(name: String) -> String { 118 | switch self { 119 | case .CodingKeys: 120 | return ".\(name)" 121 | case .String: 122 | return "\"\(name)\"" 123 | } 124 | } 125 | } 126 | 127 | private func openAPIDescriptableExtension(for type: some TypeSyntaxProtocol) -> ExtensionDeclSyntax { 128 | ExtensionDeclSyntax( 129 | extendedType: type, 130 | inheritanceClause: InheritanceClauseSyntax( 131 | inheritedTypes: InheritedTypeListSyntax { 132 | InheritedTypeSyntax( 133 | type: TypeSyntax(stringLiteral: "OpenAPIDescriptable") 134 | ) 135 | } 136 | ), 137 | memberBlock: MemberBlockSyntax(members: MemberBlockItemListSyntax()) 138 | ) 139 | } 140 | #endif 141 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPIMacros/StringError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct StringError: LocalizedError { 4 | 5 | var errorDescription: String? 6 | 7 | init(_ errorDescription: String? = nil) { 8 | self.errorDescription = errorDescription 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SwiftOpenAPIMacros/SyntaxExt.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftCompilerPlugin) 2 | import SwiftCompilerPlugin 3 | import SwiftSyntax 4 | import SwiftSyntaxMacros 5 | 6 | extension TokenSyntax { 7 | 8 | var isWillSetOrDidSet: Bool { 9 | tokenKind == .keyword(.didSet) || tokenKind == .keyword(.willSet) 10 | } 11 | 12 | var isStaticOrLazy: Bool { 13 | isStatic || tokenKind == .keyword(.lazy) 14 | } 15 | 16 | var isStatic: Bool { 17 | tokenKind == .keyword(.static) 18 | } 19 | } 20 | 21 | extension SyntaxProtocol { 22 | 23 | func documentation(onlyDocComment: Bool) -> String? { 24 | leadingTrivia.documentation(onlyDocComment: onlyDocComment) 25 | } 26 | } 27 | 28 | extension Trivia { 29 | 30 | func documentation(onlyDocComment: Bool) -> String? { 31 | let lines = compactMap { $0.documentation(onlyDocComment: onlyDocComment) } 32 | guard lines.count > 1 else { return lines.first?.trimmingCharacters(in: .whitespaces) } 33 | 34 | let indentation = lines.compactMap { $0.firstIndex(where: { !$0.isWhitespace })?.utf16Offset(in: $0) } 35 | .min() ?? 0 36 | 37 | return lines.map { 38 | guard $0.count > indentation else { return String($0) } 39 | return String($0.suffix($0.count - indentation)) 40 | }.joined(separator: "\\n") 41 | } 42 | } 43 | 44 | extension TriviaPiece { 45 | 46 | func documentation(onlyDocComment: Bool) -> String? { 47 | switch self { 48 | case let .docLineComment(comment): 49 | let startIndex = comment.index(comment.startIndex, offsetBy: 3) 50 | return String(comment.suffix(from: startIndex)) 51 | case let .lineComment(comment): 52 | guard !onlyDocComment else { return nil } 53 | let startIndex = comment.index(comment.startIndex, offsetBy: 2) 54 | return String(comment.suffix(from: startIndex)) 55 | case let .docBlockComment(comment): 56 | let startIndex = comment.index(comment.startIndex, offsetBy: 3) 57 | let endIndex = comment.index(comment.endIndex, offsetBy: -2) 58 | return String(comment[startIndex ..< endIndex]) 59 | case let .blockComment(comment): 60 | guard !onlyDocComment else { return nil } 61 | let startIndex = comment.index(comment.startIndex, offsetBy: 2) 62 | let endIndex = comment.index(comment.endIndex, offsetBy: -2) 63 | return String(comment[startIndex ..< endIndex]) 64 | default: 65 | return nil 66 | } 67 | } 68 | } 69 | 70 | extension String { 71 | 72 | var wrapped: String { 73 | "#\"\(self)\"#" 74 | } 75 | } 76 | 77 | extension LabeledExprListSyntax { 78 | 79 | func bool(_ name: String) -> Bool? { 80 | first { $0.label?.text == name }?.bool 81 | } 82 | } 83 | 84 | extension LabeledExprListSyntax.Element { 85 | 86 | var bool: Bool? { 87 | (expression.as(BooleanLiteralExprSyntax.self)?.literal.text).map { 88 | $0 == "true" 89 | } 90 | } 91 | } 92 | 93 | extension AttributeSyntax.Arguments { 94 | 95 | func bool(_ name: String) -> Bool? { 96 | self.as(LabeledExprListSyntax.self)?.bool(name) 97 | } 98 | } 99 | #endif 100 | -------------------------------------------------------------------------------- /Tests/SwiftOpenAPIMacrosTests/SwiftOpenAPIMacrosTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftSyntaxMacros) && canImport(SwiftOpenAPIMacros) 2 | import Foundation 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import XCTest 6 | import SwiftOpenAPIMacros 7 | import SwiftOpenAPI 8 | 9 | let testMacros: [String: Macro.Type] = [ 10 | "OpenAPIDescriptionMacro": OpenAPIDescriptionMacro.self 11 | ] 12 | 13 | final class OpenAPIDescriptionMacroTests: XCTestCase { 14 | 15 | func test_created_extension() { 16 | XCTAssertEqual( 17 | Person.openAPIDescription?.asStringOpenAPIDescription, 18 | OpenAPIDescription("A person.") 19 | .add(for: "name", "The person's name.") 20 | ) 21 | } 22 | } 23 | 24 | /// A person. 25 | @OpenAPIDescriptable 26 | struct Person: Codable { 27 | 28 | /// The person's name. 29 | let name: String 30 | 31 | /// Computed property 32 | var computedProperty: Int { 33 | 0 34 | } 35 | 36 | /// Lazy 37 | lazy var someLazyVar = 0 38 | 39 | /// Static value 40 | static var someStatic = "" 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Tests/SwiftOpenAPITests/AnyValueTests/AnyValueTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftOpenAPI 2 | import XCTest 3 | 4 | class AnyValueTests: XCTestCase { 5 | 6 | func testInit() throws { 7 | let _ = AnyValue("hi") 8 | let _: AnyValue = nil 9 | let _: AnyValue = true 10 | let _: AnyValue = 10 11 | let _: AnyValue = 3.4 12 | let _: AnyValue = "hello" 13 | let _: AnyValue = ["hi", "there"] 14 | let _: AnyValue = ["hi": "there"] 15 | } 16 | 17 | func testEquality() throws { 18 | XCTAssertEqual(AnyValue(nilLiteral: ()), .null) 19 | XCTAssertEqual(true as AnyValue, .bool(true)) 20 | XCTAssertEqual(2 as AnyValue, .int(2)) 21 | XCTAssertEqual(2.0 as AnyValue, .double(2)) 22 | XCTAssertEqual("hi" as AnyValue, .string("hi")) 23 | XCTAssertEqual(["hi": 2] as AnyValue, .object(["hi": .int(2)])) 24 | XCTAssertEqual(["hi", "there"] as AnyValue, .array([.string("hi"), .string("there")])) 25 | XCTAssertEqual(["hi": 1] as AnyValue, .object(["hi": .int(1)])) 26 | XCTAssertEqual(["hi": 1.2] as AnyValue, .object(["hi": .double(1.2)])) 27 | XCTAssertEqual([1] as AnyValue, .array([.int(1)])) 28 | XCTAssertEqual([1.2] as AnyValue, .array([.double(1.2)])) 29 | XCTAssertEqual([true] as AnyValue, .array([.bool(true)])) 30 | } 31 | 32 | func testVoidDescription() { 33 | XCTAssertEqual(String(describing: AnyValue(nilLiteral: ())), "nil") 34 | } 35 | 36 | func testJSONDecoding() throws { 37 | let json = """ 38 | { 39 | "boolean": true, 40 | "integer": 1, 41 | "string": "string", 42 | "array": [1, 2, 3], 43 | "nested": { 44 | "a": "alpha", 45 | "b": "bravo", 46 | "c": "charlie" 47 | } 48 | } 49 | """.data(using: .utf8)! 50 | 51 | let decoder = JSONDecoder() 52 | let dictionary = try decoder.decode([String: AnyValue].self, from: json) 53 | 54 | XCTAssertEqual(dictionary["boolean"], true) 55 | XCTAssertEqual(dictionary["integer"], 1) 56 | XCTAssertEqual(dictionary["string"], "string") 57 | XCTAssertEqual(dictionary["array"], [1, 2, 3]) 58 | XCTAssertEqual(dictionary["nested"], ["a": "alpha", "b": "bravo", "c": "charlie"]) 59 | } 60 | 61 | func testJSONEncoding() throws { 62 | let dictionary: [String: AnyValue] = [ 63 | "boolean": true, 64 | "integer": 1, 65 | "string": "string", 66 | "array": [1, 2, 3], 67 | "nested": [ 68 | "a": "alpha", 69 | "b": "bravo", 70 | "c": "charlie", 71 | ], 72 | ] 73 | 74 | let result = try testStringFromEncoding(of: dictionary) 75 | 76 | assertJSONEquivalent( 77 | result, 78 | """ 79 | { 80 | "array" : [ 81 | 1, 82 | 2, 83 | 3 84 | ], 85 | "boolean" : true, 86 | "integer" : 1, 87 | "nested" : { 88 | "a" : "alpha", 89 | "b" : "bravo", 90 | "c" : "charlie" 91 | }, 92 | "string" : "string" 93 | } 94 | """ 95 | ) 96 | } 97 | 98 | let testEncoder: JSONEncoder = { 99 | let encoder = JSONEncoder() 100 | if #available(macOS 10.13, *) { 101 | encoder.outputFormatting = .sortedKeys 102 | } 103 | return encoder 104 | }() 105 | 106 | func test_encodeNil() throws { 107 | let data = try JSONEncoder().encode(Wrapper(value: nil as AnyValue)) 108 | 109 | let string = String(data: data, encoding: .utf8) 110 | 111 | XCTAssertEqual(string, #"{"value":null}"#) 112 | } 113 | 114 | func test_encodeBool() throws { 115 | let data = try JSONEncoder().encode(Wrapper(value: false as AnyValue)) 116 | 117 | let string = String(data: data, encoding: .utf8) 118 | 119 | XCTAssertEqual(string, #"{"value":false}"#) 120 | } 121 | 122 | func test_encodeInt() throws { 123 | let data = try JSONEncoder().encode(Wrapper(value: 2 as AnyValue)) 124 | 125 | let string = String(data: data, encoding: .utf8) 126 | 127 | XCTAssertEqual(string, #"{"value":2}"#) 128 | } 129 | 130 | func test_encodeString() throws { 131 | let data = try JSONEncoder().encode(Wrapper(value: "hi" as AnyValue)) 132 | 133 | let string = String(data: data, encoding: .utf8) 134 | 135 | XCTAssertEqual(string, #"{"value":"hi"}"#) 136 | } 137 | 138 | func test_encodeURL() throws { 139 | let data = try JSONEncoder().encode(Wrapper(value: AnyValue.string("https://hello.com"))) 140 | 141 | let string = String(data: data, encoding: .utf8) 142 | 143 | XCTAssertEqual(string, #"{"value":"https:\/\/hello.com"}"#) 144 | } 145 | } 146 | 147 | private struct Wrapper: Codable { 148 | let value: AnyValue 149 | } 150 | -------------------------------------------------------------------------------- /Tests/SwiftOpenAPITests/AnyValueTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | let testEncoder = { () -> JSONEncoder in 4 | let encoder = JSONEncoder() 5 | if #available(macOS 10.13, *) { 6 | encoder.dateEncodingStrategy = .iso8601 7 | encoder.keyEncodingStrategy = .useDefaultKeys 8 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 9 | } 10 | #if os(Linux) 11 | encoder.dateEncodingStrategy = .iso8601 12 | encoder.keyEncodingStrategy = .useDefaultKeys 13 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 14 | #endif 15 | return encoder 16 | }() 17 | 18 | func testStringFromEncoding(of entity: T) throws -> String? { 19 | String(data: try testEncoder.encode(entity), encoding: .utf8) 20 | } 21 | 22 | func assertJSONEquivalent(_ str1: String?, _ str2: String?, file: StaticString = #file, line: UInt = #line) { 23 | 24 | // when testing on Linux, pretty printing has slightly different 25 | // meaning so the tests pass on OS X as written but need whitespace 26 | // stripped to pass on Linux 27 | #if os(Linux) 28 | var str1 = str1 29 | var str2 = str2 30 | 31 | str1?.removeAll { $0.isWhitespace } 32 | str2?.removeAll { $0.isWhitespace } 33 | #endif 34 | 35 | XCTAssertEqual( 36 | str1, 37 | str2, 38 | file: file, 39 | line: line 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftOpenAPITests/ArrayDecodingTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import SwiftOpenAPI 3 | import XCTest 4 | 5 | class ArrayDecodingTests: XCTestCase { 6 | 7 | func testDecodeArray() throws { 8 | var schemas: ComponentsMap = [:] 9 | _ = try ReferenceOr.decodeSchema(Tag.ListResponse.self, into: &schemas) 10 | XCTAssertEqual( 11 | schemas, 12 | [ 13 | "TagResponse": .object( 14 | properties: [ 15 | "id": .integer, 16 | "value": .string 17 | ], 18 | required: [ 19 | "id", 20 | "value" 21 | ] 22 | ), 23 | "TagListResponse": .object( 24 | properties: [ 25 | "tags": .array(of: .ref(components: \.schemas, "TagResponse")) 26 | ], 27 | required: ["tags"] 28 | ) 29 | ] 30 | ) 31 | } 32 | 33 | func testEncodeRecursiveArray() throws { 34 | var schemas: ComponentsMap = [:] 35 | _ = try ReferenceOr.encodeSchema(ProductDependency.example, into: &schemas) 36 | XCTAssertEqual(schemas, ["ProductDependency": .value(ProductDependency.scheme)]) 37 | } 38 | 39 | func testDecodeRecursiveArray() throws { 40 | var schemas: ComponentsMap = [:] 41 | _ = try ReferenceOr.encodeSchema(ProductDependency.example, into: &schemas) 42 | XCTAssertEqual(schemas, ["ProductDependency": .value(ProductDependency.scheme)]) 43 | } 44 | 45 | func testDecodeEmbeddedRecursiveOptionalArray() throws { 46 | var schemas: ComponentsMap = [:] 47 | let _ = try ReferenceOr.encodeSchema(EmbeddedOptionalRecursiveTypeInArray.example, into: &schemas) 48 | XCTAssertEqual( 49 | schemas["EmbeddedOptionalRecursiveTypeInArray"], 50 | .value(EmbeddedOptionalRecursiveTypeInArray.scheme) 51 | ) 52 | XCTAssertEqual( 53 | schemas["ProductDependency"], 54 | .value(ProductDependency.scheme) 55 | ) 56 | } 57 | 58 | func testURL() throws { 59 | var schemas: ComponentsMap = [:] 60 | let _ = try ReferenceOr.decodeSchema(URL.self, into: &schemas) 61 | prettyPrint(schemas) 62 | } 63 | } 64 | 65 | enum Tag { 66 | 67 | struct Response: Codable { 68 | 69 | let id: Int 70 | let value: String 71 | } 72 | 73 | struct ListResponse: Codable { 74 | let tags: [Response] 75 | } 76 | } 77 | 78 | public struct EmbeddedOptionalRecursiveTypeInArray: Codable, Equatable { 79 | public var name: String 80 | public var dependencies: [ProductDependency]? 81 | 82 | public static let example = EmbeddedOptionalRecursiveTypeInArray( 83 | name: "DTOExample", 84 | dependencies: [ProductDependency.example] 85 | ) 86 | 87 | static let scheme: SchemaObject = .object( 88 | properties: [ 89 | "name": .string, 90 | "dependencies": .array(of: .ref(components: \.schemas, "ProductDependency")) 91 | .with(\.nullable, true) 92 | ], 93 | required: ["name"] 94 | ) 95 | } 96 | 97 | public struct ProductDependency: Codable, Equatable { 98 | 99 | public var identity: String 100 | public var name: String 101 | public var url: String 102 | public var dependencies: [ProductDependency] 103 | 104 | public init(identity: String, name: String, url: String, dependencies: [ProductDependency]) { 105 | self.identity = identity 106 | self.name = name 107 | self.url = url 108 | self.dependencies = dependencies 109 | } 110 | 111 | public static let example = ProductDependency( 112 | identity: "0", 113 | name: "name", 114 | url: "http://vapor.com", 115 | dependencies: [] 116 | ) 117 | 118 | static let scheme: SchemaObject = .object( 119 | properties: [ 120 | "identity": .string, 121 | "name": .string, 122 | "url": .string, 123 | "dependencies": .array(of: .ref(components: \.schemas, "ProductDependency")) 124 | ], 125 | required: ["identity", "name", "url", "dependencies"] 126 | ) 127 | } 128 | 129 | public struct WebSocketBody: Codable { 130 | 131 | public var counter: Int 132 | public var content: URL? 133 | 134 | public init(_ content: URL?, counter: Int) { 135 | self.content = content 136 | self.counter = counter 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/SwiftOpenAPITests/Mocks.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Mocks: String { 4 | 5 | case petsSwagger = "pets-swagger.json" 6 | 7 | var url: URL { 8 | let thisSourceFile = URL(fileURLWithPath: #file) 9 | let thisDirectory = thisSourceFile.deletingLastPathComponent() 10 | return thisDirectory.appendingPathComponent("Mocks/\(rawValue)") 11 | } 12 | 13 | func getData() throws -> Data { 14 | try Data(contentsOf: url) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/SwiftOpenAPITests/SwiftOpenAPITests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import SwiftOpenAPI 3 | import XCTest 4 | 5 | final class SwiftOpenAPITests: XCTestCase { 6 | 7 | func testDecoding() async throws { 8 | let file = try Mocks.petsSwagger.getData() 9 | let decoder = JSONDecoder() 10 | let _ = try decoder.decode(OpenAPIObject.self, from: file) 11 | } 12 | 13 | func testSchemeEncoding() throws { 14 | var references: ComponentsMap = [:] 15 | try SchemaObject.encode(LoginBody.example, into: &references) 16 | XCTAssertEqual( 17 | references, 18 | [ 19 | "SomeEnum": .enum(cases: ["first", "second"]), 20 | "LoginBody": .object( 21 | properties: [ 22 | "username": .string, 23 | "password": .string, 24 | "tags": .array(of: .string, uniqueItems: true).with(\.nullable, true), 25 | "comments": .dictionary(of: .string).with(\.nullable, true), 26 | "enumValue": .ref(components: \.schemas, "SomeEnum"), 27 | "url": .uri.with(\.nullable, true), 28 | "id": .uuid, 29 | "int": .integer.with(\.nullable, true), 30 | ], 31 | required: ["id", "username", "password"] 32 | ), 33 | ] 34 | ) 35 | } 36 | 37 | func testDescriptions() throws { 38 | var references: ComponentsMap = [:] 39 | try SchemaObject.decode(CardMeta.self, into: &references) 40 | guard let schema = references["CardMeta"]?.object else { 41 | XCTFail() 42 | return 43 | } 44 | XCTAssertEqual(schema.description, .cardMeta) 45 | switch schema.context { 46 | case let .object(context): 47 | XCTAssertEqual(context.properties?["cardNumber"]?.object?.description, .cardNumber) 48 | XCTAssertEqual(context.properties?["expiryYear"]?.object?.description, .expiryYear) 49 | XCTAssertEqual(context.properties?["expiryMonth"]?.object?.description, .expiryMonth) 50 | XCTAssertEqual(context.properties?["cvv"]?.object?.description, .cvv) 51 | default: 52 | XCTFail() 53 | } 54 | } 55 | 56 | func testSpecificationExtensionsWrapper() throws { 57 | var withSpec = WithSpecExtensions(wrappedValue: SchemaObject.string) 58 | let key: SpecificationExtensions.Key = "x-some-value" 59 | let value: AnyValue = 1 60 | withSpec.projectedValue[key] = value 61 | let data = try JSONEncoder().encode(withSpec) 62 | let decoded = try JSONDecoder().decode(WithSpecExtensions.self, from: data) 63 | XCTAssertEqual(decoded.projectedValue[key], value) 64 | } 65 | 66 | func testSpecificationExtensionsWrapperWithDictionary0() throws { 67 | var withSpec = WithSpecExtensions(wrappedValue: CallbackObject()) 68 | withSpec.wrappedValue["some"] = .value(.delete(OperationObject(description: "Delete"))) 69 | let key: SpecificationExtensions.Key = "x-some-value" 70 | let value: AnyValue = 1 71 | withSpec.projectedValue[key] = value 72 | let data = try JSONEncoder().encode(withSpec) 73 | let decoded = try JSONDecoder().decode(WithSpecExtensions.self, from: data) 74 | XCTAssertEqual(decoded.projectedValue[key], value) 75 | XCTAssertEqual(decoded.wrappedValue.value.count, 1) 76 | } 77 | 78 | func testSpecificationExtensionsWrapperWithDictionary1() throws { 79 | var withSpec = WithSpecExtensions(wrappedValue: ContentObject()) 80 | withSpec.wrappedValue["some"] = .string 81 | let key: SpecificationExtensions.Key = "x-some-value" 82 | let value: AnyValue = 1 83 | withSpec.projectedValue[key] = value 84 | let data = try JSONEncoder().encode(withSpec) 85 | let decoded = try JSONDecoder().decode(WithSpecExtensions.self, from: data) 86 | XCTAssertEqual(decoded.projectedValue[key], value) 87 | XCTAssertEqual(decoded.wrappedValue.value.count, 1) 88 | } 89 | 90 | func testEnumDescriptions() throws { 91 | var references: ComponentsMap = [:] 92 | try SchemaObject.decode(SomeIntEnum.self, into: &references) 93 | guard let schema = references["SomeIntEnum"]?.object else { 94 | XCTFail() 95 | return 96 | } 97 | XCTAssertEqual(schema.description, "• 1 → first\n• 2 → second") 98 | 99 | try SchemaObject.decode(SomeDoubleEnum.self, into: &references) 100 | guard let doubleSchema = references["SomeDoubleEnum"]?.object else { 101 | XCTFail() 102 | return 103 | } 104 | XCTAssertEqual(doubleSchema.description, "• 1.1 → first\n• 2.2 → second") 105 | } 106 | 107 | func testSpecificationKeys() throws { 108 | let value = InfoObject(title: "Title", termsOfService: URL(string: "http://google.com"), version: "1.0.0") 109 | let specs = try SpecificationExtensions(from: value) 110 | XCTAssertEqual(specs["x-terms-of-service"], "http://google.com") 111 | XCTAssertEqual(specs["x-title"], "Title") 112 | XCTAssertEqual(specs["x-version"], "1.0.0") 113 | } 114 | 115 | func testSpecificationExtensions() throws { 116 | var info = InfoObject(title: "Title", termsOfService: URL(string: "http://google.com"), version: "1.0.0") 117 | let key: SpecificationExtensions.Key = "x-some-value" 118 | let value: AnyValue = 1 119 | info.specificationExtensions = [ 120 | key: value, 121 | ] 122 | let api = OpenAPIObject(info: info) 123 | let data = try JSONEncoder().encode(api) 124 | let decoded = try JSONDecoder().decode(OpenAPIObject.self, from: data).info 125 | XCTAssertEqual(decoded.specificationExtensions?[key], value) 126 | } 127 | 128 | func testKeyEncoding() throws { 129 | var references: ComponentsMap = [:] 130 | try SchemaObject.encode(LoginBody.example, keyEncodingStrategy: .convertToSnakeCase, into: &references) 131 | XCTAssertEqual( 132 | references, 133 | [ 134 | "SomeEnum": .enum(cases: ["first", "second"]), 135 | "LoginBody": .object( 136 | properties: [ 137 | "username": .string, 138 | "password": .string, 139 | "tags": .array(of: .string, uniqueItems: true).with(\.nullable, true), 140 | "comments": .dictionary(of: .string).with(\.nullable, true), 141 | "enum_value": .ref(components: \.schemas, "SomeEnum"), 142 | "url": .uri.with(\.nullable, true), 143 | "id": .uuid, 144 | "int": .integer.with(\.nullable, true), 145 | ], 146 | required: ["id", "username", "password"] 147 | ), 148 | ] 149 | ) 150 | } 151 | } 152 | 153 | func prettyPrint(_ value: some Encodable) { 154 | do { 155 | let encoder = JSONEncoder() 156 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 157 | try print( 158 | String( 159 | data: encoder.encode(value), 160 | encoding: .utf8 161 | ) ?? "" 162 | ) 163 | } catch { 164 | print(value) 165 | } 166 | } 167 | 168 | struct LoginBody: Codable { 169 | 170 | var username: String 171 | var password: String 172 | var tags: Set? 173 | var comments: [String: String]? 174 | var enumValue: SomeEnum? 175 | var url: URL? 176 | var id: UUID 177 | var int: Int? 178 | 179 | static let example = LoginBody( 180 | username: "User", 181 | password: "12345678", 182 | tags: ["tag"], 183 | comments: ["Danil": "Comment"], 184 | enumValue: .first, 185 | id: UUID(), 186 | int: 12 187 | ) 188 | } 189 | 190 | enum SomeEnum: String, Codable, CaseIterable { 191 | 192 | case first, second 193 | } 194 | 195 | enum SomeIntEnum: Int, Codable, CaseIterable { 196 | 197 | case first = 1, second = 2 198 | } 199 | 200 | enum SomeDoubleEnum: Double, Codable, CaseIterable { 201 | 202 | case first = 1.1 203 | case second = 2.2 204 | } 205 | 206 | struct CardMeta: Codable, OpenAPIDescriptable { 207 | 208 | let cardNumber: String 209 | let expiryMonth: Int 210 | let expiryYear: Int 211 | let cvv: String 212 | 213 | static var openAPIDescription: OpenAPIDescriptionType? { 214 | OpenAPIDescription(.cardMeta) 215 | .add(for: .cardNumber, .cardNumber) 216 | .add(for: .expiryYear, .expiryYear) 217 | .add(for: .expiryMonth, .expiryMonth) 218 | .add(for: .cvv, .cvv) 219 | } 220 | } 221 | 222 | private extension String { 223 | 224 | static let cardMeta = "Card meta information" 225 | static let cardNumber = "Card number" 226 | static let expiryYear = "Card expiry year" 227 | static let expiryMonth = "Card expiry month" 228 | static let cvv = "Card CVV" 229 | } 230 | --------------------------------------------------------------------------------