├── .swift-version
├── RetroluxTests
├── request.data
├── something.jpg
├── fieldsandpartscombined.data
├── testmultipleparts.data
├── RetroluxTests-Bridging-Header.h
├── Utils.swift
├── Sensitive.plist
├── HeaderTests.swift
├── ClientResponseExtensions.swift
├── Info.plist
├── QueryTests.swift
├── SwiftyJSONSerializer.swift
├── PathTests.swift
├── ClientInheritanceTests.swift
├── CustomSerializerTests.swift
├── TestingTests.swift
├── PropertyTypeTests.swift
├── AfterSerializationDeserializationTests.swift
├── URLEncodedSerializerTests.swift
├── MultipartFormDataSerializerTests.swift
├── DigestAuthTests.swift
├── InterpretedResponseTests.swift
├── PerformanceTests.swift
├── ReflectionTests.swift
├── TransformerTests.swift
├── ReflectionJSONSerializerTests.swift
├── ValueTransformerTests.swift
└── BuilderTests.swift
├── Retrolux.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ ├── xcbaselines
│ └── 34B8ED601E664A7C00030FAA.xcbaseline
│ │ ├── 2E64CC52-AEFA-4159-9A97-DAE0150BDDBA.plist
│ │ └── Info.plist
│ └── xcschemes
│ └── Retrolux.xcscheme
├── Retrolux
├── Task.swift
├── TransformerDirection.swift
├── InterpretedResponse.swift
├── BuilderArg.swift
├── MultipartEncodeable.swift
├── WrappedSerializerArg.swift
├── SelfApplyingArg.swift
├── Client.swift
├── CallFactory.swift
├── HTTPTask.swift
├── TransformerType.swift
├── URLEncodedBody.swift
├── Body.swift
├── Retrolux.h
├── HTTPCallFactory.swift
├── OptionalHelper.swift
├── Serializer.swift
├── Field.swift
├── Header.swift
├── Info.plist
├── URLTransformer.swift
├── OutboundSerializerType.swift
├── ClientResponse.swift
├── ResponseError.swift
├── HTTPClient.swift
├── DateTransformer.swift
├── Path.swift
├── ReflectableTransformer.swift
├── Query.swift
├── MultipartFormDataSerializer.swift
├── RetroluxError.swift
├── Part.swift
├── Call.swift
├── URLEncodedSerializer.swift
├── Property.swift
├── BuilderError.swift
├── Method.swift
├── PropertyConfig.swift
├── Reflection.swift
├── Response.swift
├── SerializationError.swift
├── ValueTransformer.swift
├── Reflectable.swift
├── ReflectionJSONSerializer.swift
├── ReflectionError.swift
├── PropertyType.swift
├── NestedTransformer.swift
└── Builder.swift
├── .gitignore
├── LICENSE
├── Retrolux.podspec
└── CHANGELOG
/.swift-version:
--------------------------------------------------------------------------------
1 | 3.1
2 |
--------------------------------------------------------------------------------
/RetroluxTests/request.data:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izeni-team/retrolux/HEAD/RetroluxTests/request.data
--------------------------------------------------------------------------------
/RetroluxTests/something.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izeni-team/retrolux/HEAD/RetroluxTests/something.jpg
--------------------------------------------------------------------------------
/RetroluxTests/fieldsandpartscombined.data:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izeni-team/retrolux/HEAD/RetroluxTests/fieldsandpartscombined.data
--------------------------------------------------------------------------------
/Retrolux.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Retrolux/Task.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPTaskProtocol.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 7/15/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Task {
12 | func resume()
13 | func cancel()
14 | }
15 |
--------------------------------------------------------------------------------
/RetroluxTests/testmultipleparts.data:
--------------------------------------------------------------------------------
1 | --alamofire.boundary.3ffb270bf5e2dc3b
2 | Content-Disposition: form-data; name="first_name"
3 |
4 | Bryan
5 | --alamofire.boundary.3ffb270bf5e2dc3b
6 | Content-Disposition: form-data; name="last_name"
7 |
8 | Henderson
9 | --alamofire.boundary.3ffb270bf5e2dc3b--
10 |
--------------------------------------------------------------------------------
/Retrolux/TransformerDirection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformerDirection.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/14/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum TransformerDirection {
12 | case serialize
13 | case deserialize
14 | }
15 |
--------------------------------------------------------------------------------
/Retrolux/InterpretedResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InterpretedResponse.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum InterpretedResponse {
12 | case success(T)
13 | case failure(Error)
14 | }
15 |
--------------------------------------------------------------------------------
/Retrolux/BuilderArg.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderArg.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 2/21/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct BuilderArg {
12 | public let type: Any.Type
13 | public let creation: Any?
14 | public let starting: Any?
15 | }
16 |
--------------------------------------------------------------------------------
/Retrolux/MultipartEncodeable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultipartEncodeable.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol MultipartEncodeable {
12 | static func encode(with arg: BuilderArg, using encoder: MultipartFormData)
13 | }
14 |
--------------------------------------------------------------------------------
/Retrolux/WrappedSerializerArg.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WrappedSerializerArg.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/9/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol WrappedSerializerArg {
12 | var value: Any? { get }
13 | var type: Any.Type { get }
14 | }
15 |
--------------------------------------------------------------------------------
/Retrolux/SelfApplyingArg.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelfApplyingArg.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/9/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol SelfApplyingArg {
12 | // TODO: Make this throw
13 | static func apply(arg: BuilderArg, to request: inout URLRequest)
14 | }
15 |
--------------------------------------------------------------------------------
/Retrolux/Client.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Client.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Client: class {
12 | func makeAsynchronousRequest(request: inout URLRequest, callback: @escaping (_ response: ClientResponse) -> Void) -> Task
13 | }
14 |
--------------------------------------------------------------------------------
/RetroluxTests/RetroluxTests-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // RetroluxTests-Bridging-Header.h
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/18/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | #ifndef RetroluxTests_Bridging_Header_h
10 | #define RetroluxTests_Bridging_Header_h
11 |
12 | #import
13 |
14 | #endif /* RetroluxTests_Bridging_Header_h */
15 |
--------------------------------------------------------------------------------
/Retrolux/CallFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CallFactory.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol CallFactory {
12 | func makeCall(capture: @escaping () -> RequestCapturedState, enqueue: @escaping CallEnqueueFunction, cancel: @escaping () -> Void) -> Call
13 | }
14 |
--------------------------------------------------------------------------------
/Retrolux/HTTPTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPTask.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct HTTPTask: Task {
12 | let task: URLSessionTask
13 |
14 | func resume() {
15 | task.resume()
16 | }
17 |
18 | func cancel() {
19 | task.cancel()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RetroluxTests/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 2/28/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Retrolux
11 |
12 | struct Utils {
13 | static let testImage = UIImage(named: "something.jpg", in: Bundle(for: BuilderTests.self), compatibleWith: nil)!
14 | }
15 |
16 | extension Builder {
17 | static let dummy = dry
18 | }
19 |
--------------------------------------------------------------------------------
/Retrolux/TransformerType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformerType.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/14/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol TransformerType: class {
12 | func supports(propertyType: PropertyType) -> Bool
13 |
14 | func set(value: Any?, for property: Property, instance: Reflectable) throws
15 | func value(for property: Property, instance: Reflectable) throws -> Any?
16 | }
17 |
--------------------------------------------------------------------------------
/Retrolux/URLEncodedBody.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLEncodedBody.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/2/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct URLEncodedBody {
12 | public let values: [(key: String, value: String)]
13 |
14 | public init() {
15 | values = []
16 | }
17 |
18 | public init(values: [(key: String, value: String)]) {
19 | self.values = values
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RetroluxTests/Sensitive.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | URL
6 |
7 | password
8 |
9 | username
10 |
11 | DigestRealm
12 |
13 | DigestPassword
14 |
15 | DigestUsername
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Retrolux/Body.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Body.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/9/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Body: WrappedSerializerArg {
12 | public let value: Any?
13 | public var type: Any.Type {
14 | return T.self
15 | }
16 |
17 | public init() {
18 | self.value = nil
19 | }
20 |
21 | public init(_ value: T) {
22 | self.value = value
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Retrolux/Retrolux.h:
--------------------------------------------------------------------------------
1 | //
2 | // Retrolux.h
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 2/28/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Retrolux.
12 | FOUNDATION_EXPORT double RetroluxVersionNumber;
13 |
14 | //! Project version string for Retrolux.
15 | FOUNDATION_EXPORT const unsigned char RetroluxVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Retrolux/HTTPCallFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPCallFactory.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct HTTPCallFactory: CallFactory {
12 | public init() {
13 |
14 | }
15 |
16 | public func makeCall(capture: @escaping () -> RequestCapturedState, enqueue: @escaping CallEnqueueFunction, cancel: @escaping () -> Void) -> Call {
17 | return Call(capture: capture, enqueue: enqueue, cancel: cancel)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/xcode
2 |
3 | ### Xcode ###
4 | # Xcode
5 | #
6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
7 |
8 | ## Build generated
9 | DerivedData/
10 |
11 | ## Various settings
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | xcuserdata/
21 |
22 | ## Other
23 | *.moved-aside
24 | *.xccheckout
25 | *.xcscmblueprint
26 | .DS_Store
27 |
28 | ## Pods
29 | Pods/
30 | Retrolux.xcworkspace/
31 |
--------------------------------------------------------------------------------
/Retrolux/OptionalHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OptionalHelper.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 2/21/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol OptionalHelper {
12 | var type: Any.Type { get }
13 | var value: Any? { get }
14 | }
15 |
16 | extension Optional: OptionalHelper {
17 | var type: Any.Type {
18 | return Wrapped.self
19 | }
20 |
21 | var value: Any? {
22 | if case Optional.some(let wrapped) = self {
23 | return wrapped
24 | }
25 | return nil
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Retrolux/Serializer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Serializer.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Serializer: class {}
12 |
13 | public protocol OutboundSerializer: Serializer {
14 | func supports(outboundType: Any.Type) -> Bool
15 | func apply(arguments: [BuilderArg], to request: inout URLRequest) throws
16 | }
17 |
18 | public protocol InboundSerializer: Serializer {
19 | func supports(inboundType: Any.Type) -> Bool
20 | func makeValue(from clientResponse: ClientResponse, type: T.Type) throws -> T
21 | }
22 |
--------------------------------------------------------------------------------
/Retrolux/Field.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Field.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/27/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Field: MultipartEncodeable {
12 | public let value: String
13 |
14 | public init(_ keyOrValue: String) {
15 | self.value = keyOrValue
16 | }
17 |
18 | public static func encode(with arg: BuilderArg, using encoder: MultipartFormData) {
19 | if let creation = arg.creation as? Field, let starting = arg.starting as? Field {
20 | encoder.append(starting.value.data(using: .utf8)!, withName: creation.value)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Retrolux/Header.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Header.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Header: SelfApplyingArg {
12 | private var value: String
13 |
14 | public init(_ nameOrValue: String) {
15 | self.value = nameOrValue
16 | }
17 |
18 | public static func apply(arg: BuilderArg, to request: inout URLRequest) {
19 | if let creation = arg.creation as? Header, let starting = arg.starting as? Header {
20 | request.addValue(starting.value, forHTTPHeaderField: creation.value)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/RetroluxTests/HeaderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeaderTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/27/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | class HeaderTests: XCTestCase {
14 | func testHeaders() {
15 | let request = Builder.dry().makeRequest(method: .get, endpoint: "", args: (Header("Content-Type"), Header("Custom3")), response: Void.self)
16 | let response = request((Header("test"), Header("test2"))).perform()
17 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type") == "test")
18 | XCTAssert(response.request.value(forHTTPHeaderField: "Custom3") == "test2")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Retrolux.xcodeproj/xcshareddata/xcbaselines/34B8ED601E664A7C00030FAA.xcbaseline/2E64CC52-AEFA-4159-9A97-DAE0150BDDBA.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | classNames
6 |
7 | PerformanceTests
8 |
9 | testReflectPerformance()
10 |
11 | com.apple.XCTPerformanceMetric_WallClockTime
12 |
13 | baselineAverage
14 | 0.24156
15 | baselineIntegrationDisplayName
16 | Local Baseline
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Retrolux/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Retrolux/URLTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLTransformer.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 7/12/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class URLTransformer: NestedTransformer {
12 | public typealias TypeOfProperty = URL
13 | public typealias TypeOfData = String
14 |
15 | public enum Error: Swift.Error {
16 | case invalidURL
17 | }
18 |
19 | public init() {
20 |
21 | }
22 |
23 | open func setter(_ dataValue: String, type: Any.Type) throws -> URL {
24 | guard let url = URL(string: dataValue) else {
25 | throw Error.invalidURL
26 | }
27 | return url
28 | }
29 |
30 | open func getter(_ propertyValue: URL) throws -> String {
31 | return propertyValue.absoluteString
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Retrolux/OutboundSerializerType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OutboundSerializerType.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 2/21/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum OutboundSerializerType {
12 | case auto
13 | case urlEncoded
14 | case multipart
15 | case json
16 | case custom(serializer: Any.Type)
17 |
18 | func isDesired(serializer: Any) -> Bool {
19 | switch self {
20 | case .auto:
21 | return true
22 | case .custom(serializer: let type):
23 | return type(of: serializer) == type
24 | case .json:
25 | return serializer is ReflectionJSONSerializer
26 | case .multipart:
27 | return serializer is MultipartFormDataSerializer
28 | case .urlEncoded:
29 | return serializer is URLEncodedSerializer
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/RetroluxTests/ClientResponseExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientResponseExtensions.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 |
12 | extension ClientResponse {
13 | init(base: ClientResponse, status: Int?, data: Data?, error: Error? = nil) {
14 | let response: URLResponse?
15 |
16 | if let httpUrlResponse = base.response as? HTTPURLResponse {
17 | response = HTTPURLResponse(
18 | url: httpUrlResponse.url!,
19 | statusCode: status ?? 0,
20 | httpVersion: "1.1",
21 | headerFields: httpUrlResponse.allHeaderFields as? [String: String]
22 | )
23 | } else {
24 | response = base.response
25 | }
26 |
27 | self.init(data: data, response: response, error: error)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/RetroluxTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | NSAppTransportSecurity
22 |
23 | NSAllowsArbitraryLoads
24 |
25 |
26 | CFBundleVersion
27 | 1
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Retrolux/ClientResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientResponse.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 12/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct ClientResponse {
12 | public let data: Data?
13 | public let response: URLResponse?
14 | public let error: Error?
15 |
16 | public var status: Int? {
17 | return (response as? HTTPURLResponse)?.statusCode
18 | }
19 |
20 | public init(data: Data?, response: URLResponse?, error: Error?) {
21 | self.data = data
22 | self.response = response
23 | self.error = error
24 | }
25 |
26 | public init(url: URL, data: Data? = nil, headers: [String: String] = [:], status: Int? = nil, error: Error? = nil) {
27 | self.data = data
28 | self.error = error
29 | self.response = HTTPURLResponse(url: url, statusCode: status ?? 0, httpVersion: "1.1", headerFields: headers)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/RetroluxTests/QueryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QueryTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | class QueryTests: XCTestCase {
14 | func testSingleQuery() {
15 | let request = Builder.dry().makeRequest(method: .post, endpoint: "whatever/", args: Query("name"), response: Void.self)
16 | let response = request(Query("value")).perform()
17 | XCTAssert(response.request.url?.absoluteString.hasSuffix("whatever/?name=value") == true)
18 | }
19 |
20 | func testMultipleQueries() {
21 | let request = Builder.dry().makeRequest(method: .get, endpoint: "whatever/", args: (Query("name"), Query("last")), response: Void.self)
22 | let response = request((Query("value"), Query("I wuv zis!="))).perform()
23 | XCTAssert(response.request.url?.absoluteString.hasSuffix("whatever/?name=value&last=I%20wuv%20zis!%3D") == true)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Start Studio, LLC
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Retrolux/ResponseError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResponseError.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ResponseError: RetroluxError {
12 | case invalidHttpStatusCode(code: Int?)
13 | case connectionError(Error)
14 |
15 | public var rl_error: RetroluxErrorDescription {
16 | switch self {
17 | case .invalidHttpStatusCode(code: let code):
18 | return RetroluxErrorDescription(
19 | description: code != nil ? "Unexpected HTTP status code, \(code!)." : "Expected an HTTP status code, but got no response.",
20 | suggestion: nil
21 | )
22 | case .connectionError(let error):
23 | return RetroluxErrorDescription(
24 | description: error.localizedDescription,
25 | suggestion: (error as NSError).localizedRecoverySuggestion ?? "Please check your Internet connection and try again."
26 | )
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Retrolux/HTTPClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPClient.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 7/15/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // TODO: Add support for ignoring SSL errors.
12 | open class HTTPClient: NSObject, Client, URLSessionDelegate, URLSessionTaskDelegate {
13 | open var session: URLSession!
14 |
15 | public override init() {
16 | super.init()
17 | session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
18 | }
19 |
20 | open func makeAsynchronousRequest(
21 | request: inout URLRequest,
22 | callback: @escaping (_ response: ClientResponse) -> Void
23 | ) -> Task
24 | {
25 | let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
26 | let clientResponse = ClientResponse(data: data, response: response, error: error)
27 | callback(clientResponse)
28 | })
29 | return HTTPTask(task: task)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Retrolux/DateTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateTransformer.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/16/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class DateTransformer: NestedTransformer {
12 | public typealias TypeOfProperty = Date
13 | public typealias TypeOfData = String
14 |
15 | public enum Error: Swift.Error {
16 | case invalidDateFormat
17 | }
18 |
19 | open let formatter: DateFormatter
20 |
21 | public init(format: String = "yyyy-MM-dd'T'hh:mm:ss.SSSZZZZ") {
22 | formatter = DateFormatter()
23 | formatter.dateFormat = format
24 | formatter.locale = Locale(identifier: "en_US")
25 | }
26 |
27 | open func setter(_ dataValue: String, type: Any.Type) throws -> Date {
28 | guard let date = formatter.date(from: dataValue) else {
29 | throw Error.invalidDateFormat
30 | }
31 | return date
32 | }
33 |
34 | open func getter(_ propertyValue: Date) throws -> String {
35 | return formatter.string(from: propertyValue)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Retrolux/Path.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Path.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/9/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Path: SelfApplyingArg, ExpressibleByStringLiteral {
12 | public let value: String
13 |
14 | public init(stringLiteral value: String) {
15 | self.value = value
16 | }
17 |
18 | public init(extendedGraphemeClusterLiteral value: String) {
19 | self.value = value
20 | }
21 |
22 | public init(unicodeScalarLiteral value: String) {
23 | self.value = value
24 | }
25 |
26 | public init(_ value: String) {
27 | self.value = value
28 | }
29 |
30 | public static func apply(arg: BuilderArg, to request: inout URLRequest) {
31 | if let creation = arg.creation as? Path, let starting = arg.starting as? Path {
32 | // TODO: Don't replace escaped variant. There has to be a better way...
33 | let token = "%7B" + creation.value + "%7D"
34 | request.url = URL(string: request.url!.absoluteString.replacingOccurrences(of: token, with: starting.value))!
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Retrolux.xcodeproj/xcshareddata/xcbaselines/34B8ED601E664A7C00030FAA.xcbaseline/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | runDestinationsByUUID
6 |
7 | 2E64CC52-AEFA-4159-9A97-DAE0150BDDBA
8 |
9 | localComputer
10 |
11 | busSpeedInMHz
12 | 100
13 | cpuCount
14 | 1
15 | cpuKind
16 | Intel Core i7
17 | cpuSpeedInMHz
18 | 2300
19 | logicalCPUCoresPerPackage
20 | 8
21 | modelCode
22 | Macmini6,2
23 | physicalCPUCoresPerPackage
24 | 4
25 | platformIdentifier
26 | com.apple.platform.macosx
27 |
28 | targetArchitecture
29 | x86_64
30 | targetDevice
31 |
32 | modelCode
33 | iPhone8,4
34 | platformIdentifier
35 | com.apple.platform.iphonesimulator
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Retrolux/ReflectableTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReflectableTransformer.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/16/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class ReflectableTransformer: NestedTransformer {
12 | public typealias TypeOfProperty = Reflectable
13 | public typealias TypeOfData = [String: Any]
14 |
15 | open weak var reflector: Reflector?
16 |
17 | public init(weakReflector: Reflector) {
18 | self.reflector = weakReflector
19 | }
20 |
21 | // We have to override this because of a Swift bug that prevents the default implementation
22 | // in NestedTransformer from working properly. As of Swift 3.1.0.
23 | open func supports(propertyType: PropertyType) -> Bool {
24 | if case .unknown(let type) = propertyType.bottom {
25 | return type is Reflectable.Type
26 | }
27 | return false
28 | }
29 |
30 | open func setter(_ dataValue: TypeOfData, type: Any.Type) throws -> TypeOfProperty {
31 | return try reflector!.convert(fromDictionary: dataValue, to: type as! TypeOfProperty.Type)
32 | }
33 |
34 | open func getter(_ propertyValue: TypeOfProperty) throws -> [String : Any] {
35 | return try reflector!.convertToDictionary(from: propertyValue)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Retrolux/Query.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Query.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Query: SelfApplyingArg {
12 | public let value: String
13 |
14 | public init(_ nameOrValue: String) {
15 | self.value = nameOrValue
16 | }
17 |
18 | public static func apply(arg: BuilderArg, to request: inout URLRequest) {
19 | if let creation = arg.creation as? Query, let starting = arg.starting as? Query {
20 | guard let url = request.url else {
21 | return
22 | }
23 |
24 | guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
25 | return
26 | }
27 |
28 | let name = creation.value
29 | let value = starting.value
30 |
31 | var queryItems = components.queryItems ?? []
32 | queryItems.append(URLQueryItem(name: name, value: value))
33 | components.queryItems = queryItems
34 |
35 | if let newUrl = components.url {
36 | request.url = newUrl
37 | } else {
38 | print("Error: Failed to apply query `\(name)=\(value)`")
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Retrolux/MultipartFormDataSerializer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultipartFormDataSerializer.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class MultipartFormDataSerializer: OutboundSerializer {
12 | public init() {
13 |
14 | }
15 |
16 | public func supports(outboundType: Any.Type) -> Bool {
17 | return outboundType is MultipartEncodeable.Type
18 | }
19 |
20 | public func validate(outbound: [BuilderArg]) -> Bool {
21 | return !outbound.contains { !supports(outboundType: $0.type) }
22 | }
23 |
24 | public func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
25 | let encoder = MultipartFormData()
26 |
27 | for arg in arguments {
28 | if let encodeableType = arg.type as? MultipartEncodeable.Type {
29 | encodeableType.encode(with: arg, using: encoder)
30 | }
31 | }
32 |
33 | let data = try encoder.encode()
34 | if data.count > 0 {
35 | request.httpBody = data
36 | request.setValue(encoder.contentType, forHTTPHeaderField: "Content-Type")
37 | request.setValue("\(encoder.contentLength as UInt64)", forHTTPHeaderField: "Content-Length")
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Retrolux/RetroluxError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RetroluxError.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/13/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct RetroluxErrorDescription {
12 | public let description: String
13 | public let suggestion: String?
14 |
15 | public init(description: String, suggestion: String?) {
16 | self.description = description
17 | self.suggestion = suggestion
18 | }
19 | }
20 |
21 | public protocol RetroluxError: LocalizedError, CustomStringConvertible, CustomDebugStringConvertible {
22 | var rl_error: RetroluxErrorDescription { get }
23 | }
24 |
25 | extension RetroluxError {
26 | public var description: String {
27 | let rl_error = self.rl_error
28 | if let suggestion = rl_error.suggestion {
29 | return "\(rl_error.description) \(suggestion)"
30 | }
31 | return rl_error.description
32 | }
33 |
34 | public var localizedDescription: String {
35 | return description
36 | }
37 |
38 | public var debugDescription: String {
39 | return description
40 | }
41 |
42 | public var errorDescription: String? {
43 | return rl_error.description
44 | }
45 |
46 | public var recoverySuggestion: String? {
47 | return rl_error.suggestion
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/RetroluxTests/SwiftyJSONSerializer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftyJSONSerializer.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 |
12 | enum SwiftyJSONSerializerError: Error {
13 | case invalidJSON
14 | }
15 |
16 | class SwiftyJSONSerializer: InboundSerializer, OutboundSerializer {
17 | func supports(inboundType: Any.Type) -> Bool {
18 | return inboundType is JSON.Type
19 | }
20 |
21 | func supports(outboundType: Any.Type) -> Bool {
22 | return outboundType is JSON.Type
23 | }
24 |
25 | func validate(outbound: [BuilderArg]) -> Bool {
26 | return outbound.count == 1 && outbound.first!.type is JSON.Type
27 | }
28 |
29 | func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
30 | let json = arguments.first!.starting as! JSON
31 | request.httpBody = try json.rawData()
32 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
33 | }
34 |
35 | func makeValue(from clientResponse: ClientResponse, type: T.Type) throws -> T {
36 | let result = JSON(data: clientResponse.data ?? Data())
37 | if result.object is NSNull {
38 | throw SwiftyJSONSerializerError.invalidJSON
39 | }
40 | return result as! T
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/RetroluxTests/PathTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PathTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | class PathTests: XCTestCase {
14 | func testSinglePath() {
15 | let request = Builder.dry().makeRequest(method: .get, endpoint: "whatever/{id}/", args: Path("id"), response: Void.self)
16 | let response = request(Path("some_id_thing")).perform()
17 | XCTAssert(response.request.url?.absoluteString.hasSuffix("whatever/some_id_thing/") == true)
18 | }
19 |
20 | func testMultiplePaths() {
21 | let request = Builder.dry().makeRequest(method: .get, endpoint: "whatever/{id}/{id2}/", args: (Path("id"), Path("id2")), response: Void.self)
22 | let response = request((Path("some_id_thing"), Path("another_id_thing"))).perform()
23 | XCTAssert(response.request.url?.absoluteString.hasSuffix("whatever/some_id_thing/another_id_thing/") == true)
24 | }
25 |
26 | func testStringLiteralConversion() {
27 | let request = Builder.dry().makeRequest(method: .get, endpoint: "whatever/{id}/{id2}/", args: ("id" as Path, Path("id2")), response: Void.self)
28 | let response = request(("some_id_thing", "another_id_thing")).perform()
29 | XCTAssert(response.request.url?.absoluteString.hasSuffix("whatever/some_id_thing/another_id_thing/") == true)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/RetroluxTests/ClientInheritanceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientInheritanceTests.swift
3 | // RetroluxTests
4 | //
5 | // Created by Christopher Bryan Henderson on 10/16/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | class ClientInheritanceTests: XCTestCase {
14 | func testInheritance() {
15 | class TestHTTPClient: HTTPClient {
16 | override init() {
17 | super.init()
18 | let configuration = URLSessionConfiguration.default
19 | configuration.timeoutIntervalForRequest = 5349786
20 | session = URLSession.init(configuration: configuration)
21 | }
22 | }
23 |
24 | let builder1 = Builder.dry()
25 | builder1.client = TestHTTPClient()
26 | let request1 = builder1.makeRequest(method: .post, endpoint: "endpoint", args: (), response: Void.self)
27 | _ = request1().perform()
28 |
29 | let builder2 = Builder.dry()
30 | builder2.client = HTTPClient()
31 | let request2 = builder2.makeRequest(method: .post, endpoint: "endpoint", args: (), response: Void.self)
32 | _ = request2().perform()
33 |
34 | let client1 = builder1.client as! HTTPClient
35 | XCTAssert(client1.session.configuration.timeoutIntervalForRequest == 5349786)
36 | let client2 = builder2.client as! HTTPClient
37 | XCTAssert(client2.session.configuration.timeoutIntervalForRequest != 5349786)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Retrolux/Part.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Part.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct Part: MultipartEncodeable {
12 | private var mimeType: String?
13 | private var filename: String?
14 | private var name: String?
15 | private var data: Data?
16 |
17 | public init(name: String, filename: String, mimeType: String) {
18 | self.name = name
19 | self.filename = filename
20 | self.mimeType = mimeType
21 | }
22 |
23 | public init(name: String, mimeType: String) {
24 | self.name = name
25 | self.mimeType = mimeType
26 | }
27 |
28 | public init(name: String) {
29 | self.name = name
30 | }
31 |
32 | public init(_ data: Data) {
33 | self.data = data
34 | }
35 |
36 | public init(_ string: String) {
37 | self.data = string.data(using: .utf8)!
38 | }
39 |
40 | public static func encode(with arg: BuilderArg, using encoder: MultipartFormData) {
41 | if let creation = arg.creation as? Part, let starting = arg.starting as? Part {
42 | if let filename = creation.filename, let mimeType = creation.mimeType {
43 | encoder.append(starting.data!, withName: creation.name!, fileName: filename, mimeType: mimeType)
44 | } else if let mimeType = creation.mimeType {
45 | encoder.append(starting.data!, withName: creation.name!, mimeType: mimeType)
46 | } else {
47 | encoder.append(starting.data!, withName: creation.name!)
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Retrolux/Call.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPCall.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public typealias CallEnqueueFunction = (RequestCapturedState, @escaping (Response) -> Void) -> Void
12 |
13 | open class Call {
14 | fileprivate var delegatedCapture: () -> RequestCapturedState
15 | fileprivate var delegatedEnqueue: CallEnqueueFunction
16 | fileprivate var delegatedCancel: () -> Void
17 |
18 | public init(capture: @escaping () -> RequestCapturedState, enqueue: @escaping CallEnqueueFunction, cancel: @escaping () -> Void) {
19 | self.delegatedCapture = capture
20 | self.delegatedEnqueue = enqueue
21 | self.delegatedCancel = cancel
22 | }
23 |
24 | open func perform() -> Response {
25 | let state = delegatedCapture()
26 | let semaphore = DispatchSemaphore(value: 0)
27 | var capturedResponse: Response!
28 | delegatedEnqueue(state) { (response: Response) in
29 | capturedResponse = response
30 | semaphore.signal()
31 | }
32 | semaphore.wait()
33 | return capturedResponse
34 | }
35 |
36 | open func enqueue(queue: DispatchQueue = .main, callback: @escaping (Response) -> Void) {
37 | let state = delegatedCapture()
38 |
39 | state.workerQueue.async {
40 | self.delegatedEnqueue(state) { response in
41 | queue.async {
42 | callback(response)
43 | }
44 | }
45 | }
46 | }
47 |
48 | open func cancel() {
49 | delegatedCancel()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/RetroluxTests/CustomSerializerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomSerializerTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 | import XCTest
12 |
13 | class CustomSerializerTests: XCTestCase {
14 | func testCustomSerializer() {
15 | let builder = Builder.dry()
16 | builder.serializers.append(SwiftyJSONSerializer())
17 | let function = builder.makeRequest(
18 | method: .post,
19 | endpoint: "login/",
20 | args: JSON([:]),
21 | response: JSON.self
22 | )
23 | builder.responseInterceptor = { response in
24 | response = ClientResponse(base: response, status: 200, data: "{\"token\":\"123\",\"id\":\"abc\"}".data(using: .utf8)!)
25 | }
26 |
27 | let response = function(JSON(["username": "bobby", "password": "abc123"])).perform()
28 | let requestDictionary = try! JSONSerialization.jsonObject(with: response.request.httpBody!, options: []) as! [String: Any]
29 | XCTAssert(requestDictionary.keys.count == 2)
30 | XCTAssert(requestDictionary["username"] as? String == "bobby")
31 | XCTAssert(requestDictionary["username"] as? String == "bobby")
32 |
33 | let responseDictionary = try! JSONSerialization.jsonObject(with: response.data!, options: []) as! [String: Any]
34 | XCTAssert(responseDictionary.keys.count == 2)
35 | XCTAssert(responseDictionary["token"] as? String == "123")
36 | XCTAssert(responseDictionary["id"] as? String == "abc")
37 |
38 | XCTAssert(response.body!["token"] == "123")
39 | XCTAssert(response.body!["id"] == "abc")
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Retrolux/URLEncodedSerializer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLEncodedSerializer.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 12/22/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class URLEncodedSerializer: OutboundSerializer {
12 | public init() {
13 |
14 | }
15 |
16 | public func supports(outboundType: Any.Type) -> Bool {
17 | return outboundType is Field.Type || outboundType is URLEncodedBody.Type
18 | }
19 |
20 | public func validate(outbound: [BuilderArg]) -> Bool {
21 | if outbound.isEmpty {
22 | return false
23 | }
24 |
25 | return !outbound.contains { !supports(outboundType: $0.type) }
26 | }
27 |
28 | public func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
29 | var items: [URLQueryItem] = []
30 | for arg in arguments {
31 | if let body = arg.starting as? URLEncodedBody {
32 | items.append(contentsOf: body.values.map { URLQueryItem(name: $0, value: $1) })
33 | } else if arg.type is Field.Type {
34 | if let creation = arg.creation as? Field, let starting = arg.starting as? Field {
35 | items.append(URLQueryItem(name: creation.value, value: starting.value))
36 | }
37 | } else {
38 | fatalError("Unknown/unsupported type \(type(of: arg))")
39 | }
40 | }
41 |
42 | var components = URLComponents()
43 | components.queryItems = items
44 | let string = components.percentEncodedQuery
45 | let data = string?.data(using: .utf8)
46 | request.httpBody = data
47 | request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Retrolux/Property.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Property.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 7/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Though this class is hashable and equatable, do note that if two properties are the same on two different
12 | // classes, they will be considered "equal."
13 | open class Property {
14 | // This is a recursive enum that describes the type of the property.
15 | open let type: PropertyType
16 |
17 | // This is the key name of the property as it was typed on the class itself.
18 | open let name: String
19 |
20 | open let options: [PropertyConfig.Option]
21 |
22 | open var ignored: Bool {
23 | for option in options {
24 | if case .ignored = option {
25 | return true
26 | }
27 | }
28 | return false
29 | }
30 |
31 | open var serializedName: String {
32 | var mappedTo = name
33 | for option in options {
34 | if case .serializedName(let newName) = option {
35 | mappedTo = newName
36 | }
37 | }
38 | return mappedTo
39 | }
40 |
41 | open var transformer: TransformerType? {
42 | for option in options {
43 | if case PropertyConfig.Option.transformed(let transformer) = option {
44 | return transformer
45 | }
46 | }
47 | return nil
48 | }
49 |
50 | open var nullable: Bool {
51 | for option in options {
52 | if case .nullable = option {
53 | return true
54 | }
55 | }
56 | return false
57 | }
58 |
59 | public init(type: PropertyType, name: String, options: [PropertyConfig.Option]) {
60 | self.type = type
61 | self.name = name
62 | self.options = options
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Retrolux/BuilderError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderError.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum BuilderResponseError: RetroluxError {
12 | case deserializationError(serializer: InboundSerializer, error: Error, clientResponse: ClientResponse)
13 |
14 | public var rl_error: RetroluxErrorDescription {
15 | switch self {
16 | case .deserializationError(serializer: let serializer, error: let error, clientResponse: _):
17 | return RetroluxErrorDescription(
18 | description: "The deserialization error, \(error), was thrown by \(type(of: serializer)).",
19 | suggestion: "Check the inbound serializer's documentation and/or source code to figure out why it doesn't like the remote data that was received."
20 | )
21 | }
22 | }
23 | }
24 |
25 | public enum BuilderError: RetroluxError {
26 | case unsupportedArgument(BuilderArg)
27 | case serializationError(serializer: OutboundSerializer, error: Error, arguments: [BuilderArg])
28 |
29 | public var rl_error: RetroluxErrorDescription {
30 | switch self {
31 | case .unsupportedArgument(let arg):
32 | return RetroluxErrorDescription(
33 | description: "Unsupported argument type passed into builder: \(arg.type).",
34 | suggestion: "Either pass in a supported type, such as a Path, Query, Header, Body, or a serializer argument, or add a serializer that supports your argument type, \(arg.type)."
35 | )
36 | case .serializationError(serializer: let serializer, error: let error, arguments: _):
37 | return RetroluxErrorDescription(
38 | description: "The serialization error, \(error), was thrown by \(type(of: serializer)).",
39 | suggestion: "Check the serializer's documentation and/or source code to figure out why it doesn't like the argument(s) you passed into it."
40 | )
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Retrolux/Method.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Method.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // This was taken from Alamofire.
12 | // https://github.com/Alamofire/Alamofire/blob/0ac38d7e312e87aeea608d384e44401f3d8a6b3d/Source/ParameterEncoding.swift
13 |
14 | //
15 | // ParameterEncoding.swift
16 | //
17 | // Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/)
18 | //
19 | // Permission is hereby granted, free of charge, to any person obtaining a copy
20 | // of this software and associated documentation files (the "Software"), to deal
21 | // in the Software without restriction, including without limitation the rights
22 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23 | // copies of the Software, and to permit persons to whom the Software is
24 | // furnished to do so, subject to the following conditions:
25 | //
26 | // The above copyright notice and this permission notice shall be included in
27 | // all copies or substantial portions of the Software.
28 | //
29 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35 | // THE SOFTWARE.
36 | //
37 |
38 | /// HTTP method definitions.
39 | ///
40 | /// See https://tools.ietf.org/html/rfc7231#section-4.3
41 | public enum HTTPMethod: String {
42 | case options = "OPTIONS"
43 | case get = "GET"
44 | case head = "HEAD"
45 | case post = "POST"
46 | case put = "PUT"
47 | case patch = "PATCH"
48 | case delete = "DELETE"
49 | case trace = "TRACE"
50 | case connect = "CONNECT"
51 | }
52 |
--------------------------------------------------------------------------------
/RetroluxTests/TestingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestingTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/5/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 | import XCTest
12 |
13 | class TestingTests: XCTestCase {
14 | func testTest1() {
15 | class Person: Reflection {
16 | var name = ""
17 | }
18 |
19 | let builder = Builder.dry()
20 | XCTAssert(builder.isDryModeEnabled == true)
21 | XCTAssert(Builder(base: URL(string: "https://www.google.com/")!).isDryModeEnabled == false)
22 |
23 | let createUser = builder.makeRequest(
24 | method: .post,
25 | endpoint: "users/",
26 | args: Person(),
27 | response: Person.self,
28 | testProvider: { (creation, starting, request) in
29 | ClientResponse(
30 | url: request.url!,
31 | data: "{\"name\":\"\(starting.name)\"}".data(using: .utf8)!,
32 | headers: [:],
33 | status: 200,
34 | error: nil
35 | )
36 | }
37 | )
38 |
39 | let newPerson = Person()
40 | newPerson.name = "George"
41 | var response = createUser(newPerson).perform()
42 | XCTAssert(response.status == 200)
43 | XCTAssert(response.error == nil)
44 | XCTAssert(response.body!.name == "George")
45 | XCTAssert(response.headers.isEmpty)
46 | XCTAssert(response.isSuccessful)
47 |
48 | let wetBuilder = Builder(base: URL(string: "8.8.8.8/")!)
49 | XCTAssert(wetBuilder.isDryModeEnabled == false)
50 | let wetCreateUser = wetBuilder.makeRequest(
51 | method: .post,
52 | endpoint: "users/",
53 | args: Person(),
54 | response: Person.self
55 | )
56 |
57 | response = wetCreateUser(newPerson).perform()
58 | XCTAssert(response.body?.name == nil)
59 | XCTAssert(response.isSuccessful == false)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Retrolux.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint Retrolux.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'Retrolux'
11 | s.version = '0.10.4'
12 | s.summary = 'An all in one networking solution, like Retrofit.'
13 |
14 | # This description is used to generate tags and improve search results.
15 | # * Think: What does it do? Why did you write it? What is the focus?
16 | # * Try to keep it short, snappy and to the point.
17 | # * Write the description between the DESC delimiters below.
18 | # * Finally, don't worry about the indent, CocoaPods strips it!
19 |
20 | s.description = <<-DESC
21 | There are many good networking libraries out there already for iOS. Alamofire, AFNetworking, and Moya, etc., are all great libraries.
22 |
23 | What makes this framework unique is that each endpoint can be consicely described and implemented. No subclassing, protocol implementations, functions to implement, or extra modules to download. It comes with JSON, Multipart, and URL Encoding support out of the box. In short, it aims to optimize, as much as possible, the end-to-end process of network API consumption.
24 |
25 | The purpose of this framework is not just to abstract away networking details, but also to provide a Retrofit-like workflow, where endpoints can be described--not implemented.
26 | DESC
27 |
28 | s.homepage = 'https://github.com/izeni-team/retrolux'
29 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
30 | s.license = { :type => 'MIT', :file => 'LICENSE' }
31 | s.author = { 'Bryan Henderson' => 'bhenderson@startstudio.com' }
32 | s.source = { :git => 'https://github.com/izeni-team/retrolux.git', :tag => 'v%s' % [s.version.to_s] }
33 | # s.social_media_url = 'https://twitter.com/cbh2000'
34 |
35 | s.ios.deployment_target = '8.0'
36 |
37 | s.source_files = 'Retrolux/**/*'
38 |
39 | # s.resource_bundles = {
40 | # 'Retrolux' => ['Retrolux/Assets/*.png']
41 | # }
42 |
43 | # s.public_header_files = 'Pod/Classes/**/*.h'
44 | # s.frameworks = 'UIKit', 'MapKit'
45 | # s.dependency 'AFNetworking', '~> 2.3'
46 | end
47 |
--------------------------------------------------------------------------------
/Retrolux/PropertyConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PropertyConfig.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/14/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum PropertyConfigValidationError: RetroluxError {
12 | case cannotSetOptionsForNonExistantProperty(propertyName: String, forClass: Any.Type)
13 | case serializedNameAlreadyTaken(propertyName: String, alreadyTakenBy: String, serializedName: String, onClass: Any.Type)
14 |
15 | public var rl_error: RetroluxErrorDescription {
16 | switch self {
17 | case .cannotSetOptionsForNonExistantProperty(propertyName: let propertyName, forClass: let `class`):
18 | return RetroluxErrorDescription(
19 | description: "Cannot set options for non-existant property '\(propertyName)' on \(`class`).",
20 | suggestion: "Either fix name of the property '\(propertyName)' on \(`class`), or remove it from the config."
21 | )
22 | case .serializedNameAlreadyTaken(propertyName: let propertyName, alreadyTakenBy: let alreadyTakenBy, serializedName: let serializedName, onClass: let `class`):
23 | return RetroluxErrorDescription(
24 | description: "Cannot set serialized name of property '\(propertyName)' on \(`class`) to \"\(serializedName)\", because the property '\(alreadyTakenBy)' is already set to \"\(serializedName)\"!",
25 | suggestion: "Change the serialized name of either '\(propertyName)' or '\(alreadyTakenBy)' on \(`class`), or remove one of the properties."
26 | )
27 | }
28 | }
29 | }
30 |
31 | public class PropertyConfig {
32 | public enum Option {
33 | case ignored
34 | case nullable
35 | case serializedName(String)
36 | case transformed(TransformerType)
37 | }
38 |
39 | public var storage: [String: [Option]] = [:]
40 | public var validator: (PropertyConfig, String, [Option]) throws -> Void
41 |
42 | public init(validator: @escaping (PropertyConfig, String, [Option]) throws -> Void = { _ in }) {
43 | self.validator = validator
44 | }
45 |
46 | public subscript(name: String) -> [Option] {
47 | get {
48 | return storage[name] ?? []
49 | }
50 | set {
51 | do {
52 | try validator(self, name, newValue)
53 | storage[name] = newValue
54 | } catch {
55 | assert(false, error.localizedDescription)
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/RetroluxTests/PropertyTypeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PropertyTypeTests.swift
3 | // RetroluxReflector
4 | //
5 | // Created by Christopher Bryan Henderson on 10/17/16.
6 | // Copyright © 2016 Christopher Bryan Henderson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Retrolux
11 |
12 | class PropertyTypeTests: XCTestCase {
13 | func testPropertyTypeInference() {
14 | XCTAssert(PropertyType.from(AnyObject.self) == .anyObject)
15 | XCTAssert(PropertyType.from(Optional.self) == .optional(.number(Int.self)))
16 | XCTAssert(PropertyType.from(Bool.self) == .bool)
17 | XCTAssert(PropertyType.from(Int.self) == .number(Int.self))
18 | XCTAssert(PropertyType.from(Double.self) == .number(Double.self))
19 | XCTAssert(PropertyType.from(Reflection.self) == .unknown(Reflection.self))
20 |
21 | XCTAssert(PropertyType.from([Int?].self) == .array(.optional(.number(Int.self))))
22 | XCTAssert(PropertyType.from([String: Int?].self) == .dictionary(.optional(.number(Int.self))))
23 | XCTAssert(PropertyType.from(NSDictionary.self) == .dictionary(.anyObject))
24 | XCTAssert(PropertyType.from(NSMutableDictionary.self) == .dictionary(.anyObject))
25 | let jsonDictionaryData = "{\"test\": true}".data(using: String.Encoding.utf8)!
26 | let jsonDictionaryType: Any.Type = try! type(of: (JSONSerialization.jsonObject(with: jsonDictionaryData, options: [])))
27 | XCTAssert(PropertyType.from(jsonDictionaryType) == .dictionary(.anyObject))
28 | XCTAssert(PropertyType.from(NSArray.self) == .array(.anyObject))
29 | XCTAssert(PropertyType.from(NSMutableArray.self) == .array(.anyObject))
30 | let jsonArrayData = "[1, 2, 3]".data(using: String.Encoding.utf8)!
31 | let jsonArrayType: Any.Type = try! type(of: (JSONSerialization.jsonObject(with: jsonArrayData, options: [])))
32 | XCTAssert(PropertyType.from(jsonArrayType) == .array(.anyObject))
33 |
34 | class Object2: Reflection {}
35 |
36 | class Object1: Reflection {
37 | var test = ""
38 | var test2 = [String: [Int]]()
39 | var test3 = [Object2]()
40 | }
41 |
42 | do {
43 | let properties = try Reflector().reflect(Object1())
44 | let propertyNames = properties.map({ $0.name })
45 | XCTAssert(propertyNames == [
46 | "test",
47 | "test2",
48 | "test3"
49 | ])
50 | let propertyTypes = properties.map({ $0.type })
51 | XCTAssert(propertyTypes == [
52 | PropertyType.string,
53 | PropertyType.dictionary(.array(.number(Int.self))),
54 | PropertyType.array(.unknown(Object2.self))
55 | ])
56 | } catch let error {
57 | XCTFail("\(error)")
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Retrolux/Reflection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reflection.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 7/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Subclassing Reflectable objects exposes a limitation of Swift protocols:
12 | // Consider the example:
13 | //
14 | // class Base: NSObject, Reflectable {
15 | // /* Nothing */
16 | // }
17 | //
18 | // class Person: Base {
19 | // ...
20 | // var ignored = SomeUnsupportedType()
21 | // ...
22 | // static var ignoredProperties: [String] {
23 | // return ["ignored"]
24 | // }
25 | // }
26 | //
27 | // In the above example, the reflector will be unable to read Person.ignoredProperties.
28 | // In order for it to work properly, you have to make sure it's implemented in the base
29 | // class and overridden in the subclass, like this:
30 | //
31 | // class Base: NSObject, Reflectable {
32 | // class var ignoredProperties: [String] {
33 | // return []
34 | // }
35 | // }
36 | //
37 | // class Person: Base {
38 | // ...
39 | // var ignored = SomeUnsupportedType()
40 | // ...
41 | // override class var ignoredProperties: [String] {
42 | // return ["ignored"]
43 | // }
44 | // }
45 | //
46 | // Of the two examples provided here, the first won't work, but the second will. If you understand the
47 | // risks and want to create your own custom base class, make your base class conform to
48 | // ReflectableSubclassingIsAllowed, which will tell the reflector to allow subclassing for your custom
49 | // type. This protocol is here for safety and to prevent people from shooting themselves in the foot
50 | // unknowingly when their functions don't get called properly.
51 | public protocol ReflectableSubclassingIsAllowed {}
52 |
53 | // This class only exists to eliminate need of overriding init() and to aide in subclassing.
54 | // It's technically possible to subclass without subclassing Reflection, but it was disabled to prevent
55 | // hard-to-find corner cases that might crop up. In particular, the issue where protocols with default implementations
56 | // and subclassing doesn't work well together (default implementation will be used in some cases even if you provide
57 | // your own implementation later down the road).
58 | open class Reflection: NSObject, Reflectable, ReflectableSubclassingIsAllowed {
59 | public required override init() {
60 | super.init()
61 | }
62 |
63 | open func validate() throws {
64 |
65 | }
66 |
67 | open class func config(_ c: PropertyConfig) {
68 |
69 | }
70 |
71 | open func afterDeserialization(remoteData: [String : Any]) throws {
72 |
73 | }
74 |
75 | open func afterSerialization(remoteData: inout [String : Any]) throws {
76 |
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/RetroluxTests/AfterSerializationDeserializationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AfterSerializationDeserializationTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 7/16/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 | import XCTest
12 |
13 | class AfterSerializationDeserializationTests: XCTestCase {
14 | class Test1: Reflection {
15 | var test_one = ""
16 | }
17 |
18 | class Test2: Reflection {
19 | var test_two = ""
20 | }
21 |
22 | class ResultsResponse: Reflection {
23 | var results: [T] = []
24 |
25 | override func afterDeserialization(remoteData: [String : Any]) throws {
26 | results = try Reflector.shared.convert(fromArray: remoteData["results"] as? [[String: Any]] ?? [], to: T.self) as! [T]
27 | }
28 |
29 | override func afterSerialization(remoteData: inout [String: Any]) throws {
30 | remoteData["results"] = try Reflector.shared.convertToArray(from: results)
31 | }
32 |
33 | override class func config(_ c: PropertyConfig) {
34 | c["results"] = [.ignored]
35 | }
36 | }
37 |
38 | func testAfterSerializationAndDeserialization() {
39 | let builder = Builder.dry()
40 |
41 | let request1 = builder.makeRequest(method: .get, endpoint: "", args: ResultsResponse(), response: ResultsResponse.self) {
42 | let data = "{\"results\":[{\"test_one\":\"SUCCESS_1\"}]}".data(using: .utf8)!
43 | return ClientResponse(url: $0.2.url!, data: data, headers: [:], status: 200, error: nil)
44 | }
45 | let body2 = ResultsResponse()
46 | let test2 = Test2()
47 | test2.test_two = "SEND_2"
48 | body2.results = [
49 | test2
50 | ]
51 | let response1 = request1(body2).perform()
52 | XCTAssert(response1.request.httpBody == "{\"results\":[{\"test_two\":\"SEND_2\"}]}".data(using: .utf8)!)
53 | XCTAssert(response1.body?.results.first?.test_one == "SUCCESS_1")
54 |
55 | let request2 = builder.makeRequest(method: .get, endpoint: "", args: ResultsResponse(), response: ResultsResponse.self) {
56 | let data = "{\"results\":[{\"test_two\":\"SUCCESS_2\"}]}".data(using: .utf8)!
57 | return ClientResponse(url: $0.2.url!, data: data, headers: [:], status: 200, error: nil)
58 | }
59 | let body1 = ResultsResponse()
60 | let test1 = Test1()
61 | test1.test_one = "SEND_1"
62 | body1.results = [
63 | test1
64 | ]
65 | let response2 = request2(body1).perform()
66 | XCTAssert(response2.request.httpBody == "{\"results\":[{\"test_one\":\"SEND_1\"}]}".data(using: .utf8)!)
67 | XCTAssert(response2.body?.results.first?.test_two == "SUCCESS_2")
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/RetroluxTests/URLEncodedSerializerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLEncodedSerializerTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/2/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | class URLEncodedSerializerTests: XCTestCase {
14 | func testSerializer() {
15 | let request = Builder.dry().makeRequest(method: .post, endpoint: "test", args: Body(), response: Void.self)
16 | let body = URLEncodedBody(values: [
17 | ("Hello", "3"),
18 | ("Another", "Way"),
19 | ("Test", "Yay!"),
20 | ("3+3=", "24/4"),
21 | ("Another", ""),
22 | ("", ""),
23 | ("Misc", "!@#$%^&*()=:/? \"'")
24 | ])
25 | let response = request(Body(body)).perform()
26 | XCTAssert(response.request.httpBody! == "Hello=3&Another=Way&Test=Yay!&3+3%3D=24/4&Another=&=&Misc=!@%23$%25%5E%26*()%3D:/?%20%22'".data(using: .utf8)!)
27 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type") == "application/x-www-form-urlencoded")
28 | }
29 |
30 | func testSerializerWithNoValues() {
31 | let request = Builder.dry().makeRequest(method: .post, endpoint: "test", args: Body(), response: Void.self)
32 | let body = URLEncodedBody(values: [])
33 | let response = request(Body(body)).perform()
34 | XCTAssert(response.request.httpBody! == "".data(using: .utf8)!)
35 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type") == "application/x-www-form-urlencoded")
36 | }
37 |
38 | func testWithoutBodyWrapper() {
39 | let request = Builder.dry().makeRequest(method: .post, endpoint: "test", args: URLEncodedBody(), response: Void.self)
40 | let body = URLEncodedBody(values: [
41 | ("Hello", "3"),
42 | ("Another", "Way"),
43 | ("Test", "Yay!"),
44 | ("3+3=", "24/4"),
45 | ("Another", ""),
46 | ("", ""),
47 | ("Misc", "!@#$%^&*()=:/? \"'")
48 | ])
49 | let response = request(body).perform()
50 | XCTAssert(response.request.httpBody! == "Hello=3&Another=Way&Test=Yay!&3+3%3D=24/4&Another=&=&Misc=!@%23$%25%5E%26*()%3D:/?%20%22'".data(using: .utf8)!)
51 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type") == "application/x-www-form-urlencoded")
52 | }
53 |
54 | func testFields() {
55 | let request = Builder.dry().makeRequest(type: .urlEncoded, method: .post, endpoint: "test", args: (Field("first_name"), Field("last_name")), response: Void.self)
56 | let response = request((Field("Christopher Bryan"), Field("Henderson"))).perform()
57 | XCTAssert(response.request.httpBody! == "first_name=Christopher%20Bryan&last_name=Henderson".data(using: .utf8)!)
58 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type") == "application/x-www-form-urlencoded")
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Retrolux/Response.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Response.swift
3 | // Retrolux
4 | //
5 | // Created by Mitchell Tenney on 9/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class UninterpretedResponse {
12 | // Do we want the NSURLRequest or NSHTTPURLResponse?
13 | public let request: URLRequest
14 | public let data: Data?
15 | public let error: Error?
16 | public let urlResponse: URLResponse?
17 | public let body: T?
18 |
19 | public var isSuccessful: Bool {
20 | return body != nil && isHttpStatusOk
21 | }
22 |
23 | public var isHttpStatusOk: Bool {
24 | return (200...299).contains(status ?? 0)
25 | }
26 |
27 | public var httpUrlResponse: HTTPURLResponse? {
28 | return urlResponse as? HTTPURLResponse
29 | }
30 |
31 | public var status: Int? {
32 | return httpUrlResponse?.statusCode
33 | }
34 |
35 | public var headers: [String: String] {
36 | return (httpUrlResponse?.allHeaderFields as? [String: String]) ?? [:]
37 | }
38 |
39 | public init(
40 | request: URLRequest,
41 | data: Data?,
42 | error: Error?,
43 | urlResponse: URLResponse?,
44 | body: T?
45 | )
46 | {
47 | self.request = request
48 | self.data = data
49 | self.error = error
50 | self.urlResponse = urlResponse
51 | self.body = body
52 | }
53 | }
54 |
55 | public class Response: UninterpretedResponse {
56 | // Do we want the NSURLRequest or NSHTTPURLResponse?
57 | public let interpreted: InterpretedResponse
58 |
59 | internal convenience init(
60 | request: URLRequest,
61 | data: Data?,
62 | error: Error?,
63 | urlResponse: URLResponse?,
64 | body: T?,
65 | interpreter: (UninterpretedResponse) -> InterpretedResponse
66 | )
67 | {
68 | let uninterpreted = UninterpretedResponse(
69 | request: request,
70 | data: data,
71 | error: error,
72 | urlResponse: urlResponse,
73 | body: body
74 | )
75 | self.init(
76 | request: request,
77 | data: data,
78 | error: error,
79 | urlResponse: urlResponse,
80 | body: body,
81 | interpreted: interpreter(uninterpreted)
82 | )
83 | }
84 |
85 | public init(
86 | request: URLRequest,
87 | data: Data?,
88 | error: Error?,
89 | urlResponse: URLResponse?,
90 | body: T?,
91 | interpreted _interpreted: InterpretedResponse
92 | )
93 | {
94 | interpreted = _interpreted
95 | super.init(
96 | request: request,
97 | data: data,
98 | error: error,
99 | urlResponse: urlResponse,
100 | body: body
101 | )
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Retrolux/SerializationError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReflectorReflectorSerializationError.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ReflectorSerializationError: RetroluxError {
12 | case keyNotFound(propertyName: String, key: String, forClass: Any.Type)
13 | case propertyDoesNotSupportNullValues(propertyName: String, forClass: Any.Type)
14 | case typeMismatch(expected: PropertyType, got: Any.Type?, propertyName: String, forClass: Any.Type)
15 | case expectedDictionaryRootButGotArrayRoot(type: Any.Type)
16 | case expectedArrayRootButGotDictionaryRoot(type: Any.Type)
17 | case invalidJSONData(Error)
18 |
19 | public var rl_error: RetroluxErrorDescription {
20 | switch self {
21 | case .keyNotFound(propertyName: let propertyName, key: let key, forClass: let `class`):
22 | return RetroluxErrorDescription(
23 | description: "Could not find the key '\(key)' in data for the property '\(propertyName)' on \(`class`).",
24 | suggestion: nil // TODO
25 | )
26 | case .propertyDoesNotSupportNullValues(propertyName: let propertyName, forClass: let `class`):
27 | return RetroluxErrorDescription(
28 | description: "The property '\(propertyName)' on \(`class`) does not support null.",
29 | suggestion: "Either make the property '\(propertyName)' on \(`class`) optional, or allow errors to be ignored."
30 | )
31 | case .typeMismatch(expected: let expected, got: let got, propertyName: let propertyName, forClass: let `class`):
32 | return RetroluxErrorDescription(
33 | description: "The property '\(propertyName)' on \(`class`) was given an incompatible value type, \(got). Expected type \(expected).", // TODO: Fix 'got'.
34 | suggestion: nil // TODO
35 | )
36 | case .expectedDictionaryRootButGotArrayRoot(type: let type):
37 | return RetroluxErrorDescription(
38 | description: "Wrong root type in JSON. Expected a dictionary, but got an array.",
39 | suggestion: "Either change your JSON root object to a dictionary, or change the expected type to an array, i.e. [\(type)].self instead of just \(type.self)."
40 | )
41 | case .expectedArrayRootButGotDictionaryRoot(type: let type):
42 | return RetroluxErrorDescription(
43 | description: "Wrong root type in JSON. Expected an array, but got a dictionary.",
44 | suggestion: "Either change your JSON root object to an array, or change the expected type to a dictionary, i.e. \(type.self) instead of [\(type)].self."
45 | )
46 | case .invalidJSONData(let error):
47 | return RetroluxErrorDescription(
48 | description: "The JSON data was invalid: \(error.localizedDescription)",
49 | suggestion: "Valid JSON is required. Check to see if the JSON is malformatted, or corrupt, or something other than JSON."
50 | )
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Retrolux/ValueTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValueTransformer.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/16/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ValueTransformerDirection {
12 | case forwards
13 | case backwards
14 | }
15 |
16 | public enum ValueTransformationError: Error {
17 | case typeMismatch(got: Any.Type?)
18 | }
19 |
20 | // If property is like:
21 | // var friend: Person?
22 | // then targetType will be Person.self.
23 | //
24 | // If property is like:
25 | // var friend: [Person] = []
26 | // then targetType will still be Person.self.
27 | public protocol ValueTransformer {
28 | func supports(targetType: Any.Type) -> Bool
29 | func transform(_ value: Any, targetType: Any.Type, direction: ValueTransformerDirection) throws -> Any
30 | }
31 |
32 | internal func reflectable_transform(value: Any, propertyName: String, classType: Any.Type, type: PropertyType, transformer: TransformerType, direction: TransformerDirection) throws -> Any {
33 | return value
34 | // switch type {
35 | // case .anyObject:
36 | // return value
37 | // case .optional(let wrapped):
38 | // return try reflectable_transform(
39 | // value: value,
40 | // propertyName: propertyName,
41 | // classType: classType,
42 | // type: wrapped,
43 | // transformer: transformer,
44 | // direction: direction
45 | // )
46 | // case .bool:
47 | // return value
48 | // case .number:
49 | // return value
50 | // case .string:
51 | // return value
52 | // case .transformable(transformer: let transformer, targetType: let targetType):
53 | // return try transformer.transform(value, targetType: targetType, direction: direction)
54 | // case .array(let element):
55 | // guard let array = value as? [Any] else {
56 | // throw ValueTransformationError.typeMismatch(got: type(of: value))
57 | // }
58 | // return try array.map {
59 | // try reflectable_transform(
60 | // value: $0,
61 | // propertyName: propertyName,
62 | // classType: classType,
63 | // type: element,
64 | // transformer: transformer,
65 | // direction: direction
66 | // )
67 | // }
68 | // case .dictionary(let valueType):
69 | // guard let dictionary = value as? [String: Any] else {
70 | // // TODO: Add a test for this.
71 | // throw ReflectorSerializationError.typeMismatch(expected: type, got: type(of: value), propertyName: propertyName, forClass: classType)
72 | // }
73 | // var result: [String: Any] = [:]
74 | // for (key, value) in dictionary {
75 | // result[key] = try reflectable_transform(
76 | // value: value,
77 | // propertyName: propertyName,
78 | // classType: classType,
79 | // type: valueType,
80 | // transformer: transformer,
81 | // direction: direction
82 | // )
83 | // }
84 | // return result
85 | // }
86 | }
87 |
--------------------------------------------------------------------------------
/Retrolux/Reflectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reflectable.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 7/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func reflectable_setProperty(_ property: Property, value: Any?, instance: Reflectable) throws {
12 | if property.ignored {
13 | return
14 | }
15 |
16 | guard property.type.isCompatible(with: value, transformer: property.transformer) else {
17 | if case .optional(let wrapped) = property.type {
18 | /* Nothing */ // TODO: This needs a unit test, and isn't correct behavior.
19 |
20 | } else if value == nil {
21 | throw ReflectorSerializationError.keyNotFound(propertyName: property.name, key: property.serializedName, forClass: type(of: instance))
22 | } else if value is NSNull {
23 | if !property.nullable {
24 | throw ReflectorSerializationError.propertyDoesNotSupportNullValues(propertyName: property.name, forClass: type(of: instance))
25 | }
26 | } else {
27 | throw ReflectorSerializationError.typeMismatch(expected: property.type, got: type(of: value), propertyName: property.name, forClass: type(of: instance))
28 | }
29 | return
30 | }
31 |
32 | guard let value = value, value is NSNull == false else {
33 | instance.setValue(nil, forKey: property.name)
34 | return
35 | }
36 |
37 | if let transformer = property.transformer {
38 | try transformer.set(value: value, for: property, instance: instance)
39 | } else {
40 | instance.setValue(value, forKey: property.name)
41 | }
42 | }
43 |
44 | public func reflectable_value(for property: Property, instance: Reflectable) throws -> Any? {
45 | if let transformer = property.transformer {
46 | return try transformer.value(for: property, instance: instance)
47 | }
48 | return instance.value(forKey: property.name)
49 | }
50 |
51 | public protocol Reflectable: NSObjectProtocol {
52 | // Read/write properties
53 | func responds(to aSelector: Selector!) -> Bool // To check if property can be bridged to Obj-C
54 | func setValue(_ value: Any?, forKey key: String) // For JSON -> Reflection deserialization
55 | func value(forKey key: String) -> Any? // For Reflection -> JSON serialization
56 |
57 | init() // Required for proper reflection support
58 |
59 | // TODO: Consider adding these functions?
60 | //func copy() -> Self // Lower priority--this is primarily for copying/detaching database models
61 | //func changes() -> [String: AnyObject]
62 | //var hasChanges: Bool { get }
63 | //func clearChanges() resetChanges() markAsHavingNoChanges() What to name this thing?
64 | //func revertChanges() // MAYBE?
65 |
66 | /// Use this to make customizations to the object on a worker queue after
67 | /// deserialization from remote data to a class is complete.
68 | func afterDeserialization(remoteData: [String: Any]) throws
69 |
70 | /// Use this to make customizations to the remote data on a worker queue after
71 | /// serialization from a class to remote data is complete.
72 | func afterSerialization(remoteData: inout [String: Any]) throws
73 |
74 | func validate() throws
75 | static func config(_ c: PropertyConfig)
76 | }
77 |
78 | extension Reflectable {
79 | public func validate() throws {
80 |
81 | }
82 |
83 | public static func config(_ c: PropertyConfig) {
84 |
85 | }
86 |
87 | public func afterDeserialization(remoteData: [String: Any]) throws {
88 |
89 | }
90 |
91 | public func afterSerialization(remoteData: inout [String: Any]) throws {
92 |
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Retrolux.xcodeproj/xcshareddata/xcschemes/Retrolux.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
66 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/RetroluxTests/MultipartFormDataSerializerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultipartFormDataSerializerTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | class MultipartFormDataSerializerTests: XCTestCase {
14 | func testSinglePart() {
15 | let request = Builder.dry().makeRequest(method: .post, endpoint: "whatever/", args: Part(name: "file", filename: "image.png", mimeType: "image/png"), response: Void.self)
16 |
17 | let image = Utils.testImage
18 | let imageData = UIImagePNGRepresentation(image)!
19 |
20 | let response = request(Part(imageData)).perform()
21 | let bundle = Bundle(for: type(of: self))
22 | let url = bundle.url(forResource: "request", withExtension: "data")!
23 | let responseData = try! Data(contentsOf: url)
24 | let asciiExpected = String(data: responseData, encoding: .ascii)!
25 |
26 | var asciiRequest = String(data: response.request.httpBody!, encoding: .ascii)!
27 | let staticBoundary = "alamofire.boundary.b990eade8c5319d6"
28 | asciiRequest = asciiRequest.replacingOccurrences(of: "alamofire\\.boundary\\.[0-9a-f]{16,16}", with: staticBoundary, options: .regularExpression)
29 |
30 | XCTAssert(asciiExpected == asciiRequest)
31 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=alamofire.boundary.") == true)
32 | }
33 |
34 | func testMultipleParts() {
35 | let request = Builder.dry().makeRequest(method: .post, endpoint: "whatever/", args: (Field("first_name"), Field("last_name")), response: Void.self)
36 |
37 | let response = request((Field("Bryan"), Field("Henderson"))).perform()
38 | let bundle = Bundle(for: type(of: self))
39 | let url = bundle.url(forResource: "testmultipleparts", withExtension: "data")!
40 | let data = try! Data(contentsOf: url)
41 | let asciiExpected = String(data: data, encoding: .ascii)!
42 |
43 | var asciiRequest = String(data: response.request.httpBody!, encoding: .ascii)!
44 | let staticBoundary = "alamofire.boundary.3ffb270bf5e2dc3b"
45 | asciiRequest = asciiRequest.replacingOccurrences(of: "alamofire\\.boundary\\.[0-9a-f]{16,16}", with: staticBoundary, options: .regularExpression)
46 |
47 | XCTAssert(asciiExpected == asciiRequest)
48 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=alamofire.boundary.") == true)
49 | }
50 |
51 | func testFieldsAndPartsCombined() {
52 | struct RequestData {
53 | let firstName: Field
54 | let imagePart: Part
55 | }
56 |
57 | let request = Builder.dry().makeRequest(
58 | method: .post,
59 | endpoint: "whatever/",
60 | args: RequestData(firstName: Field("first_name"), imagePart: Part(name: "file", filename: "image.png", mimeType: "image/png")),
61 | response: Void.self
62 | )
63 |
64 | let image = Utils.testImage
65 | let imageData = UIImagePNGRepresentation(image)!
66 |
67 | let requestData = RequestData(
68 | firstName: Field("Bob"),
69 | imagePart: Part(imageData)
70 | )
71 |
72 | let response = request(requestData).perform()
73 | let bundle = Bundle(for: type(of: self))
74 | let url = bundle.url(forResource: "fieldsandpartscombined", withExtension: "data")!
75 | let data = try! Data(contentsOf: url)
76 | let asciiExpected = String(data: data, encoding: .ascii)!
77 |
78 | var asciiRequest = String(data: response.request.httpBody!, encoding: .ascii)!
79 | let staticBoundary = "alamofire.boundary.5483ad401099117f"
80 | asciiRequest = asciiRequest.replacingOccurrences(of: "alamofire\\.boundary\\.[0-9a-f]{16,16}", with: staticBoundary, options: .regularExpression)
81 |
82 | XCTAssert(asciiExpected == asciiRequest)
83 | XCTAssert(response.request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=alamofire.boundary.") == true)
84 | }
85 |
86 | func testScopeForField() {
87 | let f = Field("username")
88 | XCTAssert(f.value == "username")
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | v0.10.4
2 | - Made it possible to change configuration of underlying URLSessionConfiguration
3 |
4 | v0.10.3
5 | - Wrap all serializer errors in a common enum
6 |
7 | v0.10.2
8 | - Made Response's initializer accept interpreted response
9 |
10 | v0.10.1
11 | - Made Response's initializer public
12 |
13 | v0.10.0
14 | - Added Reflectable.afterDeserialization and Reflectable.afterSerialization
15 |
16 | v0.9.0
17 | - Added Reflector.update
18 | - Removed assert that prevented creation args from differing from starting args
19 |
20 | v0.8.2
21 | - Polished up connection error reporting
22 |
23 | v0.8.1
24 | - Made Diff's initializers public
25 |
26 | v0.8.0
27 | - Added Reflector.copy and Reflector.diff
28 |
29 | v0.7.0
30 | - Added a built-in and ready to go URL <--> String transformer
31 |
32 | v0.6.7
33 | - Added DateTransformer.swift to git repo
34 |
35 | v0.6.6
36 | - Fixed DateTransformer visibility
37 |
38 | v0.6.5
39 | - Made Path and Query values public
40 |
41 | v0.6.4
42 | - Builder cleanup, plus a test for enqueue blocking
43 | - Slight wording change for missing JSON key error message
44 | - Added unit test for HTTP 400s, and global vs local transformers
45 | - Added a DateTransformer
46 |
47 | v0.6.3
48 | - Fixed a bug where sending off 100+ requests at once would cause a deadlock.
49 |
50 | v0.6.2
51 | - Fixed a bug that prevented simple transformations, like from String to Bool.
52 |
53 | v0.6.1
54 | - Fixes a bug that only manifests itself when being used in a 3rd-party project where ReflectableTransformer would claim it didn't support Reflectables. Caused by Swift bug where type is TypeOfProperty.Type would fail in the protocol extension but work fine in the actual class--again, only in 3rd-party projects. UGH!
55 |
56 | v0.6.0
57 | - Overhauled transformers API for Reflectables
58 | - Removed the following properties/functions from Reflectable:
59 | - `func set(value:, for property:) throws`, use `Reflector.set(value:for:on:)` instead
60 | - `func value(for:) throws -> Any?`, use `Reflector.value(for:on:)` instead
61 | - `static var ignoredProperties: [String]`, refer to "New Config API" instead
62 | - `static var ignoreErrorsForProperties: [String]`, refer to "New Config API" instead
63 | - `static var mappedProperties: [String: String]`, refer to "New Config API" instead
64 | - `static var transformedProperties: [String: Retrolux.ValueTransformer]`, refer to "New Config API" instead
65 | - New Config API: override/implement `static func config(_ c: PropertyConfig)` and then do something like `c["nameOfProperty"] = [.serializedName("name_of_property", .nullable]`
66 | - Reflector now has a `globalTransformers` property, to allow adding support globally for things like `Date` conversion
67 |
68 | v0.5.3
69 | - Error messages are more human readable and have recovery suggestions
70 |
71 | v0.5.2
72 | - Fixed a bug where responses with Void.self as the return type would always fail, with isSuccessful always returning false.
73 |
74 | v0.5.1
75 | - Made Field.value public.
76 |
77 | v0.5.0
78 | - Added Builder.dry() for easier testing
79 | - Added Call.perform() for synchronous network calls
80 | - Added testProvider: as a parameter to Builder.makeRequest
81 | - Requests capture base URL when starting requests, not when creating requests
82 |
83 | v0.4.2
84 | - Added support for Cocoapods.
85 |
86 | v0.4.1
87 | - Added keyNotFound error
88 | - Here be dragons: Removed "base class must be a Reflection" check. If your overridden functions/properties on Reflectable aren't being called, be sure to make sure they are implemented in your base class and overridden properly! This exposes a limitation of Swift protocols.
89 |
90 | v0.4.0
91 | - Added Response.interpreted, removed Response.result. This enables you to customize how the response from the server is interpreted.
92 | - Simplified Response by moving ClientResponse's members directly into Response.
93 | - Fixed a regression where requests that have no parameters (i.e., HTTP GET) would fail.
94 | - Added overridable logging.
95 | - Moved requestInterceptor and responseInterceptor from Client to Builder.
96 |
97 | v0.3.2
98 | - Made reflectable_setValue public, for all your hacking needs!
99 |
100 | v0.3.1
101 | - Fixed git submodule error by deleting .gitsubmodules file.
102 |
103 | v0.3
104 | - RetroluxReflector is now vendored in, because it prevents uploading App Store builds when installed via Carthage
105 | - Faster reflection thanks to caching
106 | - More errors handled by Builder
107 | - "Max depth 2" bug fixed
108 |
109 | v0.2.4
110 | - Initial release
111 |
--------------------------------------------------------------------------------
/Retrolux/ReflectionJSONSerializer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONSerializer.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/8/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol ReflectionDiffType {
12 | var value1: Reflectable { get }
13 | var value2: Reflectable { get }
14 | var granular: Bool { get }
15 | }
16 |
17 | public struct Diff: ReflectionDiffType {
18 | public let value1: Reflectable
19 | public let value2: Reflectable
20 | public let granular: Bool
21 |
22 | public init() {
23 | self.value1 = Reflection()
24 | self.value2 = Reflection()
25 | self.granular = true
26 | }
27 |
28 | public init(from value1: T, to value2: T, granular: Bool = true) {
29 | self.value1 = value1
30 | self.value2 = value2
31 | self.granular = granular
32 | }
33 | }
34 |
35 | public enum ReflectionJSONSerializerError: RetroluxError {
36 | case unsupportedType(type: Any.Type)
37 | case noData
38 |
39 | public var rl_error: RetroluxErrorDescription {
40 | switch self {
41 | case .unsupportedType(type: let type):
42 | return RetroluxErrorDescription(
43 | description: "The type given, \(type), is not supported.",
44 | suggestion: "Make sure to pass in an object that conforms to the \(Reflectable.self) protocol. The type given, \(type), does not conform to \(Reflectable.self)."
45 | )
46 | case .noData:
47 | return RetroluxErrorDescription(
48 | description: "The HTTP body was empty, but a response was expected.",
49 | suggestion: nil
50 | )
51 | }
52 | }
53 | }
54 |
55 | fileprivate protocol GetTypeFromArray {
56 | static func getReflectableType() -> Reflectable.Type?
57 | }
58 |
59 | extension Array: GetTypeFromArray {
60 | fileprivate static func getReflectableType() -> Reflectable.Type? {
61 | return Element.self as? Reflectable.Type
62 | }
63 | }
64 |
65 | public class ReflectionJSONSerializer: OutboundSerializer, InboundSerializer {
66 | open let reflector = Reflector.shared
67 |
68 | public init() {
69 |
70 | }
71 |
72 | public func supports(outboundType: Any.Type) -> Bool {
73 | if outboundType is ReflectionDiffType.Type {
74 | return true
75 | }
76 | if outboundType is Reflectable.Type {
77 | return true
78 | }
79 | return (outboundType as? GetTypeFromArray.Type)?.getReflectableType() != nil
80 | }
81 |
82 | public func supports(inboundType: Any.Type) -> Bool {
83 | if inboundType is Reflectable.Type {
84 | return true
85 | }
86 | return (inboundType as? GetTypeFromArray.Type)?.getReflectableType() != nil
87 | }
88 |
89 | public func validate(outbound: [BuilderArg]) -> Bool {
90 | return outbound.filter { supports(outboundType: $0.type) }.count == 1
91 | }
92 |
93 | public func makeValue(from clientResponse: ClientResponse, type: T.Type) throws -> T {
94 | guard let data = clientResponse.data else {
95 | throw ReflectionJSONSerializerError.noData
96 | }
97 |
98 | if let reflectable = T.self as? Reflectable.Type {
99 | return try reflector.convert(fromJSONDictionaryData: data, to: reflectable) as! T
100 | } else if let array = T.self as? GetTypeFromArray.Type {
101 | guard let type = array.getReflectableType() else {
102 | throw ReflectionJSONSerializerError.unsupportedType(type: T.self)
103 | }
104 | return try reflector.convert(fromJSONArrayData: data, to: type) as! T
105 | } else {
106 | throw ReflectionJSONSerializerError.unsupportedType(type: T.self)
107 | }
108 | }
109 |
110 | public func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
111 | let arg = arguments.first!
112 |
113 | if let diff = arg.starting as? ReflectionDiffType {
114 | let dictionary = try reflector.diff(from: diff.value1, to: diff.value2, granular: diff.granular)
115 | let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
116 | request.httpBody = data
117 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
118 | } else if let reflectable = arg.starting as? Reflectable {
119 | let dictionary = try reflector.convertToDictionary(from: reflectable)
120 | let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
121 | request.httpBody = data
122 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
123 | } else if let array = arg.starting as? [Reflectable] {
124 | let dictionaries = try array.map { try reflector.convertToDictionary(from: $0) }
125 | let data = try JSONSerialization.data(withJSONObject: dictionaries, options: [])
126 | request.httpBody = data
127 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
128 | } else {
129 | throw ReflectionJSONSerializerError.unsupportedType(type: arg.type)
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/RetroluxTests/DigestAuthTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DigestAuthTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/23/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | import Retrolux
12 |
13 | fileprivate func plist(_ key: String) -> String {
14 | // http://stackoverflow.com/a/38035382/2406857
15 | let bundle = Bundle(for: BuilderTests.self)
16 | let path = bundle.path(forResource: "Sensitive", ofType: "plist")!
17 | let url = URL(fileURLWithPath: path)
18 | let data = try! Data(contentsOf: url)
19 | let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil)
20 | let dictionary = plist as! [String: Any]
21 | return dictionary[key] as! String
22 | }
23 |
24 | class DigestAuthTests: XCTestCase {
25 | /*func testDigestAuth() {
26 | class MyBuilder: Builder {
27 | let baseURL = URL(string: plist("URL"))!
28 | let client: Client = HTTPClient()
29 | let callFactory: CallFactory = HTTPCallFactory()
30 | let serializers: [Serializer] = [
31 | ReflectionJSONSerializer(),
32 | URLEncodedSerializer()
33 | ]
34 | }
35 |
36 | let builder = MyBuilder()
37 |
38 | builder.client.interceptor = { request in
39 | func md5(_ string: String) -> String {
40 | var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
41 |
42 | let data = string.data(using: .utf8)!
43 | _ = data.withUnsafeBytes { bytes in
44 | CC_MD5(bytes, CC_LONG(data.count), &digest)
45 | }
46 |
47 | var digestHex = ""
48 | for index in 0..(), response: Body())
80 | let expectation = self.expectation(description: "Waiting for network callback")
81 | let params = URLEncodedBody(values: [
82 | ("username", plist("username")),
83 | ("password", plist("password"))
84 | ])
85 | request(Body(params)).enqueue { (response: Response) in
86 | let status = response.raw?.status ?? 0
87 | XCTAssert(status == 200)
88 | print("HTTP \(status)")
89 | if status != 200 {
90 | if let data = response.raw?.data {
91 | print("\n--- TEST FAILURE ---")
92 | let string = String(data: data, encoding: .utf8)!
93 | print("1: \(string)")
94 | print("--- TEST FAILURE ---\n")
95 | }
96 | XCTFail("Unexpected status code of \(status)")
97 | } else {
98 | let pubs = builder.makeRequest(method: .get, endpoint: "online/api/v2/app/publications", args: (), response: Body())
99 | pubs().enqueue { (response: Response) in
100 | let status2 = response.raw?.status ?? 0
101 | print("HTTP \(status2)")
102 | XCTAssert(status2 == 200)
103 | if status2 != 200 {
104 | if let data = response.raw?.data {
105 | print("\n--- TEST FAILURE ---")
106 | let string = String(data: data, encoding: .utf8)!
107 | print("2: \(string)")
108 | print("--- TEST FAILURE ---\n")
109 | }
110 | }
111 | expectation.fulfill()
112 | }
113 | }
114 | }
115 |
116 | waitForExpectations(timeout: 5) { (error) in
117 | if let error = error {
118 | XCTFail("Failed to wait for expectation: \(error)")
119 | }
120 | }
121 | }*/
122 | }
123 |
--------------------------------------------------------------------------------
/RetroluxTests/InterpretedResponseTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InterpretedResponseTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 | import XCTest
12 |
13 | class InterpretedResponseTests: XCTestCase {
14 | func testUnsupportedArg() {
15 | let builder = Builder.dry()
16 | builder.responseInterceptor = { response in
17 | response = ClientResponse(base: response, status: 200, data: nil)
18 | }
19 | let function = builder.makeRequest(method: .get, endpoint: "whateverz", args: 1, response: Void.self)
20 | switch function(2).perform().interpreted {
21 | case .success(_):
22 | XCTFail("Should not have succeeded.")
23 | case .failure(let error):
24 | if case BuilderError.unsupportedArgument(let arg) = error {
25 | XCTAssert(arg.creation as? Int == 1)
26 | XCTAssert(arg.starting as? Int == 2)
27 | XCTAssert(arg.type == Int.self)
28 | } else {
29 | XCTFail("Wrong error returned: \(error); expected an unsupported argument error instead.")
30 | }
31 | }
32 | }
33 |
34 | func testInvalidHttpStatusCode() {
35 | class Person: Reflection {
36 | var name: String = ""
37 |
38 | required init() {
39 |
40 | }
41 |
42 | init(name: String) {
43 | self.name = name
44 | }
45 | }
46 |
47 | let function = Builder.dry().makeRequest(method: .post, endpoint: "whateverz", args: Person(name: "Alice"), response: Person.self, testProvider: {
48 | ClientResponse(url: $0.2.url!, data: "{\"name\":null}".data(using: .utf8)!, headers: [:], status: 400, error: nil)
49 | })
50 | switch function(Person(name: "Bob")).perform().interpreted {
51 | case .success(_):
52 | XCTFail("Should not have succeeded.")
53 | case .failure(let error):
54 | if case ResponseError.invalidHttpStatusCode(code: let code) = error {
55 | XCTAssert(code == 400)
56 | } else {
57 | XCTFail("Wrong error returned: \(error); expected an invalid HTTP status code error instead.")
58 | }
59 | }
60 | }
61 |
62 | func testResponseSerializationError() {
63 | class Person: Reflection {
64 | var name: String = ""
65 | }
66 |
67 | let function = Builder.dry().makeRequest(method: .post, endpoint: "whateverz", args: (), response: Person.self) {
68 | ClientResponse(url: $0.2.url!, data: "{\"name\":null}".data(using: .utf8)!, status: 200)
69 | }
70 | switch function().perform().interpreted {
71 | case .success(_):
72 | XCTFail("Should not have succeeded.")
73 | case .failure(let error):
74 | if case BuilderResponseError.deserializationError(serializer: _, error: let error, clientResponse: _) = error {
75 | if case ReflectorSerializationError.propertyDoesNotSupportNullValues(propertyName: let propertyName, forClass: let `class`) = error {
76 | XCTAssert(propertyName == "name")
77 | XCTAssert(`class` == Person.self)
78 | } else {
79 | XCTFail("Wrong error returned: \(error).")
80 | }
81 | } else {
82 | XCTFail("Wrong error returned: \(error).")
83 | }
84 | }
85 | }
86 |
87 | func testSuccess() {
88 | class Person: Reflection {
89 | var name: String = ""
90 | }
91 |
92 | let function = Builder.dry().makeRequest(method: .post, endpoint: "whateverz", args: (), response: Person.self) {
93 | ClientResponse(url: $0.2.url!, data: "{\"name\":\"bobby\"}".data(using: .utf8)!, status: 200)
94 | }
95 | switch function().perform().interpreted {
96 | case .success(let person):
97 | XCTAssert(person.name == "bobby")
98 | case .failure(let error):
99 | XCTFail("Response interpreted as failure: \(error)")
100 | }
101 | }
102 |
103 | func testHTTP400ErrorWithInvalidJSONResponse() {
104 | class Person: Reflection {
105 | var name = ""
106 | }
107 |
108 | let function = Builder.dry().makeRequest(method: .post, endpoint: "whatevers", args: (), response: Person.self) {
109 | ClientResponse(url: $0.2.url!, data: "ERROR".data(using: .utf8)!, status: 400)
110 | }
111 | switch function().perform().interpreted {
112 | case .success:
113 | XCTFail("Should not have succeeded")
114 | case .failure(let error):
115 | if case ResponseError.invalidHttpStatusCode = error {
116 |
117 | } else {
118 | XCTFail("Expected an invalid HTTP error, but got the following instead: \(error)")
119 | }
120 | }
121 | }
122 |
123 | func testNoInternet() {
124 | let builder = Builder.dry()
125 | let request = builder.makeRequest(
126 | method: .get,
127 | endpoint: "",
128 | args: (),
129 | response: Void.self
130 | ) { _ in
131 | return ClientResponse(data: nil, response: nil, error: NSError(domain: "Whatever", code: 2, userInfo: [
132 | NSLocalizedDescriptionKey: "Localized Description",
133 | NSLocalizedRecoverySuggestionErrorKey: "Recovery Suggestion"
134 | ]))
135 | }
136 |
137 | switch request().perform().interpreted {
138 | case .success:
139 | XCTFail("Should not have succeeded.")
140 | case .failure(let error):
141 | if case ResponseError.connectionError = error {
142 | XCTAssert(error.localizedDescription == "Localized Description")
143 | XCTAssert((error as NSError).localizedRecoverySuggestion == "Recovery Suggestion")
144 | } else {
145 | XCTFail("Unexpected error code: \(error)")
146 | }
147 | }
148 | }
149 |
150 | func testResponsePublicInitializer() {
151 | _ = Response(
152 | request: URLRequest(url: URL(string: "https://www.google.com/")!),
153 | data: nil,
154 | error: nil,
155 | urlResponse: nil,
156 | body: nil,
157 | interpreted: InterpretedResponse.failure(NSError(domain: "ERR_TEST", code: 3, userInfo: nil))
158 | )
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/Retrolux/ReflectionError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReflectionError.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/4/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ReflectionError: RetroluxError {
12 | case subclassingNotAllowed(Any.Type)
13 | case cannotIgnoreNonExistantProperty(propertyName: String, forClass: Any.Type)
14 | case cannotIgnoreErrorsForNonExistantProperty(propertyName: String, forClass: Any.Type)
15 | case cannotIgnoreErrorsAndIgnoreProperty(propertyName: String, forClass: Any.Type)
16 | case cannotMapNonExistantProperty(propertyName: String, forClass: Any.Type)
17 | case cannotTransformNonExistantProperty(propertyName: String, forClass: Any.Type)
18 | case mappedPropertyConflict(properties: [String], conflictKey: String, forClass: Any.Type)
19 | case cannotMapAndIgnoreProperty(propertyName: String, forClass: Any.Type)
20 | case cannotTransformAndIgnoreProperty(propertyName: String, forClass: Any.Type)
21 | case optionalNumericTypesAreNotSupported(propertyName: String, unwrappedType: Any.Type, forClass: Any.Type)
22 |
23 | /*
24 | If you get this error, try adding dynamic keyword to your property.
25 | If that still doesnt work, try adding the dynamic (or @objc) attribute.
26 | If that STILL doesnt work, your property type is not supported. :-(
27 | */
28 | case propertyNotSupported(propertyName: String, type: PropertyType, forClass: Any.Type)
29 |
30 | public var rl_error: RetroluxErrorDescription {
31 | switch self {
32 | case .subclassingNotAllowed(let `class`):
33 | return RetroluxErrorDescription(
34 | description: "Subclassing is not allowed for \(`class`).",
35 | suggestion: "If you wish to proceed anyways, and you understand the risks and subclassing quirks, make \(`class`) conform to \(ReflectableSubclassingIsAllowed.self)."
36 | )
37 | case .cannotIgnoreNonExistantProperty(propertyName: let propertyName, forClass: let `class`):
38 | return RetroluxErrorDescription(
39 | description: "The property '\(propertyName)' on \(`class`) cannot be marked as ignored, because no such property exists.",
40 | suggestion: "Either remove the property '\(propertyName)' on class \(`class`), or remove '\(propertyName)' from the list of ignored properties."
41 | )
42 | case .cannotIgnoreErrorsForNonExistantProperty(propertyName: let propertyName, forClass: let `class`):
43 | return RetroluxErrorDescription(
44 | description: "The property '\(propertyName)' on \(`class`) cannot be marked as errors allowed, because no such property exists.",
45 | suggestion: "Either create a property '\(propertyName)' on class \(`class`), or remove '\(propertyName)' from the list of properties where errors are allowed."
46 | )
47 | case .cannotIgnoreErrorsAndIgnoreProperty(propertyName: let propertyName, forClass: let `class`):
48 | return RetroluxErrorDescription(
49 | description: "The property '\(propertyName)' on \(`class`) cannot be marked as ignored and allow errors.",
50 | suggestion: "Either remove the property '\(propertyName)' on class \(`class`), or remove it from the list of ignored properties, or remove it from the list of properties where errors are allowed."
51 | )
52 | case .cannotMapNonExistantProperty(propertyName: let propertyName, forClass: let `class`):
53 | return RetroluxErrorDescription(
54 | description: "The property '\(propertyName)' on \(`class`) cannot be remapped, because no such property exists.",
55 | suggestion: "Either create a property '\(propertyName)' on class \(`class`), or remove it from the list of remapped properties."
56 | )
57 | case .cannotTransformNonExistantProperty(propertyName: let propertyName, forClass: let `class`):
58 | return RetroluxErrorDescription(
59 | description: "The property '\(propertyName)' on \(`class`) cannot be transformed, because no such property exists.",
60 | suggestion: "Either create a property '\(propertyName)' on \(`class`), or remove the transformer."
61 | )
62 | case .cannotTransformAndIgnoreProperty(propertyName: let propertyName, forClass: let `class`):
63 | return RetroluxErrorDescription(
64 | description: "The property '\(propertyName)' on \(`class`) cannot be ignored and transformed.",
65 | suggestion: "Either remove the property '\(propertyName)' on \(`class`), or remove it from the list of remapped properties, or remove the transformer."
66 | )
67 | case .mappedPropertyConflict(properties: let properties, conflictKey: let conflictKey, forClass: let `class`):
68 | return RetroluxErrorDescription(
69 | description: "The properties, \(properties), on class \(`class`), all map to the same key, \"\(conflictKey)\".",
70 | suggestion: "Change the values of the remapped properties, \(properties), on \(`class`), such that each property is mapped to a unique value (currently, all are assigned to the key, \"\(conflictKey)\")."
71 | )
72 | case .cannotMapAndIgnoreProperty(propertyName: let propertyName, forClass: let `class`):
73 | return RetroluxErrorDescription(
74 | description: "The property '\(propertyName)' on class \(`class`) cannot be ignored and remapped.",
75 | suggestion: "Either remove the property '\(propertyName)' on \(`class`), or remove it from the list of remapped properties, or remove it from the list of ignored properties."
76 | )
77 | case .optionalNumericTypesAreNotSupported(propertyName: let propertyName, unwrappedType: let unwrappedType, forClass: let `class`):
78 | return RetroluxErrorDescription(
79 | description: "The property '\(propertyName)' on class \(`class`) has type \(unwrappedType)?, which isn't supported as an optional.",
80 | suggestion: "Either change the type of the property '\(propertyName)' on \(`class`) to \(unwrappedType) (like `var \(propertyName) = \(unwrappedType)()`), or make it an optional NSNumber (i.e., NSNumber?), or add it to the list of ignored properties."
81 | )
82 | case .propertyNotSupported(propertyName: let propertyName, type: let type, forClass: let `class`):
83 | return RetroluxErrorDescription(
84 | description: "The property '\(propertyName)' on class \(`class`) has an unsupported type, \(type).",
85 | suggestion: "Change the type of the property '\(propertyName)' on \(`class`) to a supported type, or create a transformer that supports the property's type, \(type), or add it to the list of ignored properties. To see a more descriptive reason for why this property isn't supported, try adding the @objc attribute to your property and see if the compiler reveals any new errors."
86 | )
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Retrolux/PropertyType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PropertyType.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 7/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | private protocol RLArrayType {
12 | static func rl_type() -> Any.Type
13 | }
14 |
15 | extension Array: RLArrayType {
16 | fileprivate static func rl_type() -> Any.Type {
17 | return Element.self
18 | }
19 | }
20 |
21 | extension NSArray: RLArrayType {
22 | fileprivate static func rl_type() -> Any.Type {
23 | return AnyObject.self
24 | }
25 | }
26 |
27 | private protocol RLDictionaryType {
28 | static func rl_type() -> Any.Type
29 | }
30 |
31 | extension Dictionary: RLDictionaryType {
32 | fileprivate static func rl_type() -> Any.Type {
33 | assert(String.self is Key.Type, "Dictionaries must have strings as keys")
34 | return Value.self
35 | }
36 | }
37 |
38 | extension NSDictionary: RLDictionaryType {
39 | fileprivate static func rl_type() -> Any.Type {
40 | return AnyObject.self
41 | }
42 | }
43 |
44 | fileprivate protocol RLOptionalType {
45 | static func rl_type() -> Any.Type
46 | }
47 |
48 | extension Optional: RLOptionalType {
49 | fileprivate static func rl_type() -> Any.Type {
50 | return Wrapped.self
51 | }
52 | }
53 |
54 | private protocol RLStringType {}
55 | extension String: RLStringType {}
56 | extension NSString: RLStringType {}
57 |
58 | private protocol RLNumberType {}
59 | extension Int: RLNumberType {}
60 | extension Int8: RLNumberType {}
61 | extension Int16: RLNumberType {}
62 | extension Int32: RLNumberType {}
63 | extension Int64: RLNumberType {}
64 | extension UInt: RLNumberType {}
65 | extension UInt8: RLNumberType {}
66 | extension UInt16: RLNumberType {}
67 | extension UInt32: RLNumberType {}
68 | extension UInt64: RLNumberType {}
69 | extension Float: RLNumberType {}
70 | extension Float64: RLNumberType {}
71 | extension NSNumber: RLNumberType {}
72 |
73 | public indirect enum PropertyType: CustomStringConvertible, Equatable {
74 | case any
75 | case anyObject
76 | case optional(PropertyType)
77 | case bool
78 | case number(Any.Type)
79 | case string
80 | case array(PropertyType)
81 | case dictionary(PropertyType)
82 | case unknown(Any.Type)
83 |
84 | public static func from(_ type: Any.Type) -> PropertyType {
85 | if type == Bool.self {
86 | return .bool
87 | } else if type == AnyObject.self {
88 | return .anyObject
89 | } else if type == Any.self {
90 | return .any
91 | } else if type is RLStringType.Type {
92 | return .string
93 | } else if type is RLNumberType.Type {
94 | return .number(type)
95 | } else if let arr = type as? RLArrayType.Type {
96 | return .array(from(arr.rl_type()))
97 | } else if let opt = type as? RLOptionalType.Type {
98 | return .optional(from(opt.rl_type()))
99 | } else if let dict = type as? RLDictionaryType.Type {
100 | return .dictionary(from(dict.rl_type()))
101 | } else {
102 | return .unknown(type)
103 | }
104 | }
105 |
106 | public var bottom: PropertyType {
107 | switch self {
108 | case .any, .anyObject, .bool, .string, .unknown, .number:
109 | return self
110 | case .array(let inner):
111 | return inner.bottom
112 | case .dictionary(let inner):
113 | return inner.bottom
114 | case .optional(let wrapped):
115 | return wrapped.bottom
116 | }
117 | }
118 |
119 | public func isCompatible(with value: Any?, transformer: TransformerType?) -> Bool {
120 | if transformer?.supports(propertyType: self) == true {
121 | return true
122 | }
123 |
124 | switch self {
125 | case .any, .anyObject:
126 | return true
127 | case .optional(let wrapped):
128 | return value is NSNull || value == nil || wrapped.isCompatible(with: value, transformer: transformer)
129 | case .bool:
130 | return value is NSNumber
131 | case .number:
132 | return value is RLNumberType
133 | case .string:
134 | return value is RLStringType
135 | case .unknown:
136 | // There is no pre-assignment validation for transformables.
137 | // Validation will have to be done during actual assignment.
138 | return true
139 | case .array(let element):
140 | guard let array = value as? [AnyObject] else {
141 | return false
142 | }
143 | return !array.contains {
144 | !element.isCompatible(with: $0, transformer: transformer)
145 | }
146 | case .dictionary(let valueType):
147 | guard let dictionary = value as? [String : AnyObject] else {
148 | return false
149 | }
150 | return !dictionary.values.contains {
151 | !valueType.isCompatible(with: $0, transformer: transformer)
152 | }
153 | }
154 | }
155 |
156 | public var description: String {
157 | switch self {
158 | case .any: return "any"
159 | case .anyObject: return "anyObject"
160 | case .dictionary(let innerType): return "dictionary<\(innerType)>"
161 | case .array(let innerType): return "array<\(innerType)>"
162 | case .bool: return "bool"
163 | case .number: return "number"
164 | case .unknown(let type): return "unknown(\(type))"
165 | case .optional(let wrapped): return "optional<\(wrapped)>"
166 | case .string: return "string"
167 | }
168 | }
169 | }
170 |
171 | /*
172 | Intended for unit testing, but hey, if you find it useful, power to you!
173 | */
174 | public func ==(lhs: PropertyType, rhs: PropertyType) -> Bool {
175 | switch lhs {
176 | case .any:
177 | if case .any = rhs {
178 | return true
179 | }
180 | case .anyObject:
181 | if case .anyObject = rhs {
182 | return true
183 | }
184 | case .array(let innerType):
185 | if case .array(let innerType2) = rhs {
186 | return innerType == innerType2
187 | }
188 | case .bool:
189 | if case .bool = rhs {
190 | return true
191 | }
192 | case .dictionary(let innerType):
193 | if case .dictionary(let innerType2) = rhs {
194 | return innerType == innerType2
195 | }
196 | case .number(let exactType):
197 | if case .number(let otherExactType) = rhs {
198 | return exactType == otherExactType
199 | }
200 | case .unknown(let type):
201 | if case .unknown(let otherType) = rhs {
202 | return type == otherType
203 | }
204 | case .optional(let wrapped):
205 | if case .optional(let wrapped2) = rhs {
206 | return wrapped == wrapped2
207 | }
208 | case .string:
209 | if case .string = rhs {
210 | return true
211 | }
212 | }
213 | return false
214 | }
215 |
--------------------------------------------------------------------------------
/RetroluxTests/PerformanceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerformanceTests.swift
3 | // RetroluxReflector
4 | //
5 | // Created by Bryan Henderson on 3/30/17.
6 | // Copyright © 2017 Christopher Bryan Henderson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 | import XCTest
12 |
13 | class PerformanceTests: XCTestCase {
14 | class Nested: Reflection {
15 | var a1 = 0; var a2 = 0; var a3 = 0; var a4 = 0; var a5 = 0; var a6 = 0; var a7 = 0; var a8 = 0; var a9 = 0; var a10 = 0; var a11 = 0; var a12 = 0; var a13 = 0; var a14 = 0; var a15 = 0; var a16 = 0; var a17 = 0; var a18 = 0; var a19 = 0; var a20 = 0; var a21 = 0; var a22 = 0; var a23 = 0; var a24 = 0; var a25 = 0; var a26 = 0; var a27 = 0; var a28 = 0; var a29 = 0; var a30 = 0; var a31 = 0; var a32 = 0; var a33 = 0; var a34 = 0; var a35 = 0; var a36 = 0; var a37 = 0; var a38 = 0; var a39 = 0; var a40 = 0; var a41 = 0; var a42 = 0; var a43 = 0; var a44 = 0; var a45 = 0; var a46 = 0; var a47 = 0; var a48 = 0; var a49 = 0; var a50 = 0; var a51 = 0; var a52 = 0; var a53 = 0; var a54 = 0; var a55 = 0; var a56 = 0; var a57 = 0; var a58 = 0; var a59 = 0; var a60 = 0; var a61 = 0; var a62 = 0; var a63 = 0; var a64 = 0; var a65 = 0; var a66 = 0; var a67 = 0; var a68 = 0; var a69 = 0; var a70 = 0; var a71 = 0; var a72 = 0; var a73 = 0; var a74 = 0; var a75 = 0; var a76 = 0; var a77 = 0; var a78 = 0; var a79 = 0; var a80 = 0; var a81 = 0; var a82 = 0; var a83 = 0; var a84 = 0; var a85 = 0; var a86 = 0; var a87 = 0; var a88 = 0; var a89 = 0; var a90 = 0; var a91 = 0; var a92 = 0; var a93 = 0; var a94 = 0; var a95 = 0; var a96 = 0; var a97 = 0; var a98 = 0; var a99 = 0; var a100 = 0;
16 |
17 | var a101 = ""; var a102 = ""; var a103 = ""; var a104 = ""; var a105 = ""; var a106 = ""; var a107 = ""; var a108 = ""; var a109 = ""; var a110 = ""; var a111 = ""; var a112 = ""; var a113 = ""; var a114 = ""; var a115 = ""; var a116 = ""; var a117 = ""; var a118 = ""; var a119 = ""; var a120 = ""; var a121 = ""; var a122 = ""; var a123 = ""; var a124 = ""; var a125 = ""; var a126 = ""; var a127 = ""; var a128 = ""; var a129 = ""; var a130 = ""; var a131 = ""; var a132 = ""; var a133 = ""; var a134 = ""; var a135 = ""; var a136 = ""; var a137 = ""; var a138 = ""; var a139 = ""; var a140 = ""; var a141 = ""; var a142 = ""; var a143 = ""; var a144 = ""; var a145 = ""; var a146 = ""; var a147 = ""; var a148 = ""; var a149 = ""; var a150 = ""; var a151 = ""; var a152 = ""; var a153 = ""; var a154 = ""; var a155 = ""; var a156 = ""; var a157 = ""; var a158 = ""; var a159 = ""; var a160 = ""; var a161 = ""; var a162 = ""; var a163 = ""; var a164 = ""; var a165 = ""; var a166 = ""; var a167 = ""; var a168 = ""; var a169 = ""; var a170 = ""; var a171 = ""; var a172 = ""; var a173 = ""; var a174 = ""; var a175 = ""; var a176 = ""; var a177 = ""; var a178 = ""; var a179 = ""; var a180 = ""; var a181 = ""; var a182 = ""; var a183 = ""; var a184 = ""; var a185 = ""; var a186 = ""; var a187 = ""; var a188 = ""; var a189 = ""; var a190 = ""; var a191 = ""; var a192 = ""; var a193 = ""; var a194 = ""; var a195 = ""; var a196 = ""; var a197 = ""; var a198 = ""; var a199 = ""; var a200 = "";
18 |
19 | var a201: Nested?; var a202: Nested?; var a203: Nested?; var a204: Nested?; var a205: Nested?; var a206: Nested?; var a207: Nested?; var a208: Nested?; var a209: Nested?; var a210: Nested?; var a211: Nested?; var a212: Nested?; var a213: Nested?; var a214: Nested?; var a215: Nested?; var a216: Nested?; var a217: Nested?; var a218: Nested?; var a219: Nested?; var a220: Nested?; var a221: Nested?; var a222: Nested?; var a223: Nested?; var a224: Nested?; var a225: Nested?; var a226: Nested?; var a227: Nested?; var a228: Nested?; var a229: Nested?; var a230: Nested?; var a231: Nested?; var a232: Nested?; var a233: Nested?; var a234: Nested?; var a235: Nested?; var a236: Nested?; var a237: Nested?; var a238: Nested?; var a239: Nested?; var a240: Nested?; var a241: Nested?; var a242: Nested?; var a243: Nested?; var a244: Nested?; var a245: Nested?; var a246: Nested?; var a247: Nested?; var a248: Nested?; var a249: Nested?; var a250: Nested?; var a251: Nested?; var a252: Nested?; var a253: Nested?; var a254: Nested?; var a255: Nested?; var a256: Nested?; var a257: Nested?; var a258: Nested?; var a259: Nested?; var a260: Nested?; var a261: Nested?; var a262: Nested?; var a263: Nested?; var a264: Nested?; var a265: Nested?; var a266: Nested?; var a267: Nested?; var a268: Nested?; var a269: Nested?; var a270: Nested?; var a271: Nested?; var a272: Nested?; var a273: Nested?; var a274: Nested?; var a275: Nested?; var a276: Nested?; var a277: Nested?; var a278: Nested?; var a279: Nested?; var a280: Nested?; var a281: Nested?; var a282: Nested?; var a283: Nested?; var a284: Nested?; var a285: Nested?; var a286: Nested?; var a287: Nested?; var a288: Nested?; var a289: Nested?; var a290: Nested?; var a291: Nested?; var a292: Nested?; var a293: Nested?; var a294: Nested?; var a295: Nested?; var a296: Nested?; var a297: Nested?; var a298: Nested?; var a299: Nested?; var a300: Nested?;
20 | }
21 |
22 | class LargeClass: Nested {
23 | var a301: Nested?; var a302: Nested?; var a303: Nested?; var a304: Nested?; var a305: Nested?; var a306: Nested?; var a307: Nested?; var a308: Nested?; var a309: Nested?; var a310: Nested?; var a311: Nested?; var a312: Nested?; var a313: Nested?; var a314: Nested?; var a315: Nested?; var a316: Nested?; var a317: Nested?; var a318: Nested?; var a319: Nested?; var a320: Nested?; var a321: Nested?; var a322: Nested?; var a323: Nested?; var a324: Nested?; var a325: Nested?; var a326: Nested?; var a327: Nested?; var a328: Nested?; var a329: Nested?; var a330: Nested?; var a331: Nested?; var a332: Nested?; var a333: Nested?; var a334: Nested?; var a335: Nested?; var a336: Nested?; var a337: Nested?; var a338: Nested?; var a339: Nested?; var a340: Nested?; var a341: Nested?; var a342: Nested?; var a343: Nested?; var a344: Nested?; var a345: Nested?; var a346: Nested?; var a347: Nested?; var a348: Nested?; var a349: Nested?; var a350: Nested?; var a351: Nested?; var a352: Nested?; var a353: Nested?; var a354: Nested?; var a355: Nested?; var a356: Nested?; var a357: Nested?; var a358: Nested?; var a359: Nested?; var a360: Nested?; var a361: Nested?; var a362: Nested?; var a363: Nested?; var a364: Nested?; var a365: Nested?; var a366: Nested?; var a367: Nested?; var a368: Nested?; var a369: Nested?; var a370: Nested?; var a371: Nested?; var a372: Nested?; var a373: Nested?; var a374: Nested?; var a375: Nested?; var a376: Nested?; var a377: Nested?; var a378: Nested?; var a379: Nested?; var a380: Nested?; var a381: Nested?; var a382: Nested?; var a383: Nested?; var a384: Nested?; var a385: Nested?; var a386: Nested?; var a387: Nested?; var a388: Nested?; var a389: Nested?; var a390: Nested?; var a391: Nested?; var a392: Nested?; var a393: Nested?; var a394: Nested?; var a395: Nested?; var a396: Nested?; var a397: Nested?; var a398: Nested?; var a399: Nested?; var a400: Nested?;
24 | }
25 |
26 | func testReflectPerformance() {
27 | measure {
28 | for _ in 1...20 {
29 | let reflector = Reflector()
30 | let instance = LargeClass()
31 | do {
32 | _ = try reflector.reflect(instance)
33 | } catch {
34 | XCTFail("Reflection failed with error: \(error)")
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/RetroluxTests/ReflectionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReflectionTests.swift
3 | // RetroluxReflector
4 | //
5 | // Created by Christopher Bryan Henderson on 10/17/16.
6 | // Copyright © 2016 Christopher Bryan Henderson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Retrolux
11 |
12 | class ReflectionTests: XCTestCase {
13 | func setPropertiesHelper(_ properties: [Property], dictionary: [String: Any], instance: Reflectable) {
14 | XCTAssert(Set(properties.map({ $0.name })) == Set(dictionary.keys))
15 | let reflector = Reflector()
16 |
17 | for property in properties {
18 | do {
19 | try reflector.set(value: dictionary[property.name], for: property, on: instance)
20 | } catch let error {
21 | XCTFail("\(error)")
22 | }
23 | }
24 |
25 | for property in properties {
26 | let value = dictionary[property.serializedName] as? NSObject
27 | do {
28 | let instanceValue = try reflector.value(for: property, on: instance)
29 | XCTAssert(instanceValue as? NSObject == value || property.ignored)
30 | } catch let error {
31 | XCTFail("\(error)")
32 | }
33 | }
34 | }
35 |
36 | func testRLObjectBasicSerialization() {
37 | class Model: Reflection {
38 | var name = ""
39 | var age = 0
40 | var whatever = false
41 | var meta = [String: String]()
42 | var model: Model?
43 | }
44 |
45 | let dictionary = [
46 | "name": "Brian",
47 | "age": 23,
48 | "whatever": true,
49 | "meta": [
50 | "place_of_birth": "Bluffdale, UT",
51 | "height": "5' 7\""
52 | ],
53 | "model": NSNull()
54 | ] as [String : Any]
55 |
56 | do {
57 | let model = Model()
58 | let properties = try Reflector().reflect(model)
59 | setPropertiesHelper(properties, dictionary: dictionary, instance: model)
60 | }
61 | catch let error {
62 | XCTFail("\(error)")
63 | }
64 | }
65 |
66 | func testRLObjectIgnoredProperties() {
67 | class Object: Reflection {
68 | var name = ""
69 | var age = 0
70 |
71 | override class func config(_ c: PropertyConfig) {
72 | c["name"] = [.ignored]
73 | }
74 | }
75 |
76 | do {
77 | let properties = try Reflector().reflect(Object())
78 |
79 | XCTAssert(Set(properties.map({ $0.name })) == Set(["age"]))
80 | } catch let error {
81 | XCTFail("\(error)")
82 | }
83 | }
84 |
85 | func testRLObjectNullablePropertyConfig() {
86 | class Object: Reflection {
87 | var name = "default_value"
88 | var age = 0
89 |
90 | override class func config(_ c: PropertyConfig) {
91 | c["name"] = [.nullable]
92 | }
93 | }
94 |
95 | do {
96 | let object = Object()
97 | let reflector = Reflector()
98 | let properties = try reflector.reflect(object)
99 | guard let nameProp = properties.filter({ $0.name == "name" }).first else {
100 | XCTFail("Name property was missing")
101 | return
102 | }
103 | try reflector.set(value: NSNull(), for: nameProp, on: object)
104 | XCTAssert(object.name == "default_value")
105 | } catch let error {
106 | XCTFail("\(error)")
107 | }
108 | }
109 |
110 | func testRLObjectMappedProperties() {
111 | class Object: Reflection {
112 | var description_ = ""
113 |
114 | override class func config(_ c: PropertyConfig) {
115 | c["description_"] = [.serializedName("description")]
116 | }
117 | }
118 |
119 | do {
120 | let properties = try Reflector().reflect(Object())
121 | let prop = properties.first
122 | XCTAssert(properties.count == 1 && prop?.serializedName == "description" && prop?.name == "description_")
123 | } catch let error {
124 | XCTFail("\(error)")
125 | }
126 | }
127 |
128 | /*
129 | Tests that inheritance is properly supported when the base class is Reflection.
130 | */
131 | func testRLObjectInheritance() {
132 | class Plain: Reflection {
133 | var bad = ""
134 |
135 | required init() {
136 | super.init()
137 | }
138 |
139 | override func setValue(_ value: Any?, forKey key: String) {
140 | super.setValue("bad", forKey: "bad")
141 | }
142 |
143 | override func value(forKey key: String) -> Any? {
144 | return "bad"
145 | }
146 |
147 | override func validate() throws {
148 | throw NSError(domain: "bad", code: 0, userInfo: [:])
149 | }
150 |
151 | override class func config(_ c: PropertyConfig) {
152 | c["bad"] = [.serializedName("bad")]
153 | }
154 | }
155 |
156 | class Problematic: Plain {
157 | required init() {
158 | super.init()
159 | bad = "good"
160 | }
161 |
162 | override func setValue(_ value: Any?, forKey key: String) {
163 | super.setValue("good", forKey: key)
164 | }
165 |
166 | override func value(forKey key: String) -> Any? {
167 | return "good"
168 | }
169 |
170 | override func validate() throws {
171 | throw NSError(domain: "good", code: 0, userInfo: [:])
172 | }
173 |
174 | override class func config(_ c: PropertyConfig) {
175 | c["bad"] = [.serializedName("good")]
176 | }
177 | }
178 |
179 | let proto: Reflectable.Type = Problematic.self
180 | let config = PropertyConfig()
181 | proto.config(config)
182 | if let option = config["bad"].first, case .serializedName(let serializedName) = option {
183 | XCTAssert(serializedName == "good")
184 | } else {
185 | XCTFail("Failed to find config option.")
186 | }
187 | let instance = proto.init()
188 | let property = Property(type: .string, name: "bad", options: [.serializedName("bad")])
189 | let reflector = Reflector()
190 | XCTAssert(try! reflector.value(for: property, on: instance) as? String == "good")
191 | try! reflector.set(value: "bad", for: property, on: instance)
192 | XCTAssert(try! reflector.value(for: property, on: instance) as? String == "good")
193 | do {
194 | try proto.init().validate()
195 | XCTFail("Did not expect to succeed.")
196 | } catch let error as NSError {
197 | XCTAssert(error.domain == "good")
198 | }
199 | }
200 |
201 | func testNoProperties() {
202 | class Object1: Reflection {
203 | }
204 |
205 | let object = Object1()
206 | do {
207 | let properties = try Reflector().reflect(object)
208 | XCTAssert(properties.isEmpty, "Reflection shouldn't have any serializable properties.")
209 | } catch {
210 | XCTFail("Reading list of properties on empty Reflection should not fail. Failed with error: \(error)")
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/Retrolux/NestedTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NestedTransformer.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/14/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum NestedTransformerError: RetroluxError {
12 | case typeMismatch(got: Any.Type, expected: Any.Type, propertyName: String, class: Any.Type, direction: NestedTransformerDirection)
13 |
14 | public var rl_error: RetroluxErrorDescription {
15 | switch self {
16 | case .typeMismatch(got: let got, expected: let expected, propertyName: let propertyName, class: let `class`, direction: let direction):
17 |
18 | let suggestion: String
19 | if direction == .serialize {
20 | suggestion = "Double check that the value set for '\(propertyName)' on \(`class`) contains an instance of type \(expected). The transformer, \(type(of: self)), reported that it doesn't support type \(got), which means the value set on the instance is incompatible."
21 | } else {
22 | suggestion = "Double check your data for '\(propertyName)' on \(`class`) to make sure that it contains a type that can be cast to \(expected). \(got) cannot be cast to \(expected)."
23 | }
24 | return RetroluxErrorDescription(
25 | description: "The transformer, \(type(of: self)), cannot convert type \(got) into type \(expected) for property '\(propertyName)' on \(`class`). Direction: \(direction.description).",
26 | suggestion: suggestion
27 | )
28 | }
29 | }
30 | }
31 |
32 | public enum NestedTransformerDirection {
33 | case serialize
34 | case deserialize
35 |
36 | var description: String {
37 | switch self {
38 | case .serialize:
39 | return "serialize"
40 | case .deserialize:
41 | return "deserialize"
42 | }
43 | }
44 | }
45 |
46 | public protocol NestedTransformer: TransformerType {
47 | associatedtype TypeOfProperty
48 | associatedtype TypeOfData
49 |
50 | func setter(_ dataValue: TypeOfData, type: Any.Type) throws -> TypeOfProperty
51 | func getter(_ propertyValue: TypeOfProperty) throws -> TypeOfData
52 | }
53 |
54 | extension NestedTransformer {
55 | public func supports(propertyType: PropertyType) -> Bool {
56 | switch propertyType.bottom {
57 | case .any:
58 | return false
59 | case .anyObject:
60 | return false
61 | case .array(_):
62 | return false
63 | case .dictionary(_):
64 | return false
65 | case .optional(_):
66 | return false
67 | case .unknown(let type):
68 | return type is TypeOfProperty.Type
69 | case .bool:
70 | return Bool.self is TypeOfProperty.Type
71 | case .number(let innerType):
72 | return innerType is TypeOfProperty.Type
73 | case .string:
74 | return String.self is TypeOfProperty.Type
75 | }
76 | }
77 |
78 | public func set(value: Any?, for property: Property, instance: Reflectable) throws {
79 | let transformed = try transform(
80 | value: value,
81 | type: property.type,
82 | for: property,
83 | instance: instance,
84 | direction: .deserialize
85 | )
86 | instance.setValue(transformed, forKey: property.name)
87 | }
88 |
89 | public func transform(value: Any?, type: PropertyType, for property: Property, instance: Reflectable, direction: NestedTransformerDirection) throws -> Any? {
90 | guard let value = value else {
91 | return nil
92 | }
93 |
94 | guard value is NSNull == false else {
95 | return nil
96 | }
97 |
98 | switch type {
99 | case .anyObject, .any, .bool, .number, .string:
100 | switch direction {
101 | case .deserialize:
102 | if let cast = value as? TypeOfData {
103 | return try setter(cast, type: type(of: value))
104 | }
105 | case .serialize:
106 | if let cast = value as? TypeOfProperty {
107 | return try getter(cast)
108 | }
109 | }
110 | throw NestedTransformerError.typeMismatch(
111 | got: type(of: value),
112 | expected: TypeOfProperty.self,
113 | propertyName: property.name,
114 | class: type(of: instance),
115 | direction: direction
116 | )
117 | case .optional(let wrapped):
118 | return try transform(
119 | value: value,
120 | type: wrapped,
121 | for: property,
122 | instance: instance,
123 | direction: direction
124 | )
125 | case .unknown(let unknownType):
126 | switch direction {
127 | case .serialize:
128 | if let cast = value as? TypeOfProperty {
129 | return try getter(cast)
130 | } else {
131 | throw NestedTransformerError.typeMismatch(
132 | got: type(of: value),
133 | expected: TypeOfProperty.self,
134 | propertyName: property.name,
135 | class: type(of: instance),
136 | direction: direction
137 | )
138 | }
139 | case .deserialize:
140 | if let cast = value as? TypeOfData {
141 | return try setter(cast, type: unknownType)
142 | } else {
143 | throw NestedTransformerError.typeMismatch(
144 | got: type(of: value),
145 | expected: TypeOfData.self,
146 | propertyName: property.name,
147 | class: type(of: instance),
148 | direction: direction
149 | )
150 | }
151 | }
152 | case .array(let inner):
153 | guard let array = value as? [Any] else {
154 | throw NestedTransformerError.typeMismatch(
155 | got: type(of: value),
156 | expected: [Any].self,
157 | propertyName: property.name,
158 | class: type(of: instance),
159 | direction: direction
160 | )
161 | }
162 | return try array.map {
163 | try transform(
164 | value: $0,
165 | type: inner,
166 | for: property,
167 | instance: instance,
168 | direction: direction
169 | )
170 | }
171 | case .dictionary(let inner):
172 | guard let dictionary = value as? [String: Any] else {
173 | // TODO: Add a test for this.
174 | throw NestedTransformerError.typeMismatch(
175 | got: type(of: value),
176 | expected: [String: Any].self,
177 | propertyName: property.name,
178 | class: type(of: instance),
179 | direction: direction
180 | )
181 | }
182 | var result: [String: Any] = [:]
183 | for (key, value) in dictionary {
184 | result[key] = try transform(
185 | value: value,
186 | type: inner,
187 | for: property,
188 | instance: instance,
189 | direction: direction
190 | )
191 | }
192 | return result
193 | }
194 | }
195 |
196 | public func value(for property: Property, instance: Reflectable) throws -> Any? {
197 | let raw = instance.value(forKey: property.name)
198 | let transformed = try transform(
199 | value: raw,
200 | type: property.type,
201 | for: property,
202 | instance: instance,
203 | direction: .serialize
204 | )
205 | return transformed
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/RetroluxTests/TransformerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransformerTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 4/14/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Retrolux
11 | import XCTest
12 |
13 | class TransformerTests: XCTestCase {
14 | func testBasicTransformer() {
15 | class BasicTransformer: NestedTransformer {
16 | static let shared = BasicTransformer()
17 |
18 | typealias TypeOfProperty = Data
19 | typealias TypeOfData = String
20 |
21 | func setter(_ dataValue: String, type: Any.Type) throws -> Data {
22 | return dataValue.data(using: .utf8)!
23 | }
24 |
25 | func getter(_ propertyValue: Data) throws -> String {
26 | return String(data: propertyValue, encoding: .utf8)!
27 | }
28 | }
29 |
30 | class Person: NSObject, Reflectable {
31 | var data = Data()
32 |
33 | static func config(_ c: PropertyConfig) {
34 | c["data"] = [.transformed(BasicTransformer.shared)]
35 | }
36 |
37 | required override init() {
38 | super.init()
39 | }
40 | }
41 |
42 | do {
43 | let reflector = Reflector()
44 | let person = Person()
45 | let properties = try reflector.reflect(person)
46 | XCTAssert(properties.count == 1)
47 | XCTAssert(properties.first?.name == "data")
48 | XCTAssert(properties.first?.type == .unknown(Data.self))
49 | XCTAssert(properties.first?.transformer === BasicTransformer.shared)
50 | XCTAssert(properties.first?.ignored == false)
51 | XCTAssert(properties.first?.nullable == false)
52 | XCTAssert(properties.first?.options.count == 1)
53 | if let option = properties.first?.options.first, case .transformed(let transformer) = option {
54 | XCTAssert(transformer === BasicTransformer.shared)
55 | } else {
56 | XCTFail("Wrong customization options on property.")
57 | }
58 | XCTAssert(properties.first?.serializedName == "data")
59 | XCTAssert(properties.first?.type == .unknown(Data.self))
60 |
61 | try reflector.set(value: "123", for: properties.first!, on: person)
62 | XCTAssert(person.data == "123".data(using: .utf8)!)
63 | let value = try reflector.value(for: properties.first!, on: person)
64 | XCTAssert(value as? String == "123")
65 | } catch {
66 | XCTFail("Failed with error: \(error)")
67 | }
68 | }
69 |
70 | // This test cannot be run multiple times without recycling state.
71 | func testLocalTransformerOverridesGlobalTransformer() {
72 | class Transformer: ReflectableTransformer {
73 | var wasCalled = false
74 |
75 | static let reflector = Reflector()
76 | static let global = Transformer(weakReflector: reflector)
77 | static let local = Transformer(weakReflector: reflector)
78 |
79 | override func setter(_ dataValue: ReflectableTransformer.TypeOfData, type: Any.Type) throws -> ReflectableTransformer.TypeOfProperty {
80 | wasCalled = true
81 | return try super.setter(dataValue, type: type)
82 | }
83 | }
84 |
85 | class Person: Reflection {
86 | var custom: Person?
87 |
88 | static var useLocalTransformer = true
89 |
90 | override class func config(_ c: PropertyConfig) {
91 | if useLocalTransformer {
92 | c["custom"] = [.transformed(Transformer.local)]
93 | }
94 | }
95 | }
96 |
97 | Transformer.reflector.globalTransformers = [Transformer.global]
98 |
99 | let data = "{\"custom\": {}}".data(using: .utf8)!
100 | do {
101 | _ = try Transformer.reflector.convert(fromJSONDictionaryData: data, to: Person.self) as! Person
102 | XCTAssert(Transformer.global.wasCalled == false)
103 | XCTAssert(Transformer.local.wasCalled == true)
104 |
105 | // Clear state
106 | Transformer.local.wasCalled = false
107 | Transformer.global.wasCalled = false
108 | Transformer.reflector.cache.removeAll()
109 |
110 | // Disable local transformer
111 | Person.useLocalTransformer = false
112 |
113 | _ = try Transformer.reflector.convert(fromJSONDictionaryData: data, to: Person.self) as! Person
114 | XCTAssert(Transformer.global.wasCalled == true)
115 | XCTAssert(Transformer.local.wasCalled == false)
116 | } catch {
117 | XCTFail("Should have succeeded, but got error: \(error)")
118 | }
119 | }
120 |
121 | func testURLTransformer() {
122 | class Person: Reflection {
123 | var image_url: URL?
124 | }
125 |
126 | // The escaped forwards slash is intentional, and is just NSJSONSerialization trying to conform to HTML
127 | // standards:
128 | // https://stackoverflow.com/questions/19651009/how-to-prevent-nsjsonserialization-from-adding-extra-escapes-in-url
129 | let data = "{\"image_url\":\"https:\\/\\/www.google.com\\/somepath%20with%20spaces\"}".data(using: .utf8)!
130 | let image_url = URL(string: "https://www.google.com/somepath%20with%20spaces")!
131 | do {
132 | let person = try Reflector().convert(fromJSONDictionaryData: data, to: Person.self) as! Person
133 | XCTAssert(person.image_url == URL(string: "https://www.google.com/somepath%20with%20spaces")!)
134 | XCTAssert(person.image_url == image_url)
135 | XCTAssert(person.image_url?.scheme == "https")
136 | XCTAssert(person.image_url?.host == "www.google.com")
137 | XCTAssert(person.image_url?.path == "/somepath with spaces")
138 | } catch {
139 | XCTFail("Failed with error: \(error)")
140 | }
141 |
142 | do {
143 | let person = Person()
144 | person.image_url = image_url
145 | let out = try Reflector().convertToJSONDictionaryData(from: person)
146 | XCTAssert(out == data)
147 | } catch {
148 | XCTFail("Failed with error: \(error)")
149 | }
150 |
151 | do {
152 | let nullData = "{\"image_url\":null}".data(using: .utf8)!
153 | let person = try Reflector().convert(fromJSONDictionaryData: nullData, to: Person.self) as! Person
154 | XCTAssert(person.image_url == nil)
155 | } catch {
156 | XCTFail("Failed with error: \(error)")
157 | }
158 |
159 | do {
160 | let boringStringData = "{\"image_url\":\"boring\"}".data(using: .utf8)!
161 | let person = try Reflector().convert(fromJSONDictionaryData: boringStringData, to: Person.self) as! Person
162 | XCTAssert(person.image_url == URL(string: "boring")!)
163 | } catch {
164 | XCTFail("Failed with error: \(error)")
165 | }
166 |
167 | do {
168 | let invalidUrlData = "{\"image_url\":\"a string with spaces\"}".data(using: .utf8)!
169 | _ = try Reflector().convert(fromJSONDictionaryData: invalidUrlData, to: Person.self) as! Person
170 | XCTFail("Should not have succeeded.")
171 | } catch {
172 | if case URLTransformer.Error.invalidURL = error {
173 | /* SUCCESS */
174 | } else {
175 | XCTFail("Expected an invalidURL error, but got the following instead: \(error)")
176 | }
177 | }
178 |
179 | do {
180 | let invalidUrlData = "{\"image_url\":\"|\"}".data(using: .utf8)!
181 | _ = try Reflector().convert(fromJSONDictionaryData: invalidUrlData, to: Person.self) as! Person
182 | XCTFail("Should not have succeeded.")
183 | } catch {
184 | if case URLTransformer.Error.invalidURL = error {
185 | /* SUCCESS */
186 | } else {
187 | XCTFail("Expected an invalidURL error, but got the following instead: \(error)")
188 | }
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/RetroluxTests/ReflectionJSONSerializerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RLObjectJSONSerializerTests.swift
3 | // Retrolux
4 | //
5 | // Created by Christopher Bryan Henderson on 10/15/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import Retrolux
12 | import Retrolux
13 |
14 | class ReflectionJSONSerializerTests: XCTestCase {
15 | func makeResponse(from jsonObject: Any) -> ClientResponse {
16 | let data = try! JSONSerialization.data(withJSONObject: jsonObject, options: [])
17 | return ClientResponse(data: data, response: nil, error: nil)
18 | }
19 |
20 | func makeEmptyURLRequest() -> URLRequest {
21 | let url = URL(string: "https://default.thing.any")!
22 | return URLRequest(url: url)
23 | }
24 |
25 | func testBasicSendReceive() {
26 | class Person: Reflection {
27 | var name = ""
28 | var age = 0
29 | }
30 |
31 | let serializer = ReflectionJSONSerializer()
32 | do {
33 | let response = makeResponse(from: [
34 | "name": "Bob",
35 | "age": 24
36 | ])
37 | let object = try serializer.makeValue(from: response, type: Person.self)
38 |
39 | XCTAssert(object.name == "Bob")
40 | XCTAssert(object.age == 24)
41 |
42 | var request = makeEmptyURLRequest()
43 | let originalURL = request.url!
44 | try serializer.apply(arguments: [BuilderArg(type: Person.self, creation: object, starting: object)], to: &request)
45 | XCTAssert(request.url == originalURL, "URL should not have changed")
46 | XCTAssert(request.value(forHTTPHeaderField: "Content-Type") == "application/json", "Missing Content-Type header.")
47 | XCTAssert(request.allHTTPHeaderFields?.count == 1, "Only Content-Type should be set.")
48 | XCTAssert(request.httpBody?.count == "{\"name\":\"Bob\",\"age\":24}".characters.count)
49 | XCTAssert(request.httpMethod == "GET", "Default value should be GET, but the serializer changed it to \(request.httpMethod!).")
50 |
51 | guard let body = request.httpBody else {
52 | XCTFail("Missing body on URL request")
53 | return
54 | }
55 |
56 | guard let dictionary = try JSONSerialization.jsonObject(with: body, options: []) as? [String: Any] else {
57 | XCTFail("Invalid root type for http body--expected dictionary")
58 | return
59 | }
60 |
61 | XCTAssert(dictionary["name"] as? String == "Bob")
62 | XCTAssert(dictionary["age"] as? Int == 24)
63 | } catch {
64 | XCTFail("Error: \(error)")
65 | }
66 | }
67 |
68 | func testSupports() {
69 | class NotSupported: NSObject {}
70 | class Supported: Reflection {}
71 | class AlsoSupported: NSObject, Reflectable {
72 | override required init() {
73 | super.init()
74 | }
75 | }
76 |
77 | let serializer = ReflectionJSONSerializer()
78 | XCTAssert(serializer.supports(inboundType: NotSupported.self) == false)
79 | XCTAssert(serializer.supports(outboundType: NotSupported.self) == false)
80 | XCTAssert(serializer.validate(outbound: [BuilderArg(type: NotSupported.self, creation: NotSupported(), starting: NotSupported())]) == false)
81 |
82 | XCTAssert(serializer.supports(inboundType: Supported.self) == true)
83 | XCTAssert(serializer.supports(outboundType: Supported.self) == true)
84 | XCTAssert(serializer.validate(outbound: [BuilderArg(type: Supported.self, creation: Supported(), starting: Supported())]) == true)
85 |
86 | XCTAssert(serializer.supports(inboundType: AlsoSupported.self) == true)
87 | XCTAssert(serializer.supports(outboundType: AlsoSupported.self) == true)
88 | XCTAssert(serializer.validate(outbound: [BuilderArg(type: AlsoSupported.self, creation: AlsoSupported(), starting: AlsoSupported())]) == true)
89 | }
90 |
91 | func testToJSONError() {
92 | class Invalid: Reflection {
93 | var name = Data() // This is not a supported type
94 | }
95 |
96 | let serializer = ReflectionJSONSerializer()
97 | var request = makeEmptyURLRequest()
98 |
99 | do {
100 | try serializer.apply(arguments: [BuilderArg(type: Invalid.self, creation: nil, starting: Invalid())], to: &request)
101 | XCTFail("Should not have succeeded.")
102 | } catch ReflectionError.propertyNotSupported(propertyName: let propertyName, type: let type, forClass: let `class`) {
103 | XCTAssert(propertyName == "name")
104 | XCTAssert(type == .unknown(Data.self))
105 | XCTAssert(`class` == Invalid.self)
106 | } catch {
107 | XCTFail("Unexpected error \(error)")
108 | }
109 | }
110 |
111 | func testFromJSONError() {
112 | class Valid: Reflection {
113 | var age: Int = 0
114 | }
115 |
116 | let serializer = ReflectionJSONSerializer()
117 | let inputDictionary = [
118 | "age": "0" // Is wrong type on purpose
119 | ]
120 |
121 | do {
122 | let response = makeResponse(from: inputDictionary)
123 | _ = try serializer.makeValue(from: response, type: Valid.self)
124 | XCTFail("Should not have succeeded.")
125 | } catch ReflectorSerializationError.typeMismatch(expected: let expected, got: _, propertyName: let propertyName, forClass: let forClass) {
126 | // TODO: Add unit test for got.
127 | XCTAssert(expected == .number(Int.self))
128 | XCTAssert(propertyName == "age")
129 | XCTAssert(forClass == Valid.self)
130 | } catch {
131 | XCTFail("Unexpected error \(error)")
132 | }
133 |
134 | do {
135 | let response = makeResponse(from: [inputDictionary])
136 | _ = try serializer.makeValue(from: response, type: [Valid].self)
137 | XCTFail("Should not have succeeded.")
138 | } catch ReflectorSerializationError.typeMismatch(expected: let expected, got: _, propertyName: let propertyName, forClass: let forClass) {
139 | // TODO: Add unit test for got.
140 | XCTAssert(expected == .number(Int.self))
141 | XCTAssert(propertyName == "age")
142 | XCTAssert(forClass == Valid.self)
143 | } catch {
144 | XCTFail("Unexpected error \(error)")
145 | }
146 | }
147 |
148 | func testNoDataError() {
149 | class Whatever: Reflection {}
150 |
151 | let serializer = ReflectionJSONSerializer()
152 | let clientData = ClientResponse(data: nil, response: nil, error: nil)
153 |
154 | do {
155 | _ = try serializer.makeValue(from: clientData, type: Whatever.self)
156 | XCTFail("Should not have passed.")
157 | } catch ReflectionJSONSerializerError.noData {
158 | // Works!
159 | } catch {
160 | XCTFail("Failed with error: \(error)")
161 | }
162 | }
163 |
164 | func testInvalidJSON() {
165 | class Whatever: Reflection {}
166 |
167 | let serializer = ReflectionJSONSerializer()
168 | let clientData = ClientResponse(data: "{".data(using: .utf8), response: nil, error: nil)
169 |
170 | do {
171 | _ = try serializer.makeValue(from: clientData, type: Whatever.self)
172 | XCTFail("Should not have succeeded.")
173 | } catch ReflectorSerializationError.invalidJSONData(_) {
174 | // WORKS!
175 | } catch {
176 | XCTFail("Failed with error \(error)")
177 | }
178 |
179 | do {
180 | _ = try serializer.makeValue(from: clientData, type: [Whatever].self)
181 | XCTFail("Should not have succeeded.")
182 | } catch ReflectorSerializationError.invalidJSONData(_) {
183 | // WORKS!
184 | } catch {
185 | XCTFail("Failed with error \(error)")
186 | }
187 | }
188 |
189 | func testUnsupportedType() {
190 | let serializer = ReflectionJSONSerializer()
191 | let clientData = ClientResponse(data: "asdf".data(using: .utf8), response: nil, error: nil)
192 | var request = makeEmptyURLRequest()
193 |
194 | // Test array nested type.
195 |
196 | do {
197 | _ = try serializer.makeValue(from: clientData, type: [String].self)
198 | XCTFail("Should not have succeeded.")
199 | } catch ReflectionJSONSerializerError.unsupportedType(let type) {
200 | XCTAssert(type == [String].self)
201 | } catch {
202 | XCTFail("Unknown error: \(error)")
203 | }
204 |
205 | do {
206 | _ = try serializer.apply(arguments: [BuilderArg(type: [Int].self, creation: nil, starting: [0])], to: &request)
207 | XCTFail("Should not have succeeded.")
208 | } catch ReflectionJSONSerializerError.unsupportedType(let type) {
209 | XCTAssert(type == [Int].self)
210 | } catch {
211 | XCTFail("Unknown error: \(error)")
212 | }
213 |
214 | // Test individual type.
215 |
216 | do {
217 | _ = try serializer.makeValue(from: clientData, type: String.self)
218 | XCTFail("Should not have succeeded.")
219 | } catch ReflectionJSONSerializerError.unsupportedType(let type) {
220 | XCTAssert(type == String.self)
221 | } catch {
222 | XCTFail("Unknown error: \(error)")
223 | }
224 |
225 | do {
226 | _ = try serializer.apply(arguments: [BuilderArg(type: Int.self, creation: 0, starting: nil)], to: &request)
227 | XCTFail("Should not have succeeded.")
228 | } catch ReflectionJSONSerializerError.unsupportedType(let type) {
229 | XCTAssert(type == Int.self)
230 | } catch {
231 | XCTFail("Unknown error: \(error)")
232 | }
233 | }
234 |
235 | func testSendDiff() {
236 | let serializer = ReflectionJSONSerializer()
237 |
238 | do {
239 | class Person: Reflection {
240 | var name = ""
241 | var age = 0
242 | }
243 | let p1 = Person()
244 | let p2 = Person()
245 | p1.name = "Bob"
246 | p1.age = 33
247 | p2.name = "Alice"
248 | p2.age = 33
249 |
250 | var request = URLRequest(url: URL(string: "https://www.google.com/")!)
251 |
252 | let diff = Diff(from: p1, to: p2)
253 | let arg = BuilderArg(type: type(of: diff), creation: Diff(), starting: diff)
254 | _ = try serializer.apply(arguments: [arg], to: &request)
255 | print(String(data: request.httpBody!, encoding: .utf8)!)
256 | XCTAssert(request.httpBody == "{\"name\":\"Alice\"}".data(using: .utf8)!)
257 | XCTAssert(request.allHTTPHeaderFields ?? [:] == ["Content-Type": "application/json"])
258 |
259 | let diff2 = Diff(from: p2, to: p1)
260 | let arg2 = BuilderArg(type: type(of: diff2), creation: Diff(), starting: diff2)
261 | _ = try serializer.apply(arguments: [arg2], to: &request)
262 | print(String(data: request.httpBody!, encoding: .utf8)!)
263 | XCTAssert(request.httpBody == "{\"name\":\"Bob\"}".data(using: .utf8)!)
264 | XCTAssert(request.allHTTPHeaderFields ?? [:] == ["Content-Type": "application/json"])
265 | } catch {
266 | XCTFail("Unknown error: \(error)")
267 | }
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/RetroluxTests/ValueTransformerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValueTransformerTests.swift
3 | // RetroluxReflector
4 | //
5 | // Created by Christopher Bryan Henderson on 10/17/16.
6 | // Copyright © 2016 Christopher Bryan Henderson. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Retrolux
11 |
12 | class ValueTransformerTests: XCTestCase {
13 | func testForwards() {
14 | class Test: Reflection {
15 | var name = ""
16 | var test: Test?
17 |
18 | override class func config(_ c: PropertyConfig) {
19 | c["test"] = [.nullable]
20 | }
21 | }
22 |
23 | let dictionary: [String: Any] = [
24 | "name": "Bob"
25 | ]
26 |
27 | let reflector = Reflector()
28 | let transformer = ReflectableTransformer(weakReflector: reflector)
29 |
30 | do {
31 | let test = Test()
32 | let config = PropertyConfig()
33 | Test.config(config)
34 | let property = Property(type: .unknown(Test.self), name: "test", options: config["test"])
35 | try transformer.set(value: dictionary, for: property, instance: test)
36 | } catch {
37 | XCTFail("Failed with error: \(error)")
38 | }
39 | }
40 |
41 | func testNested() {
42 | class Test: Reflection {
43 | var name = ""
44 | var friends: [String: Test]? = nil
45 |
46 | convenience init(name: String) {
47 | self.init()
48 | self.name = name
49 | }
50 | }
51 |
52 | let reflector = Reflector()
53 | let transformer = ReflectableTransformer(weakReflector: reflector)
54 | XCTAssert(transformer.supports(propertyType: .unknown(Test.self)))
55 |
56 | do {
57 | let test = Test()
58 | test.name = "bob"
59 | let dictionary = try reflector.convertToDictionary(from: test)
60 | XCTAssert(dictionary["name"] as? String == "bob")
61 | XCTAssert(dictionary["friends"] is NSNull)
62 |
63 | test.friends = [
64 | "sally": Test(name: "Sally"),
65 | "robert": Test(name: "Robert")
66 | ]
67 | let d2 = try reflector.convertToDictionary(from: test)
68 | XCTAssert(d2["name"] as? String == "bob")
69 |
70 | let friends = d2["friends"] as? [String: Any]
71 |
72 | let sally = friends?["sally"] as? [String: Any]
73 | XCTAssert(sally?["name"] as? String == "Sally")
74 |
75 | let robert = friends?["robert"] as? [String: Any]
76 | XCTAssert(robert?["name"] as? String == "Robert")
77 |
78 | let back = try reflector.convert(fromDictionary: d2, to: Test.self) as! Test
79 | XCTAssert(back.name == "bob")
80 | XCTAssert(back.friends?.count == 2)
81 | XCTAssert(back.friends?["sally"]?.name == "Sally")
82 | XCTAssert(back.friends?["robert"]?.name == "Robert")
83 | } catch {
84 | XCTFail("Failed with error: \(error)")
85 | }
86 | }
87 |
88 | func testNestedBackwards() {
89 | class Test: Reflection {
90 | var name = ""
91 | var age = 0
92 | var another: Test?
93 | }
94 |
95 | let test = Test()
96 | test.name = "Bob"
97 | test.age = 30
98 | test.another = Test()
99 | test.another?.name = "Another"
100 | test.another?.age = 25
101 | test.another?.another = Test()
102 | test.another?.another?.name = "Another 2"
103 | test.another?.another?.age = 20
104 |
105 | do {
106 | let reflector = Reflector()
107 | let transformer = ReflectableTransformer(weakReflector: reflector)
108 | let output = try transformer.getter(test)
109 | XCTAssert(output["name"] as? String == "Bob")
110 | XCTAssert(output["age"] as? Int == 30)
111 | let another = output["another"] as? [String: Any]
112 | XCTAssert(another?["name"] as? String == "Another")
113 | XCTAssert(another?["age"] as? Int == 25)
114 | let another2 = another?["another"] as? [String: Any]
115 | XCTAssert(another2?["name"] as? String == "Another 2")
116 | XCTAssert(another2?["age"] as? Int == 20)
117 | XCTAssert(another2?["another"] is NSNull)
118 | } catch {
119 | XCTFail("Failed with error: \(error)")
120 | }
121 | }
122 |
123 | func testErrorHandling() {
124 | class Test: Reflection {
125 | var name = ""
126 | }
127 |
128 | let dictionary: [String: Any] = [:] // Should trigger a key not found error.
129 |
130 | let reflector = Reflector()
131 | let transformer = ReflectableTransformer(weakReflector: reflector)
132 |
133 | do {
134 | _ = try transformer.setter(dictionary, type: Test.self)
135 | XCTFail("Should not have passed.")
136 | } catch ReflectorSerializationError.keyNotFound(propertyName: let propertyName, key: let key, forClass: let `class`) {
137 | XCTAssert(propertyName == "name")
138 | XCTAssert(key == "name")
139 | XCTAssert(`class` == Test.self)
140 | } catch {
141 | XCTFail("Failed with error: \(error)")
142 | }
143 | }
144 |
145 | func testNullable() {
146 | class CustomTransformer: NestedTransformer {
147 | typealias TypeOfData = String
148 | typealias TypeOfProperty = Data
149 |
150 | func setter(_ dataValue: String, type: Any.Type) throws -> Data {
151 | XCTFail("Shouldn't be called.")
152 | return Data()
153 | }
154 |
155 | func getter(_ propertyValue: Data) throws -> String {
156 | XCTFail("Shouldn't be called.")
157 | return ""
158 | }
159 | }
160 |
161 | class Test: NSObject, Reflectable {
162 | var data: Data?
163 |
164 | required override init() {
165 | super.init()
166 | }
167 |
168 | static func config(_ c: PropertyConfig) {
169 | c["data"] = [.transformed(CustomTransformer())]
170 | }
171 | }
172 |
173 | let test = Test()
174 |
175 | let reflector = Reflector()
176 |
177 | do {
178 | let properties = try reflector.reflect(test)
179 | XCTAssert(properties.count == 1)
180 | XCTAssert(properties.first?.name == "data")
181 | let value = try reflector.value(for: properties.first!, on: test)
182 | XCTAssert(value is NSNull)
183 | } catch {
184 | XCTFail("Error getting value: \(error)")
185 | }
186 | }
187 |
188 | func testBoolStringConversion() {
189 | class BoolTransformer: NestedTransformer {
190 | static var setterWasCalled = 0
191 | static var getterWasCalled = 0
192 |
193 | typealias TypeOfData = String
194 | typealias TypeOfProperty = Bool
195 |
196 | func setter(_ dataValue: String, type: Any.Type) throws -> Bool {
197 | BoolTransformer.setterWasCalled += 1
198 | return dataValue == "t"
199 | }
200 |
201 | func getter(_ propertyValue: Bool) throws -> String {
202 | BoolTransformer.getterWasCalled += 1
203 | return propertyValue ? "t" : "f"
204 | }
205 | }
206 |
207 | class Test: Reflection {
208 | var randomize: Bool = false
209 |
210 | override class func config(_ c: PropertyConfig) {
211 | c["randomize"] = [.transformed(BoolTransformer())]
212 | }
213 | }
214 |
215 | let reflector = Reflector()
216 | do {
217 | XCTAssert(BoolTransformer.setterWasCalled == 0)
218 | XCTAssert(BoolTransformer.getterWasCalled == 0)
219 |
220 | let data = "{\"randomize\":\"t\"}".data(using: .utf8)!
221 | let test = try reflector.convert(fromJSONDictionaryData: data, to: Test.self) as! Test
222 | XCTAssert(test.randomize == true)
223 |
224 | XCTAssert(BoolTransformer.setterWasCalled == 1)
225 | XCTAssert(BoolTransformer.getterWasCalled == 0)
226 |
227 | let data2 = "{\"randomize\":\"f\"}".data(using: .utf8)!
228 | let test2 = try reflector.convert(fromJSONDictionaryData: data2, to: Test.self) as! Test
229 | XCTAssert(test2.randomize == false)
230 |
231 | XCTAssert(BoolTransformer.setterWasCalled == 2)
232 | XCTAssert(BoolTransformer.getterWasCalled == 0)
233 |
234 | let output = try reflector.convertToJSONDictionaryData(from: test)
235 | XCTAssert(output == "{\"randomize\":\"t\"}".data(using: .utf8)!)
236 |
237 | XCTAssert(BoolTransformer.setterWasCalled == 2)
238 | XCTAssert(BoolTransformer.getterWasCalled == 1)
239 |
240 | let output2 = try reflector.convertToJSONDictionaryData(from: test2)
241 | XCTAssert(output2 == "{\"randomize\":\"f\"}".data(using: .utf8)!)
242 |
243 | XCTAssert(BoolTransformer.setterWasCalled == 2)
244 | XCTAssert(BoolTransformer.getterWasCalled == 2)
245 | } catch {
246 | XCTFail("Failed with error: \(error)")
247 | }
248 | }
249 |
250 | // This test is broken and always fails:
251 | // error: -[RetroluxTests.ValueTransformerTests testEnumConversion] : failed: caught "NSInvalidArgumentException", "-[_SwiftValue longLongValue]: unrecognized selector sent to instance 0x7fe0cb51f310"
252 | // func testEnumConversion() {
253 | // @objc enum AssessmentQuestionType: Int {
254 | // case good = 0
255 | // case bad = 1
256 | //
257 | // static func getQuestionType(for string: String) -> AssessmentQuestionType {
258 | // return string == "good" ? good : bad
259 | // }
260 | //
261 | // var value: String {
262 | // return self == .good ? "good" : "bad"
263 | // }
264 | // }
265 | //
266 | // class StringToQuestionTypeTransformer: NestedTransformer {
267 | // typealias TypeOfData = String
268 | // typealias TypeOfProperty = AssessmentQuestionType
269 | //
270 | // let setterProp: (Any?, ClassType) -> Void
271 | //
272 | // init(setter: @escaping (Any?, ClassType) -> Void) {
273 | // self.setterProp = setter
274 | // }
275 | //
276 | // func setter(_ dataValue: String, type: Any.Type) throws -> AssessmentQuestionType {
277 | // return AssessmentQuestionType.getQuestionType(for: dataValue)
278 | // }
279 | //
280 | // func getter(_ propertyValue: AssessmentQuestionType) throws -> String {
281 | // return propertyValue.value
282 | // }
283 | //
284 | // func internalSetter(value: Any?, for property: Property, on instance: Reflectable) throws {
285 | // setterProp(value, instance as! ClassType)
286 | // }
287 | // }
288 | //
289 | // class MyClass: Reflection {
290 | // var questionType: AssessmentQuestionType = .bad
291 | //
292 | // override class func config(_ c: PropertyConfig) {
293 | // c["questionType"] = [.transformed(StringToQuestionTypeTransformer(setter: { (value: Any?, instance: MyClass) -> Void in
294 | // instance.questionType = value as! AssessmentQuestionType
295 | // }))]
296 | // }
297 | // }
298 | //
299 | // let data = "{\"questionType\":\"good\"}".data(using: .utf8)!
300 | // do {
301 | // let obj = try Reflector().convert(fromJSONDictionaryData: data, to: MyClass.self) as! MyClass
302 | // XCTAssert(obj.questionType == .good)
303 | // } catch {
304 | // XCTFail("Failed with error: \(error)")
305 | // }
306 | // }
307 | }
308 |
--------------------------------------------------------------------------------
/RetroluxTests/BuilderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderTests.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 12/12/16.
6 | // Copyright © 2016 Bryan. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import Retrolux
11 |
12 | fileprivate class GreedyOutbound: OutboundSerializer {
13 | enum Error: Swift.Error {
14 | case whatever
15 | }
16 | var forceFail = false
17 | var supports: Bool?
18 | var applyWasCalled = false
19 |
20 | fileprivate func supports(outboundType: Any.Type) -> Bool {
21 | supports = outboundType is T.Type || T.self == Any.self
22 | return supports!
23 | }
24 |
25 | fileprivate func validate(outbound: [BuilderArg]) -> Bool {
26 | return !forceFail
27 | }
28 |
29 | fileprivate func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
30 | if forceFail {
31 | throw Error.whatever
32 | }
33 | applyWasCalled = true
34 | }
35 | }
36 |
37 | class BuilderTests: XCTestCase {
38 | func testVoidResponse() {
39 | let builder = Builder.dummy()
40 | let request = builder.makeRequest(method: .post, endpoint: "something", args: (), response: Void.self) { (creation, starting, request) in
41 | ClientResponse(
42 | url: request.url!,
43 | data: "Something happened!".data(using: .utf8)!,
44 | status: 200
45 | )
46 | }
47 | let response = request().perform()
48 | XCTAssert(response.isSuccessful)
49 | }
50 |
51 | func testAsyncCapturing() {
52 | let builder = Builder.dummy()
53 | let request = builder.makeRequest(method: .get, endpoint: "", args: (), response: Void.self)
54 | let originalURL = builder.base
55 | let expectation = self.expectation(description: "Waiting for response.")
56 | var hasRequestComeBackYet = false
57 | request().enqueue { response in
58 | hasRequestComeBackYet = true
59 | XCTAssert(response.request.url == originalURL)
60 | expectation.fulfill()
61 | }
62 | let newURL = URL(string: "8.8.8.8/")!
63 | builder.base = newURL
64 | XCTAssert(!hasRequestComeBackYet)
65 | waitForExpectations(timeout: 1) { (error) in
66 | if let error = error {
67 | XCTFail("\(error)")
68 | }
69 | }
70 |
71 | let expectation2 = self.expectation(description: "Waiting for response.")
72 | request().enqueue { response in
73 | XCTAssert(response.request.url == newURL)
74 | expectation2.fulfill()
75 | }
76 | waitForExpectations(timeout: 1) { (error) in
77 | if let error = error {
78 | XCTFail("\(error)")
79 | }
80 | }
81 | }
82 |
83 | func testURLEscaping() {
84 | let builder = Builder.dry()
85 | let request = builder.makeRequest(method: .post, endpoint: "some_endpoint/?query=value a", args: (), response: Void.self)
86 | let response = request().perform()
87 | XCTAssert(response.request.url?.absoluteString == "\(builder.base.absoluteString)some_endpoint/%3Fquery=value%20a")
88 | }
89 |
90 | func testOptionalArgs() {
91 | let builder = Builder.dry()
92 |
93 | let arg: Path? = Path("id")
94 | let request = builder.makeRequest(method: .post, endpoint: "/some_endpoint/{id}/", args: arg, response: Void.self)
95 | let response = request(Path("it_worked")).perform()
96 | XCTAssert(response.request.url!.absoluteString.contains("it_worked"))
97 |
98 | let arg2: Path? = Path("id")
99 | let request2 = builder.makeRequest(method: .post, endpoint: "/some_endpoint/{id}/", args: arg2, response: Void.self)
100 | let response2 = request2(nil).perform()
101 | XCTAssert(response2.request.url!.absoluteString.removingPercentEncoding!.contains("{id}"))
102 |
103 | struct Args3 {
104 | let field: Field?
105 | }
106 | let args3 = Args3(field: Field("username"))
107 | let request3 = builder.makeRequest(method: .post, endpoint: "/some_endpoint/", args: args3, response: Void.self)
108 | let response3 = request3(Args3(field: Field("IT_WORKED"))).perform()
109 | let data = response3.request.httpBody!
110 | let string = String(data: data, encoding: .utf8)!
111 | XCTAssert(string.contains("IT_WORKED"))
112 |
113 | struct Args4 {
114 | let field: Field?
115 | }
116 | let args4 = Args4(field: Field("username"))
117 | let request4 = builder.makeRequest(method: .post, endpoint: "/some_endpoint/", args: args4, response: Void.self)
118 | let response4 = request4(Args4(field: nil)).perform()
119 | XCTAssert(response4.request.httpBody == nil)
120 |
121 | struct Args5 {
122 | let field: Field?
123 | let field2: Field?
124 | let field3: Field
125 | let field4: Field?
126 | }
127 | let args5 = Args5(field: nil, field2: Field("username"), field3: Field("password"), field4: nil)
128 | let request5 = builder.makeRequest(method: .post, endpoint: "/some_endpoint/", args: args5, response: Void.self)
129 | let response5 = request5(Args5(field: nil, field2: Field("TEST_USERNAME"), field3: Field("TEST_PASSWORD"), field4: nil)).perform()
130 | let data5 = response5.request.httpBody!
131 | let string5 = String(data: data5, encoding: .utf8)!
132 | XCTAssert(string5.contains("TEST_USERNAME") && string5.contains("TEST_PASSWORD"))
133 |
134 | let args6: Field? = nil
135 | let request6 = builder.makeRequest(
136 | method: .post,
137 | endpoint: "/some_endpoint/",
138 | args: args6,
139 | response: Void.self
140 | )
141 | let response6 = request6(nil).perform()
142 | XCTAssert(response6.request.httpBody == nil)
143 | }
144 |
145 | func testDepthRecursion1() {
146 | @objc(Person)
147 | class Person: Reflection {
148 | var name = ""
149 | }
150 |
151 | let call = Builder.dry().makeRequest(method: .post, endpoint: "login", args: Person(), response: Void.self)
152 | let response = call(Person()).perform()
153 | XCTAssert(response.request.httpBody! == "{\"name\":\"\"}".data(using: .utf8)!)
154 | }
155 |
156 | func testMultipleMatchingSerializers() {
157 | let builder = Builder.dry()
158 | builder.serializers = [GreedyOutbound(), GreedyOutbound()]
159 | let function = builder.makeRequest(method: .post, endpoint: "whatever", args: (Int(), String()), response: Void.self)
160 | _ = function((3, "a")).perform()
161 | XCTAssert((builder.serializers[0] as! GreedyOutbound).applyWasCalled == true)
162 | XCTAssert((builder.serializers[1] as! GreedyOutbound).applyWasCalled == false)
163 | }
164 |
165 | func testUnsupportedArgument() {
166 | let builder = Builder.dry()
167 | builder.serializers = [GreedyOutbound()]
168 | let function = builder.makeRequest(method: .post, endpoint: "whateverz", args: String(), response: Void.self)
169 | let response = function("a").perform()
170 | if let error = response.error, case BuilderError.unsupportedArgument(let arg) = error {
171 | XCTAssert(arg.type == String.self)
172 | XCTAssert(arg.creation as? String == "")
173 | XCTAssert(arg.starting as? String == "a")
174 | } else {
175 | XCTFail("Expected to fail.")
176 | }
177 | }
178 |
179 | func testNestedArgs() {
180 | struct MyArgs {
181 | struct MoreArgs {
182 | struct EvenMoreArgs {
183 | let path: Path
184 | }
185 |
186 | let evenMore: EvenMoreArgs
187 | }
188 |
189 | let args: MoreArgs
190 | }
191 |
192 | let creationArgs = MyArgs(args: MyArgs.MoreArgs(evenMore: MyArgs.MoreArgs.EvenMoreArgs(path: Path("id"))))
193 | let builder = Builder.dry()
194 | let function = builder.makeRequest(method: .delete, endpoint: "users/{id}/", args: creationArgs, response: Void.self)
195 |
196 | let startingArgs = MyArgs(args: MyArgs.MoreArgs(evenMore: MyArgs.MoreArgs.EvenMoreArgs(path: Path("asdf"))))
197 | let response = function(startingArgs).perform()
198 | XCTAssert(response.request.url!.absoluteString.hasSuffix("users/asdf/"))
199 | print(response.request.url!)
200 | }
201 |
202 | func testNestedUnsupportedArgument() {
203 | struct Container {
204 | let object = NSObject()
205 | let arg = String()
206 | let another = Int()
207 | }
208 |
209 | let builder = Builder.dry()
210 | builder.serializers = [GreedyOutbound()]
211 | let function = builder.makeRequest(method: .post, endpoint: "whateverz", args: Container(), response: Void.self)
212 | let response = function(Container()).perform()
213 | if let error = response.error, case BuilderError.unsupportedArgument(let arg) = error {
214 | XCTAssert(arg.type == NSObject.self)
215 | XCTAssert(arg.creation is NSObject)
216 | XCTAssert(arg.starting is NSObject)
217 | } else {
218 | XCTFail("Expected to fail.")
219 | }
220 | }
221 |
222 | func testOutboundSerializerValidationError() {
223 | let builder = Builder.dry()
224 | let serializer = GreedyOutbound()
225 | serializer.forceFail = true
226 | builder.serializers = [serializer]
227 | let function = builder.makeRequest(method: .post, endpoint: "whateverz", args: Int(), response: Void.self)
228 | let response = function(3).perform()
229 | if let error = response.error, case BuilderError.serializationError(serializer: let s, error: let e, arguments: let args) = error {
230 | XCTAssert(s === serializer)
231 | XCTAssert(e is GreedyOutbound.Error)
232 | XCTAssert(args.count == 1)
233 | XCTAssert(args.first?.creation as? Int == Int())
234 | XCTAssert(args.first?.starting as? Int == 3)
235 | } else {
236 | XCTFail("Expected to fail.")
237 | }
238 | }
239 |
240 | // This tests a bug where launching 100 or more network requests at the same time
241 | // would cause the Builder to deadlock at semaphore.wait().
242 | func testLotsOfSimultaneousNetworkRequests() {
243 | let builder = Builder(base: URL(string: "http://127.0.0.1/")!)
244 | let request = builder.makeRequest(method: .get, endpoint: "", args: (), response: Void.self)
245 |
246 | var expectations = [XCTestExpectation]()
247 |
248 | for i in 0..<1000 {
249 | let expectation = self.expectation(description: "request \(i)")
250 | expectations.append(expectation)
251 | request().enqueue { response in
252 | expectation.fulfill()
253 | }
254 | }
255 |
256 | self.wait(for: expectations, timeout: 60)
257 | }
258 |
259 | func testSerializationBlocking() {
260 | class Serializer: OutboundSerializer {
261 | func supports(outboundType: Any.Type) -> Bool {
262 | return true
263 | }
264 |
265 | func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
266 | sleep(1)
267 | }
268 | }
269 |
270 | let builder = Builder.dry()
271 | builder.serializers.append(Serializer())
272 |
273 | let start = Date()
274 | let request = builder.makeRequest(method: .get, endpoint: "whatever", args: NSNull(), response: Void.self)
275 | request(NSNull()).enqueue(callback: { _ in })
276 | XCTAssert(Date() < start + 0.5)
277 |
278 | let start2 = Date()
279 | _ = request(NSNull()).perform()
280 | XCTAssert(Date() > start2 + 0.5)
281 | }
282 |
283 | func testCreationArgDiffTypeThanStartingArg() {
284 | class Base: Reflection {
285 | var base = ""
286 | }
287 |
288 | class Person: Base {
289 | var person = ""
290 | }
291 |
292 | let builder = Builder.dry()
293 | let request = builder.makeRequest(method: .get, endpoint: "", args: Base(), response: Void.self)
294 | let p = Person()
295 | p.base = "b"
296 | p.person = "p"
297 | let response = request(p).perform()
298 | XCTAssert(response.request.httpBody == "{\"person\":\"p\",\"base\":\"b\"}".data(using: .utf8)!)
299 | }
300 | }
301 |
--------------------------------------------------------------------------------
/Retrolux/Builder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder.swift
3 | // Retrolux
4 | //
5 | // Created by Bryan Henderson on 1/26/17.
6 | // Copyright © 2017 Bryan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // State for each request is captured as soon as possible, instead of when it is needed.
12 | // It is assumed that this behavior is more intuitive.
13 | public struct RequestCapturedState {
14 | public var base: URL
15 | public var workerQueue: DispatchQueue
16 | }
17 |
18 | open class Builder {
19 | private static let dryBase = URL(string: "e7c37c97-5483-4522-b400-106505fbf6ff/")!
20 | open class func dry() -> Builder {
21 | return Builder(base: self.dryBase)
22 | }
23 |
24 | open var base: URL
25 | open let isDryModeEnabled: Bool
26 | open var callFactory: CallFactory
27 | open var client: Client
28 | open var serializers: [Serializer]
29 | open var requestInterceptor: ((inout URLRequest) -> Void)?
30 | open var responseInterceptor: ((inout ClientResponse) -> Void)?
31 | open var workerQueue = DispatchQueue(
32 | label: "Retrolux.worker",
33 | qos: .background,
34 | attributes: .concurrent,
35 | autoreleaseFrequency: .inherit,
36 | target: nil
37 | )
38 | open var stateToCapture: RequestCapturedState {
39 | return RequestCapturedState(
40 | base: base,
41 | workerQueue: workerQueue
42 | )
43 | }
44 | open func outboundSerializers() -> [OutboundSerializer] {
45 | return serializers.flatMap { $0 as? OutboundSerializer }
46 | }
47 |
48 | open func inboundSerializers() -> [InboundSerializer] {
49 | return serializers.flatMap { $0 as? InboundSerializer }
50 | }
51 |
52 | public init(base: URL) {
53 | self.base = base
54 | self.isDryModeEnabled = base == type(of: self).dryBase
55 | self.callFactory = HTTPCallFactory()
56 | self.client = HTTPClient()
57 | self.serializers = [
58 | ReflectionJSONSerializer(),
59 | MultipartFormDataSerializer(),
60 | URLEncodedSerializer()
61 | ]
62 | self.requestInterceptor = nil
63 | self.responseInterceptor = nil
64 | }
65 |
66 | open func log(request: URLRequest) {
67 | print("Retrolux: \(request.httpMethod!) \(request.url!.absoluteString.removingPercentEncoding!)")
68 | }
69 |
70 | open func log(response: Response) {
71 | let status = response.status ?? 0
72 |
73 | if response.error is BuilderError == false {
74 | let requestURL = response.request.url!.absoluteString.removingPercentEncoding!
75 | print("Retrolux: \(status) \(requestURL)")
76 | }
77 |
78 | if let error = response.error {
79 | if let localized = error as? LocalizedError {
80 | if let errorDescription = localized.errorDescription {
81 | print("Retrolux: Error: \(errorDescription)")
82 | }
83 | if let recoverySuggestion = localized.recoverySuggestion {
84 | print("Retrolux: Suggestion: \(recoverySuggestion)")
85 | }
86 |
87 | if localized.errorDescription == nil && localized.recoverySuggestion == nil {
88 | print("Retrolux: Error: \(error)")
89 | }
90 | } else {
91 | print("Retrolux: Error: \(error)")
92 | }
93 | }
94 | }
95 |
96 | open func interpret(response: UninterpretedResponse) -> InterpretedResponse {
97 | // BuilderErrors are highest priority over other kinds of errors,
98 | // because they represent errors creating the request.
99 | if let error = response.error as? BuilderError {
100 | return .failure(error)
101 | }
102 |
103 | if !response.isHttpStatusOk {
104 | if response.urlResponse == nil, let error = response.error {
105 | return .failure(ResponseError.connectionError(error))
106 | }
107 | return .failure(ResponseError.invalidHttpStatusCode(code: response.status))
108 | }
109 |
110 | if let error = response.error {
111 | return .failure(error)
112 | }
113 |
114 | if let body = response.body {
115 | return .success(body)
116 | } else {
117 | assert(false, "This should be impossible.")
118 | return .failure(NSError(domain: "Retrolux.Error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to serialize response for an unknown reason."]))
119 | }
120 | }
121 |
122 | private func normalize(arg: Any, serializerType: OutboundSerializerType, serializers: [OutboundSerializer]) -> [(serializer: OutboundSerializer?, value: Any?, type: Any.Type)] {
123 | if type(of: arg) == Void.self {
124 | return []
125 | } else if let wrapped = arg as? WrappedSerializerArg {
126 | if let value = wrapped.value {
127 | return normalize(arg: value, serializerType: serializerType, serializers: serializers)
128 | } else {
129 | return [(serializers.first(where: { serializerType.isDesired(serializer: $0) && $0.supports(outboundType: wrapped.type) }), nil, wrapped.type)]
130 | }
131 | } else if let opt = arg as? OptionalHelper {
132 | if let value = opt.value {
133 | return normalize(arg: value, serializerType: serializerType, serializers: serializers)
134 | } else {
135 | return [(serializers.first(where: { serializerType.isDesired(serializer: $0) && $0.supports(outboundType: opt.type) }), nil, opt.type)]
136 | }
137 | } else if arg is SelfApplyingArg {
138 | return [(nil, arg, type(of: arg))]
139 | } else if let serializer = serializers.first(where: { serializerType.isDesired(serializer: $0) && $0.supports(outboundType: type(of: arg)) }) {
140 | return [(serializer, arg, type(of: arg))]
141 | } else {
142 | let beneath = Mirror(reflecting: arg).children.reduce([]) {
143 | $0 + normalize(arg: $1.value, serializerType: serializerType, serializers: serializers)
144 | }
145 | if beneath.isEmpty {
146 | return [(nil, arg, type(of: arg))]
147 | } else {
148 | return beneath
149 | }
150 | }
151 | }
152 |
153 | open func makeRequest(type: OutboundSerializerType = .auto, method: HTTPMethod, endpoint: String, args creationArgs: Args, response: ResponseType.Type, testProvider: ((Args, Args, URLRequest) -> ClientResponse)? = nil) -> (Args) -> Call {
154 | return { startingArgs in
155 | var task: Task?
156 |
157 | let enqueue: CallEnqueueFunction = { (state, callback) in
158 | var request = URLRequest(url: state.base.appendingPathComponent(endpoint))
159 | request.httpMethod = method.rawValue
160 |
161 | do {
162 | try self.applyArguments(
163 | type: type,
164 | state: state,
165 | endpoint: endpoint,
166 | method: method,
167 | creation: creationArgs,
168 | starting: startingArgs,
169 | modifying: &request,
170 | responseType: response
171 | )
172 |
173 | self.requestInterceptor?(&request)
174 | self.log(request: request)
175 |
176 | if self.isDryModeEnabled {
177 | let provider = testProvider ?? { _ in ClientResponse(data: nil, response: nil, error: nil) }
178 | let clientResponse = provider(creationArgs, startingArgs, request)
179 | let response = self.process(immutableClientResponse: clientResponse, request: request, responseType: ResponseType.self)
180 | self.log(response: response)
181 | callback(response)
182 | } else {
183 | task = self.client.makeAsynchronousRequest(request: &request) { (clientResponse) in
184 | state.workerQueue.async {
185 | let response = self.process(immutableClientResponse: clientResponse, request: request, responseType: ResponseType.self)
186 | self.log(response: response)
187 | callback(response)
188 | }
189 | }
190 | task!.resume()
191 | }
192 | } catch {
193 | let response = Response(
194 | request: request,
195 | data: nil,
196 | error: error,
197 | urlResponse: nil,
198 | body: nil,
199 | interpreter: self.interpret
200 | )
201 | self.log(response: response)
202 | callback(response)
203 | }
204 | }
205 |
206 | let cancel = { () -> Void in
207 | task?.cancel()
208 | }
209 |
210 | let capture = { self.stateToCapture }
211 |
212 | return self.callFactory.makeCall(capture: capture, enqueue: enqueue, cancel: cancel)
213 | }
214 | }
215 |
216 | func applyArguments(
217 | type: OutboundSerializerType,
218 | state: RequestCapturedState,
219 | endpoint: String,
220 | method: HTTPMethod,
221 | creation: Args,
222 | starting: Args,
223 | modifying request: inout URLRequest,
224 | responseType: ResponseType.Type
225 | ) throws
226 | {
227 | let outboundSerializers = self.outboundSerializers()
228 | let normalizedCreationArgs = self.normalize(arg: creation, serializerType: type, serializers: outboundSerializers) // Args used to create this request
229 | let normalizedStartingArgs = self.normalize(arg: starting, serializerType: type, serializers: outboundSerializers) // Args passed in when executing this request
230 |
231 | assert(normalizedCreationArgs.count == normalizedStartingArgs.count)
232 |
233 | var selfApplying: [BuilderArg] = []
234 |
235 | var serializer: OutboundSerializer?
236 | var serializerArgs: [BuilderArg] = []
237 |
238 | for (creation, starting) in zip(normalizedCreationArgs, normalizedStartingArgs) {
239 | assert((creation.serializer != nil) == (starting.serializer != nil), "Somehow normalize failed to produce the same results on both sides.")
240 | assert(creation.serializer == nil || creation.serializer === starting.serializer, "Normalize didn't produce the same serializer on both sides.")
241 |
242 | let arg = BuilderArg(type: creation.type, creation: creation.value, starting: starting.value)
243 |
244 | if creation.type is SelfApplyingArg.Type {
245 | selfApplying.append(arg)
246 | } else if let thisSerializer = creation.serializer {
247 | if serializer == nil {
248 | serializer = thisSerializer
249 | } else {
250 | // Serializer already set
251 | }
252 |
253 | serializerArgs.append(arg)
254 | } else {
255 | throw BuilderError.unsupportedArgument(arg)
256 | }
257 | }
258 |
259 | if let serializer = serializer {
260 | do {
261 | try serializer.apply(arguments: serializerArgs, to: &request)
262 | } catch {
263 | throw BuilderError.serializationError(serializer: serializer, error: error, arguments: serializerArgs)
264 | }
265 | }
266 |
267 | for arg in selfApplying {
268 | let type = arg.type as! SelfApplyingArg.Type
269 | type.apply(arg: arg, to: &request)
270 | }
271 | }
272 |
273 | func process(immutableClientResponse: ClientResponse, request: URLRequest, responseType: ResponseType.Type) -> Response {
274 | var clientResponse = immutableClientResponse
275 | self.responseInterceptor?(&clientResponse)
276 |
277 | let body: ResponseType?
278 | let error: Error?
279 |
280 | if let error = clientResponse.error {
281 | let response = Response(
282 | request: request,
283 | data: clientResponse.data,
284 | error: error,
285 | urlResponse: clientResponse.response,
286 | body: nil,
287 | interpreter: self.interpret
288 | )
289 | return response
290 | }
291 |
292 | if ResponseType.self == Void.self {
293 | body = (() as! ResponseType)
294 | error = nil
295 | } else {
296 | if let serializer = inboundSerializers().first(where: { $0.supports(inboundType: ResponseType.self) }) {
297 | do {
298 | body = try serializer.makeValue(from: clientResponse, type: ResponseType.self)
299 | error = nil
300 | } catch let serializerError {
301 | body = nil
302 | error = BuilderResponseError.deserializationError(serializer: serializer, error: serializerError, clientResponse: clientResponse)
303 | }
304 | } else {
305 | // This is incorrect usage of Retrolux, hence it is a fatal error.
306 | fatalError("Unsupported argument type when processing request: \(ResponseType.self)")
307 | }
308 | }
309 |
310 | let response: Response = Response(
311 | request: request,
312 | data: clientResponse.data,
313 | error: error ?? clientResponse.error,
314 | urlResponse: clientResponse.response,
315 | body: body,
316 | interpreter: self.interpret
317 | )
318 |
319 | return response
320 | }
321 | }
322 |
--------------------------------------------------------------------------------