{"id":13907528,"url":"https://github.com/vadymmarkov/Malibu","last_synced_at":"2025-07-18T05:32:48.037Z","repository":{"id":62447496,"uuid":"52473477","full_name":"vadymmarkov/Malibu","owner":"vadymmarkov","description":":surfer: Malibu is a networking library built on promises","archived":false,"fork":false,"pushed_at":"2019-04-26T10:49:47.000Z","size":939,"stargazers_count":414,"open_issues_count":11,"forks_count":39,"subscribers_count":16,"default_branch":"master","last_synced_at":"2024-11-23T19:59:47.999Z","etag":null,"topics":["networking","promises","request","swift","urlsession"],"latest_commit_sha":null,"homepage":"https://vadymmarkov.github.io","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vadymmarkov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-02-24T20:50:52.000Z","updated_at":"2024-10-29T16:30:04.000Z","dependencies_parsed_at":"2022-11-01T22:31:29.466Z","dependency_job_id":null,"html_url":"https://github.com/vadymmarkov/Malibu","commit_stats":null,"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vadymmarkov%2FMalibu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vadymmarkov%2FMalibu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vadymmarkov%2FMalibu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vadymmarkov%2FMalibu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vadymmarkov","download_url":"https://codeload.github.com/vadymmarkov/Malibu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226090846,"owners_count":17572119,"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":["networking","promises","request","swift","urlsession"],"created_at":"2024-08-06T23:01:58.796Z","updated_at":"2024-11-25T16:30:36.170Z","avatar_url":"https://github.com/vadymmarkov.png","language":"Swift","funding_links":[],"categories":["HarmonyOS"],"sub_categories":["Windows Manager"],"readme":"![Malibu logo](https://raw.githubusercontent.com/vadymmarkov/Malibu/master/Images/cover.png)\n\n[![CI Status](http://img.shields.io/travis/vadymmarkov/Malibu.svg?style=flat)](https://travis-ci.org/vadymmarkov/Malibu)\n[![Version](https://img.shields.io/cocoapods/v/Malibu.svg?style=flat)](http://cocoadocs.org/docsets/Malibu)\n[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![License](https://img.shields.io/cocoapods/l/Malibu.svg?style=flat)](http://cocoadocs.org/docsets/Malibu)\n[![Platform](https://img.shields.io/cocoapods/p/Malibu.svg?style=flat)](http://cocoadocs.org/docsets/Malibu)\n![Swift](https://img.shields.io/badge/%20in-swift%204.0-orange.svg)\n\n## Description\n\nPalm trees, coral reefs and breaking waves. Welcome to the surf club **Malibu**,\na networking library built on ***promises***. It's more than just a wrapper\naround `URLSession`, but a powerful framework that helps to chain your\nrequests, validations and request processing.\n\nUsing [When](https://github.com/vadymmarkov/When) under the hood, **Malibu**\nadds a lot of sugar helpers and moves your code up to the next level:\n\n* No more \"callback hell\".\n* Your requests are described in one place.\n* Response processing could be easily broken down into multiple logical tasks.\n* Data and errors are handled separately.\n* Your networking code is much cleaner, readable and follows `DRY` principle.\n\nEquip yourself with the necessary gears of **Malibu**, become a big wave surfer\nand let the days of shark infested asynchronous networking be a thing of the\npast. Enjoy the ride!\n\n## Features\n\n- [x] Multiple network stacks\n- [x] Declarative requests\n- [x] Chainable response callbacks built on ***promises***\n- [x] All needed content types and parameter encodings\n- [x] HTTP response validation\n- [x] Response data serialization\n- [x] Response mocking\n- [x] Request, response and error logging\n- [x] Synchronous and asynchronous modes\n- [x] Request pre-processing and middleware\n- [x] Request offline storage\n- [x] Extensive unit test coverage\n\n## Table of Contents\n\n* [Catching the wave](#catching-the-wave)\n* [RequestConvertible](#request-convertible)\n* [Request](#request)\n  * [Content types](#content-types)\n  * [Encoding](#encoding)\n  * [Cache policy](#cache-policy)\n* [Networking](#networking)\n  * [Initialization](#initialization)\n  * [Mode](#mode)\n  * [Mocks](#mocks)\n  * [Session configuration](#session-configuration)\n  * [Pre-processing](#pre-processing)\n  * [Middleware](#middleware)\n  * [Authentication](#authentication)\n  * [Making a request](#making-a-request)\n  * [Response and NetworkPromise](#response-and-networkpromise)\n  * [Offline storage](#offline-storage)\n  * [Backfoot surfer](#backfoot-surfer)\n* [Response](#response)\n  * [Serialization](#serialization)\n  * [Validation](#validation)\n  * [Decoding](#decoding)\n* [Logging](#logging)\n* [Installation](#installation)\n* [Author](#author)\n* [Credits](#credits)\n* [Contributing](#contributing)\n* [License](#license)\n\n## Catching the wave\n\nYou can start your ride straight away, not thinking about configurations:\n\n```swift\n// Create your request =\u003e GET http://sharkywaters.com/api/boards?type=1\nlet request = Request.get(\"http://sharkywaters.com/api/boards\", parameters: [\"type\": 1])\n\n// Make a call\nMalibu.request(request)\n  .validate()\n  .toJsonDictionary()\n  .then({ dictionary -\u003e [Board] in\n    // Let's say we use https://github.com/zenangst/Tailor for mapping\n    return try dictionary.relationsOrThrow(\"boards\") as [Board]\n  })\n  .done({ boards in\n    // Handle response data\n  })\n  .fail({ error in\n    // Handle errors\n  })\n  .always({ _ in\n    // Hide progress bar\n  })\n```\n\nIf you still don't see any benefits, keep scrolling down and be ready for even\nmore magic 😉...\n\n## RequestConvertible\n\nMost of the time we need separate network stacks to work with multiple API\nservices. It's super easy to archive with **Malibu**. Create an\n`enum` that conforms to **RequestConvertible** protocol and describe your\nrequests with all the properties:\n\n```swift\nenum SharkywatersEndpoint: RequestConvertible {\n  // Describe requests\n  case fetchBoards\n  case showBoard(id: Int)\n  case createBoard(type: Int, title: String)\n  case updateBoard(id: Int, type: Int, title: String)\n  case deleteBoard(id: Int)\n\n  // Every request will be scoped by the base url\n  // Base url is recommended, but optional\n  static var baseUrl: URLStringConvertible? = \"http://sharkywaters.com/api/\"\n\n  // Additional headers for every request\n  static var headers: [String: String] = [\n    \"Accept\" : \"application/json\"\n  ]\n\n  // Build requests\n  var request: Request {\n    switch self {\n    case .fetchBoards:\n      return Request.get(\"boards\")\n    case .showBoard(let id):\n      return Request.get(\"boards/\\(id)\")\n    case .createBoard(let type, let title):\n      return Request.post(\"boards\", parameters: [\"type\": type, \"title\": title])\n    case .updateBoard(let id, let title):\n      return Request.patch(\"boards/\\(id)\", parameters: [\"title\": title])\n    case .deleteBoard(let id):\n      return Request.delete(\"boards/\\(id)\")\n    }\n  }\n}\n```\n\n**Note** that `Accept-Language`, `Accept-Encoding` and `User-Agent` headers are\nincluded automatically.\n\n## Request\n\nRequest is described with a struct in **Malibu**:\n\n```swift\nlet request = Request(\n  // HTTP method\n  method: .get,\n  // Request url or path\n  resource: \"boards\",\n  // Content type\n  contentType: .query,\n  // Request parameters\n  parameters: [\"type\": 1, \"text\": \"classic\"],\n  // Headers\n  headers: [\"custom\": \"header\"],\n  // Offline storage configuration\n  storePolicy: .unspecified,\n  // Cache policy\n  cachePolicy: .useProtocolCachePolicy)\n```\n\nThere are also multiple helper methods with default values for every HTTP method:\n\n```swift\n// GET request\nlet getRequest = Request.get(\"boards\")\n\n// POST request\nlet postRequest = Request.post(\n  \"boards\",\n  // Content type is set to `.json` by default for POST\n  contentType: .formURLEncoded,\n  parameters: [\"type\" : kind, \"title\" : title])\n\n// PUT request\nlet putRequest = Request.put(\"boards/1\", parameters: [\"type\" : kind, \"title\" : title])\n\n// PATCH request\nlet patchRequest = Request.patch(\"boards/1\", parameters: [\"title\" : title])\n\n// DELETE request\nlet deleteRequest = Request.delete(\"boards/1\")\n```\n\n`URLSessionDataTask` is default for executing requests. For uploading there\nare two additional options that use `URLSessionUploadTask` instead of\n`URLSessionDataTask`.\n\n```swift\n// Upload data to url\nRequest.upload(data: data, to: \"boards\")\n\n// Upload multipart data with parameters\n// You are responsible for constructing a proper value,\n// which is normally a string created from data.\nRequest.upload(\n  multipartParameters: [\"key\": \"value\"],\n  to: \"http:/api.loc/posts\"\n)\n```\n\n### Content types\n\n* `query` - creates a query string to be appended to any existing url.\n* `formURLEncoded` - uses `application/x-www-form-urlencoded` as a\n`Content-Type` and formats your parameters with percent-encoding.\n* `json` - sets the `Content-Type` to `application/json` and sends a JSON\nrepresentation of the parameters as the body of the request.\n* `multipartFormData` - sends parameters encoded as `multipart/form-data`.\n* `custom(String)` - uses given `Content-Type` string as a header.\n\n### Encoding\n\n**Malibu** comes with 3 parameter encoding implementations:\n* `FormURLEncoder` - a percent-escaped encoding following RFC 3986.\n* `JsonEncoder` - `JSONSerialization` based encoding.\n* `MultipartFormEncoder` - multipart data builder.\n\nYou can extend default functionality by adding a custom parameter encoder\nthat conforms to `ParameterEncoding` protocol:\n\n```swift\n// Override default JSON encoder\nMalibu.parameterEncoders[.json] = CustomJsonEncoder()\n\n// Register encoder for the custom encoding type\nMalibu.parameterEncoders[.custom(\"application/xml\")] = CustomXMLEncoder()\n```\n\n### Cache policy\n\n`URLSession` handles cache based on the `URLRequest.CachePolicy` property:\n\n```swift\nlet getRequest = Request.get(\"boards\". cachePolicy: .useProtocolCachePolicy)\n```\n\n`URLRequest.CachePolicy.useProtocolCachePolicy` is the default policy for URL\nload requests. `URLSession` will automatically add the `If-None-Match` header\nin the request before sending it to the backend. When `URLSession` gets the\n`304 Not Modified` response status it will call the `URLSessionDataTask`\ncompletion block with the `200` status code and data loaded from the cached\nresponse.\n\nYou can set `cachePolicy` property to `.reloadIgnoringLocalCacheData` if you\nwant to prevent this automatic cache management. Then `URLSession` will not\nadd the `If-None-Match` header to the client requests, and the server will\nalways return a full response.\n\n## Networking\n\n`Networking` class is a core component of **Malibu** that executes actual HTTP\nrequests on a specified API service.\n\n### Initialization\n\nIt's pretty straightforward to create a new `Networking` instance:\n\n```swift\n// Simple networking that works with `SharkywatersEndpoint` requests.\nlet simpleNetworking = Networking\u003cSharkywatersEndpoint\u003e()\n\n// More advanced networking\nlet networking = Networking\u003cSharkywatersEndpoint\u003e(\n  // `OperationQueue` Mode\n  mode: .async,\n  // Optional mock provider\n  mockProvider: customMockProvider,\n  // `default`, `ephemeral`, `background` or `custom`\n  sessionConfiguration: .default,\n  // Custom `URLSessionDelegate` could set if needed\n  sessionDelegate: self\n)\n```\n\n### Mode\n\n**Malibu** uses `OperationQueue` to execute/cancel requests. It makes it\neasier to manage request lifetime and concurrency.\n\nWhen you create a new networking instance there is an optional argument to\nspecify **mode** which will be used:\n\n- `sync`\n- `async`\n- `limited(maxConcurrentOperationCount)`\n\n### Mocks\n\nMocking is great when it comes to writing your tests. But it also could speed\nup your development while the backend developers are working really hardly\non API implementation.\n\nIn order to start mocking you have to do the following:\n\n**Create a mock provider**\n\n```swift\n// Delay is optional, 0.0 by default.\nlet mockProvider = MockProvider(delay: 1.0) { endpoint in\n  switch endpoint {\n    case .fetchBoards:\n      // With response data from a file:\n      return Mock(fileName: \"boards.json\")\n    case .showBoard(let id):\n      // With response from JSON dictionary:\n      return Mock(json: [\"id\": 1, \"title\": \"Balsa Fish\"])\n    case .updateBoard(let id, let title):\n      // `Data` mock:\n      return Mock(\n        // Needed response\n        response: mockedResponse,\n        // Response data\n        data: responseData,\n        // Custom error, `nil` by default\n        error: customError\n      )\n    default:\n      return nil\n  }\n}\n```\n\n**Create a networking instance with your mock provider**\n\nBoth real and fake requests can be used in a mix:\n```swift\nlet networking = Networking\u003cSharkywatersEndpoint\u003e(mockProvider: mockProvider)\n```\n\n### Session configuration\n\n`SessionConfiguration` is a wrapper around `URLSessionConfiguration` and could\nrepresent 3 standard session types + 1 custom type:\n* `default` - configuration that uses the global singleton credential, cache and\ncookie storage objects.\n* `ephemeral` - configuration with no persistent disk storage for cookies, cache\nor credentials.\n* `background` - session configuration that can be used to perform networking\noperations on behalf of a suspended application, within certain constraints.\n* `custom(URLSessionConfiguration)` - if you're not satisfied with standard\ntypes, your custom `URLSessionConfiguration` goes here.\n\n### Pre-processing\n\n```swift\n// Use this closure to modify your `Request` value before `URLRequest`\n// is created on base of it\nnetworking.beforeEach = { request in\n  return request.adding(\n    parameters: [\"userId\": \"12345\"],\n    headers: [\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"]\n  )\n}\n\n// Use this closure to modify generated `URLRequest` object\n// before the request is made\nnetworking.preProcessRequest = { (request: URLRequest) in\n  var request = request\n  request.addValue(\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\", forHTTPHeaderField: \"token\")\n  return request\n}\n```\n\n### Middleware\n\n**Middleware** is the function which works as the first promise in the chain,\nbefore the actual request. It could be used to prepare networking, do some\nkind of pre-processing task, cancel request under particular conditions, etc.\n\nFor example, in the combination with https://github.com/hyperoslo/OhMyAuth\n\n```swift\n// Set middleware in your configuration\n// Remember to `resolve` or `reject` the promise\nnetworking.middleware = { promise in\n  AuthContainer.serviceNamed(\"service\")?.accessToken { accessToken, error in\n    if let error == error {\n      promise.reject(error)\n      return\n    }\n\n    guard let accessToken = accessToken else {\n      promise.reject(CustomError())\n      return\n    }\n\n    self.networking.authenticate(bearerToken: accessToken)\n    promise.resolve()\n  }\n}\n\n// Send your request like you usually do.\n// Valid access token will be set to headers before the each request.\nnetworking.request(request)\n  .validate()\n  .toJsonDictionary()\n```\n\n### Authentication\n\n```swift\n// HTTP basic authentication with username and password\nnetworking.authenticate(username: \"malibu\", password: \"surfingparadise\")\n\n// OAuth 2.0 authentication with Bearer token\nnetworking.authenticate(bearerToken: \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\")\n\n// Custom authorization header\nnetworking.authenticate(authorizationHeader: \"Malibu-Header\")\n```\n\n### Making a request\n\n`Networking` is set up and ready, so it's time to fire some requests.\n\n```swift\nlet networking = Networking\u003cSharkywatersEndpoint\u003e()\n\nnetworking.request(.fetchBoards)\n  .validate()\n  .toJsonDictionary()\n  .done({ data in\n    print(data)\n  })\n\nnetworking.request(.createBoard(kind: 2, title: \"Balsa Fish\"))\n  .validate()\n  .toJsonDictionary()\n  .done({ data in\n    print(data)\n  })\n\nnetworking.request(.deleteBoard(id: 11))\n  .fail({ error in\n    print(error)\n  })\n```\n\n### Response and NetworkPromise\n\n`Response` object consists of `Data`, `URLRequest` and `HTTPURLResponse`\nproperties.\n\n`NetworkPromise` is just a `typealias` to `Promise\u003cResponse\u003e`, which is returned\nby every request method. You may use `NetworkPromise` object to add different\ncallbacks and build chains of tasks. It has a range of useful helpers, such as\nvalidations and serialization.\n\n```swift\nlet networkPromise = networking.request(.fetchBoards)\n\n// Cancel the task\nnetworkPromise.cancel()\n\n// Create chains and add callbacks on promise object\nnetworkPromise\n  .validate()\n  .toString()\n  .then({ string in\n    // ...\n  })\n  .done({ _ in\n    // ...\n  })\n```\n\n### Offline storage\n\nWant to **store request** when there is no network connection?\n\n```swift\nlet request = Request.delete(\n  \"boards/1\",\n  storePolicy: .offline // Set store policy\n)\n```\n\nWant to **replay** cached requests?\n\n```swift\nnetworking.replay().done({ result\n  print(result)\n})\n```\n\nRequest storage is networking-specific, and while it replays cached requests\nit will be set to `Sync` mode. Cached request will go through normal request\nlifecycle, with applied middleware and pre-process operations. Request will be\nautomatically removed from the storage when it's completed.\n\n### Backfoot surfer\n\n**Malibu** has a shared networking object with default configurations for the\ncase when you need just something simple to catch the wave. It's not necessary\nto create a custom `RequestConvertible` type, just call the same `request` method right\non `Malibu`:\n\n```swift\nMalibu.request(Request.get(\"http://sharkywaters.com/api/boards\")\n```\n\n## Response\n\n### Serialization\n\n**Malibu** gives you a bunch of methods to serialize response data:\n\n```swift\nlet networkPromise = networking.request(.fetchBoards)\n\nnetworkPromise.toData() // -\u003e Promise\u003cData\u003e\nnetworkPromise.toString() // -\u003e Promise\u003cString\u003e\nnetworkPromise.toJsonArray() // -\u003e Promise\u003c[[String: Any]]\u003e\nnetworkPromise.toJsonDictionary() // -\u003e Promise\u003c[String: Any]\u003e\n```\n\n### Validation\n\n**Malibu** comes with 4 validation methods:\n\n```swift\n// Validates a status code to be within 200..\u003c300\n// Validates a response content type based on a request's \"Accept\" header\nnetworking.request(.fetchBoards).validate()\n\n// Validates a response content type\nnetworking.request(.fetchBoards).validate(\n  contentTypes: [\"application/json; charset=utf-8\"]\n)\n\n// Validates a status code\nnetworking.request(.fetchBoards).validate(statusCodes: [200])\n\n// Validates with custom validator conforming to `Validating` protocol\nnetworking.request(.fetchBoards).validate(using: CustomValidator())\n```\n\n### Decoding\n\n**Malibu** is able to convert the response body into models that conform to `Decodable`:\n\n```swift\n// Declare your model conforming to `Decodable` protocol\nstruct User: Decodable {\n  let name: String\n  let dob: Date\n}\n\n// Set up a `JSONDecoder`\nlet decoder = JSONDecoder()\ndecoder.keyDecodingStrategy = .convertFromSnakeCase\ndecoder.dateDecodingStrategy = .iso8601\n\n// Decode your response body\nnetworkPromise.decode(using: User.self, decoder: decoder)\n```\n\n## Logging\n\nIf you want to see some request, response and error info in the console, you\nget this for free. Just choose one of the available log levels:\n\n* `none` - logging is disabled, so your console is not littered with networking\nstuff.\n* `error` - prints only errors that occur during the request execution.\n* `info` - prints incoming request method + url, response status code and errors.\n* `verbose` - prints incoming request headers and parameters in addition to\neverything printed in the `info` level.\n\nOptionally you can set your own loggers and adjust the logging to your needs:\n\n```swift\n// Custom logger that conforms to `ErrorLogging` protocol\nMalibu.logger.errorLogger = CustomErrorLogger.self\n\n// Custom logger that conforms to `RequestLogging` protocol\nMalibu.logger.requestLogger = RequestLogger.self\n\n// Custom logger that conforms to `ResponseLogging` protocol\nMalibu.logger.responseLogger = ResponseLogger.self\n```\n\n## Author\n\nHyper Interaktiv AS, ios@hyper.no\n\n## Installation\n\n**Malibu** is available through [CocoaPods](http://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod 'Malibu'\n```\n\n**Malibu** is also available through [Carthage](https://github.com/Carthage/Carthage).\nTo install just write into your Cartfile:\n\n```ruby\ngithub \"vadymmarkov/Malibu\"\n```\n\n**Malibu** can also be installed manually.\nJust *Download* and *drop* ```/Sources``` folder in your project.  \n\n## Author\n\nVadym Markov, markov.vadym@gmail.com\n\n## Credits\n\nThis library was originally done at [Hyper](http://hyper.no), a digital\ncommunications agency with a passion for [good code](https://github.com/hyperoslo)\nand delightful user experiences.\n\nCredits go to [Alamofire](https://github.com/Alamofire/Alamofire) for\ninspiration and to [When](https://github.com/vadymmarkov/When) for ***promises***.\n\n## Contributing\n\nCheck the [CONTRIBUTING](https://github.com/vadymmarkov/Malibu/blob/master/CONTRIBUTING.md) file for more info.\n\n## License\n\n**Malibu** is available under the MIT license. See the [LICENSE](https://github.com/vadymmarkov/Malibu/blob/master/LICENSE.md) file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvadymmarkov%2FMalibu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvadymmarkov%2FMalibu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvadymmarkov%2FMalibu/lists"}