├── .buckconfig
├── .gitignore
├── .swift-version
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── Store.xcscheme
│ └── StoreTests.xcscheme
├── .travis.yml
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
└── Store
│ ├── Concurrency
│ ├── Atomics.swift
│ ├── Locking.swift
│ └── PropertyWrappers.swift
│ ├── Extensions
│ ├── Binding.swift
│ └── KeyPath+ReadableFormat.swift
│ ├── PushID.swift
│ ├── Serialization
│ ├── DictionaryCodable.swift
│ └── Diffing.swift
│ ├── Store
│ ├── Store+Forwarding.swift
│ ├── Store.swift
│ └── StoreOptions.swift
│ └── Utilities
│ ├── Assign.swift
│ ├── Partial.swift
│ ├── PropertyObservable.swift
│ ├── ReadOnly+Forwarding.swift
│ └── ReadOnly.swift
├── Tests
├── LinuxMain.swift
└── StoreTests
│ ├── StoreTests.swift
│ ├── SupportTypes.swift
│ ├── UtilitiesTests.swift
│ └── XCTestManifests.swift
├── docs
└── store_logo.png
└── format.sh
/.buckconfig:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdrone/Store/fb51754e8f88a0ab99c7994d0d26d3374155f1fc/.buckconfig
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 | .DS_Store
20 |
21 | ## Other
22 | *.moved-aside
23 | *.xcuserstate
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | .build/
40 |
41 | # CocoaPods
42 | #
43 | # We recommend against adding the Pods directory to your .gitignore. However
44 | # you should judge for yourself, the pros and cons are mentioned at:
45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
46 | #
47 | # Pods/
48 |
49 | # Carthage
50 | #
51 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
52 | # Carthage/Checkouts
53 |
54 | Carthage/Build
55 |
56 | # fastlane
57 | #
58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
59 | # screenshots whenever they are needed.
60 | # For more information about the recommended setup visit:
61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
62 |
63 | fastlane/report.xml
64 | fastlane/Preview.html
65 | fastlane/screenshots
66 | fastlane/test_output
67 | .buckd/
68 | .build_temp/
69 |
70 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.5
2 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Store.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 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/StoreTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
16 |
18 |
24 |
25 |
26 |
27 |
28 |
38 |
39 |
45 |
46 |
48 |
49 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os: osx
2 | language: swift
3 | osx_image: xcode11
4 |
5 | install:
6 | - gem install xcpretty
7 |
8 | script:
9 | - swift package generate-xcodeproj
10 | - xcodebuild clean build test -scheme Store -destination 'platform=iOS Simulator,name=iPhone X,OS=13.0' | xcpretty
11 | - xcodebuild clean build test -scheme Store -destination 'platform=macOS,arch=x86_64' | xcpretty
12 | - xcodebuild clean build test -scheme Store -destination 'platform=tvOS Simulator,name=Apple TV 4K,OS=13.0' | xcpretty
13 | - xcodebuild clean build -scheme Store -destination 'platform=watchOS Simulator,name=Apple Watch Series 5 - 44mm,OS=6.0' | xcpretty
14 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "swift-atomics",
6 | "repositoryURL": "https://github.com/apple/swift-atomics.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f",
10 | "version": "0.0.3"
11 | }
12 | },
13 | {
14 | "package": "swift-log",
15 | "repositoryURL": "https://github.com/apple/swift-log.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
19 | "version": "1.4.2"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
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: "Store",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | .tvOS(.v13),
12 | .watchOS(.v6)
13 | ],
14 | products: [
15 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
16 | .library(
17 | name: "Store",
18 | targets: ["Store"]),
19 | ],
20 | dependencies: [
21 | .package(url: "https://github.com/apple/swift-atomics.git", from: "0.0.3"),
22 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0")
23 | ],
24 | targets: [
25 | .target(
26 | name: "Store",
27 | dependencies: [
28 | .product(name: "Atomics", package: "swift-atomics"),
29 | .product(name: "Logging", package: "swift-log")
30 | ]),
31 | .testTarget(
32 | name: "StoreTests",
33 | dependencies: ["Store"]),
34 | ]
35 | )
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](#) [](https://travis-ci.org/alexdrone/Store) [](#) [](#)
2 |
3 |
4 | Unidirectional, transactional Store implementation for Swift and SwiftUI
5 |
6 | *Store is being reworked to take advantage of async/await. [See the stable pre-async version of Store here](https://github.com/alexdrone/Store/tree/legacy).*
7 |
--------------------------------------------------------------------------------
/Sources/Store/Concurrency/Atomics.swift:
--------------------------------------------------------------------------------
1 | import Atomics
2 | import Foundation
3 |
4 | extension ManagedAtomic: @unchecked Sendable {}
5 |
--------------------------------------------------------------------------------
/Sources/Store/Concurrency/Locking.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - Locking
4 |
5 | public protocol Locking {
6 | /// Create a new lock.
7 | init()
8 |
9 | /// Acquire the lock.
10 | ///
11 | /// Whenever possible, consider using `withLock` instead of this method and
12 | /// `unlock`, to simplify lock handling.
13 | func lock()
14 |
15 | /// Release the lock.
16 | ///
17 | /// Whenver possible, consider using `withLock` instead of this method and
18 | /// `lock`, to simplify lock handling.
19 | func unlock()
20 | }
21 |
22 | extension Locking {
23 | /// Acquire the lock for the duration of the given block.
24 | ///
25 | /// This convenience method should be preferred to `lock` and `unlock` in
26 | /// most situations, as it ensures that the lock will be released regardless
27 | /// of how `body` exits.
28 | @inlinable
29 | public func withLock(_ body: () throws -> T) rethrows -> T {
30 | defer {
31 | unlock()
32 | }
33 | lock()
34 | return try body()
35 | }
36 |
37 | /// Specialise Void return (for performance).
38 | @inlinable
39 | public func withLockVoid(_ body: () throws -> Void) rethrows {
40 | try self.withLock(body)
41 | }
42 |
43 | /// Async variant.
44 | @inlinable
45 | public func withLock(_ body: () async throws -> T) async rethrows -> T {
46 | defer {
47 | unlock()
48 | }
49 | lock()
50 | return try await body()
51 | }
52 | }
53 |
54 | // MARK: - Foundation Locks
55 |
56 | /// An object that coordinates the operation of multiple threads of execution within the
57 | /// same application.
58 | extension NSLock: Locking {}
59 |
60 | /// A lock that may be acquired multiple times by the same thread without causing a deadlock.
61 | extension NSRecursiveLock: Locking {}
62 |
63 | /// A lock that multiple applications on multiple hosts can use to restrict access to some
64 | /// shared resource, such as a file.
65 | extension NSConditionLock: Locking {}
66 |
67 | // MARK: - Mutex
68 |
69 | /// A mechanism that enforces limits on access to a resource when there are many threads
70 | /// of execution.
71 | public final class Mutex: Locking {
72 | private var mutex: pthread_mutex_t = {
73 | var mutex = pthread_mutex_t()
74 | pthread_mutex_init(&mutex, nil)
75 | return mutex
76 | }()
77 |
78 | public init() {}
79 |
80 | public func lock() {
81 | pthread_mutex_lock(&mutex)
82 | }
83 |
84 | public func unlock() {
85 | pthread_mutex_unlock(&mutex)
86 | }
87 | }
88 |
89 | // MARK: - UnfairLock
90 |
91 | /// A low-level lock that allows waiters to block efficiently on contention.
92 | public final class UnfairLock: Locking {
93 | private var unfairLock = os_unfair_lock_s()
94 |
95 | public init() {}
96 |
97 | public func lock() {
98 | os_unfair_lock_lock(&unfairLock)
99 | }
100 |
101 | public func unlock() {
102 | os_unfair_lock_unlock(&unfairLock)
103 | }
104 | }
105 |
106 | // MARK: - ReadersWriterLock
107 |
108 | /// A readers-writer lock provided by the platform implementation of the POSIX Threads standard.
109 | /// Read more: https://en.wikipedia.org/wiki/POSIX_Threads
110 | public final class ReadersWriterLock: @unchecked Sendable {
111 | private var rwlock: UnsafeMutablePointer
112 |
113 | public init() {
114 | rwlock = UnsafeMutablePointer.allocate(capacity: 1)
115 | assert(pthread_rwlock_init(rwlock, nil) == 0)
116 | }
117 |
118 | deinit {
119 | assert(pthread_rwlock_destroy(rwlock) == 0)
120 | rwlock.deinitialize(count: 1)
121 | rwlock.deallocate()
122 | }
123 |
124 | public func withReadLock(body: () throws -> T) rethrows -> T {
125 | pthread_rwlock_rdlock(rwlock)
126 | defer {
127 | pthread_rwlock_unlock(rwlock)
128 | }
129 | return try body()
130 | }
131 |
132 | public func withWriteLock(body: () throws -> T) rethrows -> T {
133 | pthread_rwlock_wrlock(rwlock)
134 | defer {
135 | pthread_rwlock_unlock(rwlock)
136 | }
137 | return try body()
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/Store/Concurrency/PropertyWrappers.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - Atomic
4 |
5 | @propertyWrapper
6 | public final class LockAtomic: @unchecked Sendable {
7 | private let lock: L
8 | private var value: T
9 |
10 | public init(wrappedValue: T, _ lock: L.Type) {
11 | self.lock = L.init()
12 | self.value = wrappedValue
13 | }
14 |
15 | public var wrappedValue: T {
16 | get {
17 | lock.withLock {
18 | value
19 | }
20 | }
21 | set {
22 | lock.withLock {
23 | value = newValue
24 | }
25 | }
26 | }
27 |
28 | /// Used for multi-statement atomic access to the wrapped property.
29 | /// This is especially useful to wrap index-subscripts in value type collections that
30 | /// otherwise would result in a call to get, a value copy and a subsequent call to set.
31 | public func mutate(_ block: (inout T) -> Void) {
32 | lock.withLock {
33 | block(&value)
34 | }
35 | }
36 |
37 | public var projectedValue: LockAtomic { self }
38 | }
39 |
40 | // MARK: - SyncDispatchQueueAtomic
41 |
42 | @propertyWrapper
43 | public final class SyncDispatchQueueAtomic: @unchecked Sendable {
44 | private let queue: DispatchQueue
45 | private var value: T
46 | private let concurrentReads: Bool
47 |
48 | public init(wrappedValue: T, concurrentReads: Bool = true) {
49 | self.value = wrappedValue
50 | self.concurrentReads = concurrentReads
51 | let label = "SyncDispatchQueueAtomic.\(UUID().uuidString)"
52 | self.queue = DispatchQueue(label: label, attributes: concurrentReads ? [.concurrent] : [])
53 | }
54 |
55 | public var wrappedValue: T {
56 | get { queue.sync { value } }
57 | set { queue.sync(flags: concurrentReads ? [.barrier] : []) { value = newValue } }
58 | }
59 |
60 | /// Used for multi-statement atomic access to the wrapped property.
61 | /// This is especially useful to wrap index-subscripts in value type collections that
62 | /// otherwise would result in a call to get, a value copy and a subsequent call to set.
63 | public func mutate(_ block: (inout T) -> Void) {
64 | queue.sync {
65 | block(&value)
66 | }
67 | }
68 |
69 | public var projectedValue: SyncDispatchQueueAtomic { self }
70 | }
71 |
72 | // MARK: - ReadersWriterAtomic
73 |
74 | @propertyWrapper
75 | public final class ReadersWriterAtomic: @unchecked Sendable {
76 | private let lock = ReadersWriterLock()
77 | private var value: T
78 |
79 | public init(wrappedValue: T) {
80 | self.value = wrappedValue
81 | }
82 |
83 | public var wrappedValue: T {
84 | get {
85 | lock.withReadLock {
86 | value
87 | }
88 | }
89 | set {
90 | lock.withWriteLock {
91 | self.value = newValue
92 | }
93 | }
94 | }
95 |
96 | /// Used for multi-statement atomic access to the wrapped property.
97 | /// This is especially useful to wrap index-subscripts in value type collections that
98 | /// otherwise would result in a call to get, a value copy and a subsequent call to set.
99 | public func mutate(_ block: (inout T) -> Void) {
100 | lock.withWriteLock {
101 | block(&value)
102 | }
103 | }
104 |
105 | public var projectedValue: ReadersWriterAtomic { self }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/Store/Extensions/Binding.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 | import Logging
4 |
5 | #if canImport(SwiftUI)
6 | import SwiftUI
7 | #else
8 |
9 | @propertyWrapper
10 | @dynamicMemberLookup
11 | public struct Binding {
12 | public var wrappedValue: Value {
13 | get { get() }
14 | nonmutating set { set(newValue, transaction) }
15 | }
16 | public var transaction = Transaction()
17 |
18 | private let get: () -> Value
19 | private let set: (Value, Transaction) -> Void
20 |
21 | public var projectedValue: Binding { self }
22 |
23 | public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) {
24 | self.get = get
25 | self.set = { v, _ in set(v) }
26 | }
27 |
28 | public init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void) {
29 | self.transaction = .init()
30 | self.get = get
31 | self.set = {
32 | set($0, $1)
33 | }
34 | }
35 |
36 | public static func constant(_ value: Value) -> Binding {
37 | .init(get: { value }, set: { _ in })
38 | }
39 |
40 | public subscript(
41 | dynamicMember keyPath: WritableKeyPath
42 | ) -> Binding {
43 | .init(
44 | get: { wrappedValue[keyPath: keyPath] },
45 | set: { wrappedValue[keyPath: keyPath] = $0 })
46 | }
47 |
48 | public func transaction(_ transaction: Transaction) -> Binding {
49 | fatalError()
50 | }
51 | }
52 |
53 | public struct Transaction {}
54 |
55 | #endif
56 |
57 | //MARK: - Extensions
58 |
59 | /// Optional Coalescing for `Binding`.
60 | public func ?? (lhs: Binding, rhs: T) -> Binding {
61 | Binding(
62 | get: { lhs.wrappedValue ?? rhs },
63 | set: { lhs.wrappedValue = $0 }
64 | )
65 | }
66 |
67 | extension Binding {
68 | /// When the `Binding`'s wrapped value changes, the given closure is executed.
69 | public func onUpdate(_ closure: @escaping () -> Void) -> Binding {
70 | Binding(
71 | get: { wrappedValue },
72 | set: {
73 | wrappedValue = $0
74 | closure()
75 | })
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/Store/Extensions/KeyPath+ReadableFormat.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension KeyPath {
4 |
5 | /// Returns the human readable name for the property pointed at.
6 | public var readableFormat: String? {
7 | guard let offset = MemoryLayout.offset(of: self) else {
8 | return nil
9 | }
10 | let typePtr = unsafeBitCast(Root.self, to: UnsafeMutableRawPointer.self)
11 | let metadata = typePtr.assumingMemoryBound(to: StructMetadata.self)
12 | let kind = metadata.pointee._kind
13 |
14 | // see https://github.com/apple/swift/blob/main/include/swift/ABI/MetadataKind.def
15 | guard kind == 1 || kind == 0x200 else {
16 | assertionFailure()
17 | return nil
18 | }
19 |
20 | let typeDescriptor = metadata.pointee.typeDescriptor
21 | let numberOfFields = Int(typeDescriptor.pointee.numberOfFields)
22 | let offsets = typeDescriptor.pointee.offsetToTheFieldOffsetVector.buffer(
23 | metadata: typePtr,
24 | count: numberOfFields)
25 |
26 | guard let fieldIndex = offsets.firstIndex(of: Int32(offset)) else {
27 | return nil
28 | }
29 | return typeDescriptor.pointee
30 | .fieldDescriptor.advanced().pointee
31 | .fields.pointer().advanced(by: fieldIndex).pointee
32 | .fieldName()
33 | }
34 | }
35 |
36 | // MARK: - Memory Layout
37 |
38 | private struct StructMetadata {
39 | var _kind: Int
40 | var typeDescriptor: UnsafeMutablePointer
41 | }
42 |
43 | private struct StructTypeDescriptor {
44 | var flags: Int32
45 | var parent: Int32
46 | var mangledName: RelativePointer
47 | var accessFunctionPtr: RelativePointer
48 | var fieldDescriptor: RelativePointer
49 | var numberOfFields: Int32
50 | var offsetToTheFieldOffsetVector: RelativeBufferPointer
51 | }
52 |
53 | private struct FieldDescriptor {
54 | var mangledTypeNameOffset: Int32
55 | var superClassOffset: Int32
56 | var _kind: UInt16
57 | var fieldRecordSize: Int16
58 | var numFields: Int32
59 | var fields: Buffer
60 |
61 | struct Record {
62 | var fieldRecordFlags: Int32
63 | var _mangledTypeName: RelativePointer
64 | var _fieldName: RelativePointer
65 |
66 | mutating func fieldName() -> String {
67 | String(cString: _fieldName.advanced())
68 | }
69 | }
70 | }
71 |
72 | // MARK: - Pointers
73 |
74 | private struct Buffer {
75 | var element: Element
76 |
77 | mutating func pointer() -> UnsafeMutablePointer {
78 | withUnsafePointer(to: &self) {
79 | UnsafeMutableRawPointer(mutating: UnsafeRawPointer($0)).assumingMemoryBound(to: Element.self)
80 | }
81 | }
82 | }
83 |
84 | private struct RelativePointer {
85 | var offset: Offset
86 |
87 | mutating func advanced() -> UnsafeMutablePointer {
88 | let offset = self.offset
89 | return withUnsafePointer(to: &self) { p in
90 | UnsafeMutableRawPointer(mutating: p)
91 | .advanced(by: numericCast(offset))
92 | .assumingMemoryBound(to: Pointee.self)
93 | }
94 | }
95 | }
96 |
97 | private struct RelativeBufferPointer {
98 | var strides: Offset
99 |
100 | func buffer(metadata: UnsafeRawPointer, count: Int) -> UnsafeBufferPointer {
101 | let offset = numericCast(strides) * MemoryLayout.size
102 | let ptr = metadata.advanced(by: offset).assumingMemoryBound(to: Pointee.self)
103 | return UnsafeBufferPointer(start: ptr, count: count)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/Store/PushID.swift:
--------------------------------------------------------------------------------
1 | // Forked from alexdrone/PushID
2 | // See LICENSE file.
3 |
4 | import Foundation
5 |
6 | /// Shorthand to PushID's 'make' function.
7 | public func makePushID() -> String {
8 | return PushID.default.make()
9 | }
10 |
11 | /// ID generator that creates 20-character string identifiers with the following properties:
12 | /// 1. They're based on timestamp so that they sort *after* any existing ids.
13 | /// 2. They contain 72-bits of random data after the timestamp so that IDs won't collide with
14 | /// other clients' IDs.
15 | /// 3. They sort *lexicographically* (so the timestamp is converted to characters that will
16 | /// sort properly).
17 | /// 4. They're monotonically increasing. Even if you generate more than one in the same timestamp,
18 | /// the latter ones will sort after the former ones. We do this by using the previous random bits
19 | /// but "incrementing" them by 1 (only in the case of a timestamp collision).
20 | public final class PushID {
21 |
22 | public static let `default` = PushID()
23 |
24 | // MARK: Static constants
25 |
26 | /// Modeled after base64 web-safe chars, but ordered by ASCII.
27 | private static let ascChars = Array(
28 | "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")
29 |
30 | private static let descChars = Array(ascChars.reversed())
31 |
32 | // MARK: State
33 |
34 | /// Timestamp of last push, used to prevent local collisions if you push twice in one ms.
35 | private var lastPushTime: UInt64 = 0
36 |
37 | /// We generate 72-bits of randomness which get turned into 12 characters and appended to the
38 | /// timestamp to prevent collisions with other clients. We store the last characters we
39 | /// generated because in the event of a collision, we'll use those same characters except
40 | /// "incremented" by one.
41 | private var lastRandChars = [Int](repeating: 0, count: 12)
42 |
43 | /// For testability purposes.
44 | private let dateProvider: () -> Date
45 |
46 | /// Ensure the generator synchronization.
47 | private var lock = UnfairLock()
48 |
49 | public init(dateProvider: @escaping () -> Date = { Date() }) {
50 | self.dateProvider = dateProvider
51 | }
52 |
53 | /// Generate a new push UUID.
54 | public func make(ascending: Bool = true) -> String {
55 | let pushChars = ascending ? PushID.ascChars : PushID.descChars
56 | precondition(pushChars.count > 0)
57 | var timeStampChars = [Character](repeating: pushChars.first!, count: 8)
58 |
59 | self.lock.lock()
60 |
61 | var now = UInt64(self.dateProvider().timeIntervalSince1970 * 1000)
62 | let duplicateTime = (now == self.lastPushTime)
63 |
64 | self.lastPushTime = now
65 |
66 | for i in stride(from: 7, to: 0, by: -1) {
67 | timeStampChars[i] = pushChars[Int(now % 64)]
68 | now >>= 6
69 | }
70 |
71 | assert(now == 0, "The whole timestamp should be now converted.")
72 | var id = String(timeStampChars)
73 |
74 | if !duplicateTime {
75 | for i in 0..<12 {
76 | self.lastRandChars[i] = Int(64 * Double(arc4random()) / Double(UInt32.max))
77 | }
78 | } else {
79 | // If the timestamp hasn't changed since last push, use the same random number,
80 | // except incremented by 1.
81 | var index: Int = 0
82 | for i in stride(from: 11, to: 0, by: -1) {
83 | index = i
84 | guard self.lastRandChars[i] == 63 else { break }
85 | self.lastRandChars[i] = 0
86 | }
87 | self.lastRandChars[index] += 1
88 | }
89 |
90 | // Appends the random characters.
91 | for i in 0..<12 {
92 | id.append(pushChars[self.lastRandChars[i]])
93 | }
94 | assert(id.lengthOfBytes(using: .utf8) == 20, "The id lenght should be 20.")
95 |
96 | self.lock.unlock()
97 | return id
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/Store/Serialization/DictionaryCodable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public typealias EncodedDictionary = [String: Any]
4 |
5 | // MARK: - DictionaryEncoder
6 |
7 | open class DictionaryEncoder: Encoder {
8 | open var codingPath: [CodingKey] = []
9 | open var userInfo: [CodingUserInfoKey: Any] = [:]
10 | private var storage = Storage()
11 |
12 | public init() {}
13 |
14 | open func container(keyedBy type: Key.Type) -> KeyedEncodingContainer {
15 | KeyedEncodingContainer(KeyedContainer(encoder: self, codingPath: codingPath))
16 | }
17 |
18 | open func unkeyedContainer() -> UnkeyedEncodingContainer {
19 | UnkeyedContanier(encoder: self, codingPath: codingPath)
20 | }
21 |
22 | open func singleValueContainer() -> SingleValueEncodingContainer {
23 | SingleValueContanier(encoder: self, codingPath: codingPath)
24 | }
25 |
26 | private func box(_ value: T) throws -> Any {
27 | /// @note: This results in a EXC_BAD_ACCESS on XCode 11.2 (works again in XCode 11.3).
28 | try value.encode(to: self)
29 | return storage.popContainer()
30 | }
31 | }
32 |
33 | extension DictionaryEncoder {
34 | open func encode(_ value: T) throws -> [String: Any] {
35 | do {
36 | return try castOrThrow([String: Any].self, try box(value))
37 | } catch (let error) {
38 | throw EncodingError.invalidValue(
39 | value,
40 | EncodingError.Context(
41 | codingPath: [],
42 | debugDescription: "Top-evel \(T.self) did not encode any values.",
43 | underlyingError: error))
44 | }
45 | }
46 | }
47 |
48 | extension DictionaryEncoder {
49 | private class KeyedContainer: KeyedEncodingContainerProtocol {
50 | private var encoder: DictionaryEncoder
51 | private(set) var codingPath: [CodingKey]
52 | private var storage: Storage
53 |
54 | init(encoder: DictionaryEncoder, codingPath: [CodingKey]) {
55 | self.encoder = encoder
56 | self.codingPath = codingPath
57 | self.storage = encoder.storage
58 | storage.push(container: [:] as [String: Any])
59 | }
60 |
61 | deinit {
62 | guard let dictionary = storage.popContainer() as? [String: Any] else {
63 | assertionFailure()
64 | return
65 | }
66 | storage.push(container: dictionary)
67 | }
68 |
69 | private func set(_ value: Any, forKey key: String) {
70 | guard var dictionary = storage.popContainer() as? [String: Any] else {
71 | assertionFailure()
72 | return
73 | }
74 | dictionary[key] = value
75 | storage.push(container: dictionary)
76 | }
77 |
78 | func encodeNil(forKey key: Key) throws {}
79 |
80 | func encode(
81 | _ value: Bool,
82 | forKey key: Key
83 | ) throws { set(value, forKey: key.stringValue) }
84 |
85 | func encode(
86 | _ value: Int,
87 | forKey key: Key
88 | ) throws { set(value, forKey: key.stringValue) }
89 |
90 | func encode(
91 | _ value: Int8,
92 | forKey key: Key
93 | ) throws { set(value, forKey: key.stringValue) }
94 |
95 | func encode(
96 | _ value: Int16,
97 | forKey key: Key
98 | ) throws { set(value, forKey: key.stringValue) }
99 |
100 | func encode(
101 | _ value: Int32,
102 | forKey key: Key
103 | ) throws { set(value, forKey: key.stringValue) }
104 |
105 | func encode(
106 | _ value: Int64,
107 | forKey key: Key
108 | ) throws { set(value, forKey: key.stringValue) }
109 |
110 | func encode(
111 | _ value: UInt,
112 | forKey key: Key
113 | ) throws { set(value, forKey: key.stringValue) }
114 |
115 | func encode(
116 | _ value: UInt8,
117 | forKey key: Key
118 | ) throws { set(value, forKey: key.stringValue) }
119 |
120 | func encode(
121 | _ value: UInt16,
122 | forKey key: Key
123 | ) throws { set(value, forKey: key.stringValue) }
124 |
125 | func encode(
126 | _ value: UInt32,
127 | forKey key: Key
128 | ) throws { set(value, forKey: key.stringValue) }
129 |
130 | func encode(
131 | _ value: UInt64,
132 | forKey key: Key
133 | ) throws { set(value, forKey: key.stringValue) }
134 |
135 | func encode(
136 | _ value: Float,
137 | forKey key: Key
138 | ) throws { set(value, forKey: key.stringValue) }
139 |
140 | func encode(
141 | _ value: Double,
142 | forKey key: Key
143 | ) throws { set(value, forKey: key.stringValue) }
144 |
145 | func encode(
146 | value: String,
147 | forKey key: Key
148 | ) throws { set(value, forKey: key.stringValue) }
149 |
150 | func encode(
151 | _ value: T,
152 | forKey key: Key
153 | ) throws {
154 | encoder.codingPath.append(key)
155 | defer { encoder.codingPath.removeLast() }
156 | set(try encoder.box(value), forKey: key.stringValue)
157 | }
158 |
159 | func nestedContainer(
160 | keyedBy keyType: NestedKey.Type,
161 | forKey key: Key
162 | ) -> KeyedEncodingContainer {
163 | codingPath.append(key)
164 | defer { codingPath.removeLast() }
165 | return KeyedEncodingContainer(
166 | KeyedContainer(
167 | encoder: encoder,
168 | codingPath: codingPath))
169 | }
170 |
171 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
172 | codingPath.append(key)
173 | defer { codingPath.removeLast() }
174 | return UnkeyedContanier(encoder: encoder, codingPath: codingPath)
175 | }
176 |
177 | func superEncoder() -> Encoder { encoder }
178 |
179 | func superEncoder(forKey key: Key) -> Encoder { encoder }
180 | }
181 |
182 | private class UnkeyedContanier: UnkeyedEncodingContainer {
183 | var encoder: DictionaryEncoder
184 | private(set) var codingPath: [CodingKey]
185 | private var storage: Storage
186 | var count: Int { return storage.count }
187 |
188 | init(encoder: DictionaryEncoder, codingPath: [CodingKey]) {
189 | self.encoder = encoder
190 | self.codingPath = codingPath
191 | self.storage = encoder.storage
192 | storage.push(container: [] as [Any])
193 | }
194 |
195 | deinit {
196 | guard let array = storage.popContainer() as? [Any] else {
197 | assertionFailure()
198 | return
199 | }
200 | storage.push(container: array)
201 | }
202 |
203 | private func push(_ value: Any) {
204 | guard var array = storage.popContainer() as? [Any] else {
205 | assertionFailure()
206 | return
207 | }
208 | array.append(value)
209 | storage.push(container: array)
210 | }
211 |
212 | func encodeNil() throws {}
213 |
214 | func encode(
215 | _ value: Bool
216 | ) throws {}
217 |
218 | func encode(
219 | _ value: Int
220 | ) throws { push(try encoder.box(value)) }
221 |
222 | func encode(
223 | _ value: Int8
224 | ) throws { push(try encoder.box(value)) }
225 |
226 | func encode(
227 | _ value: Int16
228 | ) throws { push(try encoder.box(value)) }
229 |
230 | func encode(
231 | _ value: Int32
232 | ) throws { push(try encoder.box(value)) }
233 |
234 | func encode(
235 | _ value: Int64
236 | ) throws { push(try encoder.box(value)) }
237 |
238 | func encode(
239 | _ value: UInt
240 | ) throws { push(try encoder.box(value)) }
241 |
242 | func encode(
243 | _ value: UInt8
244 | ) throws { push(try encoder.box(value)) }
245 |
246 | func encode(
247 | _ value: UInt16
248 | ) throws { push(try encoder.box(value)) }
249 |
250 | func encode(
251 | _ value: UInt32
252 | ) throws { push(try encoder.box(value)) }
253 |
254 | func encode(
255 | _ value: UInt64
256 | ) throws { push(try encoder.box(value)) }
257 |
258 | func encode(
259 | _ value: Float
260 | ) throws { push(try encoder.box(value)) }
261 |
262 | func encode(
263 | _ value: Double
264 | ) throws { push(try encoder.box(value)) }
265 |
266 | func encode(
267 | _ value: String
268 | ) throws { push(try encoder.box(value)) }
269 |
270 | func encode(_ value: T) throws {
271 | encoder.codingPath.append(AnyCodingKey(index: count))
272 | defer { encoder.codingPath.removeLast() }
273 | push(try encoder.box(value))
274 | }
275 |
276 | func nestedContainer(
277 | keyedBy keyType: NestedKey.Type
278 | ) -> KeyedEncodingContainer where NestedKey: CodingKey {
279 | codingPath.append(AnyCodingKey(index: count))
280 | defer { codingPath.removeLast() }
281 | return KeyedEncodingContainer(
282 | KeyedContainer(
283 | encoder: encoder,
284 | codingPath: codingPath))
285 | }
286 |
287 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
288 | codingPath.append(AnyCodingKey(index: count))
289 | defer { codingPath.removeLast() }
290 | return UnkeyedContanier(encoder: encoder, codingPath: codingPath)
291 | }
292 |
293 | func superEncoder() -> Encoder {
294 | return encoder
295 | }
296 | }
297 |
298 | private class SingleValueContanier: SingleValueEncodingContainer {
299 | var encoder: DictionaryEncoder
300 | private(set) var codingPath: [CodingKey]
301 | private var storage: Storage
302 | var count: Int { return storage.count }
303 |
304 | init(encoder: DictionaryEncoder, codingPath: [CodingKey]) {
305 | self.encoder = encoder
306 | self.codingPath = codingPath
307 | self.storage = encoder.storage
308 | }
309 |
310 | private func push(_ value: Any) {
311 | guard var array = storage.popContainer() as? [Any] else {
312 | assertionFailure()
313 | return
314 | }
315 | array.append(value)
316 | storage.push(container: array)
317 | }
318 |
319 | func encodeNil() throws {}
320 |
321 | func encode(
322 | _ value: Bool
323 | ) throws { storage.push(container: value) }
324 |
325 | func encode(
326 | _ value: Int
327 | ) throws { storage.push(container: value) }
328 |
329 | func encode(
330 | _ value: Int8
331 | ) throws { storage.push(container: value) }
332 |
333 | func encode(
334 | _ value: Int16
335 | ) throws { storage.push(container: value) }
336 |
337 | func encode(
338 | _ value: Int32
339 | ) throws { storage.push(container: value) }
340 |
341 | func encode(
342 | _ value: Int64
343 | ) throws { storage.push(container: value) }
344 |
345 | func encode(
346 | _ value: UInt
347 | ) throws { storage.push(container: value) }
348 |
349 | func encode(
350 | _ value: UInt8
351 | ) throws { storage.push(container: value) }
352 |
353 | func encode(
354 | _ value: UInt16
355 | ) throws { storage.push(container: value) }
356 |
357 | func encode(
358 | _ value: UInt32
359 | ) throws { storage.push(container: value) }
360 |
361 | func encode(
362 | _ value: UInt64
363 | ) throws { storage.push(container: value) }
364 |
365 | func encode(
366 | _ value: Float
367 | ) throws { storage.push(container: value) }
368 |
369 | func encode(
370 | _ value: Double
371 | ) throws { storage.push(container: value) }
372 |
373 | func encode(
374 | _ value: String
375 | ) throws { storage.push(container: value) }
376 |
377 | func encode(_ value: T) throws {
378 | storage.push(container: try encoder.box(value))
379 | }
380 | }
381 | }
382 |
383 | // MARK: - DictionaryDecoder
384 |
385 | open class DictionaryDecoder: Decoder {
386 | open var codingPath: [CodingKey]
387 | open var userInfo: [CodingUserInfoKey: Any] = [:]
388 | private var storage = Storage()
389 |
390 | public init() {
391 | codingPath = []
392 | }
393 |
394 | public init(container: Any, codingPath: [CodingKey] = []) {
395 | storage.push(container: container)
396 | self.codingPath = codingPath
397 | }
398 |
399 | open func container(
400 | keyedBy type: Key.Type
401 | ) throws -> KeyedDecodingContainer {
402 | let container = try lastContainer(forType: [String: Any].self)
403 | return KeyedDecodingContainer(
404 | KeyedContainer(
405 | decoder: self,
406 | codingPath: [],
407 | container: container))
408 | }
409 |
410 | open func unkeyedContainer() throws -> UnkeyedDecodingContainer {
411 | let container = try lastContainer(forType: [Any].self)
412 | return UnkeyedContanier(decoder: self, container: container)
413 | }
414 |
415 | open func singleValueContainer() throws -> SingleValueDecodingContainer {
416 | return SingleValueContanier(decoder: self)
417 | }
418 |
419 | private func unbox(_ value: Any, as type: T.Type) throws -> T {
420 | return try unbox(value, as: type, codingPath: codingPath)
421 | }
422 |
423 | private func unbox(
424 | _ value: Any,
425 | as type: T.Type,
426 | codingPath: [CodingKey]
427 | ) throws -> T {
428 | let description = "Expected to decode \(type) but found \(Swift.type(of: value)) instead."
429 | let error = DecodingError.typeMismatch(
430 | T.self,
431 | DecodingError.Context(codingPath: codingPath, debugDescription: description))
432 | return try castOrThrow(T.self, value, error: error)
433 | }
434 |
435 | private func unbox(_ value: Any, as type: T.Type) throws -> T {
436 | return try unbox(value, as: type, codingPath: codingPath)
437 | }
438 |
439 | private func unbox(
440 | _ value: Any,
441 | as type: T.Type,
442 | codingPath: [CodingKey]
443 | ) throws -> T {
444 | let description = "Expected to decode \(type) but found \(Swift.type(of: value)) instead."
445 | let error = DecodingError.typeMismatch(
446 | T.self,
447 | DecodingError.Context(codingPath: codingPath, debugDescription: description))
448 | do {
449 | return try castOrThrow(T.self, value, error: error)
450 | } catch {
451 | storage.push(container: value)
452 | defer { _ = storage.popContainer() }
453 | return try T(from: self)
454 | }
455 | }
456 |
457 | private func lastContainer(forType type: T.Type) throws -> T {
458 | guard let value = storage.last else {
459 | let description = "Expected \(type) but found nil value instead."
460 | let error = DecodingError.Context(codingPath: codingPath, debugDescription: description)
461 | throw DecodingError.valueNotFound(type, error)
462 | }
463 | return try unbox(value, as: T.self)
464 | }
465 |
466 | private func lastContainer(forType type: T.Type) throws -> T {
467 | guard let value = storage.last else {
468 | let description = "Expected \(type) but found nil value instead."
469 | let error = DecodingError.Context(codingPath: codingPath, debugDescription: description)
470 | throw DecodingError.valueNotFound(type, error)
471 | }
472 | return try unbox(value, as: T.self)
473 | }
474 |
475 | private func notFound(key: CodingKey) -> DecodingError {
476 | let error = DecodingError.Context(
477 | codingPath: codingPath,
478 | debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")
479 | return DecodingError.keyNotFound(key, error)
480 | }
481 | }
482 |
483 | extension DictionaryDecoder {
484 | open func decode(_ type: T.Type, from container: Any) throws -> T {
485 | storage.push(container: container)
486 | return try unbox(container, as: T.self)
487 | }
488 | }
489 |
490 | extension DictionaryDecoder {
491 | private class KeyedContainer: KeyedDecodingContainerProtocol {
492 | private var decoder: DictionaryDecoder
493 | private(set) var codingPath: [CodingKey]
494 | private var container: [String: Any]
495 |
496 | init(decoder: DictionaryDecoder, codingPath: [CodingKey], container: [String: Any]) {
497 | self.decoder = decoder
498 | self.codingPath = codingPath
499 | self.container = container
500 | }
501 |
502 | var allKeys: [Key] { return container.keys.compactMap { Key(stringValue: $0) } }
503 | func contains(_ key: Key) -> Bool { return container[key.stringValue] != nil }
504 |
505 | private func find(forKey key: CodingKey) throws -> Any {
506 | return try container.tryValue(forKey: key.stringValue, error: decoder.notFound(key: key))
507 | }
508 |
509 | func _decode(_ type: T.Type, forKey key: Key) throws -> T {
510 | let value = try find(forKey: key)
511 | decoder.codingPath.append(key)
512 | defer { decoder.codingPath.removeLast() }
513 | return try decoder.unbox(value, as: T.self)
514 | }
515 |
516 | func decodeNil(
517 | forKey key: Key
518 | ) throws -> Bool {
519 | // TODO: Verify, this broke in Xcode13b2.
520 | // decoder.notFound(key: key)
521 | return true
522 | }
523 |
524 | func decode(
525 | _ type: Bool.Type,
526 | forKey key: Key
527 | ) throws -> Bool { try _decode(type, forKey: key) }
528 |
529 | func decode(
530 | _ type: Int.Type,
531 | forKey key: Key
532 | ) throws -> Int { try _decode(type, forKey: key) }
533 |
534 | func decode(
535 | _ type: Int8.Type,
536 | forKey key: Key
537 | ) throws -> Int8 { try _decode(type, forKey: key) }
538 |
539 | func decode(
540 | _ type: Int16.Type,
541 | forKey key: Key
542 | ) throws -> Int16 { try _decode(type, forKey: key) }
543 |
544 | func decode(
545 | _ type: Int32.Type,
546 | forKey key: Key
547 | ) throws -> Int32 { try _decode(type, forKey: key) }
548 |
549 | func decode(
550 | _ type: Int64.Type,
551 | forKey key: Key
552 | ) throws -> Int64 { try _decode(type, forKey: key) }
553 |
554 | func decode(
555 | _ type: UInt.Type,
556 | forKey key: Key
557 | ) throws -> UInt { try _decode(type, forKey: key) }
558 |
559 | func decode(
560 | _ type: UInt8.Type,
561 | forKey key: Key
562 | ) throws -> UInt8 { try _decode(type, forKey: key) }
563 |
564 | func decode(
565 | _ type: UInt16.Type,
566 | forKey key: Key
567 | ) throws -> UInt16 { try _decode(type, forKey: key) }
568 |
569 | func decode(
570 | _ type: UInt32.Type,
571 | forKey key: Key
572 | ) throws -> UInt32 { try _decode(type, forKey: key) }
573 |
574 | func decode(
575 | _ type: UInt64.Type,
576 | forKey key: Key
577 | ) throws -> UInt64 { try _decode(type, forKey: key) }
578 |
579 | func decode(
580 | _ type: Float.Type,
581 | forKey key: Key
582 | ) throws -> Float { try _decode(type, forKey: key) }
583 |
584 | func decode(
585 | _ type: Double.Type,
586 | forKey key: Key
587 | ) throws -> Double { try _decode(type, forKey: key) }
588 |
589 | func decode(
590 | _ type: String.Type,
591 | forKey key: Key
592 | ) throws -> String { try _decode(type, forKey: key) }
593 |
594 | func decode(
595 | _ type: T.Type,
596 | forKey key: Key
597 | ) throws -> T { try _decode(type, forKey: key) }
598 |
599 | func nestedContainer(
600 | keyedBy type: NestedKey.Type,
601 | forKey key: Key
602 | ) throws -> KeyedDecodingContainer where NestedKey: CodingKey {
603 | decoder.codingPath.append(key)
604 | defer { decoder.codingPath.removeLast() }
605 | let value = try find(forKey: key)
606 | let dictionary = try decoder.unbox(value, as: [String: Any].self)
607 | return KeyedDecodingContainer(
608 | KeyedContainer(
609 | decoder: decoder,
610 | codingPath: [],
611 | container: dictionary))
612 | }
613 |
614 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
615 | decoder.codingPath.append(key)
616 | defer { decoder.codingPath.removeLast() }
617 | let value = try find(forKey: key)
618 | let array = try decoder.unbox(value, as: [Any].self)
619 | return UnkeyedContanier(decoder: decoder, container: array)
620 | }
621 |
622 | func _superDecoder(forKey key: CodingKey = AnyCodingKey.super) throws -> Decoder {
623 | decoder.codingPath.append(key)
624 | defer { decoder.codingPath.removeLast() }
625 |
626 | let value = try find(forKey: key)
627 | return DictionaryDecoder(container: value, codingPath: decoder.codingPath)
628 | }
629 |
630 | func superDecoder() throws -> Decoder {
631 | return try _superDecoder()
632 | }
633 |
634 | func superDecoder(forKey key: Key) throws -> Decoder {
635 | return try _superDecoder(forKey: key)
636 | }
637 | }
638 |
639 | private class UnkeyedContanier: UnkeyedDecodingContainer {
640 | private var decoder: DictionaryDecoder
641 | private(set) var codingPath: [CodingKey]
642 | private var container: [Any]
643 |
644 | var count: Int? { return container.count }
645 | var isAtEnd: Bool { return currentIndex >= count! }
646 |
647 | private(set) var currentIndex: Int
648 |
649 | private var currentCodingPath: [CodingKey] {
650 | return decoder.codingPath + [AnyCodingKey(index: currentIndex)]
651 | }
652 |
653 | init(decoder: DictionaryDecoder, container: [Any]) {
654 | self.decoder = decoder
655 | self.codingPath = decoder.codingPath
656 | self.container = container
657 | currentIndex = 0
658 | }
659 |
660 | private func checkIndex(_ type: T.Type) throws {
661 | if isAtEnd {
662 | let error = DecodingError.Context(
663 | codingPath: currentCodingPath,
664 | debugDescription: "container is at end.")
665 | throw DecodingError.valueNotFound(T.self, error)
666 | }
667 | }
668 |
669 | func _decode(_ type: T.Type) throws -> T {
670 | try checkIndex(type)
671 |
672 | decoder.codingPath.append(AnyCodingKey(index: currentIndex))
673 | defer {
674 | decoder.codingPath.removeLast()
675 | currentIndex += 1
676 | }
677 | return try decoder.unbox(container[currentIndex], as: T.self)
678 | }
679 |
680 | func decodeNil() throws -> Bool {
681 | try checkIndex(Any?.self)
682 | return false
683 | }
684 |
685 | func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) }
686 | func decode(_ type: Int.Type) throws -> Int { return try _decode(type) }
687 | func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) }
688 | func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) }
689 | func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) }
690 | func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) }
691 | func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) }
692 | func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) }
693 | func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) }
694 | func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) }
695 | func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) }
696 | func decode(_ type: Float.Type) throws -> Float { return try _decode(type) }
697 | func decode(_ type: Double.Type) throws -> Double { return try _decode(type) }
698 | func decode(_ type: String.Type) throws -> String { return try _decode(type) }
699 | func decode(_ type: T.Type) throws -> T { return try _decode(type) }
700 |
701 | func nestedContainer(
702 | keyedBy type: NestedKey.Type
703 | ) throws -> KeyedDecodingContainer {
704 | decoder.codingPath.append(AnyCodingKey(index: currentIndex))
705 | defer { decoder.codingPath.removeLast() }
706 | try checkIndex(UnkeyedContanier.self)
707 | let value = container[currentIndex]
708 | let dictionary = try castOrThrow([String: Any].self, value)
709 | currentIndex += 1
710 | return KeyedDecodingContainer(
711 | KeyedContainer(
712 | decoder: decoder,
713 | codingPath: [],
714 | container: dictionary))
715 | }
716 |
717 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
718 | decoder.codingPath.append(AnyCodingKey(index: currentIndex))
719 | defer { decoder.codingPath.removeLast() }
720 | try checkIndex(UnkeyedContanier.self)
721 | let value = container[currentIndex]
722 | let array = try castOrThrow([Any].self, value)
723 | currentIndex += 1
724 | return UnkeyedContanier(decoder: decoder, container: array)
725 | }
726 |
727 | func superDecoder() throws -> Decoder {
728 | decoder.codingPath.append(AnyCodingKey(index: currentIndex))
729 | defer { decoder.codingPath.removeLast() }
730 | try checkIndex(UnkeyedContanier.self)
731 | let value = container[currentIndex]
732 | currentIndex += 1
733 | return DictionaryDecoder(container: value, codingPath: decoder.codingPath)
734 | }
735 | }
736 |
737 | private class SingleValueContanier: SingleValueDecodingContainer {
738 | private var decoder: DictionaryDecoder
739 | private(set) var codingPath: [CodingKey]
740 |
741 | init(decoder: DictionaryDecoder) {
742 | self.decoder = decoder
743 | self.codingPath = decoder.codingPath
744 | }
745 |
746 | func _decode(_ type: T.Type) throws -> T {
747 | return try decoder.lastContainer(forType: type)
748 | }
749 |
750 | func decodeNil() -> Bool { return decoder.storage.last == nil }
751 | func decode(_ type: Bool.Type) throws -> Bool { return try _decode(type) }
752 | func decode(_ type: Int.Type) throws -> Int { return try _decode(type) }
753 | func decode(_ type: Int8.Type) throws -> Int8 { return try _decode(type) }
754 | func decode(_ type: Int16.Type) throws -> Int16 { return try _decode(type) }
755 | func decode(_ type: Int32.Type) throws -> Int32 { return try _decode(type) }
756 | func decode(_ type: Int64.Type) throws -> Int64 { return try _decode(type) }
757 | func decode(_ type: UInt.Type) throws -> UInt { return try _decode(type) }
758 | func decode(_ type: UInt8.Type) throws -> UInt8 { return try _decode(type) }
759 | func decode(_ type: UInt16.Type) throws -> UInt16 { return try _decode(type) }
760 | func decode(_ type: UInt32.Type) throws -> UInt32 { return try _decode(type) }
761 | func decode(_ type: UInt64.Type) throws -> UInt64 { return try _decode(type) }
762 | func decode(_ type: Float.Type) throws -> Float { return try _decode(type) }
763 | func decode(_ type: Double.Type) throws -> Double { return try _decode(type) }
764 | func decode(_ type: String.Type) throws -> String { return try _decode(type) }
765 | func decode(_ type: T.Type) throws -> T { return try _decode(type) }
766 | }
767 | }
768 |
769 | // MARK: - Helpers
770 |
771 | final class Storage {
772 | private(set) var containers: [Any] = []
773 |
774 | var count: Int {
775 | containers.count
776 | }
777 |
778 | var last: Any? {
779 | containers.last
780 | }
781 |
782 | func push(container: Any) {
783 | containers.append(container)
784 | }
785 |
786 | @discardableResult func popContainer() -> Any {
787 | precondition(containers.count > 0, "Empty container stack.")
788 | return containers.popLast()!
789 | }
790 | }
791 |
792 | public enum DictionaryCodableError: Error {
793 | case cast
794 | case unwrapped
795 | case tryValue
796 | }
797 |
798 | func castOrThrow(
799 | _ resultType: T.Type,
800 | _ object: Any,
801 | error: Error = DictionaryCodableError.cast
802 | ) throws -> T {
803 | guard let returnValue = object as? T else {
804 | throw error
805 | }
806 | return returnValue
807 | }
808 |
809 | extension Optional {
810 | func unwrapOrThrow(error: Error = DictionaryCodableError.unwrapped) throws -> Wrapped {
811 | guard let unwrapped = self else {
812 | throw error
813 | }
814 | return unwrapped
815 | }
816 | }
817 |
818 | extension Dictionary {
819 | func tryValue(forKey key: Key, error: Error = DictionaryCodableError.tryValue) throws -> Value {
820 | guard let value = self[key] else { throw error }
821 | return value
822 | }
823 | }
824 |
825 | struct AnyCodingKey: CodingKey {
826 | public var stringValue: String
827 | public var intValue: Int?
828 |
829 | public init?(stringValue: String) {
830 | self.stringValue = stringValue
831 | self.intValue = nil
832 | }
833 |
834 | public init?(intValue: Int) {
835 | self.stringValue = "\(intValue)"
836 | self.intValue = intValue
837 | }
838 |
839 | init(index: Int) {
840 | self.stringValue = "Index \(index)"
841 | self.intValue = index
842 | }
843 |
844 | static let `super` = AnyCodingKey(stringValue: "super")!
845 | }
846 |
847 | func dynamicEqual(lhs: Any?, rhs: Any?) -> Bool {
848 | if let lhs = lhs as? NSNumber, let rhs = rhs as? NSNumber {
849 | return lhs == rhs
850 | }
851 | if let lhs = lhs as? String, let rhs = rhs as? String {
852 | return lhs == rhs
853 | }
854 | if let lhs = lhs as? NSArray, let rhs = rhs as? NSArray {
855 | return lhs == rhs
856 | }
857 | if let lhs = lhs as? NSDate, let rhs = rhs as? NSDate {
858 | return lhs == rhs
859 | }
860 | return false
861 | }
862 |
863 | func dynamicEncode(value: Any, encoder: Encoder) throws {
864 | // TODO
865 | }
866 |
867 | private func isEqual(type: T.Type, a: Any, b: Any) -> Bool {
868 | guard let a = a as? T, let b = b as? T else { return false }
869 | return a == b
870 | }
871 |
--------------------------------------------------------------------------------
/Sources/Store/Serialization/Diffing.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Logging
3 |
4 | public let sharedLogger = Logger(label: "io.store")
5 |
6 | @dynamicMemberLookup
7 | public final class Query {
8 | private var segments: [String] = []
9 | private let signpostDiff: SignpostDiff
10 |
11 | init(signpostDiff: SignpostDiff) {
12 | self.signpostDiff = signpostDiff
13 | }
14 |
15 | public subscript(dynamicMember member: String) -> Query {
16 | segments.append(member)
17 | return self
18 | }
19 |
20 | /// Returns a property/property format for a given keypath format.
21 | public func toKeyPath() -> FlatEncoding.KeyPath {
22 | FlatEncoding.KeyPath(segments: segments)
23 | }
24 |
25 | /// Returns `true` if the key path was added in this signpost store update, `false` otherwise.
26 | public func toPropertyDiff() -> PropertyDiff {
27 | signpostDiff.diffs[self.toKeyPath()] ?? .unspecified
28 | }
29 | }
30 |
31 | /// A collection of changes associated to a signpost store update.
32 | @frozen public struct SignpostDiff {
33 | /// The set of (`path`, `value`) that has been **added**/**removed**/**changed**.
34 | ///
35 | /// e.g. ``` {
36 | /// user/name: ,
37 | /// user/lastname: ,
38 | /// tokens/1: ,
39 | /// } ```
40 | public var allDiffs: [String: PropertyDiff] {
41 | var dictionary: [String: PropertyDiff] = [:]
42 | for (key, value) in diffs {
43 | dictionary[key.path] = value
44 | }
45 | return dictionary
46 | }
47 | /// Internal storage.
48 | let diffs: [FlatEncoding.KeyPath: PropertyDiff]
49 |
50 | /// Reference to the signpost update that cause this change.
51 | public let signpost: String
52 |
53 | /// Returns the `diffs` map encoded as **JSON** data.
54 | public var json: Data {
55 | (try? sharedJSONEncoder.encode(diffs)) ?? Data()
56 | }
57 |
58 | /// Queries the diff collection for a keypath change.
59 | public func query(_ keyPath: (Query) -> Query) -> PropertyDiff {
60 | keyPath(Query(signpostDiff: self)).toPropertyDiff()
61 | }
62 |
63 | public init(signpost: String, diffs: [FlatEncoding.KeyPath: PropertyDiff]) {
64 | self.signpost = signpost
65 | self.diffs = diffs
66 | }
67 | }
68 |
69 | /// Represent a property change.
70 | /// A change can be an **addition**, a **removal** or a **value change**.
71 | public enum PropertyDiff {
72 | case added(new: Codable?)
73 | case changed(old: Codable?, new: Codable?)
74 | case removed
75 | case unspecified
76 |
77 | /// Returns `true` if the key path was added in this signpost update, `false` otherwise.
78 | public func isAdded() -> Bool {
79 | switch self {
80 | case .added: return true
81 | default: return false
82 | }
83 | }
84 |
85 | /// Returns `true` if the key path was removed in this signpost update, `false` otherwise.
86 | public func isRemoved() -> Bool {
87 | switch self {
88 | case .removed: return true
89 | default: return false
90 | }
91 | }
92 |
93 | /// Returns `true` if the key path was changed in this signpost update, `false` otherwise.
94 | public func isChanged() -> Bool {
95 | switch self {
96 | case .changed: return true
97 | default: return false
98 | }
99 | }
100 |
101 | /// Returns the new value if the property was just added in this signpost update, `nil` otherwise.
102 | public func asNewAddedValue() -> T? {
103 | switch self {
104 | case .added(let value): return value as? T
105 | default: return nil
106 | }
107 | }
108 |
109 | /// Returns the tuple (`old`, `new`) if the value of the property changed in this signpost update.
110 | public func asNewAddedValue() -> (T, T)? {
111 | switch self {
112 | case .changed(let old, let new):
113 | guard let newValue = new as? T, let oldValue = old as? T else { return nil }
114 | return (oldValue, newValue)
115 | default: return nil
116 | }
117 | }
118 | }
119 |
120 | /// Flatten down the dictionary into a path/value map.
121 | /// e.g. `{user: {name: "John", lastname: "Appleseed"}, tokens: ["foo", "bar"]`
122 | /// turns into ``` {
123 | /// user/name: "John",
124 | /// user/lastname: "Appleseed",
125 | /// tokens/0: "foo",
126 | /// tokens/1: "bar"
127 | /// } ```
128 | public func flatten(encodedObject: EncodedDictionary) -> FlatEncoding.Dictionary {
129 | var result: FlatEncoding.Dictionary = [:]
130 | FlatEncoding.flatten(path: "", node: .dictionary(encodedObject), result: &result)
131 | return result
132 | }
133 |
134 | public enum FlatEncoding {
135 | /// A flat non-nested dictionary.
136 | /// This representation is very efficient for object diffing.
137 | /// - note: Arrays are represented by indices in the path.
138 | /// e.g. ``` {
139 | /// user/name: "John",
140 | /// user/lastname: "Appleseed",
141 | /// tokens/0: "foo",
142 | /// tokens/1: "bar"
143 | /// } ```
144 | public typealias Dictionary = [KeyPath: Codable?]
145 |
146 | /// Represent a path in a `FlatEncoding` dictionary.
147 | public struct KeyPath: Encodable, Equatable, Hashable {
148 | /// KeyPath components separator.
149 | /// e.g. `user/address/street` or `tokens/0`.
150 | static let separator = "/"
151 |
152 | /// All of the individual components of this KeyPath.
153 | public var segments: [String]
154 |
155 | /// Whether this is an empty KeyPath or not.
156 | public var isEmpty: Bool {
157 | segments.isEmpty
158 | }
159 |
160 | /// The raw string for this KeyPath.
161 | public let path: String
162 |
163 | /// Strips off the first segment and returns a pair consisting of the first segment and the
164 | /// remaining key path.
165 | /// Returns `nil` if the key path has no segments.
166 | public func pop() -> (head: String, tail: KeyPath)? {
167 | guard !isEmpty else { return nil }
168 | var tail = segments
169 | let head = tail.removeFirst()
170 | return (head, KeyPath(segments: tail))
171 | }
172 |
173 | /// Construct a new KeyPath from a array of components.
174 | public init(segments: [String]) {
175 | self.segments = segments
176 | self.path = segments.joined(separator: KeyPath.separator)
177 | }
178 |
179 | /// Constructs a new FlatEncoding KeyPath from a given string.
180 | /// - note: Returns `nil` if the string is in not in the format `PATH(/PATH)*` where `PATH` is
181 | /// a valid descriptor, like for example: `foo`, `foo/bar`, `foo/1/bar`.
182 | public init?(_ string: String) {
183 | guard
184 | string.range(
185 | of: "(([a-zA-Z0-9])+(\\/?))*",
186 | options: [.regularExpression, .anchored]) != nil
187 | else {
188 | return nil
189 | }
190 | path = string
191 | segments = string.components(separatedBy: KeyPath.separator)
192 | }
193 |
194 | /// Encodes this value into the given encoder.
195 | public func encode(to encoder: Encoder) throws {
196 | return try path.encode(to: encoder)
197 | }
198 |
199 | /// The raw string for this KeyPath.
200 | public var description: String {
201 | path
202 | }
203 | }
204 |
205 | // MARK: - Private
206 |
207 | /// Intermediate dictionary representation for `EncodedDictionary` ⇒ `FlatEncoding.Dictionary`
208 | fileprivate enum Node {
209 | case dictionary(_ dictionary: EncodedDictionary)
210 | case array(_ array: [Any])
211 | }
212 |
213 | /// Private recursive flatten method.
214 | /// - note: See `flatten(encodedObject:)`.
215 | fileprivate static func flatten(path: String, node: Node, result: inout Dictionary) {
216 | let formattedPath = path.isEmpty ? "" : "\(path)\(KeyPath.separator)"
217 | func process(path: String, value: Any) {
218 | if let dictionary = value as? [String: Any] {
219 | flatten(path: path, node: .dictionary(dictionary), result: &result)
220 | } else if let array = value as? [Any] {
221 | flatten(path: path, node: .array(array), result: &result)
222 | } else {
223 | guard let keyPath = KeyPath(path) else {
224 | sharedLogger.error("Malformed FlatEncoding keypath: \(path).")
225 | return
226 | }
227 | result[keyPath] = value as? Codable
228 | }
229 | }
230 | switch node {
231 | case .dictionary(let dictionary):
232 | for (key, value) in dictionary {
233 | process(path: "\(formattedPath)\(key)", value: value)
234 | }
235 | case .array(let array):
236 | for (index, value) in array.enumerated() {
237 | process(path: "\(formattedPath)\(index)", value: value)
238 | }
239 | }
240 | }
241 | }
242 |
243 | /// Shared `JSON` Encoder.
244 | fileprivate let sharedJSONEncoder = JSONEncoder()
245 |
246 | // MARK: - PropertyDiff
247 |
248 | extension PropertyDiff: CustomStringConvertible, Encodable {
249 |
250 | public var description: String {
251 | switch self {
252 | case .added(let new):
253 | return ""
254 | case .changed(let old, let new):
255 | return ""
256 | case .removed:
257 | return ""
258 | case .unspecified:
259 | return ""
260 | }
261 | }
262 |
263 | public var value: Any? {
264 | switch self {
265 | case .added(let new):
266 | return new
267 | case .changed(_, let new):
268 | return new
269 | case .removed:
270 | return nil
271 | case .unspecified:
272 | return nil
273 | }
274 | }
275 |
276 | /// Encodes this value into the given encoder.
277 | public func encode(to encoder: Encoder) throws {
278 | switch self {
279 | case .added(let new):
280 | guard let value = new else { return }
281 | try value.encode(to: encoder)
282 | case .changed(_, let new):
283 | guard let value = new else { return }
284 | try value.encode(to: encoder)
285 | case .removed:
286 | var container = encoder.singleValueContainer()
287 | try container.encodeNil()
288 | case .unspecified:
289 | return
290 | }
291 | }
292 | }
293 |
294 | // MARK: - Extensions
295 |
296 | extension Dictionary where Key == FlatEncoding.KeyPath, Value == PropertyDiff {
297 |
298 | /// Debug description for a change set.
299 | func storeDebugDescription(short: Bool) -> String {
300 | let keys = self.keys.map { $0.path }.sorted()
301 | let threshold = 8
302 | let noChangesLeft = keys.count - threshold
303 | var countChanges = 0
304 | var formats: [String] = []
305 | for key in keys {
306 | if short && countChanges == threshold { break }
307 | formats.append("\n\t\t· \(key): \(self[FlatEncoding.KeyPath(key)!]!)")
308 | countChanges += 1
309 | }
310 | if short && noChangesLeft > 0 {
311 | formats.append("\n\t\t<<< \(noChangesLeft) more change(s) >>>")
312 | }
313 | return "{\(formats.joined(separator: ", "))\n\t}"
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/Sources/Store/Store/Store+Forwarding.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 | import Logging
4 |
5 | #if canImport(SwiftUI)
6 | import SwiftUI
7 | #endif
8 |
9 | // MARK: - Extensions
10 |
11 | extension Store: Equatable where T: Equatable {
12 | public static func == (lhs: Store, rhs: Store) -> Bool {
13 | lhs.object.wrappedValue == rhs.object.wrappedValue
14 | }
15 | }
16 |
17 | extension Store: Hashable where T: Hashable {
18 | /// Hashes the essential components of this value by feeding them into the given hasher.
19 | public func hash(into hasher: inout Hasher) {
20 | object.wrappedValue.hash(into: &hasher)
21 | }
22 | }
23 |
24 | extension Store: Identifiable where T: Identifiable {
25 | /// The stable identity of the entity associated with this instance.
26 | public var id: T.ID { object.wrappedValue.id }
27 | }
28 |
29 | //MARK: - forward Publishers
30 |
31 | extension Store where T: PropertyObservableObject {
32 | /// Forwards `ObservableObject.objectWillChangeSubscriber` to this proxy.
33 | public func forwardPropertyObservablePublisher() {
34 | objectSubscriptions.insert(
35 | object.wrappedValue.propertyDidChange.sink { [weak self] change in
36 | self?.propertyDidChange.send(change)
37 | })
38 | }
39 | }
40 |
41 | extension Store where T: ObservableObject {
42 | /// Forwards `ObservableObject.objectWillChangeSubscriber` to this proxy.
43 | public func forwardObservableObjectPublisher() {
44 | objectSubscriptions.insert(
45 | object.wrappedValue.objectWillChange.sink { [weak self] _ in
46 | guard let self = self else { return }
47 | self.objectDidChange.send()
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Store/Store/Store.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 | import Logging
4 |
5 | #if canImport(SwiftUI)
6 | import SwiftUI
7 | #endif
8 |
9 | /// Creates an observable store that acts as a proxy for the object passed as argument.
10 | ///
11 | /// Mutations of the wrapped object performed via `get`, `set` or the dynamic keypath subscript
12 | /// are thread-safe and trigger an event through the `objectWillChangeSubscriber` and
13 | /// `propertyDidChangeSubscriber` streams.
14 | @dynamicMemberLookup
15 | @propertyWrapper
16 | final public class Store: ObservableObject, PropertyObservableObject, @unchecked Sendable {
17 |
18 | /// Emits an event whenever any of the wrapped object has been mutated.
19 | public var propertyDidChange = PassthroughSubject()
20 |
21 | /// Subsystem logger.
22 | public private(set) lazy var logger: Logger = {
23 | let type = String(describing: T.self)
24 | let pointer = String(format: "%02x", Unmanaged.passUnretained(self).toOpaque().hashValue)
25 | let label = "io.store.\(type)(\(pointer))"
26 | return Logger(label: label)
27 | }()
28 |
29 | /// The underlying value referenced by the binding variable.
30 | /// This property provides primary access to the value's data. However, you
31 | /// don't access `wrappedValue` directly.
32 | public var wrappedValue: T { object.wrappedValue }
33 |
34 | /// The wrapped object.
35 | var object: Binding
36 |
37 | /// Internal subject used to propagate `objectWillChange` and `propertyDidChange` events.
38 | var objectDidChange = PassthroughSubject()
39 | var objectSubscriptions = Set()
40 | var subscriptions = Set()
41 |
42 | /// Synchronize the access to the wrapped object.
43 | private let objectLock: Locking
44 |
45 | /// Constructs a new proxy for the object passed as argument.
46 | public init(
47 | object: Binding,
48 | objectLock: Locking = UnfairLock(),
49 | options: StoreOptions
50 | ) {
51 | self.object = object
52 | self.objectLock = objectLock
53 |
54 | var objectWillChange = objectDidChange.eraseToAnyPublisher()
55 | let propertyDidChange = propertyDidChange.eraseToAnyPublisher()
56 |
57 | switch options.schedulingStrategy {
58 | case .debounce(let seconds):
59 | objectWillChange =
60 | objectWillChange
61 | .debounce(for: .seconds(seconds), scheduler: options.scheduler)
62 | .eraseToAnyPublisher()
63 | case .throttle(let seconds):
64 | objectWillChange =
65 | objectWillChange
66 | .throttle(for: .seconds(seconds), scheduler: options.scheduler, latest: true)
67 | .eraseToAnyPublisher()
68 | case .none:
69 | objectWillChange =
70 | objectWillChange
71 | .receive(on: options.scheduler)
72 | .eraseToAnyPublisher()
73 | }
74 |
75 | subscriptions.insert(
76 | propertyDidChange.sink { [weak self] in
77 | self?.objectDidChange.send()
78 | let property = $0.debugLabel != nil ? ".\($0.debugLabel!)" : "*"
79 | self?.logger.info("send { propertyDidChange(\(property)) }")
80 | })
81 | subscriptions.insert(
82 | objectWillChange.sink { [weak self] in
83 | self?.objectWillChange.send()
84 | self?.logger.info("send { objectWillChange }")
85 | })
86 | }
87 |
88 | convenience public init(
89 | object: Binding,
90 | objectLock: Locking = UnfairLock()
91 | ) {
92 | self.init(
93 | object: object,
94 | objectLock: objectLock,
95 | options: StoreOptions(scheduler: RunLoop.main, schedulingStrategy: .none))
96 | }
97 |
98 | /// Notifies the subscribers for the wrapped object changes.
99 | private func didSetValue(keyPath: KeyPath, value: V) {
100 | propertyDidChange.send(
101 | AnyPropertyChangeEvent(
102 | object: object.wrappedValue,
103 | keyPath: keyPath,
104 | debugLabel: keyPath.readableFormat))
105 | }
106 |
107 | /// Read the value of the property for the wrapped object.
108 | public func get(keyPath: KeyPath) -> V {
109 | objectLock.withLock {
110 | object.wrappedValue[keyPath: keyPath]
111 | }
112 | }
113 |
114 | /// Sets a new value for the property at the given keypath in the wrapped object.
115 | public func set(keyPath: WritableKeyPath, value: V, signpost: String = #function) {
116 | let oldValue = objectLock.withLock { () -> V in
117 | let oldValue = object.wrappedValue[keyPath: keyPath]
118 | object.wrappedValue[keyPath: keyPath] = value
119 | return oldValue
120 | }
121 | let keyPathReadableFormat = keyPath.readableFormat ?? "unknown"
122 | let valueChangeFormat = "\(String(describing: oldValue)) ⟶ \(value)"
123 | logger.info("set @ [\(signpost)] .\(keyPathReadableFormat) = { \(valueChangeFormat) }")
124 | didSetValue(keyPath: keyPath, value: value)
125 | }
126 |
127 | /// Perfom a batch update to the wrapped object.
128 | /// The changes applied in the `update` closure are atomic in respect of this store's
129 | /// wrapped object and a single `objectWillChange` event is being published after the update has
130 | /// been applied.
131 | public func performBatchUpdate(
132 | _ update: (inout T) async -> Void,
133 | signpost: String = #function
134 | ) async {
135 | await objectLock.withLock {
136 | await update(&object.wrappedValue)
137 | }
138 | logger.info("performBatchUpdate @ [\(signpost)]")
139 | objectDidChange.send()
140 | }
141 |
142 | /// Returns a binding to one of the properties of the wrapped object.
143 | /// The returned binding can itself be used as the argument for a new store object or can simply
144 | /// be used inside a SwiftUI view.
145 | public func binding(keyPath: WritableKeyPath) -> Binding {
146 | .init(
147 | get: { self.get(keyPath: keyPath) },
148 | set: { self.set(keyPath: keyPath, value: $0) })
149 | }
150 |
151 | public subscript(dynamicMember keyPath: KeyPath) -> V {
152 | get { get(keyPath: keyPath) }
153 | }
154 |
155 | public subscript(dynamicMember keyPath: WritableKeyPath) -> V {
156 | get { get(keyPath: keyPath) }
157 | set { set(keyPath: keyPath, value: newValue, signpost: "dynamic_member") }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Sources/Store/Store/StoreOptions.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public struct StoreOptions {
5 |
6 | public enum SchedulingStrategy {
7 | /// The time the publisher should wait before publishing an element.
8 | case debounce(Double)
9 | /// The interval at which to find and emit either the most recent expressed in the time system
10 | /// of the scheduler.
11 | case throttle(Double)
12 | /// Events are being emitted as they're sent through the publisher.
13 | case none
14 | }
15 |
16 | /// The scheduler on which this publisher delivers elements
17 | public let scheduler: S
18 |
19 | /// Schedule stratey.
20 | public let schedulingStrategy: SchedulingStrategy
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Store/Utilities/Assign.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// This function is used to copy the values of all enumerable own properties from one or more
4 | /// source struct to a target struct.
5 | /// - note: If the argument is a reference type the same refence is returned.
6 | public func assign(_ value: T, changes: (inout T) -> Void) -> T {
7 | var copy = value
8 | changes(©)
9 | return copy
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/Store/Utilities/Partial.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Constructs a type with all properties of T set to optional. This utility will return a type
4 | /// that represents all subsets of a given type.
5 | ///
6 | /// ```
7 | /// struct Todo { var title: String; var description: String }
8 | ///
9 | /// var partial = Partial { .success(Todo(
10 | /// title: $0.get(\Todo.title, default: "Untitled"),
11 | /// description: $0.get(\Todo.description, default: "No description")))
12 | /// }
13 | ///
14 | /// partial.title = "A Title"
15 | /// partial.description = "A Description"
16 | /// var todo = try! partial.build().get()
17 | ///
18 | /// partial.description = "Another Descrition"
19 | /// todo = partial.merge(&todo)
20 | /// ```
21 | ///
22 | @dynamicMemberLookup
23 | public struct Partial {
24 | /// The construction closure invoked by `build()`.
25 | public let create: (Partial) -> Result
26 |
27 | /// All of the values currently set in this partial.
28 | private var keypathToValueMap: [AnyKeyPath: Any] = [:]
29 |
30 | /// All of the `set` commands that will performed once the object is built.
31 | private var keypathToSetValueMap: [AnyKeyPath: (inout T) -> Void] = [:]
32 |
33 | public init(_ create: @escaping (Partial) -> Result) {
34 | self.create = create
35 | }
36 |
37 | /// Use `@dynamicMemberLookup` keypath subscript to store the object configuration and postpone
38 | /// the object construction.
39 | public subscript(dynamicMember keyPath: KeyPath) -> V? {
40 | get {
41 | get(keyPath)
42 | }
43 | set {
44 | keypathToValueMap[keyPath] = newValue
45 | }
46 | }
47 | /// Use `@dynamicMemberLookup` keypath subscript to store the object configuration and postpone
48 | /// the object construction.
49 | /// - note: `WritableKeyPath` properties are set on the object after construction and used
50 | /// by the `merge(:)` function.
51 | public subscript(dynamicMember keyPath: WritableKeyPath) -> V? {
52 | get {
53 | get(keyPath)
54 | }
55 | set {
56 | guard let value = newValue else {
57 | keypathToValueMap.removeValue(forKey: keyPath)
58 | keypathToSetValueMap.removeValue(forKey: keyPath)
59 | return
60 | }
61 | keypathToValueMap[keyPath] = value
62 | keypathToSetValueMap[keyPath] = { object in
63 | object[keyPath: keyPath] = value
64 | }
65 | }
66 | }
67 |
68 | /// Build the target object by using the `createInstanceClosure` passed to the constructor.
69 | public func build() -> Result {
70 | let result = create(self)
71 | switch result {
72 | case .success(var obj):
73 | for (_, setValueClosure) in keypathToSetValueMap {
74 | setValueClosure(&obj)
75 | }
76 | return result
77 | case .failure(_):
78 | return result
79 | }
80 | }
81 |
82 | /// Merge all of the properties currently set in this Partial with the destination object.
83 | public func merge(_ dest: inout T) -> T {
84 | assign(dest) {
85 | for (_, setValueClosure) in self.keypathToSetValueMap {
86 | setValueClosure(&$0)
87 | }
88 | }
89 | }
90 |
91 | /// Returns the value currently set for the given keyPath or an alternative default value.
92 | public func get(_ keyPath: KeyPath, default: V) -> V {
93 | guard let value = keypathToValueMap[keyPath] as? V else {
94 | return `default`
95 | }
96 | return value
97 | }
98 |
99 | /// Returns the value currently set for the given keyPath or `nil`
100 | public func get(_ keyPath: KeyPath) -> V? {
101 | guard let value = keypathToValueMap[keyPath] as? V else {
102 | return nil
103 | }
104 | return value
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/Store/Utilities/PropertyObservable.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public protocol PropertyObservableObject: AnyObject {
5 | /// A publisher that emits when an object property has changed.
6 | var propertyDidChange: PassthroughSubject { get }
7 | }
8 |
9 | /// Represent an object mutation.
10 | public struct AnyPropertyChangeEvent {
11 | /// The proxy's wrapped value.
12 | public let object: Any
13 |
14 | /// The mutated keyPath.
15 | public let keyPath: AnyKeyPath?
16 |
17 | /// Optional debug label for this event.
18 | public let debugLabel: String?
19 |
20 | public init(object: Any, keyPath: AnyKeyPath? = nil, debugLabel: String? = nil) {
21 | self.object = object
22 | self.keyPath = keyPath
23 | self.debugLabel = debugLabel
24 | }
25 |
26 | /// Returns the tuple `object, value` if this property change matches the `keyPath` passed as
27 | /// argument.
28 | public func match(keyPath: KeyPath) -> (T, V)? {
29 | guard self.keyPath === keyPath, let obj = self.object as? T else {
30 | return nil
31 | }
32 | return (obj, obj[keyPath: keyPath])
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Store/Utilities/ReadOnly+Forwarding.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | extension ReadOnly where T: PropertyObservableObject {
5 | /// Forwards the `ObservableObject.objectWillChangeSubscriber` to this proxy object.
6 | public func forwardPropertyObservableObject() {
7 | propertyDidChangeSubscriber = wrappedValue.propertyDidChange.sink { [weak self] change in
8 | self?.propertyDidChange.send(change)
9 | }
10 | }
11 | }
12 |
13 | extension ReadOnly where T: ObservableObject {
14 | /// Forwards the `ObservableObject.objectWillChangeSubscriber` to this proxy object.
15 | public func forwardObservableObject() {
16 | objectWillChangeSubscriber = wrappedValue.objectWillChange.sink { [weak self] change in
17 | self?.objectWillChange.send()
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Store/Utilities/ReadOnly.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | /// Constructs a type with all properties of the given generic type `T` set to readonly,
5 | /// meaning that the properties of the constructed type cannot be reassigned.
6 | ///
7 | /// - note: A read-only object can propagate change events if the wrapped type ia an
8 | /// `ObservableObject` by calling `propagateObservableObject` at construction time.
9 | ///
10 | /// ```
11 | /// struct Todo { var title: String; var description: String }
12 | /// let todo = Todo(title: "A Title", description: "A Description")
13 | /// let readOnlyTodo = ReadOnly(todo)
14 | /// readOnlyTodo.title // "A title"
15 | /// ```
16 | ///
17 | @dynamicMemberLookup
18 | @propertyWrapper
19 | open class ReadOnly: ObservableObject, PropertyObservableObject {
20 | public var propertyDidChange = PassthroughSubject()
21 | public private(set) var wrappedValue: T
22 |
23 | // Observable internals.
24 | var objectWillChangeSubscriber: Cancellable?
25 | var propertyDidChangeSubscriber: Cancellable?
26 |
27 | /// Constructs a new read-only proxy for the object passed as argument.
28 | init(object: T) {
29 | wrappedValue = object
30 | }
31 |
32 | public func read(keyPath: KeyPath) -> V {
33 | wrappedValue[keyPath: keyPath]
34 | }
35 |
36 | /// Use `@dynamicMemberLookup` keypath subscript to forward the value of the proxied object.
37 | public subscript(dynamicMember keyPath: KeyPath) -> V {
38 | wrappedValue[keyPath: keyPath]
39 | }
40 | }
41 |
42 | //MARK: - forward Publishers
43 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import StoreTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += StoreTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/StoreTests/StoreTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Combine
3 | import SwiftUI
4 | @testable import Store
5 |
6 | final class StoreTests: XCTestCase {
7 | var subscriber: Cancellable?
8 |
9 | func testInitialization() {
10 | var testData = TestData()
11 | let store = Store(object: Binding(get: { testData }, set: { testData = $0 }))
12 | XCTAssert(store.constant == 1337)
13 | XCTAssert(store.label == "initial")
14 | XCTAssert(store.number == 42)
15 | store.number = 1
16 | store.label = "change"
17 | XCTAssert(store.number == 1)
18 | XCTAssert(store.label == "change")
19 | }
20 |
21 | func testStorePropertyDidChange() {
22 | var testData = TestData()
23 | let store = Store(object: Binding(get: { testData }, set: { testData = $0 }))
24 | let expectation = XCTestExpectation(description: "didChangeEvent")
25 |
26 | subscriber = store.propertyDidChange.sink { change in
27 | XCTAssert(store.label == "changed")
28 | expectation.fulfill()
29 | }
30 | store.label = "changed"
31 | wait(for: [expectation], timeout: 1)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/StoreTests/SupportTypes.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct TestData {
4 | let constant = 1337
5 | var label = "initial"
6 | var number = 42
7 | }
8 |
--------------------------------------------------------------------------------
/Tests/StoreTests/UtilitiesTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Combine
3 | import SwiftUI
4 | @testable import Store
5 |
6 | @available(OSX 10.15, iOS 13.0, *)
7 | final class UtilitiesTests: XCTestCase {
8 | var subscriber: Cancellable?
9 |
10 | func testReadOnly() {
11 | let proxy = ReadOnly(object: TestData())
12 | XCTAssert(proxy.constant == 1337)
13 | XCTAssert(proxy.label == "initial")
14 | XCTAssert(proxy.number == 42)
15 | }
16 |
17 | func testPartial() {
18 | struct Todo { var title: String; var description: String }
19 | var partial = Partial { .success(Todo(
20 | title: $0.get(\Todo.title, default: "Untitled"),
21 | description: $0.get(\Todo.description, default: "No description")))
22 | }
23 | partial.title = "A Title"
24 | partial.description = "A Description"
25 | XCTAssert(partial.title == "A Title")
26 | XCTAssert(partial.description == "A Description")
27 | var todo = try! partial.build().get()
28 | XCTAssert(todo.title == "A Title")
29 | XCTAssert(todo.description == "A Description")
30 | partial.description = "Another Descrition"
31 | XCTAssert(partial.description == "Another Descrition")
32 | todo = partial.merge(&todo)
33 | XCTAssert(todo.title == "A Title")
34 | XCTAssert(todo.description == "Another Descrition")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/StoreTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(StoreTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/docs/store_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdrone/Store/fb51754e8f88a0ab99c7994d0d26d3374155f1fc/docs/store_logo.png
--------------------------------------------------------------------------------
/format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | swift-format -m format -i -r Sources/
--------------------------------------------------------------------------------