├── .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 | [](https://swift.org)
2 |
3 | [](http://opensource.org/licenses/MIT) 
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 |
--------------------------------------------------------------------------------