Skip to content

Commit 9ac0b62

Browse files
bfrearsonbenfrearsonczechboy0
authored
Support url form encoded bodies (apple#53)
### Motivation [Issue 182](apple/swift-openapi-generator#182) Supporting runtime update for [add URL form encoder & decoder](apple/swift-openapi-generator#283) ### Modifications Add converter methods for Codable <--> URLEncodedForm. **Note:** Trying to encode a primitive collection (e.g. an array of `String`) form a Codable type fails because the URIEncoder rejects nested object types. I didn't make any changes to the Encoder here, as I think there may be a larger discussion needed about how to approach this. It would be beneficial to support this, as there may be a form which contains multiple values for the same key. Decoding an exploded key from to a Codable type containing an array works already, but I've not included the test in this PR, as that should be included in the above. ### Result Converter methods for URLEncodedForms are now available for use in generated content. ### Test Plan Update tests for encoders and decoders. --------- Co-authored-by: benfrearson <[email protected]> Co-authored-by: bfrearson <> Co-authored-by: Honza Dvorsky <[email protected]>
1 parent 5f7e7ee commit 9ac0b62

File tree

6 files changed

+180
-0
lines changed

6 files changed

+180
-0
lines changed

Sources/OpenAPIRuntime/Conversion/Converter+Client.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,34 @@ extension Converter {
168168
)
169169
}
170170

171+
// | client | set | request body | urlEncodedForm | codable | optional | setOptionalRequestBodyAsURLEncodedForm |
172+
public func setOptionalRequestBodyAsURLEncodedForm<T: Encodable>(
173+
_ value: T,
174+
headerFields: inout [HeaderField],
175+
contentType: String
176+
) throws -> Data? {
177+
try setOptionalRequestBody(
178+
value,
179+
headerFields: &headerFields,
180+
contentType: contentType,
181+
convert: convertBodyCodableToURLFormData
182+
)
183+
}
184+
185+
// | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm |
186+
public func setRequiredRequestBodyAsURLEncodedForm<T: Encodable>(
187+
_ value: T,
188+
headerFields: inout [HeaderField],
189+
contentType: String
190+
) throws -> Data {
191+
try setRequiredRequestBody(
192+
value,
193+
headerFields: &headerFields,
194+
contentType: contentType,
195+
convert: convertBodyCodableToURLFormData
196+
)
197+
}
198+
171199
// | client | get | response body | string | required | getResponseBodyAsString |
172200
public func getResponseBodyAsString<T: Decodable, C>(
173201
_ type: T.Type,

Sources/OpenAPIRuntime/Conversion/Converter+Server.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,34 @@ extension Converter {
273273
)
274274
}
275275

