{"id":16487223,"url":"https://github.com/nerdsupremacist/protected","last_synced_at":"2025-03-23T12:33:41.141Z","repository":{"id":118265693,"uuid":"499948491","full_name":"nerdsupremacist/Protected","owner":"nerdsupremacist","description":"Experimental API for Reads and Writes protected via Phantom types","archived":false,"fork":false,"pushed_at":"2022-06-08T20:33:47.000Z","size":69,"stargazers_count":11,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-18T20:22:43.264Z","etag":null,"topics":["phantom-types","rights","safety","swift"],"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/nerdsupremacist.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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,"publiccode":null,"codemeta":null}},"created_at":"2022-06-04T22:21:06.000Z","updated_at":"2023-06-15T08:02:46.000Z","dependencies_parsed_at":null,"dependency_job_id":"2a1e56ae-e89f-4ada-b170-b10fbc1c3d93","html_url":"https://github.com/nerdsupremacist/Protected","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdsupremacist%2FProtected","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdsupremacist%2FProtected/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdsupremacist%2FProtected/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nerdsupremacist%2FProtected/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nerdsupremacist","download_url":"https://codeload.github.com/nerdsupremacist/Protected/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245104460,"owners_count":20561377,"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":["phantom-types","rights","safety","swift"],"created_at":"2024-10-11T13:33:15.886Z","updated_at":"2025-03-23T12:33:41.120Z","avatar_url":"https://github.com/nerdsupremacist.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"logo.png\" width=\"600\" max-width=\"90%\" alt=\"Proteted\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Swift-5.5-orange.svg\" /\u003e\n    \u003ca href=\"https://swift.org/package-manager\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/swiftpm-compatible-brightgreen.svg?style=flat\" alt=\"Swift Package Manager\" /\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://twitter.com/nerdsupremacist\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/twitter-@nerdsupremacist-blue.svg?style=flat\" alt=\"Twitter: @nerdsupremacist\" /\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\n# Protected\n\nAccess control can't always be static. \nSometimes the mutability, nullability and access of variables depends on context. \nWhen dealing with these scenarios, we usually end up writing wrappers or duplicate the class for each different context. Well no more!\n\nProtected is a Swift Package that allows you to specify the read and write rights for any type, depending on context by using Phantom types. \nHere's a taste of the syntax (we will explain everything in time):\n\n```swift\nstruct MyRights: RightsManifest {\n    typealias ProtectedType = Book\n    \n    let title = Write(\\.title)\n    let author = Read(\\.author)\n}\n\nfunc work(book: Protected\u003cBook, MyRights\u003e) {\n    book.title // ✅ works\n    book.title = \"Don Quixote\" // ✅ works\n    book.author // ✅ works\n    book.author = \"\" // ❌ will not compile\n    book.isbn // ❌ will not compile\n}\n```\n\nThis project is heavily inspired by [@sellmair](https://github.com/sellmair)'s [post on Phantom Read Rights](https://medium.com/@sellmair/phantom-read-rights-in-kotlin-modelling-a-pipeline-eef3523db857).\nFor those curious Protected relies on phantom types and [dynamic member look up](https://github.com/apple/swift-evolution/blob/main/proposals/0252-keypath-dynamic-member-lookup.md) to provide an easy API for specifying read and write rights for any type in Swift.\n\n## Installation\n### Swift Package Manager\n\nYou can install Sync via [Swift Package Manager](https://swift.org/package-manager/) by adding the following line to your `Package.swift`:\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n    [...]\n    dependencies: [\n        .package(url: \"https://github.com/nerdsupremacist/Protected.git\", from: \"1.0.0\")\n    ]\n)\n```\n\n## Usage\nSo let's imagine that you run a Book publishing company. Your codebase works with information about books at different stages of publishing. \nMost of the code revolves entirely around the following class:\n\n```swift\npublic class Book {\n    public var title: String?\n    public var author: String?\n    public var isbn: String?\n}\n```\n\nSo what's wrong with this code? Well plenty of things:\n1. Everything is nullable. Despite the fact that there's places in our code where we can be sure that they're not null anymore.\n1. Everyting can be read publicly.\n1. Everything is mutable, all of the time. And if anything is mutable, you can bet someone will mutate it, and probably in a part of code where you are not expecting it.\n\nOne way to address this would be to create a different version of `Book` for every scenario: `PlannedBook`, `PrePublishingBook`, `PostPublishingBook`, `PublishedBook`, etc. But this leads to an unsustainable amount of code duplication and added complexity.\nThese things might not look to bad when it comes to a simple class with three attributes, but as your classes get more complicated and we get more and more cases, keeping track of what can be read and mutated where becomes very difficult.\n\nEnter our package Protected. When working with Protected, you write your model once, and we change how you access it. \nWe are mainly working with two things:\n1. `RightsManifest`s: basically a type that specifies to what you have access to and how much.\n2. `Protected`: a wrapper that will enforce at compile time that you only read and write what's allowed by the manifest.\n\nSo for our book example, we can consider that we want to safely handle the pre-publishing stage of a book. \nAt this stage the author name is already set and should be changed. \nThe title is also set, but is open to change. The ISBN should not be read at all. For this case we can write a `RightsManifest`\n\n```swift\nstruct PrePublishRights: RightsManifest {\n    typealias ProtectedType = Book\n\n    // a) Declare that we can read and write the title\n    let title = Write(\\.title!) // b) with the ! enforce that at this stage it's no longer optional\n    // c) Declare that we can only read the name of the author\n    let author = Read(\\.author!)\n    \n    // Do not include any declaration for the ISBN\n}\n```\n\nA RightsManifest is a type that includes variables pointing to either:\n- `Write`: can be read be written to \n- `Read`: can only be read\n\nEach attribute you declare in the manifest can then be read in that context. So let's try to use it:\n\n```swift\nlet book = Protected(Book(), by: PrePublishRights())\nbook.title // ✅ works\nbook.title = \"Don Quixote\" // ✅ works\nbook.author // ✅ works\nbook.author = \"\" // ❌ will not compile\nbook.isbn // ❌ will not compile\n```\n\n### More Advanced Features\n\n#### Protecting nested types\nIf your object contains nested types, you can specify in your manifest, the manifest that corresponds to that value, and Protected will in that case return a `Protected` Value\nFor example, let's say that your books point to an Author object where you quite insecurely store the password (I've seen worse security):\n\n```swift\nclass Author {\n    var name: String?\n    var password: String?\n}\n\nclass Book {\n    var title: String?\n    var author: Author?\n}\n```\n\nAnd let's say that you want to make sure that when someone grabs the author object from your book, that they can't see the password either. \nFor that you can start by creating the manifests for both types. And when it comes to specifying the read right to the author, you can include that it should be protected by your other Manifest:\n\n```swift\nstruct AuthorBasicRights: RightsManifest {\n    typealias ProtectedType = Author\n    \n    let name = Read(\\.name)\n}\n\nstruct BookBasicRights: RightsManifest {\n    typealias ProtectedType = Book\n    \n    let title = Write(\\.title)\n    // specify that for the author you want the result to be protected by AuthorBasicRights\n    let author = Read(\\.author).protected(by: AuthorBasicRights())\n}\n```\n\nWith this when you try to use it, you won't be able to access the password:\n```swift\nlet book = Protected(Book(), by: BookBasicRights())\nbook.title // ✅ works\nlet author = book.author // returns a Protected\u003cAuthor, AuthorBasicRights\u003e?\nauthor?.name // ✅ works\nauthor?.password // ❌ will not compile\n```\n\n#### Manipulating Values and Changing Rights\n\nAll `Protected` values are designed to be changed. If you use the same object at different stages, you would like to change the rights associated with that object at any given time.\nThat's why `Protected` comes with a couple of functions prefixed by `unsafeX` to signal that you really should know what it is that you're doing with the object here.\n\nFor example let's imagine that you're writing a piece of code that will create an ISBN for a book and move it to the post publishing stage. So you can imagine that your rights look as follows:\n```swift\nstruct PrePublishRights: RightsManifest {\n    typealias ProtectedType = Book\n\n    let title = Write(\\.title!)\n    let author = Read(\\.author!)\n}\n\nstruct PostPublishRights: RightsManifest {\n    typealias ProtectedType = Book\n\n    let title = Read(\\.title!)\n    let author = Read(\\.author!)\n    let isbn = Read(\\.isbn!)\n}\n```\n\nWhen you publish the book, you will efectively transition your object to be governed by the pre publish rights to the post publish rights. You can do this with the method: `unsafeMutateAndChangeRights`:\n\n```swift\nfunc publish(book: Protected\u003cBook, PrePublishRights\u003e) -\u003e Protected\u003cBook, PostPublishRights\u003e {\n    return book.unsafeMutateAndChangeRights(to: PostPublishRights()) { book in \n        // here you have complete unsafe access to the underlying `book` object, absolutely no limitations\n        book.isbn = generateISBN()\n    }\n}\n```\n\nOther `unsafeX` functions to deal with the underlying data when needed include:\n- `unsafeMutate`: let's you mutate the underlying value however you like.\n- `unsafeChangeRights`: let's you create a new version of the protected, governed by a new manifest.\n- `unsafeMapAndChangeRights`: let's you map the value onto a new one, and wrap it in a new protected governed by a different manifest.\n- `unsafeBypassRights`: just get the value no matter what the manifest says.\n\n#### More elaborate Read rights\nRead rights don't necessarily need to be a keypath. For Read Rights you have multiple options for dealing with them. \nFor example you can provide a more elaborate getter logic:\n\n```swift\nstruct AuthorBasicRights: RightsManifest {\n    typealias ProtectedType = Author\n    \n    let name = Read(\\.name)\n    let password = Read { obfuscate($0.password) }\n}\n```\n\nYou can also include a `.map` after any `Read` to manipulate the value:\n\n```swift\nstruct AuthorBasicRights: RightsManifest {\n    typealias ProtectedType = Author\n    \n    let name = Read(\\.name)\n    let password = Read(\\.password).map { obfuscate($0) }\n}\n```\n\n### Caveats\n\nThis is not a perfect protection for no one to be able to access things they shouldn't. \nProtected is not a security framework, it will not prevent people from accessing or mutating anything. \nIt is intended as an easy way to make safe usage clear and simple depending on context.\n\n1. A code can always access everything using the `unsafeX` methods provided.\n2. You can (but really shouldn't) include more rights whithin the extension of a manifest. This allows you to include more rights than intended while still appearing to be safe. Do not do this! Protected cannot protect you from doing this. \n\n## Contributions\nContributions are welcome and encouraged!\n\n## License\nProtected is available under the MIT license. See the LICENSE file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnerdsupremacist%2Fprotected","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnerdsupremacist%2Fprotected","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnerdsupremacist%2Fprotected/lists"}