├── .github └── workflows │ ├── codecov.yml │ └── tests.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── OpenAPIReflection │ ├── AnyJSONCaseIterable.swift │ ├── Date+OpenAPI.swift │ ├── OpenAPI+Errors.swift │ ├── Optional+ZipWith.swift │ ├── Sampleable+OpenAPI.swift │ ├── SchemaProtocols.swift │ └── SwiftPrimitiveExtensions.swift └── OpenAPIReflection30 │ ├── AnyJSONCaseIterable.swift │ ├── Date+OpenAPI.swift │ ├── OpenAPI+Errors.swift │ ├── Optional+ZipWith.swift │ ├── Sampleable+OpenAPI.swift │ ├── SchemaProtocols.swift │ └── SwiftPrimitiveExtensions.swift └── Tests ├── OpenAPIReflection30Tests ├── AnyJSONCaseIterableTests.swift ├── GenericOpenAPISchemaInternalTests.swift ├── GenericOpenAPISchemaTests.swift ├── SchemaWithExampleTests.swift ├── SwiftPrimitiveExtensionsTests.swift └── TestHelpers.swift └── OpenAPIReflectionTests ├── AnyJSONCaseIterableTests.swift ├── GenericOpenAPISchemaInternalTests.swift ├── GenericOpenAPISchemaTests.swift ├── SchemaWithExampleTests.swift ├── SwiftPrimitiveExtensionsTests.swift └── TestHelpers.swift /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | codecov: 7 | container: 8 | image: swift:5.8-focal 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - run: swift test --enable-test-discovery --enable-code-coverage 13 | - uses: mattpolzin/swift-codecov-action@0.7.5 14 | with: 15 | MINIMUM_COVERAGE: 85 16 | INCLUDE_TESTS: 'true' 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | image: 16 | - swift:5.8-focal 17 | - swift:5.8-jammy 18 | - swift:5.9-focal 19 | - swift:5.9-jammy 20 | - swift:5.10-focal 21 | - swift:5.10-jammy 22 | - swift:5.10-noble 23 | - swift:6.0-focal 24 | - swift:6.0-jammy 25 | - swift:6.0-noble 26 | container: 27 | image: ${{ matrix.image }} 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v3 31 | - name: Run tests 32 | run: swift test --enable-test-discovery 33 | osx: 34 | runs-on: macOS-latest 35 | steps: 36 | - name: Select latest available Xcode 37 | uses: maxim-lobanov/setup-xcode@v1 38 | with: { 'xcode-version': 'latest' } 39 | - name: Checkout code 40 | uses: actions/checkout@v3 41 | - name: Run tests 42 | run: swift test --enable-test-discovery 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mathew Polzin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "OpenAPIKit", 6 | "repositoryURL": "https://github.com/mattpolzin/OpenAPIKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "ae98338a8e660ae547b058ebb69c010e70b64e31", 10 | "version": "3.0.0" 11 | } 12 | }, 13 | { 14 | "package": "Sampleable", 15 | "repositoryURL": "https://github.com/mattpolzin/Sampleable.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "df44bf1a860481109dcf455e3c6daf0a0f1bc259", 19 | "version": "2.1.0" 20 | } 21 | }, 22 | { 23 | "package": "Yams", 24 | "repositoryURL": "https://github.com/jpsim/Yams.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", 28 | "version": "5.0.6" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "OpenAPIReflection", 7 | products: [ 8 | .library( 9 | name: "OpenAPIReflection", 10 | targets: ["OpenAPIReflection"]), 11 | .library( 12 | name: "OpenAPIReflection30", 13 | targets: ["OpenAPIReflection30"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/mattpolzin/OpenAPIKit.git", from: "3.0.0"), 17 | .package(url: "https://github.com/mattpolzin/Sampleable.git", from: "2.1.0") 18 | ], 19 | targets: [ 20 | .target( 21 | name: "OpenAPIReflection30", 22 | dependencies: [.product(name: "OpenAPIKit30", package: "OpenAPIKit"), "Sampleable"]), 23 | .testTarget( 24 | name: "OpenAPIReflection30Tests", 25 | dependencies: ["OpenAPIReflection30"]), 26 | .target( 27 | name: "OpenAPIReflection", 28 | dependencies: [.product(name: "OpenAPIKit", package: "OpenAPIKit"), "Sampleable"]), 29 | .testTarget( 30 | name: "OpenAPIReflectionTests", 31 | dependencies: ["OpenAPIReflection"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Swift 5.8+](http://img.shields.io/badge/Swift-5.8+-blue.svg)](https://swift.org) 2 | 3 | [![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) ![Tests](https://github.com/mattpolzin/OpenAPIReflection/workflows/Tests/badge.svg) 4 | 5 | # OpenAPI support 6 | 7 | See parent library at https://github.com/mattpolzin/OpenAPIKit. 8 | 9 | To generate OpenAPI 3.1.x types, use the `OpenAPIReflection` module. To generate OpenAPI 3.0.x types, use the `OpenAPIReflection30` module. 10 | 11 | # OpenAPIReflection 12 | 13 | This library offers extended support for creating OpenAPI types from Swift types. Specifically, this library covers the subset of Swift types that require a `JSONEncoder` to either make an educated guess at the `JSONSchema` for the type or to turn arbitrary types into `AnyCodable` for use as schema examples or allowed values. 14 | 15 | ## Dates 16 | 17 | Dates will create different OpenAPI representations depending on the encoding settings of the `JSONEncoder` passed into the schema construction method. 18 | 19 | ```swift 20 | // encoder1 has `.iso8601` `dateEncodingStrategy` 21 | let schema = Date().dateOpenAPISchemaGuess(using: encoder1) 22 | // ^ equivalent to: 23 | let sameSchema = JSONSchema.string( 24 | format: .dateTime 25 | ) 26 | 27 | // encoder2 has `.secondsSince1970` `dateEncodingStrategy` 28 | let schema2 = Date().dateOpenAPISchemaGuess(using: encoder2) 29 | // ^ equivalent to: 30 | let sameSchema = JSONSchema.number( 31 | format: .double 32 | ) 33 | ``` 34 | 35 | It will even try to take a guess given a custom formatter date decoding 36 | strategy. 37 | 38 | ## Enums 39 | 40 | Swift enums produce schemas with **allowed values** specified as long as they conform to `CaseIterable`, `Encodable`, and `AnyJSONCaseIterable` (the last of which is free given the former two). 41 | ```swift 42 | enum CodableEnum: String, CaseIterable, AnyJSONCaseIterable, Codable { 43 | case one 44 | case two 45 | } 46 | 47 | let schema = CodableEnum.caseIterableOpenAPISchemaGuess(using: JSONEncoder()) 48 | // ^ equivalent, although not equatable, to: 49 | let sameSchema = JSONSchema.string( 50 | allowedValues: "one", "two" 51 | ) 52 | ``` 53 | 54 | ## Structs 55 | 56 | Swift structs produce a best-guess schema as long as they conform to `Sampleable` and `Encodable` 57 | ```swift 58 | struct Nested: Encodable, Sampleable { 59 | let string: String 60 | let array: [Int] 61 | 62 | // `Sampleable` just enables mirroring, although you could use it to produce 63 | // OpenAPI examples as well. 64 | static let sample: Self = .init( 65 | string: "", 66 | array: [] 67 | ) 68 | } 69 | 70 | let schema = Nested.genericOpenAPISchemaGuess(using: JSONEncoder()) 71 | // ^ equivalent and indeed equatable to: 72 | let sameSchema = JSONSchema.object( 73 | properties: [ 74 | "string": .string, 75 | "array": .array(items: .integer) 76 | ] 77 | ) 78 | ``` 79 | 80 | ## Custom OpenAPI type representations 81 | 82 | You can take the protocols offered by this library and OpenAPIKit and create arbitrarily complex OpenAPI types from your own Swift types. Right now, the only form of documentation on this subject is the fully realized example over in the [JSONAPI+OpenAPI](https://github.com/mattpolzin/JSONAPI-OpenAPI) library. Just look for conformances to `OpenAPISchemaType` and `OpenAPIEncodedSchemaType`. 83 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/AnyJSONCaseIterable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyJSONCaseIterable.swift 3 | // OpenAPI 4 | // 5 | // Created by Mathew Polzin on 6/22/19. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit 10 | 11 | public protocol AnyRawRepresentable { 12 | /// The `RawValue` type of this type. 13 | static var rawValueType: Any.Type { get } 14 | } 15 | 16 | extension AnyRawRepresentable where Self: RawRepresentable { 17 | /// The default `rawValueType` of a `RawRepresentable` is just the 18 | /// type of `Self.RawValue`. 19 | public static var rawValueType: Any.Type { return Self.RawValue.self } 20 | } 21 | 22 | /// Anything conforming to `AnyJSONCaseIterable` can provide a 23 | /// list of its possible values. 24 | public protocol AnyJSONCaseIterable: AnyRawRepresentable { 25 | static func allCases(using encoder: JSONEncoder) -> [AnyCodable] 26 | } 27 | 28 | extension AnyJSONCaseIterable where Self: RawRepresentable { 29 | /// The default `rawValueType` of a `RawRepresentable` is just the 30 | /// type of `Self.RawValue`. 31 | public static var rawValueType: Any.Type { return Self.RawValue.self } 32 | } 33 | 34 | public extension AnyJSONCaseIterable { 35 | /// Given an array of Codable values, retrieve an array of AnyCodables. 36 | static func allCases(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] { 37 | return try OpenAPIReflection.allCases(from: input, using: encoder) 38 | } 39 | } 40 | 41 | public extension AnyJSONCaseIterable where Self: CaseIterable, Self: Codable { 42 | static func caseIterableOpenAPISchemaGuess(using encoder: JSONEncoder) throws -> JSONSchema { 43 | guard let first = allCases.first else { 44 | throw OpenAPI.EncodableError.exampleNotCodable 45 | } 46 | let itemSchema = try OpenAPIReflection.nestedGenericOpenAPISchemaGuess(for: first, using: encoder) 47 | 48 | return itemSchema.with(allowedValues: allCases.map { AnyCodable($0) }) 49 | } 50 | } 51 | 52 | extension CaseIterable where Self: Encodable { 53 | public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 54 | return (try? OpenAPIReflection.allCases(from: Array(Self.allCases), using: encoder)) ?? [] 55 | } 56 | } 57 | 58 | fileprivate func allCases(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] { 59 | if let alreadyGoodToGo = input as? [AnyCodable] { 60 | return alreadyGoodToGo 61 | } 62 | 63 | // The following is messy, but it does get us the intended result: 64 | // Given any array of things that can be encoded, we want 65 | // to map to an array of AnyCodable so we can store later. We need to 66 | // muck with JSONSerialization because something like an `enum` may 67 | // very well be encoded as a string, and therefore representable 68 | // by AnyCodable, but AnyCodable wants it to actually BE a String 69 | // upon initialization. 70 | 71 | guard let arrayOfCodables = try JSONSerialization.jsonObject(with: encoder.encode(input), options: []) as? [Any] else { 72 | throw OpenAPI.EncodableError.allCasesArrayNotCodable 73 | } 74 | return arrayOfCodables.map(AnyCodable.init) 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/Date+OpenAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+OpenAPI.swift 3 | // OpenAPI 4 | // 5 | // Created by Mathew Polzin on 1/24/19. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit 10 | 11 | extension Date: DateOpenAPISchemaType { 12 | public static func dateOpenAPISchemaGuess(using encoder: JSONEncoder) -> JSONSchema? { 13 | 14 | switch encoder.dateEncodingStrategy { 15 | case .deferredToDate, .custom: 16 | // I don't know if we can say anything about this case without 17 | // encoding the Date and looking at it, which is what `primitiveGuess()` 18 | // does. 19 | return nil 20 | 21 | case .secondsSince1970, 22 | .millisecondsSince1970: 23 | return .number(format: .double) 24 | 25 | case .iso8601: 26 | return .string(format: .dateTime) 27 | 28 | #if !canImport(FoundationEssentials) || swift(<5.10) 29 | case .formatted(let formatter): 30 | let hasTime = formatter.timeStyle != .none 31 | let format: JSONTypeFormat.StringFormat = hasTime ? .dateTime : .date 32 | 33 | return .string(format: format) 34 | #endif 35 | 36 | @unknown default: 37 | return nil 38 | } 39 | } 40 | } 41 | 42 | extension Date: OpenAPIEncodedSchemaType { 43 | public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 44 | guard let dateSchema: JSONSchema = try openAPISchemaGuess(for: Date(), using: encoder) else { 45 | throw OpenAPI.TypeError.unknownSchemaType(type(of: self)) 46 | } 47 | 48 | return dateSchema 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/OpenAPI+Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenAPI+Errors.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 4/21/20. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit 10 | 11 | extension OpenAPI { 12 | public enum TypeError: Swift.Error, CustomDebugStringConvertible { 13 | case invalidSchema 14 | case unknownSchemaType(Any.Type) 15 | 16 | public var debugDescription: String { 17 | switch self { 18 | case .invalidSchema: 19 | return "Invalid Schema" 20 | case .unknownSchemaType(let type): 21 | return "Could not determine OpenAPI schema type of \(String(describing: type))" 22 | } 23 | } 24 | } 25 | 26 | public enum EncodableError: Swift.Error, Equatable { 27 | case allCasesArrayNotCodable 28 | case exampleNotCodable 29 | case primitiveGuessFailed 30 | case exampleNotSupported(String) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/Optional+ZipWith.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+ZipWith.swift 3 | // OpenAPIKit 4 | // 5 | // Created by Mathew Polzin on 1/19/19. 6 | // 7 | 8 | /// Zip two optionals together with the given operation performed on 9 | /// the unwrapped contents. If either optional is nil, the zip 10 | /// yields nil. 11 | func zip(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? { 12 | return left.flatMap { lft in right.map { rght in fn(lft, rght) }} 13 | } 14 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/Sampleable+OpenAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sampleable+OpenAPI.swift 3 | // OpenAPIReflection 4 | // 5 | // Created by Mathew Polzin on 1/24/19. 6 | // 7 | 8 | import Foundation 9 | import Sampleable 10 | import OpenAPIKit 11 | 12 | extension Sampleable where Self: Encodable { 13 | public static func genericOpenAPISchemaGuess(using encoder: JSONEncoder) throws -> JSONSchema { 14 | return try OpenAPIReflection.genericOpenAPISchemaGuess(for: Self.sample, using: encoder) 15 | } 16 | } 17 | 18 | public func genericOpenAPISchemaGuess(for value: T, using encoder: JSONEncoder) throws -> JSONSchema { 19 | // short circuit for dates 20 | if let date = value as? Date, 21 | let node = try type(of: date) 22 | .dateOpenAPISchemaGuess(using: encoder) 23 | ?? reencodedSchemaGuess(for: date, using: encoder) { 24 | 25 | return node 26 | } 27 | // short circuit for optionals 28 | if let optional = value as? _Optional { 29 | // we don't want to accidentally not take advantage of user-defined support 30 | // so we try for a schema guess right off the bat 31 | if let schemaGuess = try openAPISchemaGuess(for: type(of: optional), using: encoder) { 32 | return schemaGuess 33 | } 34 | 35 | // otherwise, we dig into optional by hand to avoid the below code considering .some and .none to be 36 | // "children" 37 | switch optional.value { 38 | case .some(let wrappedValue): 39 | return try genericOpenAPISchemaGuess(for: wrappedValue, using: encoder) 40 | .optionalSchemaObject() 41 | case .none: 42 | return .object(required: false) 43 | } 44 | } 45 | 46 | let mirror = Mirror(reflecting: value) 47 | let properties: [(String, JSONSchema)] = try mirror.children.compactMap { child in 48 | 49 | // see if we can enumerate the possible values 50 | let maybeAllCases: [AnyCodable]? = { 51 | switch type(of: child.value) { 52 | case let valType as AnyJSONCaseIterable.Type: 53 | return valType.allCases(using: encoder) 54 | default: 55 | return nil 56 | } 57 | }() 58 | 59 | // try to snag an OpenAPI Schema 60 | let openAPINode: JSONSchema = try openAPISchemaGuess(for: child.value, using: encoder) 61 | ?? nestedGenericOpenAPISchemaGuess(for: child.value, using: encoder) 62 | 63 | // put it all together 64 | let newNode: JSONSchema 65 | if let allCases = maybeAllCases { 66 | newNode = openAPINode.with(allowedValues: allCases) 67 | } else { 68 | newNode = openAPINode 69 | } 70 | 71 | return zip(child.label, newNode) { ($0, $1) } 72 | } 73 | 74 | if properties.count != mirror.children.count { 75 | throw OpenAPI.TypeError.unknownSchemaType(type(of: value)) 76 | } 77 | 78 | // There should not be any duplication of keys since these are 79 | // property names, but rather than risk runtime exception, we just 80 | // fail to the newer value arbitrarily 81 | let propertiesDict = OrderedDictionary(properties) { _, value2 in value2 } 82 | 83 | return .object(required: true, properties: propertiesDict) 84 | } 85 | 86 | /// Same as genericOpenAPISchemaGuess() except it checks if there's an easy 87 | /// way out via an explicit conformance to one of the OpenAPISchema protocols. 88 | internal func nestedGenericOpenAPISchemaGuess(for value: T, using encoder: JSONEncoder) throws -> JSONSchema { 89 | if let schema = try openAPISchemaGuess(for: value, using: encoder) { 90 | return schema 91 | } 92 | 93 | return try genericOpenAPISchemaGuess(for: value, using: encoder) 94 | } 95 | 96 | internal func reencodedSchemaGuess(for value: T, using encoder: JSONEncoder) throws -> JSONSchema? { 97 | let data = try encoder.encode(PrimitiveWrapper(primitive: value)) 98 | let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) 99 | 100 | guard let wrapperDict = wrappedValue as? [String: Any], 101 | wrapperDict.contains(where: { $0.key == "primitive" }) else { 102 | throw OpenAPI.EncodableError.primitiveGuessFailed 103 | } 104 | 105 | let value = (wrappedValue as! [String: Any])["primitive"]! 106 | 107 | return try openAPISchemaGuess(for: value, using: encoder) 108 | } 109 | 110 | internal func openAPISchemaGuess(for type: Any.Type, using encoder: JSONEncoder) throws -> JSONSchema? { 111 | let nodeGuess: JSONSchema? = try { 112 | switch type { 113 | case let valType as OpenAPISchemaType.Type: 114 | return valType.openAPISchema 115 | 116 | case let valType as RawOpenAPISchemaType.Type: 117 | return try valType.rawOpenAPISchema() 118 | 119 | case let valType as DateOpenAPISchemaType.Type: 120 | return valType.dateOpenAPISchemaGuess(using: encoder) 121 | 122 | case let valType as OpenAPIEncodedSchemaType.Type: 123 | return try valType.openAPISchema(using: encoder) 124 | 125 | case let valType as AnyRawRepresentable.Type: 126 | if valType.rawValueType != valType { 127 | let guess = try openAPISchemaGuess(for: valType.rawValueType, using: 128 | encoder) 129 | return valType is _Optional.Type 130 | ? guess?.optionalSchemaObject() 131 | : guess 132 | } else { 133 | return nil 134 | } 135 | default: 136 | return nil 137 | } 138 | }() 139 | 140 | return nodeGuess 141 | } 142 | 143 | internal func openAPISchemaGuess(for value: Any, using encoder: JSONEncoder) throws -> JSONSchema? { 144 | // ideally the type specifies how to get an OpenAPI node from itself. 145 | let nodeGuess: JSONSchema? = try openAPISchemaGuess(for: type(of: value), using: encoder) 146 | 147 | if nodeGuess != nil { 148 | return nodeGuess 149 | } 150 | 151 | // Second we can try for a few primitive types. 152 | // This is only necessary because when decoding 153 | // types like `NSTaggedPointerString` will be recognized 154 | // by the following switch statement even though 155 | // `NSTaggedPointerString` does not conform to 156 | // `OpenAPISchemaType` like `String` does. 157 | let primitiveGuess: JSONSchema? = try { 158 | switch value { 159 | case is String: 160 | return .string 161 | 162 | case is Int: 163 | return .integer 164 | 165 | case is Double: 166 | return .number( 167 | format: .double 168 | ) 169 | 170 | case is Bool: 171 | return .boolean 172 | 173 | case is Data: 174 | return .string( 175 | contentEncoding: .binary 176 | ) 177 | 178 | case is DateOpenAPISchemaType: 179 | // we don't know what Date will end up looking like without 180 | // trying it out. Most likely a `.string` or `.number(format: .double)` 181 | return try OpenAPIReflection.reencodedSchemaGuess(for: Date(), using: encoder) 182 | 183 | default: 184 | return nil 185 | } 186 | }() 187 | 188 | return primitiveGuess 189 | } 190 | 191 | // The following wrapper is only needed because JSONEncoder cannot yet encode 192 | // JSON fragments. It is a very unfortunate limitation that requires silly 193 | // workarounds in edge cases like this. 194 | private struct PrimitiveWrapper: Encodable { 195 | let primitive: Wrapped 196 | } 197 | 198 | private protocol _Optional { 199 | static var wrapped: Any.Type { get } 200 | var value: Any? { get } 201 | } 202 | extension Optional: _Optional { 203 | static var wrapped: Any.Type { 204 | Wrapped.self 205 | } 206 | 207 | var value: Any? { 208 | return self 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/SchemaProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaProtocols.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 3/4/20. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit 10 | import Sampleable 11 | 12 | /// Anything conforming to `OpenAPIEncodedSchemaType` can provide an 13 | /// OpenAPI schema representing itself but it may need an Encoder 14 | /// to do its job. 15 | public protocol OpenAPIEncodedSchemaType { 16 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema 17 | } 18 | 19 | extension OpenAPIEncodedSchemaType where Self: Sampleable, Self: Encodable { 20 | public static func openAPISchemaWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONSchema { 21 | let exampleData = try encoder.encode(Self.successSample ?? Self.sample) 22 | let example = try JSONDecoder().decode(AnyCodable.self, from: exampleData) 23 | return try openAPISchema(using: encoder).with(example: example) 24 | } 25 | } 26 | 27 | /// Anything conforming to `RawOpenAPISchemaType` can provide an 28 | /// OpenAPI schema representing itself. This second protocol is 29 | /// necessary so that one type can conditionally provide a 30 | /// schema and then (under different conditions) provide a 31 | /// different schema. The "different" conditions have to do 32 | /// with Raw Representability, hence the name of this protocol. 33 | public protocol RawOpenAPISchemaType { 34 | static func rawOpenAPISchema() throws -> JSONSchema 35 | } 36 | 37 | extension RawOpenAPISchemaType where Self: RawRepresentable, RawValue: OpenAPISchemaType { 38 | public static func rawOpenAPISchema() throws -> JSONSchema { 39 | return RawValue.openAPISchema 40 | } 41 | } 42 | 43 | /// Anything conforming to `DateOpenAPISchemaType` is 44 | /// able to attempt to represent itself as a date `JSONSchema` 45 | public protocol DateOpenAPISchemaType { 46 | static func dateOpenAPISchemaGuess(using encoder: JSONEncoder) -> JSONSchema? 47 | } 48 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection/SwiftPrimitiveExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftPrimitiveExtensions.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 3/4/20. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit 10 | 11 | extension Optional: AnyRawRepresentable where Wrapped: AnyRawRepresentable { 12 | public static var rawValueType: Any.Type { Wrapped.rawValueType } 13 | } 14 | 15 | extension Optional: AnyJSONCaseIterable where Wrapped: AnyJSONCaseIterable { 16 | public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 17 | return Wrapped.allCases(using: encoder) 18 | } 19 | } 20 | 21 | extension Optional: RawOpenAPISchemaType where Wrapped: RawOpenAPISchemaType { 22 | static public func rawOpenAPISchema() throws -> JSONSchema { 23 | return try Wrapped.rawOpenAPISchema().optionalSchemaObject() 24 | } 25 | } 26 | 27 | extension Optional: DateOpenAPISchemaType where Wrapped: DateOpenAPISchemaType { 28 | static public func dateOpenAPISchemaGuess(using encoder: JSONEncoder) -> JSONSchema? { 29 | return Wrapped.dateOpenAPISchemaGuess(using: encoder)?.optionalSchemaObject() 30 | } 31 | } 32 | 33 | extension Array: OpenAPIEncodedSchemaType where Element: OpenAPIEncodedSchemaType { 34 | public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 35 | return .array( 36 | .init( 37 | format: .generic, 38 | required: true 39 | ), 40 | .init( 41 | items: try Element.openAPISchema(using: encoder) 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | extension Dictionary: RawOpenAPISchemaType where Key: RawRepresentable, Key.RawValue == String, Value: OpenAPISchemaType { 48 | static public func rawOpenAPISchema() throws -> JSONSchema { 49 | return .object( 50 | .init( 51 | format: .generic, 52 | required: true 53 | ), 54 | .init( 55 | properties: [:], 56 | additionalProperties: .init(Value.openAPISchema) 57 | ) 58 | ) 59 | } 60 | } 61 | 62 | extension Dictionary: OpenAPIEncodedSchemaType where Key == String, Value: OpenAPIEncodedSchemaType { 63 | public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 64 | return .object( 65 | .init( 66 | format: .generic, 67 | required: true 68 | ), 69 | .init( 70 | properties: [:], 71 | additionalProperties: .init(try Value.openAPISchema(using: encoder)) 72 | ) 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/AnyJSONCaseIterable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyJSONCaseIterable.swift 3 | // OpenAPI 4 | // 5 | // Created by Mathew Polzin on 6/22/19. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit30 10 | 11 | public protocol AnyRawRepresentable { 12 | /// The `RawValue` type of this type. 13 | static var rawValueType: Any.Type { get } 14 | } 15 | 16 | extension AnyRawRepresentable where Self: RawRepresentable { 17 | /// The default `rawValueType` of a `RawRepresentable` is just the 18 | /// type of `Self.RawValue`. 19 | public static var rawValueType: Any.Type { return Self.RawValue.self } 20 | } 21 | 22 | /// Anything conforming to `AnyJSONCaseIterable` can provide a 23 | /// list of its possible values. 24 | public protocol AnyJSONCaseIterable: AnyRawRepresentable { 25 | static func allCases(using encoder: JSONEncoder) -> [AnyCodable] 26 | } 27 | 28 | extension AnyJSONCaseIterable where Self: RawRepresentable { 29 | /// The default `rawValueType` of a `RawRepresentable` is just the 30 | /// type of `Self.RawValue`. 31 | public static var rawValueType: Any.Type { return Self.RawValue.self } 32 | } 33 | 34 | public extension AnyJSONCaseIterable { 35 | /// Given an array of Codable values, retrieve an array of AnyCodables. 36 | static func allCases(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] { 37 | return try OpenAPIReflection30.allCases(from: input, using: encoder) 38 | } 39 | } 40 | 41 | public extension AnyJSONCaseIterable where Self: CaseIterable, Self: Codable { 42 | static func caseIterableOpenAPISchemaGuess(using encoder: JSONEncoder) throws -> JSONSchema { 43 | guard let first = allCases.first else { 44 | throw OpenAPI.EncodableError.exampleNotCodable 45 | } 46 | let itemSchema = try OpenAPIReflection30.nestedGenericOpenAPISchemaGuess(for: first, using: encoder) 47 | 48 | return itemSchema.with(allowedValues: allCases.map { AnyCodable($0) }) 49 | } 50 | } 51 | 52 | extension CaseIterable where Self: Encodable { 53 | public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 54 | return (try? OpenAPIReflection30.allCases(from: Array(Self.allCases), using: encoder)) ?? [] 55 | } 56 | } 57 | 58 | fileprivate func allCases(from input: [T], using encoder: JSONEncoder) throws -> [AnyCodable] { 59 | if let alreadyGoodToGo = input as? [AnyCodable] { 60 | return alreadyGoodToGo 61 | } 62 | 63 | // The following is messy, but it does get us the intended result: 64 | // Given any array of things that can be encoded, we want 65 | // to map to an array of AnyCodable so we can store later. We need to 66 | // muck with JSONSerialization because something like an `enum` may 67 | // very well be encoded as a string, and therefore representable 68 | // by AnyCodable, but AnyCodable wants it to actually BE a String 69 | // upon initialization. 70 | 71 | guard let arrayOfCodables = try JSONSerialization.jsonObject(with: encoder.encode(input), options: []) as? [Any] else { 72 | throw OpenAPI.EncodableError.allCasesArrayNotCodable 73 | } 74 | return arrayOfCodables.map(AnyCodable.init) 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/Date+OpenAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+OpenAPI.swift 3 | // OpenAPI 4 | // 5 | // Created by Mathew Polzin on 1/24/19. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit30 10 | 11 | extension Date: DateOpenAPISchemaType { 12 | public static func dateOpenAPISchemaGuess(using encoder: JSONEncoder) -> JSONSchema? { 13 | 14 | switch encoder.dateEncodingStrategy { 15 | case .deferredToDate, .custom: 16 | // I don't know if we can say anything about this case without 17 | // encoding the Date and looking at it, which is what `primitiveGuess()` 18 | // does. 19 | return nil 20 | 21 | case .secondsSince1970, 22 | .millisecondsSince1970: 23 | return .number(format: .double) 24 | 25 | case .iso8601: 26 | return .string(format: .dateTime) 27 | 28 | #if !canImport(FoundationEssentials) || swift(<5.10) 29 | case .formatted(let formatter): 30 | let hasTime = formatter.timeStyle != .none 31 | let format: JSONTypeFormat.StringFormat = hasTime ? .dateTime : .date 32 | 33 | return .string(format: format) 34 | #endif 35 | 36 | @unknown default: 37 | return nil 38 | } 39 | } 40 | } 41 | 42 | extension Date: OpenAPIEncodedSchemaType { 43 | public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 44 | guard let dateSchema: JSONSchema = try openAPISchemaGuess(for: Date(), using: encoder) else { 45 | throw OpenAPI.TypeError.unknownSchemaType(type(of: self)) 46 | } 47 | 48 | return dateSchema 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/OpenAPI+Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenAPI+Errors.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 4/21/20. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit30 10 | 11 | extension OpenAPI { 12 | public enum TypeError: Swift.Error, CustomDebugStringConvertible { 13 | case invalidSchema 14 | case unknownSchemaType(Any.Type) 15 | 16 | public var debugDescription: String { 17 | switch self { 18 | case .invalidSchema: 19 | return "Invalid Schema" 20 | case .unknownSchemaType(let type): 21 | return "Could not determine OpenAPI schema type of \(String(describing: type))" 22 | } 23 | } 24 | } 25 | 26 | public enum EncodableError: Swift.Error, Equatable { 27 | case allCasesArrayNotCodable 28 | case exampleNotCodable 29 | case primitiveGuessFailed 30 | case exampleNotSupported(String) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/Optional+ZipWith.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+ZipWith.swift 3 | // OpenAPIKit 4 | // 5 | // Created by Mathew Polzin on 1/19/19. 6 | // 7 | 8 | /// Zip two optionals together with the given operation performed on 9 | /// the unwrapped contents. If either optional is nil, the zip 10 | /// yields nil. 11 | func zip(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? { 12 | return left.flatMap { lft in right.map { rght in fn(lft, rght) }} 13 | } 14 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/Sampleable+OpenAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sampleable+OpenAPI.swift 3 | // OpenAPIReflection 4 | // 5 | // Created by Mathew Polzin on 1/24/19. 6 | // 7 | 8 | import Foundation 9 | import Sampleable 10 | import OpenAPIKit30 11 | 12 | extension Sampleable where Self: Encodable { 13 | public static func genericOpenAPISchemaGuess(using encoder: JSONEncoder) throws -> JSONSchema { 14 | return try OpenAPIReflection30.genericOpenAPISchemaGuess(for: Self.sample, using: encoder) 15 | } 16 | } 17 | 18 | public func genericOpenAPISchemaGuess(for value: T, using encoder: JSONEncoder) throws -> JSONSchema { 19 | // short circuit for dates 20 | if let date = value as? Date, 21 | let node = try type(of: date) 22 | .dateOpenAPISchemaGuess(using: encoder) 23 | ?? reencodedSchemaGuess(for: date, using: encoder) { 24 | 25 | return node 26 | } 27 | 28 | let mirror = Mirror(reflecting: value) 29 | let properties: [(String, JSONSchema)] = try mirror.children.compactMap { child in 30 | 31 | // see if we can enumerate the possible values 32 | let maybeAllCases: [AnyCodable]? = { 33 | switch type(of: child.value) { 34 | case let valType as AnyJSONCaseIterable.Type: 35 | return valType.allCases(using: encoder) 36 | default: 37 | return nil 38 | } 39 | }() 40 | 41 | // try to snag an OpenAPI Schema 42 | let openAPINode: JSONSchema = try openAPISchemaGuess(for: child.value, using: encoder) 43 | ?? nestedGenericOpenAPISchemaGuess(for: child.value, using: encoder) 44 | 45 | // put it all together 46 | let newNode: JSONSchema 47 | if let allCases = maybeAllCases { 48 | newNode = openAPINode.with(allowedValues: allCases) 49 | } else { 50 | newNode = openAPINode 51 | } 52 | 53 | return zip(child.label, newNode) { ($0, $1) } 54 | } 55 | 56 | if properties.count != mirror.children.count { 57 | throw OpenAPI.TypeError.unknownSchemaType(type(of: value)) 58 | } 59 | 60 | // There should not be any duplication of keys since these are 61 | // property names, but rather than risk runtime exception, we just 62 | // fail to the newer value arbitrarily 63 | let propertiesDict = OrderedDictionary(properties) { _, value2 in value2 } 64 | 65 | return .object(required: true, properties: propertiesDict) 66 | } 67 | 68 | /// Same as genericOpenAPISchemaGuess() except it checks if there's an easy 69 | /// way out via an explicit conformance to one of the OpenAPISchema protocols. 70 | internal func nestedGenericOpenAPISchemaGuess(for value: T, using encoder: JSONEncoder) throws -> JSONSchema { 71 | if let schema = try openAPISchemaGuess(for: value, using: encoder) { 72 | return schema 73 | } 74 | 75 | return try genericOpenAPISchemaGuess(for: value, using: encoder) 76 | } 77 | 78 | internal func reencodedSchemaGuess(for value: T, using encoder: JSONEncoder) throws -> JSONSchema? { 79 | let data = try encoder.encode(PrimitiveWrapper(primitive: value)) 80 | let wrappedValue = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) 81 | 82 | guard let wrapperDict = wrappedValue as? [String: Any], 83 | wrapperDict.contains(where: { $0.key == "primitive" }) else { 84 | throw OpenAPI.EncodableError.primitiveGuessFailed 85 | } 86 | 87 | let value = (wrappedValue as! [String: Any])["primitive"]! 88 | 89 | return try openAPISchemaGuess(for: value, using: encoder) 90 | } 91 | 92 | internal func openAPISchemaGuess(for type: Any.Type, using encoder: JSONEncoder) throws -> JSONSchema? { 93 | let nodeGuess: JSONSchema? = try { 94 | switch type { 95 | case let valType as OpenAPISchemaType.Type: 96 | return valType.openAPISchema 97 | 98 | case let valType as RawOpenAPISchemaType.Type: 99 | return try valType.rawOpenAPISchema() 100 | 101 | case let valType as DateOpenAPISchemaType.Type: 102 | return valType.dateOpenAPISchemaGuess(using: encoder) 103 | 104 | case let valType as OpenAPIEncodedSchemaType.Type: 105 | return try valType.openAPISchema(using: encoder) 106 | 107 | case let valType as AnyRawRepresentable.Type: 108 | if valType.rawValueType != valType { 109 | let guess = try openAPISchemaGuess(for: valType.rawValueType, using: 110 | encoder) 111 | return valType is _Optional.Type 112 | ? guess?.optionalSchemaObject() 113 | : guess 114 | } else { 115 | return nil 116 | } 117 | 118 | default: 119 | return nil 120 | } 121 | }() 122 | 123 | return nodeGuess 124 | } 125 | 126 | internal func openAPISchemaGuess(for value: Any, using encoder: JSONEncoder) throws -> JSONSchema? { 127 | // ideally the type specifies how to get an OpenAPI node from itself. 128 | let nodeGuess: JSONSchema? = try openAPISchemaGuess(for: type(of: value), using: encoder) 129 | 130 | if nodeGuess != nil { 131 | return nodeGuess 132 | } 133 | 134 | // Second we can try for a few primitive types. 135 | // This is only necessary because when decoding 136 | // types like `NSTaggedPointerString` will be recognized 137 | // by the following switch statement even though 138 | // `NSTaggedPointerString` does not conform to 139 | // `OpenAPISchemaType` like `String` does. 140 | let primitiveGuess: JSONSchema? = try { 141 | switch value { 142 | case is String: 143 | return .string 144 | 145 | case is Int: 146 | return .integer 147 | 148 | case is Double: 149 | return .number( 150 | format: .double 151 | ) 152 | 153 | case is Bool: 154 | return .boolean 155 | 156 | case is Data: 157 | return .string( 158 | format: .binary 159 | ) 160 | 161 | case is DateOpenAPISchemaType: 162 | // we don't know what Date will end up looking like without 163 | // trying it out. Most likely a `.string` or `.number(format: .double)` 164 | return try OpenAPIReflection30.reencodedSchemaGuess(for: Date(), using: encoder) 165 | 166 | default: 167 | return nil 168 | } 169 | }() 170 | 171 | return primitiveGuess 172 | } 173 | 174 | // The following wrapper is only needed because JSONEncoder cannot yet encode 175 | // JSON fragments. It is a very unfortunate limitation that requires silly 176 | // workarounds in edge cases like this. 177 | private struct PrimitiveWrapper: Encodable { 178 | let primitive: Wrapped 179 | } 180 | 181 | private protocol _Optional {} 182 | extension Optional: _Optional {} 183 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/SchemaProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaProtocols.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 3/4/20. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit30 10 | import Sampleable 11 | 12 | /// Anything conforming to `OpenAPIEncodedSchemaType` can provide an 13 | /// OpenAPI schema representing itself but it may need an Encoder 14 | /// to do its job. 15 | public protocol OpenAPIEncodedSchemaType { 16 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema 17 | } 18 | 19 | extension OpenAPIEncodedSchemaType where Self: Sampleable, Self: Encodable { 20 | public static func openAPISchemaWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONSchema { 21 | let exampleData = try encoder.encode(Self.successSample ?? Self.sample) 22 | let example = try JSONDecoder().decode(AnyCodable.self, from: exampleData) 23 | return try openAPISchema(using: encoder).with(example: example) 24 | } 25 | } 26 | 27 | /// Anything conforming to `RawOpenAPISchemaType` can provide an 28 | /// OpenAPI schema representing itself. This second protocol is 29 | /// necessary so that one type can conditionally provide a 30 | /// schema and then (under different conditions) provide a 31 | /// different schema. The "different" conditions have to do 32 | /// with Raw Representability, hence the name of this protocol. 33 | public protocol RawOpenAPISchemaType { 34 | static func rawOpenAPISchema() throws -> JSONSchema 35 | } 36 | 37 | extension RawOpenAPISchemaType where Self: RawRepresentable, RawValue: OpenAPISchemaType { 38 | public static func rawOpenAPISchema() throws -> JSONSchema { 39 | return RawValue.openAPISchema 40 | } 41 | } 42 | 43 | /// Anything conforming to `DateOpenAPISchemaType` is 44 | /// able to attempt to represent itself as a date `JSONSchema` 45 | public protocol DateOpenAPISchemaType { 46 | static func dateOpenAPISchemaGuess(using encoder: JSONEncoder) -> JSONSchema? 47 | } 48 | -------------------------------------------------------------------------------- /Sources/OpenAPIReflection30/SwiftPrimitiveExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftPrimitiveExtensions.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 3/4/20. 6 | // 7 | 8 | import Foundation 9 | import OpenAPIKit30 10 | 11 | extension Optional: AnyRawRepresentable where Wrapped: AnyRawRepresentable { 12 | public static var rawValueType: Any.Type { Wrapped.rawValueType } 13 | } 14 | 15 | extension Optional: AnyJSONCaseIterable where Wrapped: AnyJSONCaseIterable { 16 | public static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 17 | return Wrapped.allCases(using: encoder) 18 | } 19 | } 20 | 21 | extension Optional: RawOpenAPISchemaType where Wrapped: RawOpenAPISchemaType { 22 | static public func rawOpenAPISchema() throws -> JSONSchema { 23 | return try Wrapped.rawOpenAPISchema().optionalSchemaObject() 24 | } 25 | } 26 | 27 | extension Optional: DateOpenAPISchemaType where Wrapped: DateOpenAPISchemaType { 28 | static public func dateOpenAPISchemaGuess(using encoder: JSONEncoder) -> JSONSchema? { 29 | return Wrapped.dateOpenAPISchemaGuess(using: encoder)?.optionalSchemaObject() 30 | } 31 | } 32 | 33 | extension Array: OpenAPIEncodedSchemaType where Element: OpenAPIEncodedSchemaType { 34 | public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 35 | return .array( 36 | .init( 37 | format: .generic, 38 | required: true 39 | ), 40 | .init( 41 | items: try Element.openAPISchema(using: encoder) 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | extension Dictionary: RawOpenAPISchemaType where Key: RawRepresentable, Key.RawValue == String, Value: OpenAPISchemaType { 48 | static public func rawOpenAPISchema() throws -> JSONSchema { 49 | return .object( 50 | .init( 51 | format: .generic, 52 | required: true 53 | ), 54 | .init( 55 | properties: [:], 56 | additionalProperties: .init(Value.openAPISchema) 57 | ) 58 | ) 59 | } 60 | } 61 | 62 | extension Dictionary: OpenAPIEncodedSchemaType where Key == String, Value: OpenAPIEncodedSchemaType { 63 | public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 64 | return .object( 65 | .init( 66 | format: .generic, 67 | required: true 68 | ), 69 | .init( 70 | properties: [:], 71 | additionalProperties: .init(try Value.openAPISchema(using: encoder)) 72 | ) 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflection30Tests/AnyJSONCaseIterableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyJSONCaseIterableTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 8/4/19. 6 | // 7 | 8 | import XCTest 9 | import OpenAPIKit30 10 | import OpenAPIReflection30 11 | 12 | class AnyJSONCaseIterableTests: XCTestCase { 13 | func test_CodableToAllCases() { 14 | let testEncoder = JSONEncoder() 15 | 16 | let allCases = CodableEnum.allCases(using: testEncoder) 17 | 18 | XCTAssertEqual(allCases.count, 2) 19 | XCTAssertTrue(allCases.contains("one")) 20 | XCTAssertTrue(allCases.contains("two")) 21 | } 22 | 23 | func testAnyCodableToAllCases() { 24 | let testEncoder = JSONEncoder() 25 | 26 | let allCases = try! CodableEnum.allCases(from: CodableEnum.allCases(using: testEncoder), using: testEncoder) 27 | 28 | XCTAssertEqual(allCases.count, 2) 29 | XCTAssertTrue(allCases.contains("one")) 30 | XCTAssertTrue(allCases.contains("two")) 31 | } 32 | } 33 | 34 | enum CodableEnum: String, CaseIterable, AnyJSONCaseIterable, Codable { 35 | case one 36 | case two 37 | } 38 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflection30Tests/GenericOpenAPISchemaInternalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericOpenAPISchemaInternalTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 1/11/20. 6 | // 7 | 8 | import XCTest 9 | @testable import OpenAPIReflection30 10 | 11 | final class GenericOpenAPISchemaInternalTests: XCTestCase { 12 | func test_reencodedSchemaGuess() throws { 13 | XCTAssertEqual(try reencodedSchemaGuess(for: "hello", using: testEncoder), .string) 14 | XCTAssertEqual(try reencodedSchemaGuess(for: 10, using: testEncoder), .integer) 15 | XCTAssertEqual(try reencodedSchemaGuess(for: 11.5, using: testEncoder), .number(format: .double)) 16 | XCTAssertEqual(try reencodedSchemaGuess(for: true, using: testEncoder), .integer) 17 | XCTAssertEqual(try reencodedSchemaGuess(for: TestEnum.one, using: testEncoder), .string) 18 | } 19 | 20 | func test_openAPINodeGuessForType() { 21 | XCTAssertEqual(try openAPISchemaGuess(for: String.self, using: testEncoder), .string) 22 | XCTAssertEqual(try openAPISchemaGuess(for: Int.self, using: testEncoder), .integer) 23 | XCTAssertEqual(try openAPISchemaGuess(for: Float.self, using: testEncoder), .number(format: .float)) 24 | XCTAssertEqual(try openAPISchemaGuess(for: Double.self, using: testEncoder), .number(format: .double)) 25 | XCTAssertEqual(try openAPISchemaGuess(for: Bool.self, using: testEncoder), .boolean) 26 | XCTAssertEqual(try openAPISchemaGuess(for: TestEnum.self, using: testEncoder), .string) 27 | } 28 | 29 | func test_openAPINodeGuessForValue() { 30 | XCTAssertEqual(try openAPISchemaGuess(for: "hello", using: testEncoder), .string) 31 | XCTAssertEqual(try openAPISchemaGuess(for: 10, using: testEncoder), .integer) 32 | XCTAssertEqual(try openAPISchemaGuess(for: 11.5 as Float, using: testEncoder), .number(format: .float)) 33 | XCTAssertEqual(try openAPISchemaGuess(for: 11.5, using: testEncoder), .number(format: .double)) 34 | XCTAssertEqual(try openAPISchemaGuess(for: true, using: testEncoder), .boolean) 35 | XCTAssertEqual(try openAPISchemaGuess(for: TestEnum.one, using: testEncoder), .string) 36 | } 37 | } 38 | 39 | extension GenericOpenAPISchemaInternalTests { 40 | enum TestEnum: String, Codable, AnyRawRepresentable { 41 | case one 42 | case two 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflection30Tests/GenericOpenAPISchemaTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericOpenAPINodeTests.swift 3 | // OpenAPIKitTests 4 | // 5 | // Created by Mathew Polzin on 12/15/19. 6 | // 7 | 8 | import XCTest 9 | import OpenAPIKit30 10 | import OpenAPIReflection30 11 | import Sampleable 12 | 13 | final class GenericOpenAPISchemaTests: XCTestCase { 14 | 15 | func test_emptyObject() throws { 16 | let node = try EmptyObjectType.genericOpenAPISchemaGuess(using: JSONEncoder()) 17 | 18 | XCTAssertEqual( 19 | node, 20 | JSONSchema.object( 21 | properties: [ 22 | "empty": .object 23 | ] 24 | ) 25 | ) 26 | } 27 | 28 | func test_basicTypes() throws { 29 | let node = try BasicTypes.genericOpenAPISchemaGuess(using: JSONEncoder()) 30 | 31 | XCTAssertEqual( 32 | node, 33 | JSONSchema.object( 34 | properties: [ 35 | "string": .string, 36 | "int": .integer, 37 | "double": .number(format: .double), 38 | "float": .number(format: .float), 39 | "bool": .boolean 40 | ] 41 | ) 42 | ) 43 | } 44 | 45 | func test_dateType() throws { 46 | let node = try DateType.genericOpenAPISchemaGuess(using: JSONEncoder()) 47 | 48 | XCTAssertEqual( 49 | node, 50 | JSONSchema.object( 51 | properties: [ 52 | "date": .number(format: .double) 53 | ] 54 | ) 55 | ) 56 | } 57 | 58 | func test_dateTypeFormats() throws { 59 | let e1 = JSONEncoder() 60 | #if os(Linux) 61 | e1.dateEncodingStrategy = .iso8601 62 | #else 63 | if #available(macOS 10.12, *) { 64 | e1.dateEncodingStrategy = .iso8601 65 | } 66 | #endif 67 | 68 | let node1 = try DateType.genericOpenAPISchemaGuess(using: e1) 69 | 70 | XCTAssertEqual( 71 | node1, 72 | JSONSchema.object( 73 | properties: [ 74 | "date": .string(format: .dateTime) 75 | ] 76 | ) 77 | ) 78 | 79 | let e2 = JSONEncoder() 80 | e2.dateEncodingStrategy = .secondsSince1970 81 | let e3 = JSONEncoder() 82 | e3.dateEncodingStrategy = .millisecondsSince1970 83 | 84 | let node2 = try DateType.genericOpenAPISchemaGuess(using: e2) 85 | let node3 = try DateType.genericOpenAPISchemaGuess(using: e3) 86 | 87 | XCTAssertEqual(node2, node3) 88 | XCTAssertEqual( 89 | node2, 90 | JSONSchema.object( 91 | properties: [ 92 | "date": .number(format: .double) 93 | ] 94 | ) 95 | ) 96 | 97 | let e4 = JSONEncoder() 98 | let df1 = DateFormatter() 99 | df1.timeStyle = .none 100 | e4.dateEncodingStrategy = .formatted(df1) 101 | #if canImport(FoundationEssentials) && swift(>=5.10) 102 | throw XCTSkip("Not supported for Swift 5.10+ on Linux.") 103 | #else 104 | let node4 = try DateType.genericOpenAPISchemaGuess(using: e4) 105 | 106 | XCTAssertEqual( 107 | node4, 108 | JSONSchema.object( 109 | properties: [ 110 | "date": .string(format: .date) 111 | ] 112 | ) 113 | ) 114 | 115 | let e5 = JSONEncoder() 116 | let df2 = DateFormatter() 117 | df2.timeStyle = .full 118 | e5.dateEncodingStrategy = .formatted(df2) 119 | 120 | let node5 = try DateType.genericOpenAPISchemaGuess(using: e5) 121 | 122 | XCTAssertEqual( 123 | node5, 124 | JSONSchema.object( 125 | properties: [ 126 | "date": .string(format: .dateTime) 127 | ] 128 | ) 129 | ) 130 | #endif 131 | } 132 | 133 | func test_nested() throws { 134 | let node = try Nested.genericOpenAPISchemaGuess(using: JSONEncoder()) 135 | 136 | XCTAssertEqual( 137 | node, 138 | JSONSchema.object( 139 | properties: [ 140 | "array1": .array(items: .string), 141 | "array2": .array(items: .number(format: .double)), 142 | "array3": .array(items: .number(format: .double)), 143 | "dict1": .object( 144 | additionalProperties: .init(.string) 145 | ), 146 | "dict2": .object( 147 | additionalProperties: .init(.boolean) 148 | ), 149 | "dictArray": .object( 150 | additionalProperties: .init(.array(items: .integer)) 151 | ), 152 | "arrayDict": .array( 153 | items: .object( 154 | additionalProperties: .init(.number(format: .double)) 155 | ) 156 | ), 157 | "structure": .object( 158 | properties: [ 159 | "bool": .boolean, 160 | "array": .array(items: .string), 161 | "dict": .object( 162 | additionalProperties: .init(.integer) 163 | ) 164 | ] 165 | ) 166 | ] 167 | ) 168 | ) 169 | } 170 | 171 | func test_enumTypes() throws { 172 | let node = try EnumTypes.genericOpenAPISchemaGuess(using: JSONEncoder()) 173 | let schema = node.value 174 | 175 | XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) 176 | 177 | guard case .object(_, let ctx) = schema else { 178 | XCTFail("Expected object") 179 | return 180 | } 181 | 182 | XCTAssertEqual(ctx.properties["stringEnum"], .string) 183 | XCTAssertEqual(ctx.properties["intEnum"], .integer) 184 | XCTAssertEqual(ctx.properties["doubleEnum"], .number(format: .double)) 185 | XCTAssertEqual(ctx.properties["boolEnum"], .boolean) 186 | XCTAssertEqual(ctx.properties["optionalStringEnum"], .string(required: false)) 187 | XCTAssertEqual(ctx.properties["optionalIntEnum"], .integer(required: false)) 188 | XCTAssertEqual(ctx.properties["optionalDoubleEnum"], .number(format: .double, required: false)) 189 | XCTAssertEqual(ctx.properties["optionalBoolEnum"], .boolean(required: false)) 190 | } 191 | 192 | func test_allowedValues() throws { 193 | let node = try AllowedValues.genericOpenAPISchemaGuess(using: JSONEncoder()) 194 | let schema = node.value 195 | 196 | guard case let .object(_, objCtx) = schema else { 197 | XCTFail("Expected object") 198 | return 199 | } 200 | 201 | guard case let .string(ctx2, _) = objCtx.properties["stringEnum"]?.value else { 202 | XCTFail("Expected stringEnum property to be a .string") 203 | return 204 | } 205 | 206 | XCTAssert(ctx2.allowedValues?.count == 2) 207 | XCTAssert(ctx2.allowedValues?.contains("hello") ?? false) 208 | XCTAssert(ctx2.allowedValues?.contains("world") ?? false) 209 | 210 | guard case let .string(ctx3, _) = objCtx.properties["optionalStringEnum"]?.value else { 211 | XCTFail("Expected optionalStringEnum property to be a .string") 212 | return 213 | } 214 | 215 | XCTAssert(ctx3.allowedValues?.count == 2) 216 | XCTAssert(ctx3.allowedValues?.contains("hello") ?? false) 217 | XCTAssert(ctx3.allowedValues?.contains("world") ?? false) 218 | XCTAssertFalse(ctx3.required) 219 | 220 | guard case let .string(ctx4, _) = objCtx.properties["stringStruct"]?.value else { 221 | XCTFail("Expected stringStruct property to be a .string") 222 | return 223 | } 224 | 225 | XCTAssert(ctx4.allowedValues?.count == 2) 226 | XCTAssert(ctx4.allowedValues?.contains("hi") ?? false) 227 | XCTAssert(ctx4.allowedValues?.contains("there") ?? false) 228 | 229 | guard case let .string(ctx5, _) = objCtx.properties["optionalStringStruct"]?.value else { 230 | XCTFail("Expected optionalStringStruct property to be a .string") 231 | return 232 | } 233 | 234 | XCTAssert(ctx5.allowedValues?.count == 2) 235 | XCTAssert(ctx5.allowedValues?.contains("hi") ?? false) 236 | XCTAssert(ctx5.allowedValues?.contains("there") ?? false) 237 | XCTAssertFalse(ctx5.required) 238 | } 239 | 240 | func test_enumDirectly() throws { 241 | let schemaGuess = try AllowedValues.StringEnum.caseIterableOpenAPISchemaGuess(using: JSONEncoder()) 242 | guard case let .string(ctx, _) = schemaGuess.value else { 243 | XCTFail("Expected string.") 244 | return 245 | } 246 | 247 | XCTAssertEqual(ctx.allowedValues?.first?.value as? AllowedValues.StringEnum, .hello) 248 | XCTAssertEqual(ctx.allowedValues?.last?.value as? AllowedValues.StringEnum, .world) 249 | 250 | XCTAssertThrowsError(try CaselessEnum.caseIterableOpenAPISchemaGuess(using: JSONEncoder())) { err in 251 | XCTAssertEqual(err as? OpenAPI.EncodableError, .exampleNotCodable) 252 | } 253 | } 254 | 255 | func test_sampleableInSampleable() throws { 256 | XCTAssertEqual( 257 | try SampleableInSampleable.genericOpenAPISchemaGuess(using: JSONEncoder()), 258 | .object( 259 | properties: [ 260 | "sampleable": .string 261 | ] 262 | ) 263 | ) 264 | } 265 | 266 | func test_customCallsGeneric() throws { 267 | let schema = try CustomImplementationCallsGeneric.openAPISchema(using: JSONEncoder()) 268 | 269 | XCTAssertEqual( 270 | schema, 271 | .object( 272 | properties: [ 273 | "stringValue": .string 274 | ] 275 | ) 276 | ) 277 | } 278 | } 279 | 280 | // MARK: - Test Types 281 | 282 | extension GenericOpenAPISchemaTests { 283 | struct BasicTypes: Codable, Sampleable { 284 | let string: String 285 | let int: Int 286 | let double: Double 287 | let float: Float 288 | let bool: Bool 289 | 290 | static let sample: BasicTypes = .init(string: "hello", int: 10, double: 2.3, float: 1.1, bool: true) 291 | } 292 | 293 | struct DateType: Codable, Sampleable { 294 | let date: Date 295 | 296 | static let sample: DateType = .init(date: Date()) 297 | } 298 | 299 | struct Nested: Codable, Sampleable { 300 | let array1: [String] 301 | let array2: [Double] 302 | let array3: [Date] 303 | 304 | let dict1: [String: String] 305 | let dict2: [String: Bool] 306 | 307 | let dictArray: [String: [Int]] 308 | 309 | let arrayDict: [[String: Date]] 310 | 311 | let structure: Structure 312 | 313 | struct Structure: Codable { 314 | let bool: Bool 315 | let array: [String] 316 | let dict: [String: Int] 317 | } 318 | 319 | static let sample: Nested = .init( 320 | array1: [], 321 | array2: [], 322 | array3: [], 323 | dict1: [:], 324 | dict2: [:], 325 | dictArray: [:], 326 | arrayDict: [], 327 | structure: .init(bool: true, array: [], dict: [:]) 328 | ) 329 | } 330 | 331 | struct EnumTypes: Codable, Sampleable { 332 | let stringEnum: StringEnum 333 | let intEnum: IntEnum 334 | let doubleEnum: DoubleEnum 335 | let boolEnum: BoolEnum 336 | 337 | let optionalStringEnum: StringEnum? 338 | let optionalIntEnum: IntEnum? 339 | let optionalDoubleEnum: DoubleEnum? 340 | let optionalBoolEnum: BoolEnum? 341 | 342 | enum StringEnum: String, Codable, AnyRawRepresentable { 343 | case hello 344 | case world 345 | } 346 | 347 | enum IntEnum: Int, Codable, AnyRawRepresentable { 348 | case zero 349 | case one 350 | } 351 | 352 | enum DoubleEnum: Double, Codable, AnyRawRepresentable { 353 | case twoPointFive = 2.5 354 | case onePointTwo = 1.2 355 | } 356 | 357 | enum BoolEnum: RawRepresentable, Codable, AnyRawRepresentable { 358 | case `true` 359 | case `false` 360 | 361 | init?(rawValue: Bool) { 362 | self = rawValue ? .true : .false 363 | } 364 | 365 | var rawValue: Bool { 366 | switch self { 367 | case .true: return true 368 | case .false: return false 369 | } 370 | } 371 | } 372 | 373 | static let sample: EnumTypes = .init( 374 | stringEnum: .hello, 375 | intEnum: .one, 376 | doubleEnum: .onePointTwo, 377 | boolEnum: .true, 378 | optionalStringEnum: nil, 379 | optionalIntEnum: nil, 380 | optionalDoubleEnum: nil, 381 | optionalBoolEnum: nil 382 | ) 383 | } 384 | 385 | struct AllowedValues: Codable, Sampleable { 386 | let stringEnum: StringEnum 387 | let optionalStringEnum: StringEnum? 388 | 389 | let stringStruct: StringStruct 390 | let optionalStringStruct: StringStruct? 391 | 392 | enum StringEnum: String, Codable, CaseIterable, AnyJSONCaseIterable { 393 | case hello 394 | case world 395 | } 396 | 397 | struct StringStruct: RawRepresentable, Codable, AnyJSONCaseIterable { 398 | 399 | let val: String 400 | 401 | var rawValue: String { val } 402 | 403 | static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 404 | return ["hi", "there"] 405 | } 406 | 407 | init(val: String) { 408 | self.val = val 409 | } 410 | 411 | init?(rawValue: String) { 412 | self.val = rawValue 413 | } 414 | } 415 | 416 | static let sample: AllowedValues = .init( 417 | stringEnum: .hello, 418 | optionalStringEnum: nil, 419 | stringStruct: .init(val: "hi"), 420 | optionalStringStruct: nil 421 | ) 422 | } 423 | 424 | struct EmptyObjectType: Codable, Sampleable { 425 | let empty: EmptyObject 426 | 427 | struct EmptyObject: Codable {} 428 | 429 | static let sample: EmptyObjectType = .init(empty: .init()) 430 | } 431 | 432 | enum CaselessEnum: RawRepresentable, Codable, CaseIterable, AnyJSONCaseIterable { 433 | init?(rawValue: String) { 434 | return nil 435 | } 436 | var rawValue: String { "" } 437 | 438 | typealias RawValue = String 439 | 440 | static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 441 | [] 442 | } 443 | } 444 | 445 | struct SampleableInSampleable: Codable, Sampleable { 446 | let sampleable: NestedSampleable 447 | 448 | static let sample: Self = .init(sampleable: .sample) 449 | 450 | enum NestedSampleable: String, Codable, CaseIterable, Sampleable, AnyRawRepresentable { 451 | case one 452 | case two 453 | 454 | static let sample: Self = .one 455 | } 456 | } 457 | 458 | struct CustomImplementationCallsGeneric: Codable, Sampleable, OpenAPIEncodedSchemaType { 459 | 460 | let stringValue: String 461 | 462 | static let sample: Self = .init(stringValue: "hello") 463 | 464 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 465 | try OpenAPIReflection30.genericOpenAPISchemaGuess(for: sample, using: encoder) 466 | } 467 | } 468 | 469 | // struct EncodesAsPrimitive: Codable, SampleableOpenAPIType { 470 | // let asString: AsString 471 | // let asInt: AsInt 472 | // let asDouble: AsDouble 473 | // let asBool: AsBool 474 | // 475 | // struct AsString: Codable { 476 | // func encode(to encoder: Encoder) throws { 477 | // var container = encoder.singleValueContainer() 478 | // 479 | // try container.encode("hello world") 480 | // } 481 | // } 482 | // 483 | // struct AsInt: Codable { 484 | // func encode(to encoder: Encoder) throws { 485 | // var container = encoder.singleValueContainer() 486 | // 487 | // try container.encode(10) 488 | // } 489 | // } 490 | // 491 | // struct AsDouble: Codable { 492 | // func encode(to encoder: Encoder) throws { 493 | // var container = encoder.singleValueContainer() 494 | // 495 | // try container.encode(5.5) 496 | // } 497 | // } 498 | // 499 | // struct AsBool: Codable { 500 | // func encode(to encoder: Encoder) throws { 501 | // var container = encoder.singleValueContainer() 502 | // 503 | // try container.encode(true) 504 | // } 505 | // } 506 | // 507 | // static let sample: EncodesAsPrimitive = .init( 508 | // asString: .init(), 509 | // asInt: .init(), 510 | // asDouble: .init(), 511 | // asBool: .init() 512 | // ) 513 | // } 514 | } 515 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflection30Tests/SchemaWithExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaWithExampleTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 9/10/20. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import OpenAPIKit30 11 | import OpenAPIReflection30 12 | import Sampleable 13 | 14 | final class SchemaWithExampleTests: XCTestCase { 15 | func test_structWithExample() throws { 16 | 17 | let schemaGuess = try Test.openAPISchemaWithExample(using: testEncoder) 18 | let expectedSchema = try JSONSchema.object( 19 | properties: [ 20 | "string": .string, 21 | "int": .integer, 22 | "bool": .boolean, 23 | "double": .number(format: .double) 24 | ] 25 | ).with(example: 26 | [ 27 | "bool" : true, 28 | "double" : 2.34, 29 | "int" : 10, 30 | "string" : "hello" 31 | ] 32 | ) 33 | 34 | XCTAssertNotNil(schemaGuess.jsonType) 35 | 36 | XCTAssertEqual( 37 | schemaGuess.jsonType, 38 | expectedSchema.jsonType 39 | ) 40 | 41 | XCTAssertNotNil(schemaGuess.objectContext) 42 | 43 | XCTAssertEqual( 44 | schemaGuess.objectContext, 45 | expectedSchema.objectContext 46 | ) 47 | 48 | XCTAssertNotNil(schemaGuess.example) 49 | 50 | // equality checks on AnyCodable are finicky but 51 | // they compare equally when encoded to data. 52 | XCTAssertEqual( 53 | try testEncoder.encode(schemaGuess.example), 54 | try testEncoder.encode(expectedSchema.example) 55 | ) 56 | } 57 | } 58 | 59 | extension SchemaWithExampleTests { 60 | struct Test: Codable, Sampleable, OpenAPIEncodedSchemaType { 61 | let string: String 62 | let int: Int 63 | let bool: Bool 64 | let double: Double 65 | 66 | public static let sample: SchemaWithExampleTests.Test = .init( 67 | string: "hello", 68 | int: 10, 69 | bool: true, 70 | double: 2.34 71 | ) 72 | 73 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 74 | return .object( 75 | properties: [ 76 | "string": .string, 77 | "int": .integer, 78 | "bool": .boolean, 79 | "double": .number(format: .double) 80 | ] 81 | ) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflection30Tests/SwiftPrimitiveExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftPrimitiveExtensionsTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 3/4/20. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import OpenAPIKit30 11 | import OpenAPIReflection30 12 | 13 | class SwiftPrimitiveTypesTests: XCTestCase { 14 | func test_OptionalCaseIterableNodeAllCases() { 15 | XCTAssertTrue(RawRepStringEnum?.allCases(using: SwiftPrimitiveTypesTests.localTestEncoder).contains("hello")) 16 | XCTAssertTrue(RawRepStringEnum?.allCases(using: SwiftPrimitiveTypesTests.localTestEncoder).contains("world")) 17 | XCTAssertEqual(RawRepStringEnum?.allCases(using: SwiftPrimitiveTypesTests.localTestEncoder).count, 2) 18 | } 19 | 20 | func test_OptionalDateNodeType() { 21 | XCTAssertEqual(Date?.dateOpenAPISchemaGuess(using: testEncoder), .string(format: .dateTime, required: false)) 22 | } 23 | 24 | func test_RawNodeType() { 25 | XCTAssertEqual(try! RawRepStringEnum.rawOpenAPISchema(), .string) 26 | XCTAssertEqual(try! RawRepIntEnum.rawOpenAPISchema(), .integer) 27 | } 28 | 29 | func test_OptionalRawRepresentable() { 30 | XCTAssertEqual(try! RawRepStringEnum?.rawOpenAPISchema(), .string(required: false)) 31 | 32 | XCTAssertEqual(try! RawRepIntEnum?.rawOpenAPISchema(), .integer(required: false)) 33 | } 34 | 35 | func test_OptionalRawNodeType() { 36 | XCTAssertEqual(try! RawRepStringEnum?.rawOpenAPISchema(), .string(required: false)) 37 | 38 | XCTAssertEqual(try! RawRepIntEnum?.rawOpenAPISchema(), .integer(required: false)) 39 | } 40 | 41 | func test_DoubleWrappedRawNodeType() { 42 | XCTAssertEqual(try! RawRepStringEnum??.rawOpenAPISchema(), .string(required: false)) 43 | 44 | XCTAssertEqual(try! RawRepIntEnum??.rawOpenAPISchema(), .integer(required: false)) 45 | 46 | XCTAssertEqual(try! RawRepStringEnum??.rawOpenAPISchema(), .string(required: false)) 47 | 48 | XCTAssertEqual(try! RawRepIntEnum??.rawOpenAPISchema(), .integer(required: false)) 49 | } 50 | 51 | static let localTestEncoder = JSONEncoder() 52 | } 53 | 54 | fileprivate enum RawRepStringEnum: String, RawOpenAPISchemaType, CaseIterable, Codable, AnyJSONCaseIterable { 55 | case hello 56 | case world 57 | } 58 | 59 | fileprivate enum RawRepIntEnum: Int, RawOpenAPISchemaType { 60 | case one 61 | case two 62 | } 63 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflection30Tests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 6/23/19. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | let testEncoder = { () -> JSONEncoder in 12 | let encoder = JSONEncoder() 13 | if #available(macOS 10.13, *) { 14 | encoder.dateEncodingStrategy = .iso8601 15 | encoder.keyEncodingStrategy = .useDefaultKeys 16 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 17 | } 18 | #if os(Linux) 19 | encoder.dateEncodingStrategy = .iso8601 20 | encoder.keyEncodingStrategy = .useDefaultKeys 21 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 22 | #endif 23 | return encoder 24 | }() 25 | 26 | func testStringFromEncoding(of entity: T) throws -> String? { 27 | return String(data: try testEncoder.encode(entity), encoding: .utf8) 28 | } 29 | 30 | let testDecoder = { () -> JSONDecoder in 31 | let decoder = JSONDecoder() 32 | if #available(macOS 10.12, *) { 33 | decoder.dateDecodingStrategy = .iso8601 34 | decoder.keyDecodingStrategy = .useDefaultKeys 35 | } 36 | #if os(Linux) 37 | decoder.dateDecodingStrategy = .iso8601 38 | decoder.keyDecodingStrategy = .useDefaultKeys 39 | #endif 40 | return decoder 41 | }() 42 | 43 | func assertJSONEquivalent(_ str1: String?, _ str2: String?, file: StaticString = #file, line: UInt = #line) { 44 | 45 | // when testing on Linux, pretty printing has slightly different 46 | // meaning so the tests pass on OS X as written but need whitespace 47 | // stripped to pass on Linux 48 | #if os(Linux) 49 | var str1 = str1 50 | var str2 = str2 51 | 52 | str1?.removeAll { $0.isWhitespace } 53 | str2?.removeAll { $0.isWhitespace } 54 | #endif 55 | 56 | XCTAssertEqual( 57 | str1, 58 | str2, 59 | file: (file), 60 | line: line 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflectionTests/AnyJSONCaseIterableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyJSONCaseIterableTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 8/4/19. 6 | // 7 | 8 | import XCTest 9 | import OpenAPIKit 10 | import OpenAPIReflection 11 | 12 | class AnyJSONCaseIterableTests: XCTestCase { 13 | func test_CodableToAllCases() { 14 | let testEncoder = JSONEncoder() 15 | 16 | let allCases = CodableEnum.allCases(using: testEncoder) 17 | 18 | XCTAssertEqual(allCases.count, 2) 19 | XCTAssertTrue(allCases.contains("one")) 20 | XCTAssertTrue(allCases.contains("two")) 21 | } 22 | 23 | func testAnyCodableToAllCases() { 24 | let testEncoder = JSONEncoder() 25 | 26 | let allCases = try! CodableEnum.allCases(from: CodableEnum.allCases(using: testEncoder), using: testEncoder) 27 | 28 | XCTAssertEqual(allCases.count, 2) 29 | XCTAssertTrue(allCases.contains("one")) 30 | XCTAssertTrue(allCases.contains("two")) 31 | } 32 | } 33 | 34 | enum CodableEnum: String, CaseIterable, AnyJSONCaseIterable, Codable { 35 | case one 36 | case two 37 | } 38 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflectionTests/GenericOpenAPISchemaInternalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericOpenAPISchemaInternalTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 1/11/20. 6 | // 7 | 8 | import XCTest 9 | @testable import OpenAPIReflection 10 | 11 | final class GenericOpenAPISchemaInternalTests: XCTestCase { 12 | func test_reencodedSchemaGuess() throws { 13 | XCTAssertEqual(try reencodedSchemaGuess(for: "hello", using: testEncoder), .string) 14 | XCTAssertEqual(try reencodedSchemaGuess(for: 10, using: testEncoder), .integer) 15 | XCTAssertEqual(try reencodedSchemaGuess(for: 11.5, using: testEncoder), .number(format: .double)) 16 | XCTAssertEqual(try reencodedSchemaGuess(for: true, using: testEncoder), .integer) 17 | XCTAssertEqual(try reencodedSchemaGuess(for: TestEnum.one, using: testEncoder), .string) 18 | } 19 | 20 | func test_openAPINodeGuessForType() { 21 | XCTAssertEqual(try openAPISchemaGuess(for: String.self, using: testEncoder), .string) 22 | XCTAssertEqual(try openAPISchemaGuess(for: Int.self, using: testEncoder), .integer) 23 | XCTAssertEqual(try openAPISchemaGuess(for: Float.self, using: testEncoder), .number(format: .float)) 24 | XCTAssertEqual(try openAPISchemaGuess(for: Double.self, using: testEncoder), .number(format: .double)) 25 | XCTAssertEqual(try openAPISchemaGuess(for: Bool.self, using: testEncoder), .boolean) 26 | XCTAssertEqual(try openAPISchemaGuess(for: TestEnum.self, using: testEncoder), .string) 27 | XCTAssertEqual(try openAPISchemaGuess(for: Optional.self, using: testEncoder), .string(required: false)) 28 | } 29 | 30 | func test_openAPINodeGuessForValue() { 31 | XCTAssertEqual(try openAPISchemaGuess(for: "hello", using: testEncoder), .string) 32 | XCTAssertEqual(try openAPISchemaGuess(for: 10, using: testEncoder), .integer) 33 | XCTAssertEqual(try openAPISchemaGuess(for: 11.5 as Float, using: testEncoder), .number(format: .float)) 34 | XCTAssertEqual(try openAPISchemaGuess(for: 11.5, using: testEncoder), .number(format: .double)) 35 | XCTAssertEqual(try openAPISchemaGuess(for: true, using: testEncoder), .boolean) 36 | XCTAssertEqual(try openAPISchemaGuess(for: TestEnum.one, using: testEncoder), .string) 37 | XCTAssertEqual(try openAPISchemaGuess(for: Optional("hello") as Any, using: testEncoder), .string(required: false)) 38 | } 39 | } 40 | 41 | extension GenericOpenAPISchemaInternalTests { 42 | enum TestEnum: String, Codable, AnyRawRepresentable { 43 | case one 44 | case two 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflectionTests/GenericOpenAPISchemaTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericOpenAPINodeTests.swift 3 | // OpenAPIKitTests 4 | // 5 | // Created by Mathew Polzin on 12/15/19. 6 | // 7 | 8 | import XCTest 9 | import OpenAPIKit 10 | import OpenAPIReflection 11 | import Sampleable 12 | 13 | final class GenericOpenAPISchemaTests: XCTestCase { 14 | 15 | func test_emptyObject() throws { 16 | let node = try EmptyObjectType.genericOpenAPISchemaGuess(using: JSONEncoder()) 17 | 18 | XCTAssertEqual( 19 | node, 20 | JSONSchema.object( 21 | properties: [ 22 | "empty": .object 23 | ] 24 | ) 25 | ) 26 | } 27 | 28 | func test_basicTypes() throws { 29 | let node = try BasicTypes.genericOpenAPISchemaGuess(using: JSONEncoder()) 30 | 31 | XCTAssertEqual( 32 | node, 33 | JSONSchema.object( 34 | properties: [ 35 | "string": .string, 36 | "int": .integer, 37 | "double": .number(format: .double), 38 | "float": .number(format: .float), 39 | "bool": .boolean, 40 | "optionalString": .string(required: false) 41 | ] 42 | ) 43 | ) 44 | } 45 | 46 | func test_opaqueStruct() throws { 47 | // "opaque" in that the `OnlyCodable` type does not itself define an OpenAPI schema and so we must 48 | // encode it to have any chance at guessing its structure. 49 | let node = try OpaqueStructs.genericOpenAPISchemaGuess(using: JSONEncoder()) 50 | 51 | XCTAssertEqual( 52 | node, 53 | JSONSchema.object( 54 | properties: [ 55 | "opaque": .object(properties: ["value": .string]), 56 | "optionalOpaque": .object(required: false, properties: ["value": .string]) 57 | ] 58 | ) 59 | ) 60 | } 61 | 62 | func test_dateType() throws { 63 | let node = try DateType.genericOpenAPISchemaGuess(using: JSONEncoder()) 64 | 65 | XCTAssertEqual( 66 | node, 67 | JSONSchema.object( 68 | properties: [ 69 | "date": .number(format: .double) 70 | ] 71 | ) 72 | ) 73 | } 74 | 75 | func test_dateTypeFormats() throws { 76 | let e1 = JSONEncoder() 77 | #if os(Linux) 78 | e1.dateEncodingStrategy = .iso8601 79 | #else 80 | if #available(macOS 10.12, *) { 81 | e1.dateEncodingStrategy = .iso8601 82 | } 83 | #endif 84 | 85 | let node1 = try DateType.genericOpenAPISchemaGuess(using: e1) 86 | 87 | XCTAssertEqual( 88 | node1, 89 | JSONSchema.object( 90 | properties: [ 91 | "date": .string(format: .dateTime) 92 | ] 93 | ) 94 | ) 95 | 96 | let e2 = JSONEncoder() 97 | e2.dateEncodingStrategy = .secondsSince1970 98 | let e3 = JSONEncoder() 99 | e3.dateEncodingStrategy = .millisecondsSince1970 100 | 101 | let node2 = try DateType.genericOpenAPISchemaGuess(using: e2) 102 | let node3 = try DateType.genericOpenAPISchemaGuess(using: e3) 103 | 104 | XCTAssertEqual(node2, node3) 105 | #if canImport(FoundationEssentials) && swift(>=5.10) 106 | throw XCTSkip("Not supported for Swift 5.10+ on Linux.") 107 | #else 108 | XCTAssertEqual( 109 | node2, 110 | JSONSchema.object( 111 | properties: [ 112 | "date": .number(format: .double) 113 | ] 114 | ) 115 | ) 116 | 117 | let e4 = JSONEncoder() 118 | let df1 = DateFormatter() 119 | df1.timeStyle = .none 120 | e4.dateEncodingStrategy = .formatted(df1) 121 | 122 | let node4 = try DateType.genericOpenAPISchemaGuess(using: e4) 123 | 124 | XCTAssertEqual( 125 | node4, 126 | JSONSchema.object( 127 | properties: [ 128 | "date": .string(format: .date) 129 | ] 130 | ) 131 | ) 132 | 133 | let e5 = JSONEncoder() 134 | let df2 = DateFormatter() 135 | df2.timeStyle = .full 136 | e5.dateEncodingStrategy = .formatted(df2) 137 | 138 | let node5 = try DateType.genericOpenAPISchemaGuess(using: e5) 139 | 140 | XCTAssertEqual( 141 | node5, 142 | JSONSchema.object( 143 | properties: [ 144 | "date": .string(format: .dateTime) 145 | ] 146 | ) 147 | ) 148 | #endif 149 | } 150 | 151 | func test_nested() throws { 152 | let node = try Nested.genericOpenAPISchemaGuess(using: JSONEncoder()) 153 | 154 | XCTAssertEqual( 155 | node, 156 | JSONSchema.object( 157 | properties: [ 158 | "array1": .array(items: .string), 159 | "array2": .array(items: .number(format: .double)), 160 | "array3": .array(items: .number(format: .double)), 161 | "dict1": .object( 162 | additionalProperties: .init(.string) 163 | ), 164 | "dict2": .object( 165 | additionalProperties: .init(.boolean) 166 | ), 167 | "dictArray": .object( 168 | additionalProperties: .init(.array(items: .integer)) 169 | ), 170 | "arrayDict": .array( 171 | items: .object( 172 | additionalProperties: .init(.number(format: .double)) 173 | ) 174 | ), 175 | "structure": .object( 176 | properties: [ 177 | "bool": .boolean, 178 | "array": .array(items: .string), 179 | "dict": .object( 180 | additionalProperties: .init(.integer) 181 | ) 182 | ] 183 | ) 184 | ] 185 | ) 186 | ) 187 | } 188 | 189 | func test_enumTypes() throws { 190 | let node = try EnumTypes.genericOpenAPISchemaGuess(using: JSONEncoder()) 191 | let schema = node.value 192 | 193 | XCTAssertEqual(node.jsonTypeFormat, .object(.generic)) 194 | 195 | guard case .object(_, let ctx) = schema else { 196 | XCTFail("Expected object") 197 | return 198 | } 199 | 200 | XCTAssertEqual(ctx.properties["stringEnum"], .string) 201 | XCTAssertEqual(ctx.properties["intEnum"], .integer) 202 | XCTAssertEqual(ctx.properties["doubleEnum"], .number(format: .double)) 203 | XCTAssertEqual(ctx.properties["boolEnum"], .boolean) 204 | XCTAssertEqual(ctx.properties["optionalStringEnum"], .string(required: false)) 205 | XCTAssertEqual(ctx.properties["optionalIntEnum"], .integer(required: false)) 206 | XCTAssertEqual(ctx.properties["optionalDoubleEnum"], .number(format: .double, required: false)) 207 | XCTAssertEqual(ctx.properties["optionalBoolEnum"], .boolean(required: false)) 208 | } 209 | 210 | func test_allowedValues() throws { 211 | let node = try AllowedValues.genericOpenAPISchemaGuess(using: JSONEncoder()) 212 | let schema = node.value 213 | 214 | guard case let .object(_, objCtx) = schema else { 215 | XCTFail("Expected object") 216 | return 217 | } 218 | 219 | guard case let .string(ctx2, _) = objCtx.properties["stringEnum"]?.value else { 220 | XCTFail("Expected stringEnum property to be a .string") 221 | return 222 | } 223 | 224 | XCTAssert(ctx2.allowedValues?.count == 2) 225 | XCTAssert(ctx2.allowedValues?.contains("hello") ?? false) 226 | XCTAssert(ctx2.allowedValues?.contains("world") ?? false) 227 | 228 | guard case let .string(ctx3, _) = objCtx.properties["optionalStringEnum"]?.value else { 229 | XCTFail("Expected optionalStringEnum property to be a .string") 230 | return 231 | } 232 | 233 | XCTAssert(ctx3.allowedValues?.count == 2) 234 | XCTAssert(ctx3.allowedValues?.contains("hello") ?? false) 235 | XCTAssert(ctx3.allowedValues?.contains("world") ?? false) 236 | XCTAssertFalse(ctx3.required) 237 | 238 | guard case let .string(ctx4, _) = objCtx.properties["stringStruct"]?.value else { 239 | XCTFail("Expected stringStruct property to be a .string") 240 | return 241 | } 242 | 243 | XCTAssert(ctx4.allowedValues?.count == 2) 244 | XCTAssert(ctx4.allowedValues?.contains("hi") ?? false) 245 | XCTAssert(ctx4.allowedValues?.contains("there") ?? false) 246 | 247 | guard case let .string(ctx5, _) = objCtx.properties["optionalStringStruct"]?.value else { 248 | XCTFail("Expected optionalStringStruct property to be a .string") 249 | return 250 | } 251 | 252 | XCTAssert(ctx5.allowedValues?.count == 2) 253 | XCTAssert(ctx5.allowedValues?.contains("hi") ?? false) 254 | XCTAssert(ctx5.allowedValues?.contains("there") ?? false) 255 | XCTAssertFalse(ctx5.required) 256 | } 257 | 258 | func test_enumDirectly() throws { 259 | let schemaGuess = try AllowedValues.StringEnum.caseIterableOpenAPISchemaGuess(using: JSONEncoder()) 260 | guard case let .string(ctx, _) = schemaGuess.value else { 261 | XCTFail("Expected string.") 262 | return 263 | } 264 | 265 | XCTAssertEqual(ctx.allowedValues?.first?.value as? AllowedValues.StringEnum, .hello) 266 | XCTAssertEqual(ctx.allowedValues?.last?.value as? AllowedValues.StringEnum, .world) 267 | 268 | XCTAssertThrowsError(try CaselessEnum.caseIterableOpenAPISchemaGuess(using: JSONEncoder())) { err in 269 | XCTAssertEqual(err as? OpenAPI.EncodableError, .exampleNotCodable) 270 | } 271 | } 272 | 273 | func test_sampleableInSampleable() throws { 274 | XCTAssertEqual( 275 | try SampleableInSampleable.genericOpenAPISchemaGuess(using: JSONEncoder()), 276 | .object( 277 | properties: [ 278 | "sampleable": .string 279 | ] 280 | ) 281 | ) 282 | } 283 | 284 | func test_customCallsGeneric() throws { 285 | let schema = try CustomImplementationCallsGeneric.openAPISchema(using: JSONEncoder()) 286 | 287 | XCTAssertEqual( 288 | schema, 289 | .object( 290 | properties: [ 291 | "stringValue": .string 292 | ] 293 | ) 294 | ) 295 | } 296 | } 297 | 298 | // MARK: - Test Types 299 | 300 | extension GenericOpenAPISchemaTests { 301 | struct BasicTypes: Codable, Sampleable { 302 | let string: String 303 | let int: Int 304 | let double: Double 305 | let float: Float 306 | let bool: Bool 307 | let optionalString: String? 308 | 309 | static let sample: BasicTypes = .init(string: "hello", int: 10, double: 2.3, float: 1.1, bool: true, optionalString: "world") 310 | } 311 | 312 | struct OnlyCodable: Codable { 313 | let value: String 314 | } 315 | 316 | struct OpaqueStructs: Codable, Sampleable { 317 | // "opaque" in that the `OnlyCodable` type does not itself define an OpenAPI schema and so we must 318 | // encode it to have any chance at guessing its structure. 319 | let opaque: OnlyCodable 320 | let optionalOpaque: OnlyCodable? 321 | 322 | static let sample: OpaqueStructs = .init( 323 | opaque: .init(value: "hello"), 324 | optionalOpaque: .init(value: "world") 325 | ) 326 | } 327 | 328 | struct DateType: Codable, Sampleable { 329 | let date: Date 330 | 331 | static let sample: DateType = .init(date: Date()) 332 | } 333 | 334 | struct Nested: Codable, Sampleable { 335 | let array1: [String] 336 | let array2: [Double] 337 | let array3: [Date] 338 | 339 | let dict1: [String: String] 340 | let dict2: [String: Bool] 341 | 342 | let dictArray: [String: [Int]] 343 | 344 | let arrayDict: [[String: Date]] 345 | 346 | let structure: Structure 347 | 348 | struct Structure: Codable { 349 | let bool: Bool 350 | let array: [String] 351 | let dict: [String: Int] 352 | } 353 | 354 | static let sample: Nested = .init( 355 | array1: [], 356 | array2: [], 357 | array3: [], 358 | dict1: [:], 359 | dict2: [:], 360 | dictArray: [:], 361 | arrayDict: [], 362 | structure: .init(bool: true, array: [], dict: [:]) 363 | ) 364 | } 365 | 366 | struct EnumTypes: Codable, Sampleable { 367 | let stringEnum: StringEnum 368 | let intEnum: IntEnum 369 | let doubleEnum: DoubleEnum 370 | let boolEnum: BoolEnum 371 | 372 | let optionalStringEnum: StringEnum? 373 | let optionalIntEnum: IntEnum? 374 | let optionalDoubleEnum: DoubleEnum? 375 | let optionalBoolEnum: BoolEnum? 376 | 377 | enum StringEnum: String, Codable, AnyRawRepresentable { 378 | case hello 379 | case world 380 | } 381 | 382 | enum IntEnum: Int, Codable, AnyRawRepresentable { 383 | case zero 384 | case one 385 | } 386 | 387 | enum DoubleEnum: Double, Codable, AnyRawRepresentable { 388 | case twoPointFive = 2.5 389 | case onePointTwo = 1.2 390 | } 391 | 392 | enum BoolEnum: RawRepresentable, Codable, AnyRawRepresentable { 393 | case `true` 394 | case `false` 395 | 396 | init?(rawValue: Bool) { 397 | self = rawValue ? .true : .false 398 | } 399 | 400 | var rawValue: Bool { 401 | switch self { 402 | case .true: return true 403 | case .false: return false 404 | } 405 | } 406 | } 407 | 408 | static let sample: EnumTypes = .init( 409 | stringEnum: .hello, 410 | intEnum: .one, 411 | doubleEnum: .onePointTwo, 412 | boolEnum: .true, 413 | optionalStringEnum: nil, 414 | optionalIntEnum: nil, 415 | optionalDoubleEnum: nil, 416 | optionalBoolEnum: nil 417 | ) 418 | } 419 | 420 | struct AllowedValues: Codable, Sampleable { 421 | let stringEnum: StringEnum 422 | let optionalStringEnum: StringEnum? 423 | 424 | let stringStruct: StringStruct 425 | let optionalStringStruct: StringStruct? 426 | 427 | enum StringEnum: String, Codable, CaseIterable, AnyJSONCaseIterable { 428 | case hello 429 | case world 430 | } 431 | 432 | struct StringStruct: RawRepresentable, Codable, AnyJSONCaseIterable { 433 | 434 | let val: String 435 | 436 | var rawValue: String { val } 437 | 438 | static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 439 | return ["hi", "there"] 440 | } 441 | 442 | init(val: String) { 443 | self.val = val 444 | } 445 | 446 | init?(rawValue: String) { 447 | self.val = rawValue 448 | } 449 | } 450 | 451 | static let sample: AllowedValues = .init( 452 | stringEnum: .hello, 453 | optionalStringEnum: nil, 454 | stringStruct: .init(val: "hi"), 455 | optionalStringStruct: nil 456 | ) 457 | } 458 | 459 | struct EmptyObjectType: Codable, Sampleable { 460 | let empty: EmptyObject 461 | 462 | struct EmptyObject: Codable {} 463 | 464 | static let sample: EmptyObjectType = .init(empty: .init()) 465 | } 466 | 467 | enum CaselessEnum: RawRepresentable, Codable, CaseIterable, AnyJSONCaseIterable { 468 | init?(rawValue: String) { 469 | return nil 470 | } 471 | var rawValue: String { "" } 472 | 473 | typealias RawValue = String 474 | 475 | static func allCases(using encoder: JSONEncoder) -> [AnyCodable] { 476 | [] 477 | } 478 | } 479 | 480 | struct SampleableInSampleable: Codable, Sampleable { 481 | let sampleable: NestedSampleable 482 | 483 | static let sample: Self = .init(sampleable: .sample) 484 | 485 | enum NestedSampleable: String, Codable, CaseIterable, Sampleable, AnyRawRepresentable { 486 | case one 487 | case two 488 | 489 | static let sample: Self = .one 490 | } 491 | } 492 | 493 | struct CustomImplementationCallsGeneric: Codable, Sampleable, OpenAPIEncodedSchemaType { 494 | 495 | let stringValue: String 496 | 497 | static let sample: Self = .init(stringValue: "hello") 498 | 499 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 500 | try OpenAPIReflection.genericOpenAPISchemaGuess(for: sample, using: encoder) 501 | } 502 | } 503 | 504 | // struct EncodesAsPrimitive: Codable, SampleableOpenAPIType { 505 | // let asString: AsString 506 | // let asInt: AsInt 507 | // let asDouble: AsDouble 508 | // let asBool: AsBool 509 | // 510 | // struct AsString: Codable { 511 | // func encode(to encoder: Encoder) throws { 512 | // var container = encoder.singleValueContainer() 513 | // 514 | // try container.encode("hello world") 515 | // } 516 | // } 517 | // 518 | // struct AsInt: Codable { 519 | // func encode(to encoder: Encoder) throws { 520 | // var container = encoder.singleValueContainer() 521 | // 522 | // try container.encode(10) 523 | // } 524 | // } 525 | // 526 | // struct AsDouble: Codable { 527 | // func encode(to encoder: Encoder) throws { 528 | // var container = encoder.singleValueContainer() 529 | // 530 | // try container.encode(5.5) 531 | // } 532 | // } 533 | // 534 | // struct AsBool: Codable { 535 | // func encode(to encoder: Encoder) throws { 536 | // var container = encoder.singleValueContainer() 537 | // 538 | // try container.encode(true) 539 | // } 540 | // } 541 | // 542 | // static let sample: EncodesAsPrimitive = .init( 543 | // asString: .init(), 544 | // asInt: .init(), 545 | // asDouble: .init(), 546 | // asBool: .init() 547 | // ) 548 | // } 549 | } 550 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflectionTests/SchemaWithExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaWithExampleTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 9/10/20. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import OpenAPIKit 11 | import OpenAPIReflection 12 | import Sampleable 13 | 14 | final class SchemaWithExampleTests: XCTestCase { 15 | func test_structWithExample() throws { 16 | 17 | let schemaGuess = try Test.openAPISchemaWithExample(using: testEncoder) 18 | let expectedSchema = try JSONSchema.object( 19 | properties: [ 20 | "string": .string, 21 | "int": .integer, 22 | "bool": .boolean, 23 | "double": .number(format: .double) 24 | ] 25 | ).with(example: 26 | [ 27 | "bool" : true, 28 | "double" : 2.34, 29 | "int" : 10, 30 | "string" : "hello" 31 | ] 32 | ) 33 | 34 | XCTAssertNotNil(schemaGuess.jsonType) 35 | 36 | XCTAssertEqual( 37 | schemaGuess.jsonType, 38 | expectedSchema.jsonType 39 | ) 40 | 41 | XCTAssertNotNil(schemaGuess.objectContext) 42 | 43 | XCTAssertEqual( 44 | schemaGuess.objectContext, 45 | expectedSchema.objectContext 46 | ) 47 | 48 | XCTAssertFalse(schemaGuess.examples.isEmpty) 49 | 50 | // equality checks on AnyCodable are finicky but 51 | // they compare equally when encoded to data. 52 | XCTAssertEqual( 53 | try testEncoder.encode(schemaGuess.examples), 54 | try testEncoder.encode(expectedSchema.examples) 55 | ) 56 | } 57 | } 58 | 59 | extension SchemaWithExampleTests { 60 | struct Test: Codable, Sampleable, OpenAPIEncodedSchemaType { 61 | let string: String 62 | let int: Int 63 | let bool: Bool 64 | let double: Double 65 | 66 | public static let sample: SchemaWithExampleTests.Test = .init( 67 | string: "hello", 68 | int: 10, 69 | bool: true, 70 | double: 2.34 71 | ) 72 | 73 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 74 | return .object( 75 | properties: [ 76 | "string": .string, 77 | "int": .integer, 78 | "bool": .boolean, 79 | "double": .number(format: .double) 80 | ] 81 | ) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflectionTests/SwiftPrimitiveExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftPrimitiveExtensionsTests.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 3/4/20. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import OpenAPIReflection 11 | import OpenAPIKit 12 | 13 | class SwiftPrimitiveTypesTests: XCTestCase { 14 | func test_OptionalCaseIterableNodeAllCases() { 15 | XCTAssertTrue(RawRepStringEnum?.allCases(using: SwiftPrimitiveTypesTests.localTestEncoder).contains("hello")) 16 | XCTAssertTrue(RawRepStringEnum?.allCases(using: SwiftPrimitiveTypesTests.localTestEncoder).contains("world")) 17 | XCTAssertEqual(RawRepStringEnum?.allCases(using: SwiftPrimitiveTypesTests.localTestEncoder).count, 2) 18 | } 19 | 20 | func test_OptionalDateNodeType() { 21 | XCTAssertEqual(Date?.dateOpenAPISchemaGuess(using: testEncoder), .string(format: .dateTime, required: false)) 22 | } 23 | 24 | func test_RawNodeType() { 25 | XCTAssertEqual(try! RawRepStringEnum.rawOpenAPISchema(), .string) 26 | XCTAssertEqual(try! RawRepIntEnum.rawOpenAPISchema(), .integer) 27 | } 28 | 29 | func test_OptionalRawRepresentable() { 30 | XCTAssertEqual(try! RawRepStringEnum?.rawOpenAPISchema(), .string(required: false)) 31 | 32 | XCTAssertEqual(try! RawRepIntEnum?.rawOpenAPISchema(), .integer(required: false)) 33 | } 34 | 35 | func test_OptionalRawNodeType() { 36 | XCTAssertEqual(try! RawRepStringEnum?.rawOpenAPISchema(), .string(required: false)) 37 | 38 | XCTAssertEqual(try! RawRepIntEnum?.rawOpenAPISchema(), .integer(required: false)) 39 | } 40 | 41 | func test_DoubleWrappedRawNodeType() { 42 | XCTAssertEqual(try! RawRepStringEnum??.rawOpenAPISchema(), .string(required: false)) 43 | 44 | XCTAssertEqual(try! RawRepIntEnum??.rawOpenAPISchema(), .integer(required: false)) 45 | 46 | XCTAssertEqual(try! RawRepStringEnum??.rawOpenAPISchema(), .string(required: false)) 47 | 48 | XCTAssertEqual(try! RawRepIntEnum??.rawOpenAPISchema(), .integer(required: false)) 49 | } 50 | 51 | static let localTestEncoder = JSONEncoder() 52 | } 53 | 54 | fileprivate enum RawRepStringEnum: String, RawOpenAPISchemaType, CaseIterable, Codable, AnyJSONCaseIterable { 55 | case hello 56 | case world 57 | } 58 | 59 | fileprivate enum RawRepIntEnum: Int, RawOpenAPISchemaType { 60 | case one 61 | case two 62 | } 63 | -------------------------------------------------------------------------------- /Tests/OpenAPIReflectionTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 6/23/19. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | let testEncoder = { () -> JSONEncoder in 12 | let encoder = JSONEncoder() 13 | if #available(macOS 10.13, *) { 14 | encoder.dateEncodingStrategy = .iso8601 15 | encoder.keyEncodingStrategy = .useDefaultKeys 16 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 17 | } 18 | #if os(Linux) 19 | encoder.dateEncodingStrategy = .iso8601 20 | encoder.keyEncodingStrategy = .useDefaultKeys 21 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 22 | #endif 23 | return encoder 24 | }() 25 | 26 | func testStringFromEncoding(of entity: T) throws -> String? { 27 | return String(data: try testEncoder.encode(entity), encoding: .utf8) 28 | } 29 | 30 | let testDecoder = { () -> JSONDecoder in 31 | let decoder = JSONDecoder() 32 | if #available(macOS 10.12, *) { 33 | decoder.dateDecodingStrategy = .iso8601 34 | decoder.keyDecodingStrategy = .useDefaultKeys 35 | } 36 | #if os(Linux) 37 | decoder.dateDecodingStrategy = .iso8601 38 | decoder.keyDecodingStrategy = .useDefaultKeys 39 | #endif 40 | return decoder 41 | }() 42 | 43 | func assertJSONEquivalent(_ str1: String?, _ str2: String?, file: StaticString = #file, line: UInt = #line) { 44 | 45 | // when testing on Linux, pretty printing has slightly different 46 | // meaning so the tests pass on OS X as written but need whitespace 47 | // stripped to pass on Linux 48 | #if os(Linux) 49 | var str1 = str1 50 | var str2 = str2 51 | 52 | str1?.removeAll { $0.isWhitespace } 53 | str2?.removeAll { $0.isWhitespace } 54 | #endif 55 | 56 | XCTAssertEqual( 57 | str1, 58 | str2, 59 | file: (file), 60 | line: line 61 | ) 62 | } 63 | --------------------------------------------------------------------------------