Skip to content

Commit dacfb9a

Browse files
committed
Add vendor extensions support to Content
1 parent e3b0c55 commit dacfb9a

File tree

3 files changed

+183
-2
lines changed

3 files changed

+183
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ This library *is* opinionated about a few defaults when you use the Swift types,
135135
- [x] example
136136
- [ ] examples
137137
- [x] encoding
138+
- [x] specification extensions (`vendorExtensions`)
138139

139140
### Encoding Object (`OpenAPI.Content.Encoding`)
140141
- [x] contentType

Sources/OpenAPIKit/Content.swift

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,27 @@ extension OpenAPI {
1616
case form = "application/x-www-form-urlencoded"
1717
}
1818

19-
public struct Content: Codable, Equatable {
19+
public struct Content: Equatable {
2020
public let schema: Either<JSONReference<Components, JSONSchema>, JSONSchema>
2121
public let example: AnyCodable?
2222
// public let examples:
2323
public let encoding: [String: Encoding]?
2424

25+
/// Dictionary of vendor extensions.
26+
///
27+
/// These should be of the form:
28+
/// `[ "x-extensionKey": <anything>]`
29+
/// where the values are anything codable.
30+
public let vendorExtensions: [String: AnyCodable]
31+
2532
public init(schema: Either<JSONReference<Components, JSONSchema>, JSONSchema>,
2633
example: AnyCodable? = nil,
27-
encoding: [String: Encoding]? = nil) {
34+
encoding: [String: Encoding]? = nil,
35+
vendorExtensions: [String: AnyCodable] = [:]) {
2836
self.schema = schema
2937
self.example = example
3038
self.encoding = encoding
39+
self.vendorExtensions = vendorExtensions
3140
}
3241
}
3342
}
@@ -53,3 +62,93 @@ extension OpenAPI.Content {
5362
}
5463
}
5564
}
65+
66+
// MARK: - Codable
67+
68+
extension OpenAPI.Content: Encodable {
69+
public func encode(to encoder: Encoder) throws {
70+
var container = encoder.container(keyedBy: CodingKeys.self)
71+
72+
try container.encode(schema, forKey: .schema)
73+
74+
if example != nil {
75+
try container.encode(example, forKey: .example)
76+
}
77+
78+
if encoding != nil {
79+
try container.encode(encoding, forKey: .encoding)
80+
}
81+
82+
if vendorExtensions != [:] {
83+
for (key, value) in vendorExtensions {
84+
let xKey = key.starts(with: "x-") ? key : "x-\(key)"
85+
try container.encode(value, forKey: .extended(xKey))
86+
}
87+
}
88+
}
89+
}
90+
91+
extension OpenAPI.Content: Decodable {
92+
public init(from decoder: Decoder) throws {
93+
let container = try decoder.container(keyedBy: CodingKeys.self)
94+
95+
schema = try container.decode(Either<JSONReference<OpenAPI.Components, JSONSchema>, JSONSchema>.self, forKey: .schema)
96+
example = try container.decodeIfPresent(AnyCodable.self, forKey: .example)
97+
encoding = try container.decodeIfPresent([String: Encoding].self, forKey: .encoding)
98+
99+
let decodedAny = (try AnyCodable(from: decoder)).value as? [String: Any]
100+
101+
vendorExtensions = decodedAny?.filter {
102+
guard let key = CodingKeys(stringValue: $0.key) else { return false }
103+
104+
return !CodingKeys.allBuiltinCases.contains(key)
105+
}.mapValues(AnyCodable.init) ?? [:]
106+
}
107+
}
108+
109+
extension OpenAPI.Content {
110+
private enum CodingKeys: CodingKey, Equatable {
111+
case schema
112+
case example
113+
case encoding
114+
case extended(String)
115+
116+
init?(stringValue: String) {
117+
switch stringValue {
118+
case "schema":
119+
self = .schema
120+
case "example":
121+
self = .example
122+
case "encoding":
123+
self = .encoding
124+
default:
125+
self = .extended(stringValue)
126+
}
127+
}
128+
129+
init?(intValue: Int) {
130+
return nil
131+
}
132+
133+
var stringValue: String {
134+
switch self {
135+
case .schema:
136+
return "schema"
137+
case .example:
138+
return "example"
139+
case .encoding:
140+
return "encoding"
141+
case .extended(let key):
142+
return key
143+
}
144+
}
145+
146+
var intValue: Int? {
147+
return nil
148+
}
149+
150+
static var allBuiltinCases: [CodingKeys] {
151+
return [.schema, .example, .encoding]
152+
}
153+
}
154+
}

Tests/OpenAPIKitTests/ContentTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import Foundation
99
import XCTest
1010
import OpenAPIKit
11+
import AnyCodable
1112

1213
final class ContentTests: XCTestCase {
1314
func test_init() {
@@ -114,6 +115,86 @@ extension ContentTests {
114115

115116
XCTAssertEqual(content, OpenAPI.Content(schema: .init(.string(required: false))))
116117
}
118+
119+
func test_exampleAndSchemaContent_encode() {
120+
// TODO: write test
121+
}
122+
123+
func test_exampleAndSchemaContent_decode() {
124+
// TODO: write test
125+
}
126+
127+
func test_encodingAndSchema_encode() {
128+
// TODO: write test
129+
}
130+
131+
func test_encodingAndSchema_decode() {
132+
// TODO: write test
133+
}
134+
135+
func test_vendorExtensions_encode() {
136+
let content = OpenAPI.Content(schema: .init(.string),
137+
vendorExtensions: [ "x-hello": [ "world": 123 ] ])
138+
139+
let encodedContent = try! testStringFromEncoding(of: content)
140+
141+
XCTAssertEqual(encodedContent,
142+
"""
143+
{
144+
"schema" : {
145+
"type" : "string"
146+
},
147+
"x-hello" : {
148+
"world" : 123
149+
}
150+
}
151+
"""
152+
)
153+
}
154+
155+
func test_vendorExtensions_encode_fixKey() {
156+
let content = OpenAPI.Content(schema: .init(.string),
157+
vendorExtensions: [ "hello": [ "world": 123 ] ])
158+
159+
let encodedContent = try! testStringFromEncoding(of: content)
160+
161+
XCTAssertEqual(encodedContent,
162+
"""
163+
{
164+
"schema" : {
165+
"type" : "string"
166+
},
167+
"x-hello" : {
168+
"world" : 123
169+
}
170+
}
171+
"""
172+
)
173+
}
174+
175+
func test_vendorExtensions_decode() {
176+
let contentData =
177+
"""
178+
{
179+
"schema" : {
180+
"type" : "string"
181+
},
182+
"x-hello" : {
183+
"world" : 123
184+
}
185+
}
186+
""".data(using: .utf8)!
187+
let content = try! testDecoder.decode(OpenAPI.Content.self, from: contentData)
188+
189+
let contentToMatch = OpenAPI.Content(schema: .init(.string(required: false)),
190+
vendorExtensions: ["x-hello": AnyCodable(["world": 123])])
191+
192+
// needs to be broken down due to difficulties with equality comparing of AnyCodable
193+
// created from code with a semantically equivalent AnyCodable from Data.
194+
XCTAssertEqual(content.schema, contentToMatch.schema)
195+
XCTAssertEqual(content.vendorExtensions.keys, contentToMatch.vendorExtensions.keys)
196+
XCTAssertEqual(content.vendorExtensions["x-hello"]?.value as? [String: Int], contentToMatch.vendorExtensions["x-hello"]?.value as? [String: Int]?)
197+
}
117198
}
118199

119200
// MARK: Content.Encoding

0 commit comments

Comments
 (0)