276+
// | server | get | request body | URLEncodedForm | codable | optional | getOptionalRequestBodyAsURLEncodedForm |
277+
func getOptionalRequestBodyAsURLEncodedForm<T: Decodable, C>(
278+
_ type: T.Type,
279+
from data: Data?,
280+
transforming transform: (T) -> C
281+
) throws -> C? {
282+
try getOptionalRequestBody(
283+
type,
284+
from: data,
285+
transforming: transform,
286+
convert: convertURLEncodedFormToCodable
287+
)
288+
}
289+
290+
// | server | get | request body | URLEncodedForm | codable | required | getRequiredRequestBodyAsURLEncodedForm |
291+
public func getRequiredRequestBodyAsURLEncodedForm<T: Decodable, C>(
292+
_ type: T.Type,
293+
from data: Data?,
294+
transforming transform: (T) -> C
295+
) throws -> C {
296+
try getRequiredRequestBody(
297+
type,
298+
from: data,
299+
transforming: transform,
300+
convert: convertURLEncodedFormToCodable
301+
)
302+
}
303+
276304
// | server | set | response body | string | required | setResponseBodyAsString |
277305
public func setResponseBodyAsString<T: Encodable>(
278306
_ value: T,

Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,43 @@ extension Converter {
177177
try decoder.decode(T.self, from: data)
178178
}
179179

180+
func convertURLEncodedFormToCodable<T: Decodable>(
181+
_ data: Data
182+
) throws -> T {
183+
let decoder = URIDecoder(
184+
configuration: .init(
185+
style: .form,
186+
explode: true,
187+
spaceEscapingCharacter: .plus,
188+
dateTranscoder: configuration.dateTranscoder
189+
)
190+
)
191+
let uriString = String(decoding: data, as: UTF8.self)
192+
return try decoder.decode(T.self, from: uriString)
193+
}
194+
180195
func convertBodyCodableToJSON<T: Encodable>(
181196
_ value: T
182197
) throws -> Data {
183198
try encoder.encode(value)
184199
}
185200

201+
func convertBodyCodableToURLFormData<T: Encodable>(
202+
_ value: T
203+
) throws -> Data {
204+
let encoder = URIEncoder(
205+
configuration: .init(
206+
style: .form,
207+
explode: true,
208+
spaceEscapingCharacter: .plus,
209+
dateTranscoder: configuration.dateTranscoder
210+
)
211+
)
212+
let encodedString = try encoder.encode(value, forKey: "")
213+
let data = Data(encodedString.utf8)
214+
return data
215+
}
216+
186217
func convertHeaderFieldCodableToJSON<T: Encodable>(
187218
_ value: T
188219
) throws -> String {

Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,46 @@ final class Test_ClientConverterExtensions: Test_Runtime {
234234
)
235235
}
236236

237+
// | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm |
238+
func test_setOptionalRequestBodyAsURLEncodedForm_codable() throws {
239+
var headerFields: [HeaderField] = []
240+
let body = try converter.setOptionalRequestBodyAsURLEncodedForm(
241+
testStructDetailed,
242+
headerFields: &headerFields,
243+
contentType: "application/x-www-form-urlencoded"
244+
)
245+
246+
guard let body else {
247+
XCTFail("Expected body should not be nil")
248+
return
249+
}
250+
251+
XCTAssertEqualStringifiedData(body, testStructURLFormString)
252+
XCTAssertEqual(
253+
headerFields,
254+
[
255+
.init(name: "content-type", value: "application/x-www-form-urlencoded")
256+
]
257+
)
258+
}
259+
260+
// | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm |
261+
func test_setRequiredRequestBodyAsURLEncodedForm_codable() throws {
262+
var headerFields: [HeaderField] = []
263+
let body = try converter.setRequiredRequestBodyAsURLEncodedForm(
264+
testStructDetailed,
265+
headerFields: &headerFields,
266+
contentType: "application/x-www-form-urlencoded"
267+
)
268+
XCTAssertEqualStringifiedData(body, testStructURLFormString)
269+
XCTAssertEqual(
270+
headerFields,
271+
[
272+
.init(name: "content-type", value: "application/x-www-form-urlencoded")
273+
]
274+
)
275+
}
276+
237277
// | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary |
238278
func test_setOptionalRequestBodyAsBinary_data() throws {
239279
var headerFields: [HeaderField] = []
@@ -308,3 +348,18 @@ final class Test_ClientConverterExtensions: Test_Runtime {
308348
XCTAssertEqual(value, testStringData)
309349
}
310350
}
351+
352+
public func XCTAssertEqualStringifiedData(
353+
_ expression1: @autoclosure () throws -> Data,
354+
_ expression2: @autoclosure () throws -> String,
355+
_ message: @autoclosure () -> String = "",
356+
file: StaticString = #filePath,
357+
line: UInt = #line
358+
) {
359+
do {
360+
let actualString = String(decoding: try expression1(), as: UTF8.self)
361+
XCTAssertEqual(actualString, try expression2(), file: file, line: line)
362+
} catch {
363+
XCTFail(error.localizedDescription, file: file, line: line)
364+
}
365+
}

Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,26 @@ final class Test_ServerConverterExtensions: Test_Runtime {
339339
XCTAssertEqual(body, testStruct)
340340
}
341341

342+
// | server | get | request body | urlEncodedForm | optional | getOptionalRequestBodyAsURLEncodedForm |
343+
func test_getOptionalRequestBodyAsURLEncodedForm_codable() throws {
344+
let body: TestPetDetailed? = try converter.getOptionalRequestBodyAsURLEncodedForm(
345+
TestPetDetailed.self,
346+
from: testStructURLFormData,
347+
transforming: { $0 }
348+
)
349+
XCTAssertEqual(body, testStructDetailed)
350+
}
351+
352+
// | server | get | request body | urlEncodedForm | required | getRequiredRequestBodyAsURLEncodedForm |
353+
func test_getRequiredRequestBodyAsURLEncodedForm_codable() throws {
354+
let body: TestPetDetailed = try converter.getRequiredRequestBodyAsURLEncodedForm(
355+
TestPetDetailed.self,
356+
from: testStructURLFormData,
357+
transforming: { $0 }
358+
)
359+
XCTAssertEqual(body, testStructDetailed)
360+
}
361+
342362
// | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary |
343363
func test_getOptionalRequestBodyAsBinary_data() throws {
344364
let body: Data? = try converter.getOptionalRequestBodyAsBinary(

Tests/OpenAPIRuntimeTests/Test_Runtime.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ class Test_Runtime: XCTestCase {
8686
.init(name: "Fluffz")
8787
}
8888

89+
var testStructDetailed: TestPetDetailed {
90+
.init(name: "Rover!", type: "Golden Retriever", age: "3")
91+
}
92+
8993
var testStructString: String {
9094
#"{"name":"Fluffz"}"#
9195
}
@@ -98,6 +102,10 @@ class Test_Runtime: XCTestCase {
98102
"""#
99103
}
100104

105+
var testStructURLFormString: String {
106+
"age=3&name=Rover%21&type=Golden+Retriever"
107+
}
108+
101109
var testEnum: TestHabitat {
102110
.water
103111
}
@@ -114,6 +122,10 @@ class Test_Runtime: XCTestCase {
114122
Data(testStructPrettyString.utf8)
115123
}
116124

125+
var testStructURLFormData: Data {
126+
Data(testStructURLFormString.utf8)
127+
}
128+
117129
func _testPrettyEncoded<Value: Encodable>(_ value: Value, expectedJSON: String) throws {
118130
let encoder = JSONEncoder()
119131
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
@@ -140,6 +152,12 @@ struct TestPet: Codable, Equatable {
140152
var name: String
141153
}
142154

155+
struct TestPetDetailed: Codable, Equatable {
156+
var name: String
157+
var type: String
158+
var age: String
159+
}
160+
143161
enum TestHabitat: String, Codable, Equatable {
144162
case water
145163
case land

0 commit comments

Comments
 (0)