{"id":15041052,"url":"https://github.com/izeni-team/retrolux","last_synced_at":"2025-04-14T19:40:27.376Z","repository":{"id":62452870,"uuid":"62902724","full_name":"izeni-team/retrolux","owner":"izeni-team","description":"The all in one networking solution for iOS.","archived":false,"fork":false,"pushed_at":"2018-07-11T16:00:25.000Z","size":398,"stargazers_count":30,"open_issues_count":12,"forks_count":3,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-13T04:36:47.193Z","etag":null,"topics":["carthage","cocoapods","json","multipart-data","networking","url-encoder"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/izeni-team.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-07-08T16:34:25.000Z","updated_at":"2021-03-27T23:38:33.000Z","dependencies_parsed_at":"2022-11-01T22:32:21.791Z","dependency_job_id":null,"html_url":"https://github.com/izeni-team/retrolux","commit_stats":null,"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeni-team%2Fretrolux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeni-team%2Fretrolux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeni-team%2Fretrolux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izeni-team%2Fretrolux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/izeni-team","download_url":"https://codeload.github.com/izeni-team/retrolux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248947923,"owners_count":21187778,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["carthage","cocoapods","json","multipart-data","networking","url-encoder"],"created_at":"2024-09-24T20:45:27.734Z","updated_at":"2025-04-14T19:40:27.353Z","avatar_url":"https://github.com/izeni-team.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"Retrolux is an all in one networking framework. It is intended to become the equivalent of [Square's Retrofit for Android](http://square.github.io/retrofit/).\r\n\r\n**This framework is still in early development, with v1.0 currently being designed and prototyped.** Retrolux versions less than 1.0 won't be fully documented. [Click here to view the Retrolux v1.0 milestone.](https://github.com/izeni-team/retrolux/milestone/1)\r\n\r\n# Why?\r\n\r\nThere are many good networking libraries out there already for iOS. [Alamofire](https://github.com/Alamofire/Alamofire), [AFNetworking](https://github.com/AFNetworking/AFNetworking), and [Moya](https://github.com/Moya/Moya), etc. are all great libraries.\r\n\r\nWhat makes this framework unique is that each endpoint can be consicely described. 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.\r\n\r\nThe 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.\r\n\r\n# Installation\r\n\r\n## Cocoapods\r\n\r\nIn your `Podfile`:\r\n\r\n```\r\npod \"Retrolux\"\r\n```\r\n\r\n## Carthage\r\n\r\nIn your `Cartfile`:\r\n\r\n```\r\ngithub \"izeni-team/retrolux\"\r\n```\r\n\r\nThen follow the instructions mentioned in [Carthage's documentation](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).\r\n\r\n# Examples\r\n\r\n* [JSON](#json)\r\n* [Multipart Form-Data](#multipart-form-data)\r\n* [URL Encoded](#url-encoded)\r\n* [Queries](#queries)\r\n* [Custom Serializers](#custom-serializers)\r\n* [Testing](#testing)\r\n* [Changing Base URL](#changing-base-url)\r\n* [Logging](#logging)\r\n* [Reflection Customizations](#reflection-customizations)\r\n\r\n# JSON\r\n\r\nJSON support for Retrolux is provided by the [ReflectionJSONSerializer](ReflectionJSONSerializer) class.\r\n\r\nFor more information on reflection, refer to [Reflectable](Reflectable).\r\n\r\n---\r\n\r\nTo get a list of items:\r\n\r\n```swift\r\nclass Person: Reflection {\r\n    var name = \"\"\r\n    var age = 0\r\n}\r\n\r\nlet builder = Builder(base: URL(string: \"https://my.api.com/\")!)\r\nlet getUsers = builder.makeRequest(\r\n    method: .get,\r\n    endpoint: \"users/\",\r\n    args: Void, // Or (), because Void == ()\r\n    response: [Person].self\r\n)\r\ngetUsers().enqueue { response in\r\n    switch response.interpreted { // What .interpreted means can be customized by overriding RetroluxBuilder's interpret function\r\n    case .success(let users):\r\n        print(\"Got \\(users.count) users!\")\r\n    case .failure(let error):\r\n        print(\"Failed to get users: \\(error)\")\r\n    }\r\n}\r\n```\r\n\r\n---\r\n\r\nTo post an item:\r\n\r\n```swift\r\nclass Person: Reflection {\r\n    var name = \"\"\r\n    var age = 0\r\n}\r\n\r\nlet builder = Builder(base: URL(string: \"https://my.api.com/\")!)\r\nlet createUser = builder.makeRequest(\r\n    method: .post,\r\n    endpoint: \"users/\",\r\n    args: Person(),\r\n    response: Person.self\r\n)\r\n\r\nlet newUser = Person()\r\nnewUser.name = \"Bob\"\r\nnewUser.age = 3\r\ncreateUser(newUser).enqueue { response in\r\n    print(\"User created successfully? \\(response.isSuccessful)\")\r\n    if let echo = response.body {\r\n        print(\"Response: \\(echo)\")\r\n    }\r\n}\r\n```\r\n\r\n---\r\n\r\nTo patch an item:\r\n\r\n```swift\r\nclass Person: Reflection {\r\n    var id = \"\"\r\n    var name = \"\"\r\n    var age = 0\r\n}\r\n\r\nlet builder = Builder(base: URL(string: \"https://my.api.com/\")!)\r\nlet patchUser = builder.makeRequest(\r\n    method: .patch,\r\n    endpoint: \"users/{id}/\",\r\n    args: (Person(), Path(\"id\")),\r\n    response: Person.self\r\n)\r\n\r\nlet existingUser = getExistingUserFromSomewhere()\r\nexistingUser.name = \"Bob\"\r\nexistingUser.age = 3\r\npatchUser((newUser, Path(existingUser.id)).enqueue { response in\r\n    print(\"User updated successfully? \\(response.isSuccessful)\")\r\n    if let echo = response.body {\r\n        print(\"Response: \\(echo)\")\r\n    }\r\n}\r\n```\r\n\r\n---\r\n\r\nTo delete an item:\r\n\r\n```swift\r\nlet builder = Builder(base: URL(string: \"https://my.api.com/\")!)\r\nlet deleteUser = builder.makeRequest(\r\n    method: .delete,\r\n    endpoint: \"users/{id}/\",\r\n    args: Path(\"id\"),\r\n    response: Void.self\r\n)\r\n\r\ndeleteUser(Path(someUser.id)).enqueue { response in\r\n    print(\"User was deleted? \\(response.isSuccessful)\")\r\n}\r\n```\r\n\r\n# Multipart Form-Data\r\n\r\nMultipart support is provided via the [MultipartFormDataSerializer](MultipartFormDataSerializer) class.\r\n\r\nMultipart data can be sent by sending either [Field](Field) or [Part](Part) objects to the builder.\r\n\r\n---\r\n\r\nSimple sign-in:\r\n\r\n```swift\r\nclass LoginResponse: Reflection {\r\n    var token = \"\"\r\n    var user_id = \"\"\r\n}\r\n\r\nlet builder = Builder(base: \"https://my.api.com/\")!)\r\nlet login = builder.makeRequest(\r\n    method: .post,\r\n    endpoint: \"login/\",\r\n    args: (Field(\"username\"), Field(\"password\")),\r\n    response: LoginResponse.self\r\n)\r\nlogin((Field(\"bobby\"), Field(\"abc123\")).enqueue { response in\r\n    switch response.interpreted {\r\n    case .success(let login):\r\n        print(\"Login successful! Token: \\(login.token), user: \\(login.user_id)\")\r\n    case .failure(let error):\r\n        print(\"Failed to login: \\(error)\")\r\n    }\r\n}\r\n```\r\n\r\n---\r\n\r\nImage upload:\r\n\r\n```swift\r\nclass User: Reflection {\r\n    var id = \"\"\r\n    var name = \"\"\r\n    var image_url: URL?\r\n}\r\n\r\nlet builder = Builder(base: \"https://my.api.com/\")!)\r\nlet uploadImage = builder.makeRequest(\r\n    method: .post,\r\n    endpoint: \"media_upload/{user_id}/\",\r\n    args: (Path(\"user_id\"), Part(name: \"image\", filename: \"image.png\", mimeType: \"image/png\")),\r\n    response: User.self\r\n)\r\n\r\nlet image: UIImage = getImageFromCamera()\r\nlet imageData = UIImagePNGRepresentation(image)!\r\nuploadImage((Path(someUser.id), Part(imageData)).enqueue { response in\r\n    print(\"image URL is: \\(response.body?.image_url)\")\r\n}\r\n```\r\n\r\n# URL Encoded\r\n\r\nURL encoded bodies are provided by the [URLEncodedSerializer](URLEncodedSerializer) class.\r\n\r\nBoth [URLEncodedSerializer](URLEncodedSerializer) and [MultipartFormDataSerializer](MultipartFormDataSerializer) use [Field](Field) objects. By default, the multipart serializer will have higher priority. Thus, in order to use URL encoding, you need to manually specify the serializer that you would prefer by adding `type: .urlEncoded` to the `makeRequest` function, like below:\r\n\r\n```swift\r\nlet login = builder.makeRequest(\r\n    type: .urlEncoded, // This is required else the request will be sent as multipart form-data instead!\r\n    method: .post,\r\n    endpoint: \"login/\",\r\n    args: (Field(\"username\"), Field(\"password\")),\r\n    response: LoginResponse.self\r\n)\r\n```\r\n\r\n---\r\n\r\nBasic login example:\r\n\r\n```swift\r\nclass LoginResponse: Reflection {\r\n    var id = \"\"\r\n    var token = \"\"\r\n}\r\n\r\nlet builder = Builder(base: \"https://my.api.com/\")!)\r\nlet login = builder.makeRequest(\r\n    type: .urlEncoded,\r\n    method: .post,\r\n    endpoint: \"login\",\r\n    args: (Field(\"username\"), Field(\"password\")),\r\n    response: LoginResponse.self\r\n)\r\nlogin((Field(\"bobby\"), Field(\"abc123\")).enqueue { response in\r\n    switch response.interpreted {\r\n    case .success(let login):\r\n        print(\"id: \\(login.id), token: \\(login.token)\")\r\n    case .failure(let error):\r\n        print(\"Login failed: \\(error)\")\r\n    }\r\n}\r\n```\r\n\r\n---\r\n\r\n# Queries\r\n\r\nSupport for queries is provided by the [Query](../blob/master/Retrolux/Query.swift) object.\r\n\r\n```swift\r\nlet find = builder.makeRequest(\r\n    method: .get,\r\n    endpoint: \"users/\",\r\n    args: (Query(\"distance\"), Query(\"age_gt\")),\r\n    response: [User].self\r\n)\r\nfind((Query(\"50\"), Query(\"20\")).enqueue { response in\r\n    ...\r\n}\r\n```\r\n\r\n# Custom Serializers\r\n\r\nMost of Retrolux's functionality is in the form of a plugin. Since all the other built-in serializers are merely plugins, it is easy to add new serializers.\r\n\r\nFor example, let's say you want to send/receive JSON using [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON):\r\n\r\n```swift\r\nimport Foundation\r\nimport SwiftyJSON\r\nimport Retrolux\r\n\r\nenum SwiftyJSONSerializerError: Error {\r\n    case invalidJSON\r\n}\r\n\r\nclass SwiftyJSONSerializer: InboundSerializer, OutboundSerializer {\r\n    func supports(inboundType: Any.Type) -\u003e Bool {\r\n        return inboundType is JSON.Type\r\n    }\r\n    \r\n    func supports(outboundType: Any.Type) -\u003e Bool {\r\n        return outboundType is JSON.Type\r\n    }\r\n    \r\n    func validate(outbound: [BuilderArg]) -\u003e Bool {\r\n        return outbound.count == 1 \u0026\u0026 outbound.first!.type is JSON.Type\r\n    }\r\n    \r\n    func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {\r\n        let json = arguments.first!.starting as! JSON\r\n        request.httpBody = try json.rawData()\r\n        request.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\r\n    }\r\n    \r\n    func makeValue\u003cT\u003e(from clientResponse: ClientResponse, type: T.Type) throws -\u003e T {\r\n        let result = JSON(data: clientResponse.data ?? Data())\r\n        if result.object is NSNull {\r\n            throw SwiftyJSONSerializerError.invalidJSON\r\n        }\r\n        return result as! T\r\n    }\r\n}\r\n```\r\n\r\nAnd once you've created the serializer, you can send/receive using SwiftyJSON:\r\n\r\n```swift\r\nlet builder = Builder(base: URL(string: \"https://my.api.com/\")!)\r\n\r\n// This is how you tell Retrolux to use your serializer.\r\nbuilder.serializers.append(SwiftyJSONSerializer())\r\n\r\nlet login = builder.makeRequest(\r\n    method: .post,\r\n    endpoint: \"login/\",\r\n    args: JSON([:]),\r\n    response: JSON.self\r\n)\r\n\r\nlogin(JSON([\"username\": \"bobby\", \"password\": \"abc123\"])).enqueue { response in\r\n    switch response.interpreted {\r\n    case .success(let json):\r\n        let id = json[\"id\"].stringValue\r\n        let token = json[\"token\"].stringValue\r\n        print(\"Got id \\(id) and token \\(token)\")\r\n    case .failure(let error):\r\n        print(\"Request failed: \\(error)\")\r\n    }\r\n}\r\n```\r\n\r\n# Testing\r\n\r\nRetrolux makes unit testing easier with the concept of \"dry mode.\" When [Builder](Builder) is run in dry mode by using the builder returned by `Builder.dry()`, then all requests skip the HTTP client and fake responses are used instead. If no fake response is provided in the endpoint, then an empty response is returned instead.\r\n\r\nTo specify what data you'd like to use in the fake response, do so like the following:\r\n\r\n```swift\r\nclass LoginArgs: Reflection {\r\n    var username = \"\"\r\n    var password = \"\"\r\n}\r\n\r\nclass LoginResponse: Reflection {\r\n    var id = \"\"\r\n    var token = \"\"\r\n}\r\n\r\nlet builder = Builder.dry()\r\nlet login = builder.makeRequest(\r\n    method: .post,\r\n    endpoint: \"login/\",\r\n    args: LoginArgs(),\r\n    response: LoginResponse.self,\r\n    testProvider: { (creation, starting, request) in\r\n        ClientResponse(\r\n            url: request.url!,\r\n            data: \"{\\\"id\\\":\\\"qs492s37\\\",\\\"token\\\":\\\"0s98q3wj5s5\\\",\\\"username\\\":\\\"\\(starting.username)\\\"}\".data(using: .utf8)!,\r\n            status: 200\r\n        )\r\n    }\r\n)\r\n\r\nlet args = LoginArgs()\r\nlogin.username = \"bobby\"\r\nlogin.password = \"impenetrable\"\r\nlet response = login(args).perform()\r\nXCTAssert(response.isSuccessful)\r\nXCTAssert(response.body?.id == \"qs492s37\")\r\nXCTAssert(response.body?.token == \"0s98q3wj5s5\")\r\nXCTAssert(response.body?.username == args.username)\r\n```\r\n\r\n# Changing Base URL\r\n\r\nRequests capture the base URL when calling `.enqueue(...)` or `.perform()`.\r\n\r\nFor example:\r\n\r\n```swift\r\nlet builder = Builder(base: URL(string: \"https://www.google.com/\")!)\r\nlet first = builder.makeRequest(\r\n    method: .get,\r\n    endpoint: \"something\",\r\n    args: (),\r\n    Response: Void.self\r\n)\r\n\r\nlet call = first()\r\ncall.enqueue { response in\r\n    // response.request.url == \"https://www.google.com/something\"\r\n}\r\n\r\nbuilder.base = URL(string: \"https://www.something.else/\")!\r\ncall.enqueue { response in\r\n    // response.request.url == \"https://www.something.else/something\"\r\n}\r\n```\r\n\r\n# Logging\r\n\r\nDebug print statements are enabled by default. To customize logging, subclass Builder and override the `log` functions like so:\r\n\r\n```swift\r\nclass MyBuilder: Builder {\r\n    open override func log(request: URLRequest) {\r\n        // To silence logging, do nothing here.\r\n    }\r\n    \r\n    open override func log\u003cT\u003e(response: Response\u003cT\u003e) {\r\n        // To silence logging, do nothing here.\r\n    }\r\n}\r\n```\r\n\r\n# Reflection Customizations\r\n\r\nThe reflection API supports customizing behavior by implementing the `static func config(_:PropertyConfig)` function, like so:\r\n\r\n```swift\r\nclass MyDateTransformer: NestedTransformer {\r\n    enum DateTransformationError: Error {\r\n        case invalidDateFormat(got: String, expected: String)\r\n    }\r\n    \r\n    typealias TypeOfData = String\r\n    typealias TypeOfProperty = Date\r\n    \r\n    let formatter = { () -\u003e DateFormatter in\r\n        let f = DateFormatter()\r\n        f.dateFormat = \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\"\r\n        f.locale = Locale(identifier: \"en_US_POSIX\")\r\n        return f\r\n    }()\r\n    \r\n    func setter(_ dataValue: String, type: Any.Type) -\u003e Date {\r\n        guard let date = formatter.date(from: value) else {\r\n            throw DateTransformationError.invalidDateFormat(\r\n                got: value,\r\n                expected: formatter.dateFormat\r\n            )\r\n        }\r\n        return date\r\n    }\r\n    \r\n    func getter(_ propertyValue: TypeOfProperty) -\u003e String {\r\n        return formatter.string(from: value)\r\n    }\r\n}\r\n\r\nclass Person: Reflection {\r\n    var desc = \"DEFAULT_VALUE\"\r\n    var notSupported: Int?\r\n    var date = Date()\r\n    \r\n    override class func config(_ c: PropertyConfig) {\r\n        c[\"desc\"] = [\r\n            .serializedName(\"description\"), // Will look for the \"description\" key in JSON\r\n            .nullable // If the value is null, don't raise a \"null values not supported\" error\r\n        ]\r\n        \r\n        // 'Int?' is not a supported type, so this will tell Retrolux to ignore it instead of raising an error\r\n        c[\"notSupported\"] = [.ignored]\r\n        \r\n        // Alternatively, you can do Reflector.shared.globalTransformers.append(MyDateTransformer())\r\n        c[\"date\"] = [.transformed(MyDateTransformer())]\r\n    }\r\n}\r\n\r\nlet reflector = Reflector()\r\nlet person = try reflector.convert(\r\n    fromDictionary: [\r\n        \"description\": NSNull(),\r\n        \"date\": \"2017-04-17T12:02:04.142Z\"\r\n    ],\r\n    to: Person.self\r\n) as! Person\r\n```\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizeni-team%2Fretrolux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fizeni-team%2Fretrolux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizeni-team%2Fretrolux/lists"}