{"id":20838775,"url":"https://github.com/royalicing/syrup","last_synced_at":"2025-09-04T12:41:38.366Z","repository":{"id":64060125,"uuid":"54088260","full_name":"RoyalIcing/Syrup","owner":"RoyalIcing","description":"Asynchronous data flow in Swift using enums","archived":false,"fork":false,"pushed_at":"2023-03-17T00:22:09.000Z","size":112,"stargazers_count":2,"open_issues_count":3,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-18T23:43:00.014Z","etag":null,"topics":["async","data-flow","enum","promise","swift","swift-library"],"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/RoyalIcing.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2016-03-17T04:23:17.000Z","updated_at":"2023-12-05T05:41:58.000Z","dependencies_parsed_at":"2024-02-02T03:49:37.065Z","dependency_job_id":null,"html_url":"https://github.com/RoyalIcing/Syrup","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FSyrup","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FSyrup/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FSyrup/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RoyalIcing%2FSyrup/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RoyalIcing","download_url":"https://codeload.github.com/RoyalIcing/Syrup/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243196662,"owners_count":20251861,"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":["async","data-flow","enum","promise","swift","swift-library"],"created_at":"2024-11-18T01:11:30.779Z","updated_at":"2025-03-12T10:08:40.015Z","avatar_url":"https://github.com/RoyalIcing.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Syrup\n\n## Overview\n\nSyrup makes data flow simple in Swift. Each step is represented by its own case in an enum.\n\nAssociated values are used to keep state for each step. This makes it very easy to implement — just write what it takes to get from one step to the next, and so on.\n\nGrand Central Dispatch is then used to run the steps asynchronously on a queue.\n\nHaving explicit enum cases for each step makes it easy to test from any point in the data flow.\n\n## Installation\n\n### Carthage\n\n```\ngithub \"RoyalIcing/Syrup\"\n```\n\n## Usage\n\nA real world example for loading and saving JSON in my app Lantern can be seen here: https://github.com/RoyalIcing/Lantern/blob/9e5e8aa95e967b07a9968efaef22e8c10ea3358f/LanternModel/ModelManager.swift#L41\n\n---\n\nThe example below scopes access to a security scoped file.\n\n```swift\nstruct FileAccessProgression : Progression {\n\tlet fileURL: URL\n\tprivate var startAccess: Bool\n\tprivate var done: Bool\n\t\n\tinit(fileURL: URL) {\n\t\tself.fileURL = fileURL\n\t\tstartAccess = true\n\t\tdone = false\n\t}\n\t\n\tenum ErrorKind : Error {\n\t\tcase cannotAccess(fileURL: URL)\n\t}\n\t\n\tmutating func updateOrDeferNext() throws -\u003e Deferred\u003cFileAccessProgression\u003e? {\n\t\tif startAccess {\n\t\t\tlet accessSucceeded = fileURL.startAccessingSecurityScopedResource()\n\t\t\tif !accessSucceeded {\n\t\t\t\tthrow ErrorKind.cannotAccess(fileURL: fileURL)\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tfileURL.stopAccessingSecurityScopedResource()\n\t\t}\n\t\t\n\t\tdone = true\n\t\t\n\t\t// Mutated, so no need to return future\n\t\treturn nil\n\t}\n\t\n\ttypealias Result = FileAccessProgression\n\tvar result: FileAccessProgression? {\n\t\tguard done else { return nil }\n\t\t\n\t\tvar copy = self\n\t\tif startAccess {\n\t\t\tcopy.startAccess = false\n\t\t\tcopy.done = false\n\t\t}\n\t\treturn copy\n\t}\n}\n```\n\nEach step updates to or returns its next step. Asynchronous steps can return a Deferred which resolves to the next step.\n\nSyrup runs each step on a Grand Central Dispatch queue.\n\nTo run, create a progression and divide it by the quality of service to run on.\nThen bind `\u003e\u003e=` a callback to start the progression and receive the result.\n\nYour callback is passed a throwing function `useResult` — call it to get the result.\nErrors thrown in any of the steps will bubble up, so use Swift error\nhandling to `catch` them all here in the one place. \n\n```swift\nFileAccessProgression(fileURL: fileURL) / .utility \u003e\u003e= { useResult in\n\tdo {\n\t\tlet stopAccessing = try useResult()\n\t\t// Use stopAccessing.fileURL\n\n\t\t// Run when done accessing\n\t\tstopAccessing / .utility \u003e\u003e= { _ in\n\t\t}\n\t\tcatch {\n\t\t\t// Handle `error` here\n\t\t}\n\t}\n}\n```\n\n## Using existing asynchronous libraries\n\nSyrup can create tasks for existing asychronous libraries, such as NSURLSession.\nUse the `.future` task, and resolve the value, or resolve throwing an error.\n\n```swift\nenum HTTPRequestProgression : Progression {\n\ttypealias Result = (response: HTTPURLResponse, body: Data?)\n\t\n\tcase get(url: URL)\n\tcase post(url: URL, body: Data)\n\t\n\tcase success(Result)\n\t\n\tmutating func updateOrDeferNext() -\u003e Deferred\u003cHTTPRequestProgression\u003e? {\n\t\tswitch self {\n\t\tcase let .get(url):\n\t\t\treturn Deferred.future{ resolve in\n\t\t\t\tlet session = URLSession.shared\n\t\t\t\tlet task = session.dataTask(with: url, completionHandler: { data, response, error in\n\t\t\t\t\tif let error = error {\n\t\t\t\t\t\tresolve{ throw error }\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tresolve{ .success((response: response as! HTTPURLResponse, body: data)) }\n\t\t\t\t\t}\n\t\t\t\t}) \n\t\t\t\ttask.resume()\n\t\t\t}\n\t\tcase let .post(url, body):\n\t\t\treturn Deferred.future{ resolve in\n\t\t\t\tlet session = URLSession.shared\n\t\t\t\tvar request = URLRequest(url: url)\n\t\t\t\trequest.httpBody = body\n\t\t\t\tlet task = session.dataTask(with: request, completionHandler: { (data, response, error) in\n\t\t\t\t\tif let error = error {\n\t\t\t\t\t\tresolve { throw error }\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tresolve { .success((response: response as! HTTPURLResponse, body: data)) }\n\t\t\t\t\t}\n\t\t\t\t}) \n\t\t\t\ttask.resume()\n\t\t\t}\n\t\tcase .success:\n\t\t\treturn nil\n\t\t}\n\t}\n\t\n\tvar result: Result? {\n\t\tguard case let .success(result) = self else { return nil }\n\t\treturn result\n\t}\n}\n```\n\n## Motivations\n\n- Captures data flow in a declarative form making it easier to understand. Your progression is a reusable recipe for what to do.\n- Associated values capture the entire state at a particular stage in the flow. There’s no external state or side effects, just work with what’s stored in each case.\n- Each step is distinct, and can produce its next step easily in either a sychronous or asychronous manner.\n- Steps are able to be stored and restored at will, as they are just enums with associated data. This allows easier unit testing, since you can resume at any step in the progression.\n- Swift’s native error handling is used.\n\n## Multiple inputs or outputs\n\nStages can have multiple choices of initial stages: just add multiple cases!\n\nFor multiple choice of output, use a `enum` for the `Result` associated type.\n\n## Composing stages\n\n`Progression` includes `.map` and `.flatMap` (also `\u003e\u003e=`) methods, allowing progressions to be composed\ninside other progressions. A series of progressions can become a single progression in a combined\nenum, and so on.\n\nFor example, combining a file read with a web upload:\n\n```swift\nenum HTTPRequestProgression : Progression {\n\ttypealias Result = (response: HTTPURLResponse, body: Data?)\n\t\n\tcase get(url: URL)\n\tcase post(url: URL, body: Data)\n\t\n\tcase success(Result)\n\t\n\tmutating func updateOrDeferNext() -\u003e Deferred\u003cHTTPRequestProgression\u003e? {\n\t\tswitch self {\n\t\tcase let .get(url):\n\t\t\treturn Deferred.future{ resolve in\n\t\t\t\tlet session = URLSession.shared\n\t\t\t\tlet task = session.dataTask(with: url, completionHandler: { data, response, error in\n\t\t\t\t\tif let error = error {\n\t\t\t\t\t\tresolve{ throw error }\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tresolve{ .success((response: response as! HTTPURLResponse, body: data)) }\n\t\t\t\t\t}\n\t\t\t\t}) \n\t\t\t\ttask.resume()\n\t\t\t}\n\t\tcase let .post(url, body):\n\t\t\treturn Deferred.future{ resolve in\n\t\t\t\tlet session = URLSession.shared\n\t\t\t\tvar request = URLRequest(url: url)\n\t\t\t\trequest.httpBody = body\n\t\t\t\tlet task = session.dataTask(with: request, completionHandler: { (data, response, error) in\n\t\t\t\t\tif let error = error {\n\t\t\t\t\t\tresolve { throw error }\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tresolve { .success((response: response as! HTTPURLResponse, body: data)) }\n\t\t\t\t\t}\n\t\t\t\t}) \n\t\t\t\ttask.resume()\n\t\t\t}\n\t\tcase .success:\n\t\t\tbreak\n\t\t}\n\t\treturn nil\n\t}\n\t\n\tvar result: Result? {\n\t\tguard case let .success(result) = self else { return nil }\n\t\treturn result\n\t}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froyalicing%2Fsyrup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froyalicing%2Fsyrup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froyalicing%2Fsyrup/lists"}