├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── Protected.xcscheme
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── Protected
│ ├── Protected.swift
│ ├── Rights
│ ├── Omniscient.swift
│ ├── ReadPropertyRight.swift
│ ├── RightsManifest.swift
│ ├── Strategy
│ │ ├── AnyRightResolutionStrategy.swift
│ │ ├── ArrayRightResolutionStrategy.swift
│ │ ├── KeyPathRightResolutionStrategy.swift
│ │ ├── MapRightResolutionStrategy.swift
│ │ ├── OptionalRightResolutionStrategy.swift
│ │ ├── ProtectedRightResolutionStrategy.swift
│ │ ├── RightResolutionStrategy.swift
│ │ ├── SequentialRightResolutionStrategy.swift
│ │ └── SimpleRightResolutionStrategy.swift
│ └── WritePropertyRight.swift
│ └── UnsafeProtectedBuilder.swift
├── Tests
└── ProtectedTests
│ └── ProtectedTests.swift
└── logo.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Protected.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
48 |
54 |
55 |
56 |
57 |
58 |
68 |
69 |
75 |
76 |
82 |
83 |
84 |
85 |
87 |
88 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mathias Quintero
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Protected",
8 | products: [
9 | // Products define the executables and libraries a package produces, and make them visible to other packages.
10 | .library(
11 | name: "Protected",
12 | targets: ["Protected"]),
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | // .package(url: /* package url */, from: "1.0.0"),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
20 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
21 | .target(
22 | name: "Protected",
23 | dependencies: []),
24 | .testTarget(
25 | name: "ProtectedTests",
26 | dependencies: ["Protected"]),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # Protected
16 |
17 | Access control can't always be static.
18 | Sometimes the mutability, nullability and access of variables depends on context.
19 | When dealing with these scenarios, we usually end up writing wrappers or duplicate the class for each different context. Well no more!
20 |
21 | Protected is a Swift Package that allows you to specify the read and write rights for any type, depending on context by using Phantom types.
22 | Here's a taste of the syntax (we will explain everything in time):
23 |
24 | ```swift
25 | struct MyRights: RightsManifest {
26 | typealias ProtectedType = Book
27 |
28 | let title = Write(\.title)
29 | let author = Read(\.author)
30 | }
31 |
32 | func work(book: Protected) {
33 | book.title // ✅ works
34 | book.title = "Don Quixote" // ✅ works
35 | book.author // ✅ works
36 | book.author = "" // ❌ will not compile
37 | book.isbn // ❌ will not compile
38 | }
39 | ```
40 |
41 | This 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).
42 | For 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.
43 |
44 | ## Installation
45 | ### Swift Package Manager
46 |
47 | You can install Sync via [Swift Package Manager](https://swift.org/package-manager/) by adding the following line to your `Package.swift`:
48 |
49 | ```swift
50 | import PackageDescription
51 |
52 | let package = Package(
53 | [...]
54 | dependencies: [
55 | .package(url: "https://github.com/nerdsupremacist/Protected.git", from: "1.0.0")
56 | ]
57 | )
58 | ```
59 |
60 | ## Usage
61 | So let's imagine that you run a Book publishing company. Your codebase works with information about books at different stages of publishing.
62 | Most of the code revolves entirely around the following class:
63 |
64 | ```swift
65 | public class Book {
66 | public var title: String?
67 | public var author: String?
68 | public var isbn: String?
69 | }
70 | ```
71 |
72 | So what's wrong with this code? Well plenty of things:
73 | 1. Everything is nullable. Despite the fact that there's places in our code where we can be sure that they're not null anymore.
74 | 1. Everyting can be read publicly.
75 | 1. 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.
76 |
77 | One 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.
78 | These 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.
79 |
80 | Enter our package Protected. When working with Protected, you write your model once, and we change how you access it.
81 | We are mainly working with two things:
82 | 1. `RightsManifest`s: basically a type that specifies to what you have access to and how much.
83 | 2. `Protected`: a wrapper that will enforce at compile time that you only read and write what's allowed by the manifest.
84 |
85 | So for our book example, we can consider that we want to safely handle the pre-publishing stage of a book.
86 | At this stage the author name is already set and should be changed.
87 | The 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`
88 |
89 | ```swift
90 | struct PrePublishRights: RightsManifest {
91 | typealias ProtectedType = Book
92 |
93 | // a) Declare that we can read and write the title
94 | let title = Write(\.title!) // b) with the ! enforce that at this stage it's no longer optional
95 | // c) Declare that we can only read the name of the author
96 | let author = Read(\.author!)
97 |
98 | // Do not include any declaration for the ISBN
99 | }
100 | ```
101 |
102 | A RightsManifest is a type that includes variables pointing to either:
103 | - `Write`: can be read be written to
104 | - `Read`: can only be read
105 |
106 | Each attribute you declare in the manifest can then be read in that context. So let's try to use it:
107 |
108 | ```swift
109 | let book = Protected(Book(), by: PrePublishRights())
110 | book.title // ✅ works
111 | book.title = "Don Quixote" // ✅ works
112 | book.author // ✅ works
113 | book.author = "" // ❌ will not compile
114 | book.isbn // ❌ will not compile
115 | ```
116 |
117 | ### More Advanced Features
118 |
119 | #### Protecting nested types
120 | If 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
121 | For example, let's say that your books point to an Author object where you quite insecurely store the password (I've seen worse security):
122 |
123 | ```swift
124 | class Author {
125 | var name: String?
126 | var password: String?
127 | }
128 |
129 | class Book {
130 | var title: String?
131 | var author: Author?
132 | }
133 | ```
134 |
135 | And 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.
136 | For 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:
137 |
138 | ```swift
139 | struct AuthorBasicRights: RightsManifest {
140 | typealias ProtectedType = Author
141 |
142 | let name = Read(\.name)
143 | }
144 |
145 | struct BookBasicRights: RightsManifest {
146 | typealias ProtectedType = Book
147 |
148 | let title = Write(\.title)
149 | // specify that for the author you want the result to be protected by AuthorBasicRights
150 | let author = Read(\.author).protected(by: AuthorBasicRights())
151 | }
152 | ```
153 |
154 | With this when you try to use it, you won't be able to access the password:
155 | ```swift
156 | let book = Protected(Book(), by: BookBasicRights())
157 | book.title // ✅ works
158 | let author = book.author // returns a Protected?
159 | author?.name // ✅ works
160 | author?.password // ❌ will not compile
161 | ```
162 |
163 | #### Manipulating Values and Changing Rights
164 |
165 | All `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.
166 | That'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.
167 |
168 | For 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:
169 | ```swift
170 | struct PrePublishRights: RightsManifest {
171 | typealias ProtectedType = Book
172 |
173 | let title = Write(\.title!)
174 | let author = Read(\.author!)
175 | }
176 |
177 | struct PostPublishRights: RightsManifest {
178 | typealias ProtectedType = Book
179 |
180 | let title = Read(\.title!)
181 | let author = Read(\.author!)
182 | let isbn = Read(\.isbn!)
183 | }
184 | ```
185 |
186 | When 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`:
187 |
188 | ```swift
189 | func publish(book: Protected) -> Protected {
190 | return book.unsafeMutateAndChangeRights(to: PostPublishRights()) { book in
191 | // here you have complete unsafe access to the underlying `book` object, absolutely no limitations
192 | book.isbn = generateISBN()
193 | }
194 | }
195 | ```
196 |
197 | Other `unsafeX` functions to deal with the underlying data when needed include:
198 | - `unsafeMutate`: let's you mutate the underlying value however you like.
199 | - `unsafeChangeRights`: let's you create a new version of the protected, governed by a new manifest.
200 | - `unsafeMapAndChangeRights`: let's you map the value onto a new one, and wrap it in a new protected governed by a different manifest.
201 | - `unsafeBypassRights`: just get the value no matter what the manifest says.
202 |
203 | #### More elaborate Read rights
204 | Read rights don't necessarily need to be a keypath. For Read Rights you have multiple options for dealing with them.
205 | For example you can provide a more elaborate getter logic:
206 |
207 | ```swift
208 | struct AuthorBasicRights: RightsManifest {
209 | typealias ProtectedType = Author
210 |
211 | let name = Read(\.name)
212 | let password = Read { obfuscate($0.password) }
213 | }
214 | ```
215 |
216 | You can also include a `.map` after any `Read` to manipulate the value:
217 |
218 | ```swift
219 | struct AuthorBasicRights: RightsManifest {
220 | typealias ProtectedType = Author
221 |
222 | let name = Read(\.name)
223 | let password = Read(\.password).map { obfuscate($0) }
224 | }
225 | ```
226 |
227 | ### Caveats
228 |
229 | This is not a perfect protection for no one to be able to access things they shouldn't.
230 | Protected is not a security framework, it will not prevent people from accessing or mutating anything.
231 | It is intended as an easy way to make safe usage clear and simple depending on context.
232 |
233 | 1. A code can always access everything using the `unsafeX` methods provided.
234 | 2. 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.
235 |
236 | ## Contributions
237 | Contributions are welcome and encouraged!
238 |
239 | ## License
240 | Protected is available under the MIT license. See the LICENSE file for more info.
241 |
--------------------------------------------------------------------------------
/Sources/Protected/Protected.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | @dynamicMemberLookup
5 | public final class Protected where Rights.ProtectedType == Value {
6 | private var value: Value
7 | private let rights: Rights
8 |
9 | public init(_ value: Value, by rights: Rights) {
10 | self.value = value
11 | self.rights = rights
12 | }
13 |
14 | public subscript(dynamicMember keyPath: KeyPath>) -> T {
15 | let right = rights[keyPath: keyPath]
16 | return right.strategy.resolve(value: value)
17 | }
18 |
19 | public subscript(dynamicMember keyPath: KeyPath>) -> T {
20 | get {
21 | return value[keyPath: rights[keyPath: keyPath].keyPath]
22 | }
23 | set {
24 | value[keyPath: rights[keyPath: keyPath].keyPath] = newValue
25 | }
26 | }
27 |
28 | public func unsafeMutate(_ mutations: (inout Value) throws -> Void) rethrows {
29 | try mutations(&value)
30 | }
31 |
32 | public func unsafeChangeRights(to rights: TransformedRights) -> Protected {
33 | return Protected(value, by: rights)
34 | }
35 |
36 | public func unsafeMapAndChangeRights(to rights: TransformedRights, _ transform: (Value) throws -> Value) rethrows -> Protected {
37 | let value = try transform(value)
38 | return Protected(value, by: rights)
39 | }
40 |
41 | public func unsafeMutateAndChangeRights(to rights: TransformedRights,
42 | _ mutations: (inout Value) throws -> Void) rethrows -> Protected {
43 |
44 | try unsafeMutate(mutations)
45 | return unsafeChangeRights(to: rights)
46 | }
47 |
48 | public func unsafeBypassRights() -> Value {
49 | return value
50 | }
51 |
52 | public func unsafeChange() -> ProtectedBuilder {
53 | return ProtectedBuilder(self)
54 | }
55 | }
56 |
57 | extension Protected where Rights == Omniscient {
58 | public convenience init(_ value: Value) {
59 | self.init(value, by: .omniscient())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Omniscient.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | @dynamicMemberLookup
5 | public struct Omniscient : RightsManifest {
6 | public init() { }
7 |
8 | public subscript(dynamicMember keyPath: KeyPath) -> ReadPropertyRight {
9 | return ReadPropertyRight(keyPath)
10 | }
11 |
12 | public subscript(dynamicMember keyPath: WritableKeyPath) -> WritePropertyRight {
13 | return WritePropertyRight(keyPath)
14 | }
15 | }
16 |
17 | extension RightsManifest {
18 | static func omniscient() -> Omniscient where Self == Omniscient {
19 | return Omniscient()
20 | }
21 | }
22 |
23 | extension Rights {
24 |
25 | var omniscient: Omniscient {
26 | return Omniscient()
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/ReadPropertyRight.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | @dynamicMemberLookup
5 | public struct ReadPropertyRight {
6 | let strategy: AnyRightResolutionStrategy
7 |
8 | init(strategy: Strategy) where Strategy.Value == ProtectedType, Strategy.Resolved == Value {
9 | self.strategy = AnyRightResolutionStrategy(strategy)
10 | }
11 |
12 | init(strategy: (SimpleRightResolutionStrategy) -> Strategy) where Strategy.Value == ProtectedType, Strategy.Resolved == Value {
13 | self.init(strategy: strategy(SimpleRightResolutionStrategy()))
14 | }
15 |
16 | public subscript(dynamicMember keyPath: KeyPath) -> ReadPropertyRight {
17 | return .init(strategy: strategy.keyPath(keyPath))
18 | }
19 | }
20 |
21 | extension ReadPropertyRight {
22 | public init(_ keyPath: KeyPath) {
23 | self.init { $0.keyPath(keyPath) }
24 | }
25 |
26 | public init(_ transform: @escaping (ProtectedType) -> Value) {
27 | self.init { $0.map(transform) }
28 | }
29 | }
30 |
31 | extension ReadPropertyRight {
32 | public func protected(by rights: Rights) -> ReadPropertyRight where Value == Rights.ProtectedType {
33 | return .init(strategy: strategy.protected(by: rights))
34 | }
35 |
36 | public func protected(by rights: Rights) -> ReadPropertyRight where Value == Rights.ProtectedType? {
37 | return wrapResult { $0.protected(by: rights).optional() }
38 | }
39 |
40 | public func protected(by rights: Rights) -> ReadPropertyRight where Value == [Rights.ProtectedType] {
41 | return wrapResult { $0.protected(by: rights).array() }
42 | }
43 |
44 | public func protected(by rights: Rights) -> ReadPropertyRight where Value == [Rights.ProtectedType]? {
45 | return wrapResult { $0.protected(by: rights).array().optional() }
46 | }
47 |
48 | public func protected(by rights: Rights) -> ReadPropertyRight where Value == [Rights.ProtectedType?] {
49 | return wrapResult { $0.protected(by: rights).optional().array() }
50 | }
51 |
52 | public func protected(by rights: Rights) -> ReadPropertyRight where Value == [Rights.ProtectedType?]? {
53 | return wrapResult { $0.protected(by: rights).optional().array().optional() }
54 | }
55 |
56 | private func wrapResult(_ create: (SimpleRightResolutionStrategy) -> S) -> ReadPropertyRight where Value == S.Value {
57 | return .init(strategy: strategy.wrapResult(create))
58 | }
59 | }
60 |
61 | extension ReadPropertyRight {
62 | public subscript(dynamicMember keyPath: KeyPath, T>) -> ReadPropertyRight where Value == T.ProtectedType {
63 | return protected(by: Rights.shared[keyPath: keyPath])
64 | }
65 |
66 | public subscript(dynamicMember keyPath: KeyPath, T>) -> BuiltReadPropertyRight where Value == T.ProtectedType? {
67 | return BuiltReadPropertyRight(right: protected(by: Rights.shared[keyPath: keyPath]))
68 | }
69 |
70 | public subscript(dynamicMember keyPath: KeyPath, T>) -> BuiltReadPropertyRight where Value == [T.ProtectedType] {
71 | return BuiltReadPropertyRight(right: protected(by: Rights.shared[keyPath: keyPath]))
72 | }
73 |
74 | public subscript(dynamicMember keyPath: KeyPath, T>) -> BuiltReadPropertyRight where Value == [T.ProtectedType]? {
75 | return BuiltReadPropertyRight(right: protected(by: Rights.shared[keyPath: keyPath]))
76 | }
77 |
78 | public subscript(dynamicMember keyPath: KeyPath, T>) -> BuiltReadPropertyRight where Value == [T.ProtectedType?] {
79 | return BuiltReadPropertyRight(right: protected(by: Rights.shared[keyPath: keyPath]))
80 | }
81 |
82 | public subscript(dynamicMember keyPath: KeyPath, T>) -> BuiltReadPropertyRight where Value == [T.ProtectedType?]? {
83 | return BuiltReadPropertyRight(right: protected(by: Rights.shared[keyPath: keyPath]))
84 | }
85 | }
86 |
87 | public struct BuiltReadPropertyRight {
88 | private let right: ReadPropertyRight
89 |
90 | fileprivate init(right: ReadPropertyRight) {
91 | self.right = right
92 | }
93 |
94 | public func callAsFunction() -> ReadPropertyRight {
95 | return right
96 | }
97 | }
98 |
99 | extension ReadPropertyRight {
100 |
101 | public func map(_ transform: @escaping (Value) -> T) -> ReadPropertyRight {
102 | return .init(strategy: strategy.map(transform))
103 | }
104 |
105 | }
106 |
107 | public func ??(_ lhs: ReadPropertyRight, _ rhs: @autoclosure @escaping () -> T?) -> ReadPropertyRight {
108 | return lhs.map { $0 ?? rhs() }
109 | }
110 |
111 | public func ??(_ lhs: ReadPropertyRight, _ rhs: @autoclosure @escaping () -> T) -> ReadPropertyRight {
112 | return lhs.map { $0 ?? rhs() }
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/RightsManifest.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | public protocol RightsManifest {
5 | associatedtype ProtectedType
6 | }
7 |
8 | extension RightsManifest {
9 | public typealias Resolved = Protected
10 | public typealias Read = ReadPropertyRight
11 | public typealias Write = WritePropertyRight
12 | }
13 |
14 | public struct Rights {
15 | static var shared: Self {
16 | return Rights()
17 | }
18 |
19 | private init() {}
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/AnyRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | final class AnyRightResolutionStrategy: RightResolutionStrategy {
5 | private class BaseStorage {
6 | func resolve(value: Value) -> Resolved {
7 | fatalError()
8 | }
9 | }
10 |
11 | private class Storage: BaseStorage where Strategy.Value == Value, Strategy.Resolved == Resolved {
12 | private let strategy: Strategy
13 |
14 | init(strategy: Strategy) {
15 | self.strategy = strategy
16 | }
17 |
18 | override func resolve(value: Value) -> Resolved {
19 | return strategy.resolve(value: value)
20 | }
21 | }
22 |
23 | private let storage: BaseStorage
24 |
25 | init(_ strategy: Strategy) where Strategy.Value == Value, Strategy.Resolved == Resolved {
26 | self.storage = Storage(strategy: strategy)
27 | }
28 |
29 | func resolve(value: Value) -> Resolved {
30 | return storage.resolve(value: value)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/ArrayRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | struct ArrayRightResolutionStrategy: RightResolutionStrategy {
5 | typealias Value = [Strategy.Value]
6 | typealias Resolved = [Strategy.Resolved]
7 |
8 | let strategy: Strategy
9 |
10 | init(strategy: Strategy) {
11 | self.strategy = strategy
12 | }
13 |
14 | func resolve(value: Value) -> Resolved {
15 | return value.map { strategy.resolve(value: $0) }
16 | }
17 | }
18 |
19 | extension RightResolutionStrategy {
20 |
21 | func array() -> ArrayRightResolutionStrategy {
22 | return ArrayRightResolutionStrategy(strategy: self)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/KeyPathRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | struct KeyPathRightResolutionStrategy: RightResolutionStrategy {
5 | private let strategy: Strategy
6 | private let keyPath: KeyPath
7 |
8 | init(strategy: Strategy, keyPath: KeyPath) {
9 | self.strategy = strategy
10 | self.keyPath = keyPath
11 | }
12 |
13 | func resolve(value: Strategy.Value) -> Resolved {
14 | return strategy.resolve(value: value)[keyPath: keyPath]
15 | }
16 | }
17 |
18 | extension RightResolutionStrategy {
19 |
20 | func keyPath(_ keyPath: KeyPath) -> KeyPathRightResolutionStrategy {
21 | return KeyPathRightResolutionStrategy(strategy: self, keyPath: keyPath)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/MapRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | final class MapRightResolutionResolutionStrategy: RightResolutionStrategy where Strategy.Value == Value {
5 | private let strategy: Strategy
6 | private let transform: (Strategy.Resolved) -> Resolved
7 |
8 | init(strategy: Strategy, transform: @escaping (Strategy.Resolved) -> Resolved) {
9 | self.strategy = strategy
10 | self.transform = transform
11 | }
12 |
13 | func resolve(value: Value) -> Resolved {
14 | return transform(strategy.resolve(value: value))
15 | }
16 | }
17 |
18 | extension RightResolutionStrategy {
19 |
20 | func map(_ transform: @escaping (Resolved) -> T) -> MapRightResolutionResolutionStrategy {
21 | return MapRightResolutionResolutionStrategy(strategy: self, transform: transform)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/OptionalRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | struct OptionalRightResolutionStrategy: RightResolutionStrategy {
5 | typealias Value = Strategy.Value?
6 | typealias Resolved = Strategy.Resolved?
7 |
8 | let strategy: Strategy
9 |
10 | init(strategy: Strategy) {
11 | self.strategy = strategy
12 | }
13 |
14 | public func resolve(value: Value) -> Resolved {
15 | return value.map { strategy.resolve(value: $0) }
16 | }
17 | }
18 |
19 | extension RightResolutionStrategy {
20 |
21 | func optional() -> OptionalRightResolutionStrategy {
22 | return OptionalRightResolutionStrategy(strategy: self)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/ProtectedRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | struct ProtectedRightResolutionStrategy: RightResolutionStrategy where Rights.ProtectedType == Strategy.Resolved {
5 | typealias Value = Strategy.Value
6 | typealias Resolved = Protected
7 |
8 | private let strategy: Strategy
9 | private let rights: Rights
10 |
11 | init(strategy: Strategy, rights: Rights) {
12 | self.strategy = strategy
13 | self.rights = rights
14 | }
15 |
16 | func resolve(value: Value) -> Protected {
17 | return Protected(strategy.resolve(value: value), by: rights)
18 | }
19 | }
20 |
21 | extension RightResolutionStrategy {
22 |
23 | func protected(by rights: Rights) -> ProtectedRightResolutionStrategy where Rights.ProtectedType == Resolved {
24 | return ProtectedRightResolutionStrategy(strategy: self, rights: rights)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/RightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | protocol RightResolutionStrategy {
5 | associatedtype Value
6 | associatedtype Resolved
7 |
8 | func resolve(value: Value) -> Resolved
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/SequentialRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | struct SequentialRightResolutionStrategy: RightResolutionStrategy where Previous.Resolved == Next.Value {
5 | let previous: Previous
6 | let next: Next
7 |
8 | func resolve(value: Previous.Value) -> Next.Resolved {
9 | return next.resolve(value: previous.resolve(value: value))
10 | }
11 | }
12 |
13 | extension RightResolutionStrategy {
14 |
15 | func wrapResult(_ create: (SimpleRightResolutionStrategy) -> S) -> SequentialRightResolutionStrategy where S.Value == Resolved {
16 | let simple = SimpleRightResolutionStrategy()
17 | return SequentialRightResolutionStrategy(previous: self, next: create(simple))
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/Strategy/SimpleRightResolutionStrategy.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | struct SimpleRightResolutionStrategy: RightResolutionStrategy {
5 | init() { }
6 |
7 | func resolve(value: Value) -> Value {
8 | return value
9 | }
10 | }
11 |
12 | extension AnyRightResolutionStrategy where Value == Resolved {
13 | static func simple() -> AnyRightResolutionStrategy {
14 | return AnyRightResolutionStrategy(SimpleRightResolutionStrategy())
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Protected/Rights/WritePropertyRight.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | public struct WritePropertyRight {
5 | let keyPath: WritableKeyPath
6 |
7 | public init(_ keyPath: WritableKeyPath) {
8 | self.keyPath = keyPath
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/Protected/UnsafeProtectedBuilder.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | public func protect(_ value: Value) -> ProtectedBuilder {
5 | return ProtectedBuilder(value)
6 | }
7 |
8 | @dynamicMemberLookup
9 | public struct ProtectedBuilder {
10 | private class BaseStorage {
11 | func mutate(_ mutations: (inout Value) throws -> Void) rethrows {
12 | fatalError()
13 | }
14 |
15 | func map(_ mutations: (Value) throws -> Value) rethrows -> BaseStorage {
16 | fatalError()
17 | }
18 |
19 | func protected(by rights: T) -> Protected {
20 | fatalError()
21 | }
22 | }
23 |
24 | private final class ProtectedStorage: BaseStorage where Rights.ProtectedType == Value {
25 | private let protected: Protected
26 |
27 | init(_ protected: Protected) {
28 | self.protected = protected
29 | }
30 |
31 | override func mutate(_ mutations: (inout Value) throws -> Void) rethrows {
32 | try protected.unsafeMutate(mutations)
33 | }
34 |
35 | override func map(_ mutations: (Value) throws -> Value) rethrows -> BaseStorage {
36 | return ValueStorage(try mutations(protected.unsafeBypassRights()))
37 | }
38 |
39 | override func protected(by rights: T) -> Protected {
40 | if T.self == Rights.self {
41 | return protected as! Protected
42 | }
43 |
44 | return Protected(protected.unsafeBypassRights(), by: rights)
45 | }
46 | }
47 |
48 | private final class ValueStorage: BaseStorage {
49 | private var _value: Value
50 |
51 | init(_ value: Value) {
52 | self._value = value
53 | }
54 |
55 | override func mutate(_ mutations: (inout Value) throws -> Void) rethrows {
56 | try mutations(&_value)
57 | }
58 |
59 | override func map(_ mutations: (Value) throws -> Value) rethrows -> BaseStorage {
60 | return ValueStorage(try mutations(_value))
61 | }
62 |
63 | override func protected(by rights: T) -> Protected {
64 | return Protected(_value, by: rights)
65 | }
66 | }
67 |
68 | private let storage: BaseStorage
69 |
70 | private init(_ storage: BaseStorage) {
71 | self.storage = storage
72 | }
73 |
74 | fileprivate init(_ value: Value) {
75 | self.init(ValueStorage(value))
76 | }
77 |
78 | init(_ protected: Protected) where Rights.ProtectedType == Value {
79 | self.init(ProtectedStorage(protected))
80 | }
81 |
82 | public func mutate(_ mutations: (inout Value) throws -> Void) rethrows -> ProtectedBuilder {
83 | try storage.mutate(mutations)
84 | return self
85 | }
86 |
87 | public func map(_ mutations: (Value) throws -> Value) rethrows -> ProtectedBuilder {
88 | return ProtectedBuilder(try storage.map(mutations))
89 | }
90 |
91 | public func with(rights: Rights) -> Protected where Rights.ProtectedType == Value {
92 | return storage.protected(by: rights)
93 | }
94 |
95 | public subscript(dynamicMember keyPath: KeyPath, T>) -> BuiltProtectedCallable where T.ProtectedType == Value {
96 | return BuiltProtectedCallable(builder: self, rights: Rights.shared[keyPath: keyPath])
97 | }
98 | }
99 |
100 | public struct BuiltProtectedCallable where Rights.ProtectedType == Value {
101 | private let builder: ProtectedBuilder
102 | private let rights: Rights
103 |
104 | fileprivate init(builder: ProtectedBuilder, rights: Rights) {
105 | self.builder = builder
106 | self.rights = rights
107 | }
108 |
109 | public func callAsFunction() -> Protected {
110 | return builder.with(rights: rights)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Tests/ProtectedTests/ProtectedTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Protected
3 |
4 | final class ProtectedTests: XCTestCase {
5 | func testSampleRights() throws {
6 | let planning = createBook()
7 | XCTAssertEqual(planning.title, "Don Quixote")
8 | XCTAssertEqual(planning.title?.first, "D")
9 | XCTAssertEqual(planning.author?.name, "Miguel de Cervantes")
10 | XCTAssertEqual(planning.isbn, "0060188707")
11 | planning.author?.name = "Cervantes"
12 | XCTAssertEqual(planning.author?.name, "Cervantes")
13 |
14 | let prePublish = planning.unsafeChange().prePublish()
15 |
16 | prePublish.title = "La cueva de salamanca"
17 | XCTAssertEqual(prePublish.title, "La cueva de salamanca")
18 | XCTAssertEqual(prePublish.author, "Cervantes")
19 | prePublish.unsafeMutate { $0.isbn = nil }
20 | XCTAssertEqual(prePublish.isbn?.first, nil)
21 | }
22 | }
23 |
24 | func createBook() -> Protected {
25 | return protect(Book())
26 | .mutate { book in
27 | let author = Author()
28 | author.name = "Miguel de Cervantes"
29 | author.password = "password"
30 | book.title = "Don Quixote"
31 | book.author = author
32 | book.isbn = "0060188707"
33 | }
34 | .planning()
35 | }
36 |
37 | class Author {
38 | var name: String?
39 | var password: String?
40 | }
41 |
42 | class Book {
43 | var title: String?
44 | var author: Author?
45 | var isbn: String?
46 | }
47 |
48 | extension Rights {
49 | var planning: PlanningRights {
50 | return PlanningRights()
51 | }
52 |
53 | var prePublish: PrePublishRights {
54 | return PrePublishRights()
55 | }
56 |
57 | var basic: AuthorBasicRights {
58 | return AuthorBasicRights()
59 | }
60 | }
61 |
62 | struct PrePublishRights: RightsManifest {
63 | typealias ProtectedType = Book
64 |
65 | let title = Write(\.title!)
66 | let author = Read(\.author!.name!)
67 | let isbn = Read(\.isbn)
68 | }
69 |
70 | struct PlanningRights: RightsManifest {
71 | typealias ProtectedType = Book
72 |
73 | let title = Write(\.title)
74 | let author = Read(\.author).basic()
75 | let isbn = Read(\.isbn)
76 | }
77 |
78 | struct AuthorBasicRights: RightsManifest {
79 | typealias ProtectedType = Author
80 |
81 | let name = Write(\.name)
82 | }
83 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nerdsupremacist/Protected/90b96d52ac4c8b1eda98fec498b26ae2c70e6bfc/logo.png
--------------------------------------------------------------------------------