{"id":21414932,"url":"https://github.com/sbertix/composablerequest","last_synced_at":"2025-07-14T04:31:19.491Z","repository":{"id":43414251,"uuid":"247786587","full_name":"sbertix/ComposableRequest","owner":"sbertix","description":"A Swift library to abstract API clients.","archived":false,"fork":false,"pushed_at":"2024-10-17T22:02:46.000Z","size":2107,"stargazers_count":10,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-03T19:44:31.301Z","etag":null,"topics":["swift","url","urlrequest","urlsession"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sbertix.png","metadata":{"files":{"readme":"docs/README.md","changelog":null,"contributing":"docs/CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"docs/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["sbertix"],"custom":["https://www.paypal.me/sbertix"]}},"created_at":"2020-03-16T18:16:22.000Z","updated_at":"2024-04-10T18:09:50.000Z","dependencies_parsed_at":"2024-11-22T18:36:21.031Z","dependency_job_id":"8ef2b187-86bb-4094-bd64-559819713699","html_url":"https://github.com/sbertix/ComposableRequest","commit_stats":{"total_commits":101,"total_committers":3,"mean_commits":"33.666666666666664","dds":0.4356435643564357,"last_synced_commit":"151026359f4a2c89dcd06f6880eb4a940dcbb7bc"},"previous_names":[],"tags_count":46,"template":false,"template_full_name":null,"purl":"pkg:github/sbertix/ComposableRequest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbertix%2FComposableRequest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbertix%2FComposableRequest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbertix%2FComposableRequest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbertix%2FComposableRequest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sbertix","download_url":"https://codeload.github.com/sbertix/ComposableRequest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sbertix%2FComposableRequest/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265240098,"owners_count":23732940,"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":["swift","url","urlrequest","urlsession"],"created_at":"2024-11-22T18:34:35.785Z","updated_at":"2025-07-14T04:31:19.481Z","avatar_url":"https://github.com/sbertix.png","language":"Swift","funding_links":["https://github.com/sponsors/sbertix","https://www.paypal.me/sbertix"],"categories":[],"sub_categories":[],"readme":"\u003cbr /\u003e\n\u003cimg alt=\"Header\" src=\"https://raw.githubusercontent.com/sbertix/ComposableRequest/master/Resources/header.png\" height=\"72\" /\u003e\n\u003cbr /\u003e\n\n[![Swift](https://img.shields.io/badge/Swift-5.7-%23DE5C43?style=flat\u0026logo=swift)](https://swift.org)\n\u003cbr /\u003e\n![iOS](https://img.shields.io/badge/iOS-13.0-8CFF96)\n![macOS](https://img.shields.io/badge/macOS-10.15-8CFF96)\n![tvOS](https://img.shields.io/badge/tvOS-13.0-8CFF96)\n![watchOS](https://img.shields.io/badge/watchOS-6.0-8CFF96)\n\n\u003cbr /\u003e\n\n**ComposableRequest** is a networking layer based on a declarative interface, written in (modern) **Swift**.\n\nIt abstracts away `URLSession` implementation, in order to provide concise and powerful endpoint representations (both for their requests and responses), supporting, out-of-the-box, **Combine** `Publisher`s and _structured concurrency_ (`async`/`await`) with a single definition. \n\nIt comes with `Storage` (inside of **Storage**), a way of caching `Storable` items, and related concrete implementations (e.g. `UserDefaultsStorage`, `KeychainStorage` – for which you're gonna need to add **StorageCrypto**, depending on [**KeychainAccess**](https://github.com/kishikawakatsumi/KeychainAccess), together with the ability to provide the final user of your API wrapper to inject code through `Provider`s.\n\n## Status\n![push](https://github.com/sbertix/ComposableRequest/workflows/push/badge.svg)\n![GitHub release (latest by date)](https://img.shields.io/github/v/release/sbertix/ComposableRequest)\n\nYou can find all changelogs directly under every [release](https://github.com/sbertix/ComposableRequesst/releases).\n\n\u003e What's next?\n\n**ComposableRequest** was initially [**Swiftagram**](https://github.com/sbertix/Swiftagram)'s networking layer and it still tends to follow roughly the same development cycle.\n\n[Milestones](https://github.com/sbertix/ComposableRequest/milestones), [issues](https://github.com/sbertix/ComposableRequest/issues), are the best way to keep updated with active developement.\n\nFeel free to contribute by sending a [pull request](https://github.com/sbertix/ComposableRequest/pulls).\nJust remember to refer to our [guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) beforehand.\n\n\u003cp /\u003e\n\n## Installation\n### Swift Package Manager (Xcode 11 and above)\n1. Select `File`/`Swift Packages`/`Add Package Dependency…` from the menu.\n1. Paste `https://github.com/sbertix/ComposableRequest.git`.\n1. Follow the steps.\n1. Add **Storage** together with **Requests** for the full experience.\n\n\u003e Why not CocoaPods, or Carthage, or ~blank~?\n\nSupporting multiple _dependency managers_ makes maintaining a library exponentially more complicated and time consuming.\\\nFurthermore, with the integration of the **Swift Package Manager** in **Xcode 11** and greater, we expect the need for alternative solutions to fade quickly.\n\n\u003cdetails\u003e\u003csummary\u003e\u003cstrong\u003eTargets\u003c/strong\u003e\u003c/summary\u003e\n    \u003cp\u003e\n\n- **Requests**, an HTTP client originally integrated in **Swiftagram**, the core library.\n- **Storage**\n- **StorageCrypto**, depending on [**KeychainAccess**](https://github.com/kishikawakatsumi/KeychainAccess), can be imported together with **Storage** to extend its functionality.     \n    \u003c/p\u003e\n\u003c/details\u003e\n\n## Usage\n\nCheck out [**Swiftagram**](https://github.com/sbertix/Swiftagram) or visit the (_auto-generated_) documentation for [**Requests**](https://sbertix.github.io/ComposableRequest/Requests/), [**Storage**](https://sbertix.github.io/ComposableRequest/Storage/) and [**StorageCrypto**](https://sbertix.github.io/ComposableRequest/StorageCrypto/) to learn about use cases.  \n\n### Endpoint\n\nAs an implementation example, we can display some code related to the Instagram endpoint tasked with deleting a post.\n\n```swift\npublic extension Request {\n    /// An enum listing an error.\n    enum DeleteError: Swift.Error { case invalid }\n    \n    /// Delete one of your own posts, matching `identifier`.\n    /// Checkout https://github.com/sbertix/Swiftagram for more info.\n    ///\n    /// - parameter identifier: A valid `String`.\n    /// - returns: A locked `AnySingleEndpoint`, waiting for authentication `HTTPCookie`s.\n    func delete(_ identifier: String) -\u003e Providers.Lock\u003c[HTTPCookie], AnySingleEndpoint\u003cBool\u003e\u003e {\n        // Wait for user defined values.\n        .init { cookies in\n            // Fetch first info about the post to learn if it's a video or picture\n            // as they have slightly different endpoints for deletion.\n            Single {\n                Path(\"https://i.instagram.com/api/v1/media/\\(identifier)/info\")\n                 // Wait for the user to `inject` an array of `HTTPCookie`s.\n                // You should implement your own `model` to abstract away\n                // authentication cookies, but as this is just an example\n                // we leave it to you.\n                Headers(HTTPCookie.requestHeaderFields(with: cookies))\n                // Decode it inside an `AnyDecodable`, allowing to interrogate JSON\n                // representations of object without knowing them in advance.\n                Response {\n                    let output = try JSONDecoder().decode(AnyDecodable.self, from: $0)\n                    guard let type = output.items[0].mediaType.int,\n                                [1,2, 8].contains(type) else { \n                        throw DeleteError.invalid\n                    }\n                    return type\n                }\n            }.switch { \n                Path(\"https://i.instagram.com/api/v1/media/\\(identifier)/delete\")\n                Query($0 == 2 ? \"VIDEO\" : \"PHOTO\", forKey: \"media_type\")\n                // This will be applied exactly as before, but you can add whaterver\n                // you need to it, as it will only affect this `Request`.\n                Headers(HTTPCookie.requestHeaderFields(with: cookies))\n                Response {\n                    let output = try JSONDecoder().decode(AnyDecodable.self, from: $0)\n                    return $0.status.bool ?? false\n                }\n            }\n        }\n    }\n}\n```\n\n\u003cbr /\u003e\n\n\u003e How can the user then retreieve the information?\n\nAll the user has to do is…\n\n```swift\n/// A valid post identifier.\nlet identifier: String = /* a valid String */\n/// A valid array of cookies.\nlet cookies: [HTTPCookie] = /* an array of HTTPCookies */\n/// A *retained* collection of `AnyCancellable`s.\nvar bin: Set\u003cAnyCancellable\u003e = []\n\n/// Delete it using **Combine**.\nRequest.delete(identifier)\n    .unlock(with: cookies)\n    .resolve(with: .shared)     // The shared `URLSession`.\n    .sink { _ in } receiveValue: { print($0) }\n    .store(in: \u0026bin)\n\n[…]\n\n/// Delete it using _async/await_.\nlet result = try await Request.delete(identifier)\n    .unlock(with: cookies)\n    .resolve(with: .shared)\n```\n\n### Resume and cancel requests\n\n\u003e What about cancelling the request, or starting it a later date?\n\nConcrete implementation of `Receivable` might implement suspension and cancellation through their underlying types (like `URLSessionDataTask` or `Cancellable`).  \n\n### Caching\nCaching of `Storable`s is provided through conformance to the `Storage` protocol, specifically by implementing either `ThrowingStorage` or `NonThrowingStorage`.  \n\nThe library comes with several concrete implementations.  \n- `TransientStorage` should be used when no caching is necessary, and it's what `Authenticator`s default to when no `Storage` is provided.  \n- `UserDefaultsStorage` allows for faster, out-of-the-box, testing, although it's not recommended for production as private cookies are not encrypted.  \n- `KeychainStorage`, requiring you to add **ComposableStorageCrypto**, (**preferred**) stores them safely in the user's keychain.  \n--\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsbertix%2Fcomposablerequest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsbertix%2Fcomposablerequest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsbertix%2Fcomposablerequest/lists"}