├── .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 | --------------------------------------------------------------------------------