diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..ef9196c --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "XMLCoder", + "repositoryURL": "https://github.com/MaxDesiatov/XMLCoder", + "state": { + "branch": null, + "revision": "887de88b37b2d691d67db950770e09776229cf6d", + "version": "0.13.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index b050b60..d60f38d 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,15 @@ let package = Package( .library(name: "Postie", targets: ["Postie"]), .library(name: "PostieMock", targets: ["PostieMock"]) ], + dependencies: [ + .package(url: "https://github.com/MaxDesiatov/XMLCoder", .upToNextMajor(from: "0.1.3")) + ], targets: [ - .target(name: "Postie", dependencies: ["URLEncodedFormCoding", "PostieUtils"]), + .target(name: "Postie", dependencies: [ + "URLEncodedFormCoding", + "PostieUtils", + "XMLCoder" + ]), .testTarget(name: "PostieTests", dependencies: ["Postie", "PostieMock"]), .target(name: "PostieMock", dependencies: ["Postie"]), diff --git a/Sources/Postie/Decoder/FormURLEncodedDecodable.swift b/Sources/Postie/Decoder/Decodables/FormURLEncodedDecodable.swift similarity index 100% rename from Sources/Postie/Decoder/FormURLEncodedDecodable.swift rename to Sources/Postie/Decoder/Decodables/FormURLEncodedDecodable.swift diff --git a/Sources/Postie/Decoder/JSONDecodable.swift b/Sources/Postie/Decoder/Decodables/JSONDecodable.swift similarity index 100% rename from Sources/Postie/Decoder/JSONDecodable.swift rename to Sources/Postie/Decoder/Decodables/JSONDecodable.swift diff --git a/Sources/Postie/Decoder/PlainDecodable.swift b/Sources/Postie/Decoder/Decodables/PlainDecodable.swift similarity index 100% rename from Sources/Postie/Decoder/PlainDecodable.swift rename to Sources/Postie/Decoder/Decodables/PlainDecodable.swift diff --git a/Sources/Postie/Decoder/Decodables/XMLDecodable.swift b/Sources/Postie/Decoder/Decodables/XMLDecodable.swift new file mode 100644 index 0000000..dba8f2d --- /dev/null +++ b/Sources/Postie/Decoder/Decodables/XMLDecodable.swift @@ -0,0 +1,2 @@ +/// A type that can decode itself from an external JSON representation. +public typealias XMLDecodable = Decodable & XMLFormatProvider diff --git a/Sources/Postie/Decoder/ResponseDecoding.swift b/Sources/Postie/Decoder/ResponseDecoding.swift index f3cde4b..515ae27 100644 --- a/Sources/Postie/Decoder/ResponseDecoding.swift +++ b/Sources/Postie/Decoder/ResponseDecoding.swift @@ -1,6 +1,7 @@ import Foundation import PostieUtils import URLEncodedFormCoding +import XMLCoder internal struct ResponseDecoding: Decoder { @@ -21,7 +22,7 @@ internal struct ResponseDecoding: Decoder { } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - fatalError() + fatalError("not implemented") } func singleValueContainer() throws -> SingleValueDecodingContainer { @@ -51,7 +52,7 @@ internal struct ResponseDecoding: Decoder { } func decodeBody(to type: [E].Type) throws -> [E] { - fatalError() + fatalError("not implemented") } func decodeBody(to type: T.Type) throws -> T { @@ -64,6 +65,9 @@ internal struct ResponseDecoding: Decoder { if type is JSONDecodable.Type { return try createJSONDecoder().decode(type, from: data) } + if type is XMLDecodable.Type { + return try createXMLDecoder().decode(type, from: data) + } if type is CollectionProtocol.Type { guard let collectionType = type as? CollectionProtocol.Type else { @@ -80,6 +84,9 @@ internal struct ResponseDecoding: Decoder { if elementType is JSONDecodable.Type { return try createJSONDecoder().decode(type, from: data) } + if elementType is XMLDecodable.Type { + return try createXMLDecoder().decode(type, from: data) + } } fatalError("Unsupported body type: \(type)") } @@ -87,12 +94,6 @@ internal struct ResponseDecoding: Decoder { private func createJSONDecoder() -> JSONDecoder { let decoder = LoggingJSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - // switch dateFormat { - // case .iso8601: - // decoder.dateDecodingStrategy = .iso8601 - // case .iso8601Full: - // decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full) - // } return decoder } @@ -114,4 +115,10 @@ internal struct ResponseDecoding: Decoder { } return value } + + private func createXMLDecoder() -> XMLDecoder { + let decoder = XMLDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + } } diff --git a/Sources/Postie/Decoder/ResponseKeyedDecodingContainer.swift b/Sources/Postie/Decoder/ResponseKeyedDecodingContainer.swift index 6e50e08..e9d7bf1 100644 --- a/Sources/Postie/Decoder/ResponseKeyedDecodingContainer.swift +++ b/Sources/Postie/Decoder/ResponseKeyedDecodingContainer.swift @@ -16,7 +16,7 @@ class ResponseKeyedDecodingContainer: KeyedDecodingContainerProtocol where } func decodeNil(forKey key: Key) throws -> Bool { - fatalError() + fatalError("not implemented") } func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { @@ -28,18 +28,18 @@ class ResponseKeyedDecodingContainer: KeyedDecodingContainerProtocol where } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - fatalError() + fatalError("not implemented") } func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { - fatalError() + fatalError("not implemented") } func superDecoder() throws -> Decoder { - fatalError() + fatalError("not implemented") } func superDecoder(forKey key: Key) throws -> Decoder { - fatalError() + fatalError("not implemented") } } diff --git a/Sources/Postie/Decoder/ResponseSingleValueDecodingContainer.swift b/Sources/Postie/Decoder/ResponseSingleValueDecodingContainer.swift index 1098937..7c8137d 100644 --- a/Sources/Postie/Decoder/ResponseSingleValueDecodingContainer.swift +++ b/Sources/Postie/Decoder/ResponseSingleValueDecodingContainer.swift @@ -11,7 +11,7 @@ class ResponseSingleValueDecodingContainer: SingleValueDecodingContainer { } func decodeNil() -> Bool { - fatalError() + fatalError("not implemented") } func decode(_ type: T.Type) throws -> T where T: Decodable { diff --git a/Sources/Postie/Decoder/CollectionProtocol.swift b/Sources/Postie/Decoder/Utils/CollectionProtocol.swift similarity index 100% rename from Sources/Postie/Decoder/CollectionProtocol.swift rename to Sources/Postie/Decoder/Utils/CollectionProtocol.swift diff --git a/Sources/Postie/Encoder/FormURLEncodedEncodable.swift b/Sources/Postie/Encoder/Encodables/FormURLEncodedEncodable.swift similarity index 100% rename from Sources/Postie/Encoder/FormURLEncodedEncodable.swift rename to Sources/Postie/Encoder/Encodables/FormURLEncodedEncodable.swift diff --git a/Sources/Postie/Encoder/JSONEncodable.swift b/Sources/Postie/Encoder/Encodables/JSONEncodable.swift similarity index 100% rename from Sources/Postie/Encoder/JSONEncodable.swift rename to Sources/Postie/Encoder/Encodables/JSONEncodable.swift diff --git a/Sources/Postie/Encoder/PlainEncodable.swift b/Sources/Postie/Encoder/Encodables/PlainEncodable.swift similarity index 100% rename from Sources/Postie/Encoder/PlainEncodable.swift rename to Sources/Postie/Encoder/Encodables/PlainEncodable.swift diff --git a/Sources/Postie/Encoder/Encodables/XMLEncodable.swift b/Sources/Postie/Encoder/Encodables/XMLEncodable.swift new file mode 100644 index 0000000..f3bb751 --- /dev/null +++ b/Sources/Postie/Encoder/Encodables/XMLEncodable.swift @@ -0,0 +1,10 @@ +/// A type that should encode itself to a JSON representation. +public typealias XMLEncodable = Encodable & XMLFormatProvider & XMLBodyProvider + +public protocol XMLBodyProvider { + + associatedtype Body: Encodable + + var body: Body { get } + +} diff --git a/Sources/Postie/Encoder/RequestEncoder.swift b/Sources/Postie/Encoder/RequestEncoder.swift index 6c144cc..9139f4e 100644 --- a/Sources/Postie/Encoder/RequestEncoder.swift +++ b/Sources/Postie/Encoder/RequestEncoder.swift @@ -1,6 +1,7 @@ import Foundation import URLEncodedFormCoding import Combine +import XMLCoder public class RequestEncoder { @@ -65,6 +66,22 @@ public class RequestEncoder { return data } + // MARK: - XML + + public func encodeXML(request: Request) throws -> URLRequest where Request: XMLEncodable { + var urlRequest = try encodeToBaseURLRequest(request) + urlRequest.httpBody = try encodeXMLBody(request.body) + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("text/xml", forHTTPHeaderField: "Content-Type") + } + return urlRequest + } + + private func encodeXMLBody(_ body: Body) throws -> Data { + let encoder = XMLEncoder() + return try encoder.encode(body) + } + // MARK: - Shared private func encodeToBaseURLRequest(_ request: Request) throws -> URLRequest { diff --git a/Sources/Postie/Formats/APIDataFormat.swift b/Sources/Postie/Formats/APIDataFormat.swift index 0607944..d8838c1 100644 --- a/Sources/Postie/Formats/APIDataFormat.swift +++ b/Sources/Postie/Formats/APIDataFormat.swift @@ -3,5 +3,6 @@ public enum APIDataFormat { case plain case json case formURLEncoded + case xml } diff --git a/Sources/Postie/Formats/XMLFormatProvider.swift b/Sources/Postie/Formats/XMLFormatProvider.swift new file mode 100644 index 0000000..cc5017f --- /dev/null +++ b/Sources/Postie/Formats/XMLFormatProvider.swift @@ -0,0 +1,14 @@ +/// A type that has a default format of xml +public protocol XMLFormatProvider { + + /// Format of data, default extension is set to `.xml` + static var format: APIDataFormat { get } + +} + +extension XMLFormatProvider { + + public static var format: APIDataFormat { + .xml + } +} diff --git a/Sources/Postie/Path/RequestPathParameter.swift b/Sources/Postie/Path/RequestPathParameter.swift index 8018a0c..9335a92 100644 --- a/Sources/Postie/Path/RequestPathParameter.swift +++ b/Sources/Postie/Path/RequestPathParameter.swift @@ -1,43 +1,43 @@ /// Protocol used for untyped access to the embedded value internal protocol RequestPathParameterProtocol { - + /// Custom name of the path parameter, can be nil var name: String? { get } - + /// Path parameter value which should be serialized and inserted into the path var untypedValue: RequestPathParameterValue { get } - + } @propertyWrapper public struct RequestPathParameter where T: RequestPathParameterValue { - + public var name: String? public var wrappedValue: T - + public init(wrappedValue: T) { self.wrappedValue = wrappedValue } - + public init(name: String?, defaultValue: T) { self.name = name self.wrappedValue = defaultValue } - + public static func getParameterType() -> Any.Type { return T.self } } extension RequestPathParameter: RequestPathParameterProtocol { - + internal var untypedValue: RequestPathParameterValue { wrappedValue } } extension RequestPathParameter where T == String { - + public init(name: String?) { self.name = name self.wrappedValue = "" @@ -45,7 +45,7 @@ extension RequestPathParameter where T == String { } extension RequestPathParameter where T == Int { - + public init(name: String?) { self.name = name self.wrappedValue = -1 @@ -53,7 +53,7 @@ extension RequestPathParameter where T == Int { } extension RequestPathParameter where T == Int16 { - + public init(name: String?) { self.name = name self.wrappedValue = -1 @@ -61,7 +61,7 @@ extension RequestPathParameter where T == Int16 { } extension RequestPathParameter where T == Int32 { - + public init(name: String?) { self.name = name self.wrappedValue = -1 @@ -69,7 +69,7 @@ extension RequestPathParameter where T == Int32 { } extension RequestPathParameter where T == Int64 { - + public init(name: String?) { self.name = name self.wrappedValue = -1 @@ -79,7 +79,7 @@ extension RequestPathParameter where T == Int64 { extension RequestPathParameter: Encodable where T: Encodable {} extension RequestPathParameter: ExpressibleByNilLiteral where T: ExpressibleByNilLiteral { - + public init(nilLiteral: ()) { self.wrappedValue = nil } @@ -88,18 +88,18 @@ extension RequestPathParameter: ExpressibleByNilLiteral where T: ExpressibleByNi extension RequestPathParameter: ExpressibleByStringLiteral, ExpressibleByExtendedGraphemeClusterLiteral, ExpressibleByUnicodeScalarLiteral where T == String { - + public typealias ExtendedGraphemeClusterLiteralType = String.ExtendedGraphemeClusterLiteralType public typealias UnicodeScalarLiteralType = String.UnicodeScalarLiteralType public typealias StringLiteralType = String.StringLiteralType - + public init(stringLiteral value: String) { wrappedValue = value } } extension RequestPathParameter: ExpressibleByIntegerLiteral where T == IntegerLiteralType { - + public init(integerLiteral value: IntegerLiteralType) { wrappedValue = value } diff --git a/Sources/Postie/Path/RequestPathParameterValue.swift b/Sources/Postie/Path/RequestPathParameterValue.swift index b16b715..0d6b907 100644 --- a/Sources/Postie/Path/RequestPathParameterValue.swift +++ b/Sources/Postie/Path/RequestPathParameterValue.swift @@ -1,5 +1,5 @@ public protocol RequestPathParameterValue { - + var serialized: String { get } } diff --git a/Sources/Postie/Requests/XMLRequest.swift b/Sources/Postie/Requests/XMLRequest.swift new file mode 100644 index 0000000..27525d6 --- /dev/null +++ b/Sources/Postie/Requests/XMLRequest.swift @@ -0,0 +1,2 @@ +/// Protocol indicating a given request should be encoded into an XML request +public typealias XMLRequest = Request & XMLEncodable diff --git a/Tests/PostieTests/RequestBodyCodingTests.swift b/Tests/PostieTests/RequestBodyCodingTests.swift index eae6199..dbc7100 100644 --- a/Tests/PostieTests/RequestBodyCodingTests.swift +++ b/Tests/PostieTests/RequestBodyCodingTests.swift @@ -196,4 +196,79 @@ class RequestBodyCodingTests: XCTestCase { } XCTAssertEqual(encoded.httpBody, "some string".data(using: .utf16)!) } + + // MARK: - XML + + func testEncoding_emptyXMLBody_shouldEncodeToSingleClosedXMLTagAndSetContentTypeHeader() { + struct Foo: XMLEncodable { + + struct Body: Encodable {} + typealias Response = EmptyResponse + + var body: Body + + } + + let request = Foo(body: Foo.Body()) + let encoder = RequestEncoder(baseURL: baseURL) + let encoded: URLRequest + do { + encoded = try encoder.encodeXML(request: request) + } catch { + XCTFail("Failed to encode: " + error.localizedDescription) + return + } + XCTAssertEqual(encoded.httpBody, "".data(using: .utf8)!) + XCTAssertEqual(encoded.value(forHTTPHeaderField: "Content-Type"), "text/xml") + } + + func testEncoding_customXMLContentTypeHeader_shouldUseCustomHeader() { + struct Foo: XMLEncodable { + + struct Body: Encodable {} + typealias Response = EmptyResponse + + var body: Body + + @RequestHeader(name: "Content-Type") var customContentTypeHeader + } + + var request = Foo(body: Foo.Body()) + request.customContentTypeHeader = "postie-test" + let encoder = RequestEncoder(baseURL: baseURL) + let encoded: URLRequest + do { + encoded = try encoder.encodeXML(request: request) + } catch { + XCTFail("Failed to encode: " + error.localizedDescription) + return + } + XCTAssertEqual(encoded.httpBody, "".data(using: .utf8)!) + XCTAssertEqual(encoded.value(forHTTPHeaderField: "Content-Type"), "postie-test") + } + + func testEncoding_nonEmptyXMLBody_shouldEncodeToValidXMLAndSetContentTypeHeader() { + struct Foo: XMLEncodable { + + struct Body: Encodable { + var value: Int + } + typealias Response = EmptyResponse + + var body: Body + + } + + let request = Foo(body: Foo.Body(value: 123)) + let encoder = RequestEncoder(baseURL: baseURL) + let encoded: URLRequest + do { + encoded = try encoder.encodeXML(request: request) + } catch { + XCTFail("Failed to encode: " + error.localizedDescription) + return + } + XCTAssertEqual(encoded.httpBody, "123".data(using: .utf8)!) + XCTAssertEqual(encoded.value(forHTTPHeaderField: "Content-Type"), "text/xml") + } } diff --git a/Tests/PostieTests/ResponseBodyCodingTests.swift b/Tests/PostieTests/ResponseBodyCodingTests.swift index fb08060..a77671b 100644 --- a/Tests/PostieTests/ResponseBodyCodingTests.swift +++ b/Tests/PostieTests/ResponseBodyCodingTests.swift @@ -193,4 +193,47 @@ class ResponseBodyCodingTests: XCTestCase { XCTAssertNotNil(decoded.body) XCTAssertEqual(decoded.body?.value, "asdf") } + + // MARK: - XML + + func testXMLResponseBodyDecoding_optionalContent_shouldDecodeNil() { + struct Response: Decodable { + + struct Body: XMLDecodable { + var value: String + } + + @ResponseBody.OptionalContent var body + } + let response = HTTPURLResponse(url: baseURL, statusCode: 204, httpVersion: nil, headerFields: nil)! + let data = Data() + let decoder = ResponseDecoder() + guard let decoded = CheckNoThrow(try decoder.decode(Response.self, from: (data, response))) else { + return + } + XCTAssertNil(decoded.body) + } + + func testXMLResponseBodyDecoding_valueInData_optionalContent_shouldDecodeFromData() { + struct Response: Decodable { + + struct Body: XMLDecodable { + var value: String + } + + @ResponseBody.OptionalContent var body + } + let response = HTTPURLResponse(url: baseURL, statusCode: 200, httpVersion: nil, headerFields: nil)! + let data = """ + + asdf + + """.data(using: .utf8)! + let decoder = ResponseDecoder() + guard let decoded = CheckNoThrow(try decoder.decode(Response.self, from: (data, response)), "Failed to decode response") else { + return + } + XCTAssertNotNil(decoded.body) + XCTAssertEqual(decoded.body?.value, "asdf") + } }