├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── SpellbookBinaryParsing │ ├── BinaryParsingError.swift │ ├── BinaryReader.swift │ ├── BinaryReaderInput.swift │ ├── BinaryWriter.swift │ └── BinaryWriterOutput.swift ├── SpellbookFoundation │ ├── Combine │ │ ├── Extensions - Combine.swift │ │ └── Proxies - Combine.swift │ ├── Common │ │ ├── Benchmark.swift │ │ ├── CancellationToken.swift │ │ ├── CollectionDiff.swift │ │ ├── Environment.swift │ │ ├── Exceptions.swift │ │ ├── Extensions - Codable.swift │ │ ├── Extensions - Collections.swift │ │ ├── Extensions - Comparable.swift │ │ ├── Extensions - Formatters.swift │ │ ├── Extensions - StandardTypes.swift │ │ ├── SBUnit.swift │ │ ├── SpellbookLog.swift │ │ ├── Utils.swift │ │ ├── ValueBuilder.swift │ │ └── WildcardExpression.swift │ ├── DictionaryParsing │ │ ├── DictionaryCodingKey.swift │ │ ├── DictionaryReader.swift │ │ └── DictionaryWriter.swift │ ├── Errors │ │ ├── CommonError.swift │ │ ├── CustomErrorUpdating.swift │ │ ├── Extensions - Error.swift │ │ ├── Extensions - NSError.swift │ │ └── IOKitError.swift │ ├── Filesystem & Bundle │ │ ├── Extensions - Bundle.swift │ │ ├── Extensions - FileManager.swift │ │ ├── FileEnumerator.swift │ │ ├── FileStore.swift │ │ └── TemporaryDirectory.swift │ ├── GUI │ │ ├── Extensions - CoreGraphics.swift │ │ └── RGBColor.swift │ ├── Internal.swift │ ├── Low Level │ │ ├── AuditToken.swift │ │ ├── BridgedCEnum.swift │ │ ├── MachTime.swift │ │ ├── POD+Swift.swift │ │ └── Unsafe.swift │ ├── Private │ │ └── _ValueUpdateWrapping.swift │ ├── System & Hardware │ │ ├── DeviceInfo.swift │ │ └── Extensions - ProcessInfo.swift │ ├── Threading & Concurrency │ │ ├── Atomic.swift │ │ ├── BlockingQueue.swift │ │ ├── ConcurrentBlockOperation.swift │ │ ├── Extensions - DispatchQueue.swift │ │ ├── Extensions - Locks.swift │ │ ├── Extensions - Task.swift │ │ ├── PosixLocks.swift │ │ ├── Synchronized.swift │ │ ├── SynchronizedObjC.swift │ │ └── SynchronousExecutor.swift │ ├── Types & PropertyWrappers │ │ ├── Boxing.swift │ │ ├── Closure.swift │ │ ├── Refreshable.swift │ │ ├── Resource (RAII).swift │ │ └── Types.swift │ └── ValueObserving │ │ ├── EventAsk.swift │ │ ├── EventNotify.swift │ │ ├── ValueObservable.swift │ │ ├── ValueObserving.swift │ │ ├── ValueStore.swift │ │ └── ValueView.swift ├── SpellbookFoundationObjC │ ├── SpellbookObjC.h │ └── SpellbookObjC.mm ├── SpellbookHTTP │ ├── HTTPClient.swift │ ├── HTTPRequest.swift │ ├── HTTPResult.swift │ └── HTTPTypes.swift └── SpellbookTestUtils │ ├── Extensions - XCTestCase.swift │ ├── TestError.swift │ └── Testing.swift └── Tests ├── SpellbookTestUtilsTests └── TestingTests.swift └── SpellbookTests ├── BinaryParsing └── BinaryParsingTests.swift ├── Common ├── BlockingQueueTests.swift ├── CancellationTokenTests.swift ├── CollectionDiffTests.swift ├── ObjectBuilderTests.swift ├── OtherCommonTests.swift ├── SBLogTests.swift ├── SBUnitTests.swift ├── UtilsTests.swift └── WildcardExpressionTests.swift ├── DictionaryParsing └── DictionaryParsingTests.swift ├── Errors ├── CommonErrorTests.swift ├── ErrorExtensionsTests.swift └── NSErrorTests.swift ├── Extensions Tests ├── CodableTests.swift ├── CollectionsTests.swift └── StandardTypesTests.swift ├── Filesystem & Bundle ├── FileEnumeratorTests.swift ├── FileManagerTests.swift ├── FileStoreTests.swift └── TemporaryDirectoryTests.swift ├── GUITests └── GUITests.swift ├── LowLevel ├── AuditTokenTests.swift ├── MachTests.swift └── UnsafeTests.swift ├── Observing ├── EventAskTests.swift ├── EventNotifyTests.swift ├── ObservableTests.swift ├── ValueObservingTests.swift └── ValueStoreTests.swift ├── Other └── ObjCTests.swift ├── Threading & Concurrency ├── ConcurrentBlockOperationTests.swift ├── DispatchQueueExtensionsTests.swift └── SynchronousExecutorTests.swift └── Types & PropertyWrappers ├── PropertyWrapperTests.swift ├── RefreshableTests.swift ├── ResourceTests.swift └── TypesTests.swift /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: "*" 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | include: 14 | - xcode: "16.2" # Swift 6 15 | macOS: "15" 16 | iOS: "17.0" 17 | fail-fast: false 18 | 19 | runs-on: macos-${{ matrix.macOS }} 20 | name: Build with Xcode ${{ matrix.xcode }} on macOS ${{ matrix.macOS }} 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Xcode Select Version 26 | uses: mobiledevops/xcode-select-version-action@v1 27 | with: 28 | xcode-select-version: ${{ matrix.xcode }} 29 | - run: xcodebuild -version 30 | 31 | - name: Test macOS with Xcode ${{ matrix.xcode }} 32 | run: | 33 | set -e 34 | set -o pipefail 35 | 36 | xcodebuild test -scheme SwiftSpellbook-Package -destination "platform=macOS" SWIFT_ACTIVE_COMPILATION_CONDITIONS="SPELLBOOK_SLOW_CI_x20" | xcpretty 37 | 38 | # Temporary disabled 39 | # - name: Test iOS with Xcode ${{ matrix.xcode }} 40 | # run: | 41 | # set -e 42 | # set -o pipefail 43 | 44 | # xcodebuild test -scheme SwiftSpellbook-Package -sdk iphonesimulator -destination "$IOS_DEVICE" SWIFT_ACTIVE_COMPILATION_CONDITIONS="SPELLBOOK_SLOW_CI_x20" | xcpretty 45 | # env: 46 | # IOS_DEVICE: "platform=iOS Simulator,OS=${{ matrix.iOS }},name=iPhone 14" 47 | 48 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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.9 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: "SwiftSpellbook", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], 9 | products: [ 10 | .library( 11 | name: "SpellbookFoundation", 12 | targets: ["SpellbookFoundation"] 13 | ), 14 | .library( 15 | name: "SpellbookHTTP", 16 | targets: ["SpellbookHTTP"] 17 | ), 18 | .library( 19 | name: "SpellbookBinaryParsing", 20 | targets: ["SpellbookBinaryParsing"] 21 | ), 22 | .library( 23 | name: "SpellbookTestUtils", 24 | targets: ["SpellbookTestUtils"] 25 | ), 26 | ], 27 | dependencies: [ 28 | ], 29 | targets: [ 30 | .target( 31 | name: "SpellbookFoundation", 32 | dependencies: ["_SpellbookFoundationObjC"], 33 | linkerSettings: [ 34 | .linkedLibrary("bsm", .when(platforms: [.macOS])), 35 | ] 36 | ), 37 | .target( 38 | name: "_SpellbookFoundationObjC", 39 | path: "Sources/SpellbookFoundationObjC", 40 | publicHeadersPath: "." 41 | ), 42 | .target( 43 | name: "SpellbookHTTP", 44 | dependencies: ["SpellbookFoundation"] 45 | ), 46 | .target( 47 | name: "SpellbookBinaryParsing", 48 | dependencies: ["SpellbookFoundation"] 49 | ), 50 | .target( 51 | name: "SpellbookTestUtils", 52 | dependencies: ["SpellbookFoundation"] 53 | ), 54 | .testTarget( 55 | name: "SpellbookTests", 56 | dependencies: ["SpellbookFoundation", "SpellbookBinaryParsing", "SpellbookTestUtils"] 57 | ), 58 | .testTarget( 59 | name: "SpellbookTestUtilsTests", 60 | dependencies: ["SpellbookFoundation", "SpellbookTestUtils"] 61 | ), 62 | ] 63 | ) 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftSpellbook 2 | SwiftSpellbook is collection of additions to Swift standard library that makes development easier. 3 | 4 |

5 | 6 | 7 | 8 | 9 |

10 | 11 | If you've found this or other my libraries helpful, please buy me some pizza 12 | 13 | 14 | 15 | ## Motivation 16 | While participating in many projects (macOS and iOS) I use the same tools and standard types extensions. 17 | Once I've decided stop to copy-paste code from project to project and make single library that covers lots of developer needs in utility code. 18 | 19 | ## Content 20 | At top level, the code is organized into libraries that cover big areas. 21 | Now there are only two: 22 | - SpellbookFoundation: utility code 23 | - SpellbookBinaryParsing: convenient way to read and write binary data byte-by-byte 24 | - SpellbookHTTP: HTTP client 25 | - SpellbookTestUtils: utility code frequently used for Unit-Tests 26 | 27 | ## SpellbookFoundation 28 | The most of utility code lives here. 29 | - Combine: Combine.framework extensions 30 | - Common: Mix of commonly used entities 31 | - DictionaryParsing: deal with data nested deeply in dictionaries 32 | - Filesystem & Bundle: FileManager, Bundle and same utilities 33 | - GUI: CoreGraphics utilities. This is NOT an AppKit/UIKit/SwiftUI 34 | - Low Level: extensions to deal with (popular) C structures, unsafe types, etc. 35 | - ObjC Bridging: Caching Objective-C and C++ exceptions from Swift code 36 | - System & Hardware: UNIX and Process utilities 37 | - Threading & Concurrency: utilities that make multithead development easier 38 | - Types & PropertyWrappers: misc types and property wrappers 39 | - ValueObserving: utilities that allows observe and modify-with-observe on any types 40 | 41 | # Other 42 | If you've found this or other my libraries helpful, you could buy me some pizza 43 | 44 | 45 | 46 | You can also find Swift libraries for macOS / *OS development 47 | - [sXPC](https://github.com/Alkenso/sXPC): type-safe wrapper around NSXPCConnection and proxy object 48 | - [sLaunchctl](https://github.com/Alkenso/sLaunchctl): register and manage daemons and user-agents 49 | - [sMock](https://github.com/Alkenso/sMock): Swift unit-test mocking framework similar to gtest/gmock 50 | - [sEndpointSecurity](https://github.com/Alkenso/sEndpointSecurity.git) Swift wrapper around EndpointSecurity.framework 51 | - [SwiftSpellbook_macOS](https://github.com/Alkenso/SwiftSpellbook_macOS) macOS-related collection of additions to Swift standard library that makes development easier. 52 | -------------------------------------------------------------------------------- /Sources/SpellbookBinaryParsing/BinaryParsingError.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public enum BinaryParsingError: Error { 26 | case outOfRange 27 | case notTrivial(String) 28 | } 29 | 30 | internal func ensureTrivial(_ type: T.Type) throws { 31 | if !_isPOD(T.self) { throw BinaryParsingError.notTrivial("\(T.self)") } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SpellbookBinaryParsing/BinaryReaderInput.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public protocol BinaryReaderInput { 26 | func readBytes(to: UnsafeMutableBufferPointer, offset: Int) throws 27 | func size() throws -> Int 28 | } 29 | 30 | public struct AnyBinaryReaderInput: BinaryReaderInput { 31 | private var _readBytes: (_ to: UnsafeMutableBufferPointer, _ offset: Int) throws -> Void 32 | private var _size: () throws -> Int 33 | 34 | public init( 35 | readBytes: @escaping (UnsafeMutableBufferPointer, Int) throws -> Void, 36 | size: @escaping () throws -> Int 37 | ) { 38 | _readBytes = readBytes 39 | _size = size 40 | } 41 | 42 | public func readBytes(to: UnsafeMutableBufferPointer, offset: Int) throws { 43 | try _readBytes(to, offset) 44 | } 45 | 46 | public func size() throws -> Int { 47 | try _size() 48 | } 49 | } 50 | 51 | extension BinaryReader { 52 | public init(data: Data) { 53 | self.init( 54 | AnyBinaryReaderInput( 55 | readBytes: { dstPtr, offset in 56 | let range = Range(offset: offset, length: dstPtr.count) 57 | if dstPtr.count != data.copyBytes(to: dstPtr, from: range) { 58 | throw BinaryParsingError.outOfRange 59 | } 60 | }, 61 | size: { data.count } 62 | ) 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/SpellbookBinaryParsing/BinaryWriterOutput.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public protocol BinaryWriterOutput: AnyObject { 26 | func writeBytes(from: UnsafeBufferPointer, at offset: Int) throws 27 | func size() throws -> Int 28 | } 29 | 30 | public class AnyBinaryWriterOutput: BinaryWriterOutput { 31 | private var _writeBytes: (_ from: UnsafeBufferPointer, _ offset: Int) throws -> Void 32 | private var _size: () throws -> Int 33 | 34 | public init( 35 | writeBytes: @escaping (UnsafeBufferPointer, Int) throws -> Void, 36 | size: @escaping () -> Int 37 | ) { 38 | _writeBytes = writeBytes 39 | _size = size 40 | } 41 | 42 | public func writeBytes(from: UnsafeBufferPointer, at offset: Int) throws { 43 | try _writeBytes(from, offset) 44 | } 45 | 46 | public func size() throws -> Int { 47 | try _size() 48 | } 49 | } 50 | 51 | public class DataBinaryWriterOutput: BinaryWriterOutput { 52 | public var data: Data 53 | 54 | public init(data: Data = Data()) { 55 | self.data = data 56 | } 57 | 58 | public func writeBytes(from: UnsafeBufferPointer, at offset: Int) throws { 59 | let appendCount = offset + from.count - data.count 60 | if appendCount > 0 { 61 | data += Data(repeating: 0, count: appendCount) 62 | } 63 | data.replaceSubrange(Range(offset: offset, length: from.count), with: from) 64 | } 65 | 66 | public func size() -> Int { 67 | data.count 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Combine/Extensions - Combine.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Combine 24 | 25 | extension Publisher { 26 | public func eraseToAnyPublisher(attachingContext context: Any?) -> AnyPublisher { 27 | ProxyPublisher(self, context: context).eraseToAnyPublisher() 28 | } 29 | } 30 | 31 | extension Publisher where Output: Equatable, Failure == Never { 32 | /// Publishes value changes in order it receives the values 33 | /// - Warning: When using `mapToChange`, be sure it receives the input in right order. 34 | /// Avoid use `receive(on:)` with concurrent queues in upstream publishers. 35 | public var mapToChange: AnyPublisher, Never> { 36 | let oldValue = Atomic(wrappedValue: nil) 37 | 38 | let subject = PassthroughSubject, Never>() 39 | var proxy = ProxyPublisher(subject) 40 | proxy.context = sink { 41 | if let oldValue = oldValue.exchange($0), let change = Change(old: oldValue, new: $0) { 42 | subject.send(change) 43 | } 44 | } 45 | return proxy.eraseToAnyPublisher() 46 | } 47 | } 48 | 49 | extension Cancellable { 50 | public func capturing(_ object: Any) -> AnyCancellable { 51 | .init { withExtendedLifetime(object, self.cancel) } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Combine/Proxies - Combine.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Combine 24 | 25 | public struct ProxyPublisher: Publisher { 26 | public typealias Output = P.Output 27 | public typealias Failure = P.Failure 28 | 29 | public let proxy: P 30 | public var context: Any? 31 | 32 | public init(_ publisher: P, context: Any? = nil) { 33 | proxy = publisher 34 | } 35 | 36 | public func receive(subscriber: S) where S: Subscriber, P.Failure == S.Failure, P.Output == S.Input { 37 | proxy.receive(subscriber: subscriber) 38 | } 39 | } 40 | 41 | public final class ProxySubscriber: Subscriber { 42 | public typealias Input = S.Input 43 | public typealias Failure = S.Failure 44 | 45 | public let proxy: S 46 | public var context: Any? 47 | 48 | public init(_ subscriber: S) { 49 | proxy = subscriber 50 | } 51 | 52 | public func receive(subscription: Subscription) { 53 | proxy.receive(subscription: subscription) 54 | } 55 | 56 | public func receive(_ input: S.Input) -> Subscribers.Demand { 57 | proxy.receive(input) 58 | } 59 | 60 | public func receive(completion: Subscribers.Completion) { 61 | proxy.receive(completion: completion) 62 | } 63 | } 64 | 65 | public final class ProxySubscription: Subscription { 66 | public var context: Any? 67 | 68 | public var onDemand: ((Subscribers.Demand) -> Void)? 69 | public var onCancel: (() -> Void)? 70 | 71 | public init() {} 72 | 73 | public func request(_ demand: Subscribers.Demand) { 74 | onDemand?(demand) 75 | } 76 | 77 | public func cancel() { 78 | onCancel?() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/Benchmark.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public enum Benchmark { 26 | /// Executes the given block and returns the number of seconds 27 | /// with nanosecond precision it takes to execute the block. 28 | /// This function is for debugging and performance analysis work. 29 | public static func measure(execute: () throws -> R) rethrows -> (result: R, time: TimeInterval) { 30 | let start = DispatchTime.now() 31 | let result = try execute() 32 | let end = DispatchTime.now() 33 | 34 | let durationNs = end.uptimeNanoseconds - start.uptimeNanoseconds 35 | let durationSec = TimeInterval(durationNs) / TimeInterval(NSEC_PER_SEC) 36 | 37 | return (result, durationSec) 38 | } 39 | 40 | /// Executes the given block and returns the number of seconds 41 | /// with nanosecond precision it takes to execute the block. 42 | /// This function is for debugging and performance analysis work. 43 | public static func measure(execute: () throws -> Void) rethrows -> TimeInterval { 44 | try measure(execute: execute).time 45 | } 46 | 47 | /// Executes the given block and prints the `name` and the number of seconds 48 | /// with nanosecond precision it takes to execute the block. 49 | /// This function is for debugging and performance analysis work. 50 | public static func measure( 51 | _ name: String, 52 | print: (String) -> Void = { print($0) }, 53 | execute: () throws -> R 54 | ) rethrows -> R { 55 | let (result, durationSec) = try measure(execute: execute) 56 | print("\(name) takes \(durationSec) sec") 57 | 58 | return result 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/CancellationToken.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Combine 24 | import Foundation 25 | 26 | public class CancellationToken { 27 | private let queue: DispatchQueue 28 | private let onDeinit: Bool 29 | private let onCancel: () -> Void 30 | 31 | @Atomic private var cancelled = false 32 | private var children = Synchronized<[CancellationToken]>(.serial) 33 | 34 | public var isCancelled: Bool { cancelled } 35 | 36 | public init(on queue: DispatchQueue = .global(), onDeinit: Bool = false, cancel: @escaping () -> Void) { 37 | self.queue = queue 38 | self.onDeinit = onDeinit 39 | self.onCancel = cancel 40 | } 41 | 42 | deinit { 43 | if onDeinit { 44 | cancel() 45 | } 46 | } 47 | 48 | public func cancel() { 49 | guard !$cancelled.exchange(true) else { return } 50 | queue.async(execute: onCancel) 51 | children.exchange([]).forEach { $0.queue.async(execute: $0.cancel) } 52 | } 53 | 54 | public func addChild(_ token: CancellationToken) { 55 | children.write { 56 | if !self.isCancelled { 57 | $0.append(token) 58 | } else { 59 | token.queue.async(execute: token.cancel) 60 | } 61 | } 62 | } 63 | } 64 | 65 | extension CancellationToken { 66 | public convenience init() { 67 | self.init {} 68 | } 69 | 70 | public func addChild(on queue: DispatchQueue = .global(), cancel: @escaping () -> Void) { 71 | addChild(.init(on: queue, cancel: cancel)) 72 | } 73 | 74 | public func attach(to parent: CancellationToken) { 75 | parent.addChild(self) 76 | } 77 | } 78 | 79 | extension CancellationToken: Cancellable {} 80 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/CollectionDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionDiff.swift 3 | // SwiftSpellbook 4 | // 5 | // Created by Alkenso (Vladimir Vashurkin) on 2025-02-20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct CollectionDiff { 11 | public var added: [Element] = [] 12 | public var updated: [Change] = [] 13 | public var removed: [Element] = [] 14 | public var unchanged: [Element] = [] 15 | 16 | public init( 17 | added: [Element] = [], 18 | updated: [Change] = [], 19 | removed: [Element] = [], 20 | unchanged: [Element] = [] 21 | ) { 22 | self.added = added 23 | self.updated = updated 24 | self.removed = removed 25 | self.unchanged = unchanged 26 | } 27 | } 28 | 29 | extension CollectionDiff: Equatable where Element: Equatable {} 30 | extension CollectionDiff: Decodable where Element: Decodable {} 31 | extension CollectionDiff: Encodable where Element: Encodable {} 32 | 33 | extension CollectionDiff { 34 | public var isUnchanged: Bool { added.isEmpty && removed.isEmpty && updated.isEmpty } 35 | } 36 | 37 | extension CollectionDiff { 38 | public init( 39 | from: C, 40 | to: C, 41 | isSimilar: (Element, Element) -> Bool, 42 | isEqual: (Element, Element) -> Bool 43 | ) where C.Element == Element { 44 | self.init() 45 | 46 | for toElement in to { 47 | if let fromElement = from.first(where: { isSimilar($0, toElement) }) { 48 | if let change = Change(old: fromElement, new: toElement, isEqual: isEqual) { 49 | updated.append(change) 50 | } else { 51 | unchanged.append(toElement) 52 | } 53 | } else { 54 | added.append(toElement) 55 | } 56 | } 57 | removed = from.filter { fromElement in !to.contains { isSimilar($0, fromElement) } } 58 | } 59 | 60 | public init(from: C, to: C) where C.Element == Element, Element: Equatable { 61 | self.init(from: from, to: to, similarBy: \.self) 62 | } 63 | 64 | public init( 65 | from: C, 66 | to: C, 67 | similarBy: KeyPath 68 | ) where C.Element == Element, Element: Equatable { 69 | self.init(from: from, to: to, isSimilar: { $0[keyPath: similarBy] == $1[keyPath: similarBy] }, isEqual: ==) 70 | } 71 | } 72 | 73 | public struct DictionaryDiff { 74 | public var added: [Key: Value] = [:] 75 | public var updated: [Key: Change] = [:] 76 | public var removed: [Key: Value] = [:] 77 | public var unchanged: [Key: Value] = [:] 78 | 79 | public init( 80 | added: [Key: Value] = [:], 81 | updated: [Key: Change] = [:], 82 | removed: [Key: Value] = [:], 83 | unchanged: [Key: Value] = [:] 84 | ) { 85 | self.added = added 86 | self.updated = updated 87 | self.removed = removed 88 | self.unchanged = unchanged 89 | } 90 | } 91 | 92 | extension DictionaryDiff: Equatable where Value: Equatable {} 93 | extension DictionaryDiff: Decodable where Key: Decodable, Value: Decodable {} 94 | extension DictionaryDiff: Encodable where Key: Encodable, Value: Encodable {} 95 | 96 | extension DictionaryDiff { 97 | public var isUnchanged: Bool { added.isEmpty && removed.isEmpty && updated.isEmpty } 98 | } 99 | 100 | extension DictionaryDiff { 101 | public init(from: [Key: Value], to: [Key: Value], isEqual: (Value, Value) -> Bool) { 102 | self.init() 103 | for (toKey, toValue) in to { 104 | if let fromValue = from[toKey] { 105 | if let change = Change(old: fromValue, new: toValue, isEqual: isEqual) { 106 | updated[toKey] = change 107 | } else { 108 | unchanged[toKey] = toValue 109 | } 110 | } else { 111 | added[toKey] = toValue 112 | } 113 | } 114 | removed = from.filter { to[$0.key] == nil } 115 | } 116 | 117 | public init(from: [Key: Value], to: [Key: Value]) where Value: Equatable { 118 | self.init(from: from, to: to, isEqual: ==) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/Environment.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// Different indicators related to Application build environment. 26 | public enum BuildEnvironment { 27 | /// Runtime check if run in debug mode. 28 | public static let isDebug: Bool = { 29 | #if DEBUG 30 | return true 31 | #else 32 | return false 33 | #endif 34 | }() 35 | } 36 | 37 | /// Different indicators related to Application run environment. 38 | public enum RunEnvironment { 39 | /// Runtime check if run inside XCTest bundle. 40 | public static let isXCTesting: Bool = { 41 | let envKeys = ["XCTestBundlePath", "XCTestConfigurationFilePath", "XCTestSessionIdentifier"] 42 | if ProcessInfo.processInfo.environment.keys.contains(where: envKeys.contains) { return true } 43 | 44 | if let path = ProcessInfo.processInfo.arguments.first { 45 | if path.lastPathComponent == "xctest" { return true } 46 | if path.pathExtension == "xctest" { return true } 47 | } 48 | 49 | if (try? NSException.catchingAll({ 50 | if let observationCenterClass = NSClassFromString("XCTestObservationCenter") as? NSObject.Type, 51 | let observationCenter = observationCenterClass.perform(NSSelectorFromString("sharedTestObservationCenter")), 52 | let builtInObservers = observationCenter.takeUnretainedValue().perform(NSSelectorFromString("observers")), 53 | let builtInObserverArray = builtInObservers.takeUnretainedValue() as? [NSObject], 54 | let misuseObserverClass = NSClassFromString("XCTestMisuseObserver"), 55 | let misuseObserver = builtInObserverArray.first(where: { $0.isKind(of: misuseObserverClass) }), 56 | let currentCaseAny = misuseObserver.perform(NSSelectorFromString("currentTestCase")), 57 | let currentCase = currentCaseAny.takeUnretainedValue() as? NSObject, 58 | let testCaseClass = NSClassFromString("XCTestCase") { 59 | return currentCase.isKind(of: testCaseClass) 60 | } else { 61 | return false 62 | } 63 | })) == true { 64 | return true 65 | } 66 | 67 | return false 68 | }() 69 | 70 | /// Runtime check if run from Xcode. 71 | public static let isRunFromXcode: Bool = { 72 | guard let mode = ProcessInfo.processInfo.environment["OS_ACTIVITY_DT_MODE"] else { return false } 73 | return mode.uppercased() == "YES" || mode == "1" 74 | }() 75 | 76 | /// Runtime check if run as Xcode preview. 77 | public static let isXcodePreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"]?.isEmpty == false 78 | 79 | /// Runtime check if run in simulator. 80 | public static let isSimulator: Bool = { 81 | #if targetEnvironment(simulator) 82 | return true 83 | #else 84 | return false 85 | #endif 86 | }() 87 | } 88 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/Exceptions.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | @_implementationOnly import _SpellbookFoundationObjC 24 | 25 | import Foundation 26 | 27 | /// Error-like wrapper around Objective-C `NSException` to make it Swift.Error compatible. 28 | public struct NSExceptionError: Error, @unchecked Sendable { 29 | public var exception: NSException 30 | public init(exception: NSException) { 31 | self.exception = exception 32 | } 33 | } 34 | 35 | extension NSExceptionError: CustomStringConvertible, CustomDebugStringConvertible { 36 | public var description: String { exception.description } 37 | public var debugDescription: String { exception.debugDescription } 38 | } 39 | 40 | extension NSException: NonSwiftException { 41 | public static func evaluate(_ body: () -> Void) -> NSException? { 42 | SpellbookObjC.nsException_catching(body) 43 | } 44 | 45 | public static func create(with nonSwiftError: NSException) -> NSExceptionError { 46 | NSExceptionError(exception: nonSwiftError) 47 | } 48 | } 49 | 50 | /// Error-like wrapper around C++ `std::exception` to make it Swift.Error compatible. 51 | public struct CppException: Error { 52 | public var what: String 53 | 54 | public init(what: String) { 55 | self.what = what 56 | } 57 | 58 | public func raise() -> Never { 59 | SpellbookObjC.throwCppRuntineErrorException(what) 60 | } 61 | } 62 | 63 | extension CppException: NonSwiftException { 64 | public static func evaluate(_ body: () -> Void) -> String? { 65 | SpellbookObjC.cppException_catching(body) 66 | } 67 | 68 | public static func create(with nonSwiftError: String) -> Self { 69 | CppException(what: nonSwiftError) 70 | } 71 | } 72 | 73 | public protocol NonSwiftException { 74 | associatedtype NonSwiftError 75 | associatedtype SwiftError: Error 76 | static func evaluate(_ body: () -> Void) -> NonSwiftError? 77 | static func create(with nonSwiftError: NonSwiftError) -> SwiftError 78 | } 79 | 80 | extension NonSwiftException { 81 | public static func catching(_ body: () -> R) -> Result { 82 | var result: Result! 83 | if let reason = evaluate({ 84 | let value = body() 85 | result = .success(value) 86 | }) { 87 | result = .failure(create(with: reason)) 88 | } 89 | return result 90 | } 91 | 92 | public static func catchingAll(_ body: () throws -> R) throws -> R { 93 | try catching { () -> Result in 94 | do { 95 | return .success(try body()) 96 | } catch { 97 | return .failure(error) 98 | } 99 | }.get().get() 100 | } 101 | } 102 | 103 | /// Convenient wrapper to catch Swift, Objective-C and C++ exceptions 104 | /// that may be thrown from the `body` closure. 105 | /// - Warning: Use it reasonably, catching code related to 106 | /// Objective-C and C++ exceptions may affect the performance. 107 | public func catchingAny(_ body: () throws -> R) throws -> R { 108 | try CppException.catchingAll { 109 | try NSException.catchingAll(body) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/Extensions - Comparable.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | @propertyWrapper 26 | public struct Clamped { 27 | var value: Value 28 | let range: ClosedRange 29 | 30 | public init(wrappedValue value: Value, _ range: ClosedRange) { 31 | self.value = value.clamped(to: range) 32 | self.range = range 33 | } 34 | 35 | public var wrappedValue: Value { 36 | get { value } 37 | set { value = newValue.clamped(to: range) } 38 | } 39 | } 40 | 41 | extension Comparable { 42 | public func clamped(to limits: ClosedRange) -> Self { 43 | return min(max(self, limits.lowerBound), limits.upperBound) 44 | } 45 | } 46 | 47 | public enum ComparisonRelation: String, Hashable, CaseIterable { 48 | case lessThan = "<" 49 | case lessThanOrEqual = "<=" 50 | case equal = "==" 51 | case greaterThanOrEqual = ">=" 52 | case greaterThan = ">" 53 | } 54 | 55 | extension Comparable { 56 | public func compare(to rhs: Self, relation: ComparisonRelation) -> Bool { 57 | switch relation { 58 | case .equal: 59 | return self == rhs 60 | case .lessThan: 61 | return self < rhs 62 | case .lessThanOrEqual: 63 | return self <= rhs 64 | case .greaterThan: 65 | return self > rhs 66 | case .greaterThanOrEqual: 67 | return self >= rhs 68 | } 69 | } 70 | } 71 | 72 | public protocol RawComparable: Comparable { 73 | associatedtype RawValue 74 | } 75 | 76 | extension RawComparable where Self: RawRepresentable, Self.RawValue: Comparable { 77 | public static func < (lhs: Self, rhs: Self) -> Bool { 78 | lhs.rawValue < rhs.rawValue 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/Extensions - Formatters.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension DateFormatter { 26 | public convenience init( 27 | _ dateFormat: String, 28 | dateStyle: DateFormatter.Style? = nil, 29 | timeStyle: DateFormatter.Style? = nil, 30 | timeZone: TimeZone? = nil 31 | ) { 32 | self.init() 33 | 34 | self.dateFormat = dateFormat 35 | dateStyle.flatMap { self.dateStyle = $0 } 36 | timeStyle.flatMap { self.timeStyle = $0 } 37 | timeZone.flatMap { self.timeZone = $0 } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/SBUnit.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public protocol SBUnit: RawRepresentable {} 26 | 27 | extension SBUnit { 28 | /// Perform conversion between measurement units. 29 | /// - Parameters: 30 | /// - value: Unit magnitude. 31 | /// - from: `value` measurement units. `nil` means base units. 32 | /// - to: `resulting` measurement units. `nil` means base units. 33 | public static func convert( 34 | _ value: Double, _ from: Self? = nil, to: Self? = nil 35 | ) -> Double where RawValue: BinaryFloatingPoint { 36 | convert(value, from.flatMap { Double($0.rawValue) }, to: to.flatMap { Double($0.rawValue) }) 37 | } 38 | 39 | /// Perform conversion between measurement units. 40 | /// - Parameters: 41 | /// - value: Unit magnitude. 42 | /// - from: `value` measurement units. `nil` means base units. 43 | /// - to: `resulting` measurement units. `nil` means base units. 44 | public static func convert( 45 | _ value: Double, _ from: Self? = nil, to: Self? = nil 46 | ) -> Double where RawValue: BinaryInteger { 47 | convert(value, from.flatMap { Double($0.rawValue) }, to: to.flatMap { Double($0.rawValue) }) 48 | } 49 | 50 | private static func convert(_ value: Double, _ from: Double?, to: Double?) -> Double { 51 | value * (from ?? 1.0) / (to ?? 1.0) 52 | } 53 | } 54 | 55 | public struct SBUnitInformationStorage: SBUnit { 56 | public var rawValue: Int 57 | public init(rawValue: Int) { self.rawValue = rawValue } 58 | 59 | public static let kilobyte = Self(rawValue: 1024) 60 | public static let megabyte = Self(rawValue: 1024 * kilobyte.rawValue) 61 | public static let gigabyte = Self(rawValue: 1024 * megabyte.rawValue) 62 | } 63 | 64 | public struct SBUnitTime: SBUnit { 65 | public var rawValue: TimeInterval 66 | public init(rawValue: TimeInterval) { self.rawValue = rawValue } 67 | 68 | public static let minute = Self(rawValue: 60) 69 | public static let hour = Self(rawValue: 60 * minute.rawValue) 70 | public static let day = Self(rawValue: 24 * hour.rawValue) 71 | } 72 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/Utils.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public func updateSwap(_ a: inout T, _ b: T) -> T { 26 | let copy = a 27 | a = b 28 | return copy 29 | } 30 | 31 | public func throwingCast(name: String? = nil, _ object: Any, to: T.Type) throws -> T { 32 | try (object as? T).get(CommonError.cast(name: name, object, to: to)) 33 | } 34 | 35 | public func updateValue(_ value: Value, using transform: (inout Value) -> Void) -> Value { 36 | var value = value 37 | transform(&value) 38 | return value 39 | } 40 | 41 | public func updateValue(_ value: Root, at keyPath: WritableKeyPath, with property: Property) -> Root { 42 | var value = value 43 | value[keyPath: keyPath] = property 44 | return value 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/ValueBuilder.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public protocol ValueBuilder {} 26 | 27 | extension ValueBuilder { 28 | public func set(_ keyPath: WritableKeyPath, _ value: T?) -> Self { 29 | guard let value = value else { return self } 30 | var copy = self 31 | copy[keyPath: keyPath] = value 32 | return copy 33 | } 34 | 35 | public func `if`(_ condition: Bool, then: (inout Self) -> Void, `else`: (inout Self) -> Void = { _ in }) -> Self { 36 | var copy = self 37 | if condition { 38 | then(©) 39 | } else { 40 | `else`(©) 41 | } 42 | return copy 43 | } 44 | 45 | public func ifLet(_ value: T?, then: (inout Self, T) -> Void, `else`: (inout Self) -> Void = { _ in }) -> Self { 46 | var copy = self 47 | if let value { 48 | then(©, value) 49 | } else { 50 | `else`(©) 51 | } 52 | return copy 53 | } 54 | 55 | public func update(body: (inout Self) -> Void) -> Self { 56 | var copy = self 57 | body(©) 58 | return copy 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Common/WildcardExpression.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// In contrast to NSRegularExpression, implements matching for string by wildcards `*` and `?`. 26 | /// `WildcardExpression` is Swift wrapper around `fnmatch` function. 27 | public struct WildcardExpression { 28 | public var pattern: String 29 | public var caseSensitive: Bool 30 | 31 | /// Slash characters in string must be explicitly matched by slashes in pattern. 32 | /// If this flag is not set, then slashes are treated as regular characters. 33 | public var fileNames: Bool 34 | 35 | public init(pattern: String, caseSensitive: Bool = true, fileNames: Bool = false) { 36 | self.pattern = pattern 37 | self.caseSensitive = caseSensitive 38 | self.fileNames = fileNames 39 | } 40 | 41 | public func match(_ string: String) -> Bool { 42 | string.withCString { string in 43 | pattern.withCString { pattern in 44 | fnmatch(pattern, string, flags) == 0 45 | } 46 | } 47 | } 48 | 49 | private var flags: Int32 { 50 | var flags: Int32 = 0 51 | if !caseSensitive { 52 | flags |= FNM_CASEFOLD 53 | } 54 | if fileNames { 55 | flags |= FNM_FILE_NAME 56 | } 57 | return flags 58 | } 59 | } 60 | 61 | extension WildcardExpression { 62 | public static func caseSensitive(pattern: String) -> Self { 63 | .init(pattern: pattern, caseSensitive: true) 64 | } 65 | 66 | public static func caseInsensitive(pattern: String) -> Self { 67 | .init(pattern: pattern, caseSensitive: false) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/DictionaryParsing/DictionaryCodingKey.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public enum DictionaryCodingKey { 26 | case key(AnyHashable) 27 | case index(Int) 28 | } 29 | 30 | extension DictionaryCodingKey: ExpressibleByStringLiteral { 31 | public init(stringLiteral value: StringLiteralType) { 32 | self = .key(String(value)) 33 | } 34 | } 35 | 36 | extension DictionaryCodingKey: CustomStringConvertible { 37 | public var description: String { 38 | switch self { 39 | case .key(let key): 40 | return "key(\(key))" 41 | case .index(let index): 42 | return "index(\(index))" 43 | } 44 | } 45 | } 46 | 47 | extension DictionaryCodingKey { 48 | internal static func parse(dotPath: String) -> [DictionaryCodingKey] { 49 | guard !dotPath.isEmpty else { return [] } 50 | return dotPath.components(separatedBy: ".").map { 51 | if $0 == "[*]" { 52 | return .index(.max) 53 | } else if $0.hasPrefix("["), $0.hasSuffix("]"), let index = Int($0.dropFirst().dropLast()) { 54 | return .index(index) 55 | } else { 56 | return .key($0) 57 | } 58 | } 59 | } 60 | } 61 | 62 | public struct DictionaryCodingError: Error { 63 | public var code: Code 64 | public var codingPath: [DictionaryCodingKey] 65 | public var description: String 66 | public var underlyingError: Error? 67 | public var context: String? 68 | public var relatedObject: Any? 69 | 70 | public init( 71 | code: Code, codingPath: [DictionaryCodingKey], 72 | description: String, underlyingError: Error? = nil, 73 | context: String? = nil, relatedObject: Any? = nil 74 | ) { 75 | self.code = code 76 | self.codingPath = codingPath 77 | self.description = description 78 | self.underlyingError = underlyingError 79 | self.context = context 80 | self.relatedObject = relatedObject 81 | } 82 | } 83 | 84 | extension DictionaryCodingError { 85 | public enum Code: Int { 86 | case invalidArgument 87 | case keyNotFound 88 | case typeMismatch 89 | } 90 | } 91 | 92 | extension DictionaryCodingError: CustomNSError { 93 | public var errorCode: Int { code.rawValue } 94 | public static var errorDomain: String { "DictionaryCodingErrorDomain" } 95 | public var errorUserInfo: [String : Any] { 96 | var userInfo: [String: Any] = [:] 97 | 98 | var fullDescription = description 99 | fullDescription += context.flatMap { ". \($0)" } ?? "" 100 | fullDescription += "Coding path = \(codingPath)" 101 | userInfo[NSDebugDescriptionErrorKey] = fullDescription 102 | 103 | underlyingError.flatMap { userInfo[NSUnderlyingErrorKey] = $0 } 104 | relatedObject.flatMap { userInfo[SBRelatedObjectErrorKey] = $0 } 105 | 106 | return userInfo 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Errors/CustomErrorUpdating.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2024 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | private let log = SpellbookLogger.internal(category: "CustomErrorUpdating") 26 | 27 | public protocol CustomErrorUpdating where Self: Error { 28 | associatedtype UpdatedError: Error 29 | 30 | var userInfo: [String: Any] { get } 31 | func replacingUserInfo(_ userInfo: [String: Any]) -> UpdatedError 32 | } 33 | 34 | extension CustomErrorUpdating { 35 | /// Build new error appending underlying error. 36 | public func appendingUnderlyingError(_ error: Error) -> UpdatedError { 37 | updatingUserInfo(error, for: NSUnderlyingErrorKey) 38 | } 39 | 40 | /// Build new error with specified (or replaced) `userInfo` value for `NSDebugDescriptionErrorKey`. 41 | public func updatingDebugDescription(_ debugDescription: String) -> UpdatedError { 42 | updatingUserInfo(debugDescription, for: NSDebugDescriptionErrorKey) 43 | } 44 | 45 | /// Build new error with specified (or replaced) `userInfo` value for given key. 46 | public func updatingUserInfo(_ value: Any, for key: String) -> UpdatedError { 47 | updatingUserInfo([key: value]) 48 | } 49 | 50 | /// Build new error with `userInfo`, merged with given `userInfo`. 51 | /// If key in new `userInfo` already exists, the value is replaced. 52 | /// If key exists and is `NSUnderlyingErrorKey` or `NSMultipleUnderlyingErrorsKey`, the error(s) is appended. 53 | public func updatingUserInfo(_ userInfo: [String: Any]) -> UpdatedError { 54 | let mergedUserInfo = Self.mergeUserInfo(existing: self.userInfo, new: userInfo) 55 | return replacingUserInfo(mergedUserInfo) 56 | } 57 | 58 | private static func mergeUserInfo(existing: [String: Any], new: [String: Any]) -> [String: Any] { 59 | var merged = existing 60 | new.forEach { 61 | switch $0.key { 62 | case NSUnderlyingErrorKey: 63 | if merged[NSUnderlyingErrorKey] == nil { 64 | merged[NSUnderlyingErrorKey] = $0.value 65 | } else { 66 | let errors = merged[Self.multipleUnderlyingErrorsKey] as? [Any] ?? [] 67 | merged[Self.multipleUnderlyingErrorsKey] = errors.appending($0.value) 68 | } 69 | case Self.multipleUnderlyingErrorsKey: 70 | guard let newErrors = $0.value as? [Error] else { 71 | log.error("Value for Error userInfo key \($0.key) MUST be of type [Error]", assert: true) 72 | return 73 | } 74 | let errors = merged[$0.key] as? [Any] ?? [] 75 | merged[$0.key] = errors + newErrors 76 | default: 77 | merged[$0.key] = $0.value 78 | } 79 | } 80 | return merged 81 | } 82 | 83 | public static var multipleUnderlyingErrorsKey: String { 84 | if #available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *) { 85 | return NSMultipleUnderlyingErrorsKey 86 | } else { 87 | return "NSMultipleUnderlyingErrorsKey" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Errors/Extensions - Error.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2024 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension Error { 26 | public func `throw`() throws -> Never { 27 | throw self 28 | } 29 | } 30 | 31 | extension Error { 32 | /// Not all `Error` objects are NSSecureCoding-compilant. 33 | /// Such incompatible errors cause raising of NSException during encoding or decoding, especially in XPC messages. 34 | /// To avoid this, the method perform manual type-check and converting incompatible errors 35 | /// into most close-to-original but compatible form. 36 | public func secureCodingCompliant() -> Error { 37 | let nsError = self as NSError 38 | guard (try? NSKeyedArchiver.archivedData(withRootObject: nsError, requiringSecureCoding: true)) == nil else { 39 | return self 40 | } 41 | 42 | let compatibleError = NSError( 43 | domain: nsError.domain, 44 | code: nsError.code, 45 | userInfo: nsError.userInfo.mapValues { 46 | if $0 is NSSecureCoding { $0 } else { String(describing: $0) } 47 | } 48 | ) 49 | return compatibleError 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Filesystem & Bundle/Extensions - Bundle.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension Bundle { 26 | /// Bundle name. Value for Info.plist key "CFBundleNameKey". 27 | public var name: String? { stringValue(for: kCFBundleNameKey as String) } 28 | 29 | /// Bundle short version. Value for Info.plist key "CFBundleShortVersionString". 30 | public var shortVersion: String? { stringValue(for: "CFBundleShortVersionString") } 31 | 32 | /// Bundle version. Value for Info.plist key "CFBundleVersion". 33 | public var version: String? { stringValue(for: "CFBundleVersion") } 34 | 35 | private func stringValue(for key: String) -> String? { 36 | object(forInfoDictionaryKey: key) as? String 37 | } 38 | } 39 | 40 | extension Bundle { 41 | /// Searches for given resource inside the bundle and checks if the file exists. 42 | /// Equivalent to Bundle::url(forResource:withExtension) + FileManager::fileExists. 43 | /// - throws: NSError with code NSURLErrorFileDoesNotExist, domain NSURLErrorDomain if file does not exist. 44 | public func existingURL(forResource name: String, withExtension ext: String?) throws -> URL { 45 | guard let url = url(forResource: name, withExtension: ext), 46 | FileManager.default.fileExists(atPath: url.path) 47 | else { 48 | throw NSError( 49 | domain: NSURLErrorDomain, 50 | code: NSURLErrorFileDoesNotExist, 51 | userInfo: [ 52 | NSDebugDescriptionErrorKey: "Resource file \(name) not found.", 53 | ] 54 | ) 55 | } 56 | 57 | return url 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/GUI/RGBColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RGBColor.swift 3 | // SwiftSpellbook 4 | // 5 | // Created by Alkenso (Vladimir Vashurkin) on 2025-01-30. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | #if canImport(CoreGraphics) 11 | 12 | public struct RGBColor: Hashable, Codable, ValueBuilder { 13 | public var red: CGFloat 14 | public var green: CGFloat 15 | public var blue: CGFloat 16 | public var alpha: CGFloat 17 | 18 | public init( 19 | @Clamped(0...1) red: CGFloat, 20 | @Clamped(0...1) green: CGFloat, 21 | @Clamped(0...1) blue: CGFloat, 22 | @Clamped(0...1) alpha: CGFloat = 1.0 23 | ) { 24 | self.red = red 25 | self.green = green 26 | self.blue = blue 27 | self.alpha = alpha 28 | } 29 | } 30 | 31 | extension RGBColor { 32 | public init?(hex hexColor: String, alphaFirst: Bool = false) { 33 | var hexColor = hexColor 34 | if hexColor.hasPrefix("#") { 35 | hexColor.removeFirst() 36 | } 37 | 38 | let hasAlpha = hexColor.count == 8 39 | guard hexColor.count == 6 || hasAlpha else { return nil } 40 | 41 | func popColorComponent() -> CGFloat? { 42 | guard hexColor.count >= 2 else { return nil } 43 | 44 | let to = hexColor.index(hexColor.startIndex, offsetBy: 1) 45 | let str = hexColor[hexColor.startIndex...to] 46 | hexColor.removeFirst(2) 47 | 48 | let int = Int(str, radix: 16) 49 | return int.flatMap { CGFloat($0) / 255 } 50 | } 51 | 52 | var a: CGFloat? 53 | if hasAlpha, alphaFirst { 54 | a = popColorComponent() 55 | } 56 | guard let r = popColorComponent(), 57 | let g = popColorComponent(), 58 | let b = popColorComponent() 59 | else { 60 | return nil 61 | } 62 | if a == nil { 63 | a = popColorComponent() 64 | } 65 | 66 | self.init(red: r, green: g, blue: b, alpha: a ?? 1.0) 67 | } 68 | } 69 | 70 | extension RGBColor { 71 | public init?(cgColor: CGColor) { 72 | guard let rgb = cgColor.converted( 73 | to: CGColorSpaceCreateDeviceRGB(), 74 | intent: .defaultIntent, 75 | options: nil 76 | ) else { return nil } 77 | guard let components = rgb.components, components.count >= 3 else { return nil } 78 | self.init(red: components[0], green: components[1], blue: components[2], alpha: rgb.alpha) 79 | } 80 | 81 | public var cgColor: CGColor { 82 | CGColor(red: red, green: green, blue: blue, alpha: alpha) 83 | } 84 | } 85 | 86 | extension RGBColor { 87 | public static func random( 88 | red: ClosedRange = 0...1, 89 | green: ClosedRange = 0...1, 90 | blue: ClosedRange = 0...1 91 | ) -> RGBColor { 92 | .init(red: .random(in: red), green: .random(in: green), blue: .random(in: blue)) 93 | } 94 | 95 | public static let white = RGBColor(red: 1, green: 1, blue: 1) 96 | public static let black = RGBColor(red: 0, green: 0, blue: 0) 97 | public static func gray(_ value: CGFloat) -> RGBColor { .init(red: value, green: value, blue: value) } 98 | 99 | public func emulatingOpacity(_ opacity: CGFloat) -> RGBColor { 100 | let newR = red * opacity + (1 - opacity) 101 | let newG = green * opacity + (1 - opacity) 102 | let newB = blue * opacity + (1 - opacity) 103 | return .init(red: newR, green: newG, blue: newB) 104 | } 105 | } 106 | 107 | #if canImport(AppKit) 108 | 109 | import AppKit 110 | 111 | extension RGBColor { 112 | public init(_ nsColor: NSColor) { 113 | self.init( 114 | red: nsColor.redComponent, 115 | green: nsColor.greenComponent, 116 | blue: nsColor.blueComponent, 117 | alpha: nsColor.alphaComponent 118 | ) 119 | } 120 | } 121 | 122 | #endif 123 | 124 | #if canImport(UIKit) 125 | 126 | import UIKit 127 | 128 | extension RGBColor { 129 | public init?(_ uiColor: UIColor) { 130 | self.init(cgColor: uiColor.cgColor) 131 | } 132 | } 133 | 134 | #endif 135 | 136 | #endif 137 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Internal.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2024 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension Optional where Wrapped == DispatchQueue { 26 | @inline(__always) 27 | package func async(flags: DispatchWorkItemFlags = [], execute work: @escaping () -> Void) { 28 | if let self { 29 | self.async(flags: flags, execute: work) 30 | } else { 31 | work() 32 | } 33 | } 34 | 35 | @inline(__always) 36 | package func sync(execute work: () -> R) -> R { 37 | if let self { 38 | return self.sync(execute: work) 39 | } else { 40 | return work() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Low Level/AuditToken.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | #if os(macOS) 24 | 25 | import Foundation 26 | @_implementationOnly import _SpellbookFoundationObjC 27 | 28 | extension audit_token_t { 29 | /// Returns current task audit token. 30 | public static func current() throws -> audit_token_t { 31 | try audit_token_t(task: mach_task_self_) 32 | } 33 | 34 | /// Returns task for pid. 35 | public init(pid: pid_t) throws { 36 | let taskName = try NSError.mach.try { task_name_for_pid(mach_task_self_, pid, $0) } 37 | try self.init(task: taskName) 38 | } 39 | 40 | /// Returns task audit token. 41 | public init(task: task_name_t) throws { 42 | var size = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) 43 | self = try NSError.mach 44 | .debugDescription("Failed to get audit_token for task = \(task) using task_info()") 45 | .try { (ptr: UnsafeMutablePointer) in 46 | ptr.withMemoryRebound(to: integer_t.self, capacity: 0) { 47 | task_info(task, task_flavor_t(TASK_AUDIT_TOKEN), $0, &size) 48 | } 49 | } 50 | } 51 | } 52 | 53 | 54 | extension audit_token_t { 55 | public var auid: uid_t { audit_token_to_auid(self) } 56 | public var euid: uid_t { audit_token_to_euid(self) } 57 | public var egid: gid_t { audit_token_to_egid(self) } 58 | public var ruid: uid_t { audit_token_to_ruid(self) } 59 | public var rgid: gid_t { audit_token_to_rgid(self) } 60 | public var pid: pid_t { audit_token_to_pid(self) } 61 | public var asid: au_asid_t { audit_token_to_asid(self) } 62 | public var pidversion: Int32 { audit_token_to_pidversion(self) } 63 | } 64 | 65 | extension audit_token_t: SpellbookFoundation.SafePOD, SpellbookFoundation.UnsafePOD {} 66 | 67 | extension NSXPCConnection { 68 | public var auditToken: audit_token_t { 69 | SpellbookObjC.nsxpcConnection_auditToken(self) 70 | } 71 | } 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Low Level/BridgedCEnum.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2024 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public protocol BridgedCEnum: RawRepresentable where RawValue == UInt32 { 26 | init(_ rawValue: RawValue) 27 | init(rawValue: RawValue) 28 | var rawValue: RawValue { get set } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Low Level/MachTime.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | // MARK: - Mach Time 26 | 27 | extension TimeInterval { 28 | /// Converts mach_time into TimeInterval using mach_timebase_info. 29 | /// Returns nil if mach_timebase_info fails. 30 | public init?(machTime: UInt64) { 31 | guard let timebase = try? mach_timebase_info.system() else { return nil } 32 | self.init(machTime: machTime, timebase: timebase) 33 | } 34 | 35 | public init(machTime: UInt64, timebase: mach_timebase_info) { 36 | let nanos = TimeInterval(machTime * UInt64(timebase.numer)) / TimeInterval(timebase.denom) 37 | self = nanos / TimeInterval(NSEC_PER_SEC) 38 | } 39 | } 40 | 41 | extension Date { 42 | public init?(machTime: UInt64) { 43 | guard let machSeconds = TimeInterval(machTime: machTime) else { return nil } 44 | self = ProcessInfo.processInfo.systemBootDate.addingTimeInterval(machSeconds) 45 | } 46 | 47 | public var machTime: UInt64? { 48 | guard let timebase = try? mach_timebase_info.system() else { return nil } 49 | 50 | let seconds = timeIntervalSince(ProcessInfo.processInfo.systemBootDate) 51 | let nanos = seconds * TimeInterval(NSEC_PER_SEC) 52 | let machTime = nanos * TimeInterval(timebase.denom) / TimeInterval(timebase.numer) 53 | return UInt64(machTime) 54 | } 55 | } 56 | 57 | extension mach_timebase_info { 58 | public static func system() throws -> mach_timebase_info { 59 | var info = mach_timebase_info() 60 | let kernReturn = mach_timebase_info(&info) 61 | if kernReturn == KERN_SUCCESS { 62 | return info 63 | } else { 64 | throw NSError( 65 | domain: NSMachErrorDomain, 66 | code: Int(kernReturn), 67 | userInfo: [ 68 | NSDebugDescriptionErrorKey: "mach_timebase_info fails with result \(kernReturn)", 69 | ] 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Low Level/Unsafe.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// Checks if memory of instances is equal. Generic version of memcmp. 26 | public func unsafeMemoryEquals(_ lhs: T, _ rhs: T) -> Bool { 27 | withUnsafeBytes(of: lhs) { lhsBuffer in 28 | withUnsafeBytes(of: rhs) { rhsBuffer in 29 | lhsBuffer.elementsEqual(rhsBuffer) 30 | } 31 | } 32 | } 33 | 34 | public protocol CPointer { 35 | /// Returns true is pointer is 0x0, false otherwise. 36 | var isNull: Bool { get } 37 | } 38 | 39 | public extension CPointer { 40 | /// Returns nil instead self if pointer is null. 41 | var nullable: Self? { isNull ? nil : self } 42 | } 43 | 44 | extension UnsafePointer: CPointer { 45 | public var isNull: Bool { self == Self(bitPattern: 0) } 46 | } 47 | 48 | extension UnsafeRawPointer: CPointer { 49 | public var isNull: Bool { self == Self(bitPattern: 0) } 50 | } 51 | 52 | extension UnsafeBufferPointer: CPointer { 53 | public var isNull: Bool { baseAddress == nil } 54 | } 55 | 56 | extension UnsafeRawBufferPointer: CPointer { 57 | public var isNull: Bool { baseAddress == nil } 58 | } 59 | 60 | extension UnsafeMutablePointer: CPointer { 61 | public var isNull: Bool { self == Self(bitPattern: 0) } 62 | } 63 | 64 | extension UnsafeMutableRawPointer: CPointer { 65 | public var isNull: Bool { self == Self(bitPattern: 0) } 66 | } 67 | 68 | extension UnsafeMutableBufferPointer: CPointer { 69 | public var isNull: Bool { baseAddress == nil } 70 | } 71 | 72 | extension UnsafeMutableRawBufferPointer: CPointer { 73 | public var isNull: Bool { baseAddress == nil } 74 | } 75 | 76 | extension AutoreleasingUnsafeMutablePointer: CPointer { 77 | public var isNull: Bool { self == Self(bitPattern: 0) } 78 | } 79 | 80 | public extension UnsafeMutablePointer { 81 | func bzero() { 82 | UnsafeMutableRawPointer(self).bzero(MemoryLayout.stride) 83 | } 84 | } 85 | 86 | public extension UnsafeMutableRawPointer { 87 | func bzero(_ size: Int) { 88 | Darwin.bzero(self, size) 89 | } 90 | } 91 | 92 | public extension UnsafeMutableBufferPointer { 93 | func bzero() { 94 | UnsafeMutableRawBufferPointer(self).bzero() 95 | } 96 | } 97 | 98 | public extension UnsafeMutableRawBufferPointer { 99 | func bzero() { 100 | baseAddress?.bzero(count) 101 | } 102 | } 103 | 104 | public extension AutoreleasingUnsafeMutablePointer { 105 | func bzero() { 106 | UnsafeMutablePointer(mutating: self).bzero() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/System & Hardware/DeviceInfo.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public enum DeviceInfo {} 26 | 27 | // MARK: - macOS 28 | 29 | #if os(macOS) 30 | public extension DeviceInfo { 31 | /// Machine's UUID. Same as 'Hardware UUID' found in 'About this Mac'. 32 | static func hardwareUUID() throws -> UUID { 33 | let property = try search(property: kIOPlatformUUIDKey) 34 | guard let uuid = UUID(uuidString: property) else { 35 | throw IOKitError( 36 | .badArgument, 37 | userInfo: [NSDebugDescriptionErrorKey: "Failed to create UUID from string \"\(property)\""] 38 | ) 39 | } 40 | 41 | return uuid 42 | } 43 | 44 | /// Machine's serial number. Same as 'Serial Number' found in 'About this Mac'. 45 | static func serialNumber() throws -> String { 46 | try search(property: kIOPlatformSerialNumberKey) 47 | } 48 | 49 | private static func search(property name: String) throws -> String { 50 | let platformExpert = IOServiceGetMatchingService( 51 | kIOMasterPortDefault, 52 | IOServiceMatching("IOPlatformExpertDevice") 53 | ) 54 | guard platformExpert != IO_OBJECT_NULL else { 55 | throw IOKitError( 56 | .noDevice, 57 | userInfo: [NSDebugDescriptionErrorKey: "IOServiceGetMatchingService: failed to match IOPlatformExpertDevice"] 58 | ) 59 | } 60 | 61 | let property = IORegistryEntryCreateCFProperty( 62 | platformExpert, 63 | name as CFString, 64 | kCFAllocatorDefault, 65 | 0 66 | ) 67 | 68 | guard let value = property?.takeRetainedValue() as? String else { 69 | throw IOKitError( 70 | .notFound, 71 | userInfo: [NSDebugDescriptionErrorKey: "IORegistryEntryCreateCFProperty: failed to get property \(name)"] 72 | ) 73 | } 74 | 75 | return value 76 | } 77 | } 78 | #endif 79 | 80 | // MARK: - iOS 81 | 82 | #if os(iOS) 83 | extension DeviceInfo { 84 | /// Models: https://gist.github.com/adamawolf/3048717 85 | public static var modelName: String { 86 | var systemInfo = utsname() 87 | uname(&systemInfo) 88 | 89 | let machineMirror = Mirror(reflecting: systemInfo.machine) 90 | let identifier = machineMirror.children.reduce("") { identifier, element in 91 | guard let value = element.value as? Int8, value != 0 else { return identifier } 92 | return identifier + String(UnicodeScalar(UInt8(value))) 93 | } 94 | return identifier 95 | } 96 | } 97 | #endif 98 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/System & Hardware/Extensions - ProcessInfo.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension ProcessInfo { 26 | public var systemBootDate: Date { 27 | Date().addingTimeInterval(-systemUptime) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/Atomic.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// Atomic property wrapper is designed to simple & safe operations of 26 | /// getting / setting particular value. 27 | /// For reacher thread-safe functionality consider using 'Synchronized' class. 28 | @propertyWrapper 29 | public final class Atomic { 30 | private let storage: Synchronized 31 | 32 | public convenience init(wrappedValue: Value, _ primitive: Synchronized.Primitive = .unfair) { 33 | self.init(storage: .init(primitive, wrappedValue)) 34 | } 35 | 36 | public init(storage: Synchronized) { 37 | self.storage = storage 38 | } 39 | 40 | public var wrappedValue: Value { 41 | get { storage.read() } 42 | set { storage.write { $0 = newValue } } 43 | } 44 | 45 | public var projectedValue: Synchronized { storage } 46 | 47 | public func exchange(_ value: Value) -> Value { 48 | storage.write { updateSwap(&$0, value) } 49 | } 50 | } 51 | 52 | extension Atomic where Value: AdditiveArithmetic { 53 | public func increment(by diff: Value) { 54 | storage.write { $0 += diff } 55 | } 56 | } 57 | 58 | public final class AtomicFlag { 59 | private let pointer: UnsafeMutablePointer 60 | 61 | public init() { 62 | self.pointer = .allocate(capacity: 1) 63 | pointer.pointee = atomic_flag() 64 | } 65 | 66 | deinit { 67 | pointer.deinitialize(count: 1) 68 | pointer.deallocate() 69 | } 70 | 71 | public func testAndSet() -> Bool { 72 | atomic_flag_test_and_set(pointer) 73 | } 74 | 75 | public func clear() { 76 | atomic_flag_clear(pointer) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/ConcurrentBlockOperation.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public final class ConcurrentBlockOperation: Operation, @unchecked Sendable { 26 | @Atomic private var state: Bool? 27 | private let block: (ValueView, @escaping () -> Void) -> Void 28 | 29 | public init(block: @escaping (_ isCancelled: ValueView, _ completion: @escaping () -> Void) -> Void) { 30 | self.block = block 31 | } 32 | 33 | public init(block: @escaping (_ isCancelled: ValueView) async -> Void) { 34 | self.block = { isCancelled, completion in 35 | Task.detached { 36 | await block(isCancelled) 37 | completion() 38 | } 39 | } 40 | } 41 | 42 | public override var isExecuting: Bool { 43 | state == false 44 | } 45 | 46 | public override var isFinished: Bool { 47 | state == true 48 | } 49 | 50 | public override var isAsynchronous: Bool { true } 51 | 52 | public override func start() { 53 | guard !isCancelled else { 54 | finish() 55 | return 56 | } 57 | 58 | willChangeValue(for: \.isExecuting) 59 | state = false 60 | didChangeValue(for: \.isExecuting) 61 | 62 | main() 63 | } 64 | 65 | public override func main() { 66 | block(.init { self.isCancelled }, finish) 67 | } 68 | 69 | private func finish() { 70 | willChangeValue(for: \.isExecuting) 71 | willChangeValue(for: \.isFinished) 72 | state = true 73 | didChangeValue(for: \.isExecuting) 74 | didChangeValue(for: \.isFinished) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/Extensions - DispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension DispatchQueue { 26 | /// Schedule some work to be executed on queue. 27 | /// Cancels previous execution block if it not running yet. 28 | public func debounce(with context: DebounceContext, execute: @escaping () -> Void) { 29 | context.schedule(on: self, execute: execute) 30 | } 31 | } 32 | 33 | public class DebounceContext { 34 | private let delay: TimeInterval 35 | @Atomic private var currentTask: DispatchWorkItem? 36 | 37 | public init(delay: TimeInterval) { 38 | self.delay = delay 39 | } 40 | 41 | public func schedule(on queue: DispatchQueue, execute: @escaping () -> Void) { 42 | let task = DispatchWorkItem(block: execute) 43 | let previousTask = _currentTask.exchange(task) 44 | previousTask?.cancel() 45 | queue.asyncAfter(deadline: .now() + delay, execute: task) 46 | } 47 | 48 | public func cancel() { 49 | _currentTask.exchange(nil)?.cancel() 50 | } 51 | } 52 | 53 | extension DispatchQueue { 54 | public func asyncAfter( 55 | delay: TimeInterval, 56 | qos: DispatchQoS = .unspecified, 57 | flags: DispatchWorkItemFlags = [], 58 | execute work: @escaping () -> Void 59 | ) { 60 | asyncAfter(deadline: .now() + delay, qos: qos, flags: flags, execute: work) 61 | } 62 | 63 | public func asyncAfter(delay: TimeInterval, execute: DispatchWorkItem) { 64 | asyncAfter(deadline: .now() + delay, execute: execute) 65 | } 66 | 67 | public func asyncPeriodically( 68 | interval: TimeInterval, 69 | immediately: Bool, 70 | qos: DispatchQoS = .unspecified, 71 | flags: DispatchWorkItemFlags = [], 72 | execute: @escaping () -> Bool 73 | ) { 74 | func schedule(firstRun: Bool) { 75 | asyncAfter(delay: (firstRun && immediately) ? 0 : interval, qos: qos, flags: flags) { 76 | if execute() { 77 | schedule(firstRun: false) 78 | } 79 | } 80 | } 81 | schedule(firstRun: true) 82 | } 83 | 84 | public func asyncPeriodically( 85 | interval: TimeInterval, 86 | immediately: Bool, 87 | qos: DispatchQoS = .unspecified, 88 | flags: DispatchWorkItemFlags = [], 89 | execute: @escaping (@escaping () -> Void) -> Void 90 | ) { 91 | func schedule(firstRun: Bool) { 92 | asyncAfter(delay: (firstRun && immediately) ? 0 : interval, qos: qos, flags: flags) { 93 | execute { 94 | schedule(firstRun: false) 95 | } 96 | } 97 | } 98 | schedule(firstRun: true) 99 | } 100 | } 101 | 102 | extension DispatchQueue { 103 | /// Performs `work` on the main thread. 104 | /// Usual `sync` method with check that the caller context is already main queue. 105 | public static func syncOnMain(execute work: () throws -> T) rethrows -> T { 106 | if Thread.isMainThread { 107 | return try work() 108 | } else { 109 | return try main.sync(execute: work) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/Extensions - Locks.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | extension NSLocking { 26 | public func withLock(_ body: () throws -> R) rethrows -> R { 27 | lock() 28 | defer { unlock() } 29 | return try body() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/Extensions - Task.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Combine 24 | import Foundation 25 | 26 | extension Task { 27 | public static func runWithCompletion( 28 | _: R.Type = R.self, 29 | receiveOn queue: DispatchQueue? = nil, 30 | _ body: @escaping () async throws -> R, 31 | completion: @escaping (Result) -> Void 32 | ) where Success == Void, Failure == Never { 33 | Task { 34 | let result: Result 35 | do { 36 | result = .success(try await body()) 37 | } catch { 38 | result = .failure(error) 39 | } 40 | queue.async { completion(result) } 41 | } 42 | } 43 | 44 | public static func runWithCompletion( 45 | receiveOn queue: DispatchQueue? = nil, 46 | _ body: @escaping () async -> R, 47 | completion: @escaping (R) -> Void 48 | ) where Success == Void, Failure == Never { 49 | Task { 50 | let result = await body() 51 | queue.async { completion(result) } 52 | } 53 | } 54 | 55 | public static func runWithCompletion( 56 | receiveOn queue: DispatchQueue? = nil, 57 | _ body: @escaping () async throws -> Void, 58 | completion: @escaping (Error?) -> Void 59 | ) where Success == Void, Failure == Never { 60 | runWithCompletion(receiveOn: queue, body) { 61 | completion($0.failure) 62 | } 63 | } 64 | } 65 | 66 | extension Task where Success == Never, Failure == Never { 67 | public static func sleep(forTimeInterval interval: TimeInterval) async throws { 68 | try await sleep(nanoseconds: UInt64(interval * TimeInterval(NSEC_PER_SEC))) 69 | } 70 | 71 | @available(macOS, deprecated: 13.0, message: "Use `sleep(for:)` method instead") 72 | @available(iOS, deprecated: 16.0, message: "Use `sleep(for:)` method instead") 73 | @available(watchOS, deprecated: 9.0, message: "Use `sleep(for:)` method instead") 74 | @available(tvOS, deprecated: 16.0, message: "Use `sleep(for:)` method instead") 75 | public static func sleep(seconds duration: Int) async throws { 76 | try await sleep(nanoseconds: UInt64(duration) * NSEC_PER_SEC) 77 | } 78 | 79 | @available(macOS, deprecated: 13.0, message: "Use `sleep(for:)` method instead") 80 | @available(iOS, deprecated: 16.0, message: "Use `sleep(for:)` method instead") 81 | @available(watchOS, deprecated: 9.0, message: "Use `sleep(for:)` method instead") 82 | @available(tvOS, deprecated: 16.0, message: "Use `sleep(for:)` method instead") 83 | public static func sleep(milliseconds duration: Int) async throws { 84 | try await sleep(nanoseconds: UInt64(duration) * NSEC_PER_MSEC) 85 | } 86 | } 87 | 88 | extension Task: Combine.Cancellable {} 89 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/PosixLocks.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// Swift-safe wrapper around `os_unfair_lock`. 26 | /// More explanation at [OSAllocatedUnfairLock](https://developer.apple.com/documentation/os/osallocatedunfairlock) 27 | @available(macOS, deprecated: 13.0, message: "Use `OSAllocatedUnfairLock` lock") 28 | @available(iOS, deprecated: 16.0, message: "Use `OSAllocatedUnfairLock` lock") 29 | @available(watchOS, deprecated: 9.0, message: "Use `OSAllocatedUnfairLock` lock") 30 | @available(tvOS, deprecated: 16.0, message: "Use `OSAllocatedUnfairLock` lock") 31 | public final class UnfairLock: @unchecked Sendable { 32 | private let raw: UnsafeMutablePointer 33 | 34 | public init() { 35 | self.raw = UnsafeMutablePointer.allocate(capacity: 1) 36 | raw.initialize(to: os_unfair_lock()) 37 | } 38 | 39 | deinit { 40 | raw.deallocate() 41 | } 42 | 43 | @available(*, noasync) 44 | public func lock() { 45 | os_unfair_lock_lock(raw) 46 | } 47 | 48 | @available(*, noasync) 49 | public func tryLock() -> Bool { 50 | os_unfair_lock_trylock(raw) 51 | } 52 | 53 | @available(*, noasync) 54 | public func unlock() { 55 | os_unfair_lock_unlock(raw) 56 | } 57 | 58 | public func withLock(_ body: () throws -> R) rethrows -> R { 59 | lock() 60 | defer { unlock() } 61 | return try body() 62 | } 63 | } 64 | 65 | /// Swift-safe wrapper around `pthread_rwlock_t`. 66 | /// More explanation at [OSAllocatedUnfairLock](https://developer.apple.com/documentation/os/osallocatedunfairlock) 67 | public final class RWLock: @unchecked Sendable { 68 | private let raw: UnsafeMutablePointer 69 | 70 | public init(attrs: UnsafePointer? = nil) { 71 | self.raw = UnsafeMutablePointer.allocate(capacity: 1) 72 | guard pthread_rwlock_init(raw, attrs) == 0 else { fatalError("Failed to pthread_rwlock_init") } 73 | } 74 | 75 | deinit { 76 | raw.deallocate() 77 | } 78 | 79 | @available(*, noasync) 80 | public func readLock() { 81 | pthread_rwlock_rdlock(raw) 82 | } 83 | 84 | @available(*, noasync) 85 | public func tryReadLock() -> Bool { 86 | pthread_rwlock_tryrdlock(raw) == 0 87 | } 88 | 89 | @available(*, noasync) 90 | public func writeLock() { 91 | pthread_rwlock_wrlock(raw) 92 | } 93 | 94 | @available(*, noasync) 95 | public func tryWriteLock() -> Bool { 96 | pthread_rwlock_trywrlock(raw) == 0 97 | } 98 | 99 | @available(*, noasync) 100 | public func unlock() { 101 | pthread_rwlock_unlock(raw) 102 | } 103 | 104 | public func withReadLock(_ body: () throws -> R) rethrows -> R { 105 | readLock() 106 | defer { unlock() } 107 | return try body() 108 | } 109 | 110 | public func withWriteLock(_ body: () throws -> R) rethrows -> R { 111 | writeLock() 112 | defer { unlock() } 113 | return try body() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Threading & Concurrency/SynchronizedObjC.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// Swift replacement of ObjC @synchronized(...) approach. 26 | /// Benchmarks shows this approach: 27 | /// - faster than `DispatchQueue` 28 | /// - slower than `os_unfair_lock` 29 | @inline(__always) 30 | public func synchronized(_ obj: AnyObject, _ body: () throws -> R) rethrows -> R { 31 | objc_sync_enter(obj) 32 | defer { objc_sync_exit(obj) } 33 | return try body() 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Types & PropertyWrappers/Boxing.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public struct Weak { 26 | public weak var value: Value? 27 | 28 | public init(_ value: Value?) { 29 | self.value = value 30 | } 31 | } 32 | 33 | public struct Unowned { 34 | public unowned var value: Value? 35 | 36 | public init(_ value: Value?) { 37 | self.value = value 38 | } 39 | } 40 | 41 | @dynamicMemberLookup 42 | public final class Box { 43 | public var value: Value 44 | 45 | public init(_ value: Value) { 46 | self.value = value 47 | } 48 | 49 | public subscript(dynamicMember keyPath: KeyPath) -> Property { 50 | value[keyPath: keyPath] 51 | } 52 | 53 | public subscript(dynamicMember keyPath: WritableKeyPath) -> Property { 54 | get { value[keyPath: keyPath] } 55 | set { value[keyPath: keyPath] = newValue } 56 | } 57 | } 58 | 59 | extension Box { 60 | public convenience init() where Value == T? { 61 | self.init(nil) 62 | } 63 | } 64 | 65 | extension Box: Equatable where Value: Equatable { 66 | public static func == (lhs: Box, rhs: Box) -> Bool { 67 | lhs.value == rhs.value 68 | } 69 | } 70 | 71 | extension Box: Hashable where Value: Hashable { 72 | public func hash(into hasher: inout Hasher) { 73 | value.hash(into: &hasher) 74 | } 75 | } 76 | 77 | extension Box: Identifiable where Value: Identifiable { 78 | public var id: Value.ID { value.id } 79 | } 80 | 81 | public typealias WeakBox = Box> 82 | 83 | @propertyWrapper 84 | public enum Indirect { 85 | indirect case wrappedValue(Value) 86 | 87 | public init(wrappedValue: Value) { 88 | self = .wrappedValue(wrappedValue) 89 | } 90 | 91 | public var wrappedValue: Value { 92 | get { switch self { case .wrappedValue(let value): return value } } 93 | set { self = .wrappedValue(newValue) } 94 | } 95 | } 96 | 97 | extension Indirect: Equatable where Value: Equatable {} 98 | extension Indirect: Hashable where Value: Hashable {} 99 | extension Indirect: Encodable, PropertyWrapperEncodable where Value: Encodable {} 100 | extension Indirect: Decodable, PropertyWrapperDecodable where Value: Decodable {} 101 | 102 | extension Indirect: Identifiable where Value: Identifiable { 103 | public var id: Value.ID { wrappedValue.id } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Types & PropertyWrappers/Closure.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | /// Wrapper around the closure that allows extending. 26 | /// Design follows 'Decorator' pattern. 27 | public struct Closure { 28 | public let body: (T) -> R 29 | 30 | public init(_ body: @escaping (T) -> R) { 31 | self.body = body 32 | } 33 | 34 | public func callAsFunction(_ args: repeat each Arg) -> R where T == (repeat each Arg) { 35 | body(makeTuple(repeat each args)) 36 | } 37 | } 38 | 39 | extension Closure { 40 | public func oneShot() -> Self where R == Void { 41 | let once = AtomicFlag() 42 | return Self { result in 43 | if !once.testAndSet() { 44 | self(result) 45 | } 46 | } 47 | } 48 | 49 | public func sync(on queue: DispatchQueue) -> Self { 50 | Self { result in queue.sync { self(result) } } 51 | } 52 | 53 | public func async(on queue: DispatchQueue) -> Self where R == Void { 54 | Self { result in queue.async { self(result) } } 55 | } 56 | } 57 | 58 | /// Throwing version of `Closure` 59 | public struct ThrowingClosure { 60 | public let body: (T) throws -> R 61 | 62 | public init(_ body: @escaping (T) throws -> R) { 63 | self.body = body 64 | } 65 | 66 | public func callAsFunction(_ args: repeat each Arg) throws -> R where T == (repeat each Arg) { 67 | try body(makeTuple(repeat each args)) 68 | } 69 | } 70 | 71 | extension ThrowingClosure { 72 | public func oneShot() -> Self where R == Void { 73 | let once = AtomicFlag() 74 | return ThrowingClosure { value in 75 | if !once.testAndSet() { 76 | try self(value) 77 | } 78 | } 79 | } 80 | 81 | public func sync(on queue: DispatchQueue) -> Self { 82 | ThrowingClosure { value in try queue.sync { try self(value) } } 83 | } 84 | } 85 | 86 | // Workaround to make Swift 5.9/5.10 compilers happy. 87 | private func makeTuple(_ element: repeat each Element) -> (repeat each Element) { 88 | (repeat each element) 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/Types & PropertyWrappers/Refreshable.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | @propertyWrapper 26 | public struct Refreshable { 27 | private var innerValue: Value? { didSet { expiration.onUpdate(innerValue!) } } 28 | private let expiration: Expiration 29 | private let source: Source 30 | 31 | public init(wrappedValue: Value? = nil, expire: Expiration, source: Source) { 32 | innerValue = wrappedValue 33 | expiration = expire 34 | self.source = source 35 | 36 | if let innerValue = innerValue { 37 | expiration.onUpdate(innerValue) 38 | } 39 | } 40 | 41 | public var wrappedValue: Value { 42 | mutating get { 43 | if let innerValue = innerValue { 44 | if expiration.checkExpired(innerValue) { 45 | self.innerValue = source.newValue(innerValue) 46 | } 47 | } else { 48 | innerValue = source.newValue(nil) 49 | } 50 | return innerValue! 51 | } 52 | set { 53 | innerValue = newValue 54 | } 55 | } 56 | } 57 | 58 | extension Refreshable { 59 | public init(wrappedValue: Value? = nil, expire: Expiration) where Value: ExpressibleByNilLiteral { 60 | self.init(wrappedValue: wrappedValue, expire: expire, source: .defaultNil()) 61 | } 62 | } 63 | 64 | extension Refreshable { 65 | public struct Expiration { 66 | public var checkExpired: (Value) -> Bool 67 | public var onUpdate: (Value) -> Void 68 | 69 | public init(checkExpired: @escaping (Value) -> Bool, onUpdate: @escaping (Value) -> Void) { 70 | self.checkExpired = checkExpired 71 | self.onUpdate = onUpdate 72 | } 73 | } 74 | 75 | public struct Source { 76 | public var newValue: (_ old: Value?) -> Value 77 | 78 | public init(newValue: @escaping (Value?) -> Value) { 79 | self.newValue = newValue 80 | } 81 | } 82 | } 83 | 84 | extension Refreshable.Expiration { 85 | public static func ttl(_ duration: TimeInterval) -> Self { 86 | var expirationDate = Date() 87 | return .init( 88 | checkExpired: { _ in expirationDate < Date() }, 89 | onUpdate: { _ in expirationDate = Date().addingTimeInterval(duration) } 90 | ) 91 | } 92 | } 93 | 94 | extension Refreshable.Source { 95 | public static func defaultValue(_ defaultValue: Value) -> Self { 96 | .init(newValue: { _ in defaultValue }) 97 | } 98 | 99 | public static func defaultNil() -> Self where Value: ExpressibleByNilLiteral { 100 | .init(newValue: { _ in nil }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/ValueObserving/EventNotify.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public final class EventNotify: ValueObserving { 26 | private typealias Handler = (T, Any?) -> Void 27 | 28 | private let lock = NSRecursiveLock() 29 | private var subscriptions: [UUID: Handler] = [:] 30 | private var lastValue: T? 31 | 32 | /// Queue to be used to notify subscribers. If not set, it will be notified on caller thread. 33 | public var notifyQueue: DispatchQueue? 34 | 35 | /// Create EventNotify. 36 | public convenience init() { 37 | self.init(initialValue: nil) 38 | } 39 | 40 | /// Create EventNotify with initial value. When new subscriber makes a subscription, 41 | /// `EventNotify` will use the `initialValue` or last value passed to `notify` method 42 | /// to notify the subscriber immediately. 43 | public init(initialValue: T? = nil) { 44 | self.lastValue = initialValue 45 | } 46 | 47 | public var value: T? { lock.withLock { lastValue } } 48 | 49 | public func subscribe( 50 | suppressInitialNotify: Bool, 51 | receiveValue: @escaping (T, _ context: Any?) -> Void 52 | ) -> SubscriptionToken { 53 | let id = UUID() 54 | lock.withLock { 55 | subscriptions[id] = receiveValue 56 | if let lastValue, !suppressInitialNotify { 57 | notifyOne(lastValue, nil, action: receiveValue) 58 | } 59 | } 60 | return .init { [weak self] in 61 | guard let self else { return } 62 | self.lock.withLock { _ = self.subscriptions.removeValue(forKey: id) } 63 | } 64 | } 65 | 66 | public func notify(_ value: T, context: Any? = nil) { 67 | let subscriptions = lock.withLock { 68 | lastValue = value 69 | return Array(self.subscriptions.values) 70 | } 71 | subscriptions.forEach { notifyOne(value, context, action: $0) } 72 | } 73 | 74 | private func notifyOne(_ value: T, _ context: Any?, action: @escaping (T, Any?) -> Void) { 75 | if let notifyQueue = notifyQueue { 76 | notifyQueue.async { action(value, context) } 77 | } else { 78 | action(value, context) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/ValueObserving/ValueObservable.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | @propertyWrapper 26 | public final class ValueObserved { 27 | private let observable: ValueObservable 28 | 29 | public init(wrappedValue: Value) { 30 | self.observable = .constant(wrappedValue) 31 | } 32 | 33 | public init(observable: ValueObservable) { 34 | self.observable = observable 35 | } 36 | 37 | public var wrappedValue: Value { observable.value } 38 | public var projectedValue: ValueObservable { observable } 39 | } 40 | 41 | 42 | @dynamicMemberLookup 43 | public final class ValueObservable: ValueObserving { 44 | private let subscribeReceive: (Bool, @escaping (Value, Any?) -> Void) -> SubscriptionToken 45 | 46 | public init( 47 | view: ValueView, 48 | subscribeReceiveValue: @escaping (Bool, @escaping (Value, _ context: Any?) -> Void) -> SubscriptionToken 49 | ) { 50 | self._value = .init(view) 51 | self.subscribeReceive = subscribeReceiveValue 52 | } 53 | 54 | @ValueViewed public var value: Value 55 | 56 | public subscript(dynamicMember keyPath: KeyPath) -> Property { 57 | value[keyPath: keyPath] 58 | } 59 | 60 | public func subscribe( 61 | suppressInitialNotify: Bool, 62 | receiveValue: @escaping (Value, _ context: Any?) -> Void 63 | ) -> SubscriptionToken { 64 | subscribeReceive(suppressInitialNotify, receiveValue) 65 | } 66 | } 67 | 68 | extension ValueObservable { 69 | public func scope(_ keyPath: KeyPath) -> ValueObservable { 70 | scope { $0[keyPath: keyPath] } 71 | } 72 | 73 | public func scope(_ transform: @escaping (Value) -> U) -> ValueObservable { 74 | ValueObservable( 75 | view: .init { transform(self.value) }, 76 | subscribeReceiveValue: { suppressInitialNotify, localNotify in 77 | self.subscribe(suppressInitialNotify: suppressInitialNotify) { globalValue, context in 78 | localNotify(transform(globalValue), context) 79 | } 80 | } 81 | ) 82 | } 83 | 84 | public func optional() -> ValueObservable { 85 | .init(view: .init { self.value }) { suppressInitialNotify, receiveValue in 86 | self.subscribe(suppressInitialNotify: suppressInitialNotify) { receiveValue($0, $1) } 87 | } 88 | } 89 | 90 | public func unwrapped(default defaultValue: U) -> ValueObservable where Value == U? { 91 | .init(view: .init { self.value ?? defaultValue }) { suppressInitialNotify, receiveValue in 92 | self.subscribe(suppressInitialNotify: suppressInitialNotify) { receiveValue($0 ?? defaultValue, $1) } 93 | } 94 | } 95 | } 96 | 97 | extension ValueObservable { 98 | public static func constant(_ value: Value) -> ValueObservable { 99 | .init(view: .constant(value)) { suppressInitialNotiry, receiveValue in 100 | if !suppressInitialNotiry { 101 | receiveValue(value, nil) 102 | } 103 | return .init {} 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundation/ValueObserving/ValueView.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2024 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | @propertyWrapper 26 | public final class ValueViewed { 27 | private var view: ValueView 28 | 29 | public init(_ view: ValueView) { 30 | self.view = view 31 | } 32 | 33 | public convenience init(wrappedValue: @autoclosure @escaping () -> Value) { 34 | self.init(.init(wrappedValue)) 35 | } 36 | 37 | public var wrappedValue: Value { view.value } 38 | public var projectedValue: ValueView { view } 39 | 40 | public func unsafeSetView(_ view: ValueView) { self.view = view } 41 | } 42 | 43 | /// Wrapper that provides access to value. Useful when value is a struct that may be changed over time. 44 | @dynamicMemberLookup 45 | public final class ValueView { 46 | private var accessor: () -> Value 47 | 48 | public init(_ accessor: @escaping () -> Value) { 49 | self.accessor = accessor 50 | } 51 | 52 | public var value: Value { accessor() } 53 | 54 | public subscript(dynamicMember keyPath: KeyPath) -> Property { 55 | value[keyPath: keyPath] 56 | } 57 | 58 | public func unsafeSetAccessor(_ accessor: @escaping () -> Value) { self.accessor = accessor } 59 | } 60 | 61 | extension ValueView { 62 | public static func constant(_ value: Value) -> ValueView { 63 | .init { value } 64 | } 65 | 66 | public static func weak(_ value: U?) -> ValueView { 67 | .init { [weak value] in value } 68 | } 69 | 70 | // public subscript(dynamicMember keyPath: KeyPath) -> Property? where Value == U? { 71 | // wrappedValue?[keyPath: keyPath] 72 | // } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundationObjC/SpellbookObjC.h: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | #import 24 | #import 25 | 26 | NS_ASSUME_NONNULL_BEGIN 27 | 28 | @interface SpellbookObjC : NSObject 29 | 30 | + (nullable NSException *)NSException_catching:(void(NS_NOESCAPE ^)(void))block; 31 | + (nullable NSString *)CppException_catching:(void(NS_NOESCAPE ^)(void))block; 32 | 33 | + (void) __attribute__((noreturn)) throwCppRuntineErrorException:(NSString *)reason; 34 | 35 | #if TARGET_OS_OSX == 1 36 | + (audit_token_t)NSXPCConnection_auditToken:(NSXPCConnection *)connection; 37 | #endif 38 | 39 | @end 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /Sources/SpellbookFoundationObjC/SpellbookObjC.mm: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | #import "SpellbookObjC.h" 24 | 25 | #include 26 | 27 | #if TARGET_OS_OSX == 1 28 | @interface NSXPCConnection (SpellbookPrivate) 29 | @property (nonatomic, readonly) audit_token_t auditToken; 30 | @end 31 | #endif 32 | 33 | @implementation SpellbookObjC 34 | 35 | + (nullable NSException *)NSException_catching:(void(NS_NOESCAPE ^)(void))block 36 | { 37 | @try 38 | { 39 | block(); 40 | return nil; 41 | } 42 | @catch (NSException *exception) 43 | { 44 | return exception; 45 | } 46 | } 47 | 48 | + (nullable NSString *)CppException_catching:(void(NS_NOESCAPE ^)(void))block 49 | { 50 | try 51 | { 52 | block(); 53 | return nil; 54 | } 55 | catch (const std::exception& ex) 56 | { 57 | const char* reason = ex.what(); 58 | return reason ? @(reason) : @"unknown std::exception"; 59 | } 60 | catch (...) 61 | { 62 | return @"unknown C++ exception"; 63 | } 64 | } 65 | 66 | + (void)throwCppRuntineErrorException:(NSString *)reason 67 | { 68 | throw std::runtime_error(reason.UTF8String ?: ""); 69 | } 70 | 71 | #if TARGET_OS_OSX == 1 72 | + (audit_token_t)NSXPCConnection_auditToken:(NSXPCConnection *)connection 73 | { 74 | return connection.auditToken; 75 | } 76 | #endif 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /Sources/SpellbookHTTP/HTTPResult.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public struct HTTPResult { 26 | public var value: T 27 | public var response: HTTPURLResponse 28 | 29 | public init(value: T, response: HTTPURLResponse) { 30 | self.value = value 31 | self.response = response 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SpellbookTestUtils/Extensions - XCTestCase.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import SpellbookFoundation 24 | import XCTest 25 | 26 | extension XCTestCase { 27 | #if SPELLBOOK_SLOW_CI_x10 28 | public static var waitRate = 10.0 29 | #elseif SPELLBOOK_SLOW_CI_x20 30 | public static var waitRate = 20.0 31 | #elseif SPELLBOOK_SLOW_CI_x30 32 | public static var waitRate = 30.0 33 | #elseif SPELLBOOK_SLOW_CI_x50 34 | public static var waitRate = 50.0 35 | #elseif SPELLBOOK_SLOW_CI_x100 36 | public static var waitRate = 100.0 37 | #else 38 | public static var waitRate = 1.0 39 | #endif 40 | 41 | public static var waitTimeout: TimeInterval = 0.5 42 | 43 | public static var testBundle: Bundle { 44 | return Bundle(for: Self.self) 45 | } 46 | 47 | public var testBundle: Bundle { 48 | Self.testBundle 49 | } 50 | 51 | public static var testTemporaryDirectory: TemporaryDirectory { 52 | .init(name: testBundle.bundleIdentifier ?? testBundle.bundlePath.lastPathComponent) 53 | } 54 | 55 | public var testTemporaryDirectory: TemporaryDirectory { 56 | Self.testTemporaryDirectory 57 | } 58 | 59 | @discardableResult 60 | public func waitForExpectations(timeout: TimeInterval = XCTestCase.waitTimeout) -> Error? { 61 | waitForExpectations(timeout: timeout, ignoreWaitRate: false) 62 | } 63 | 64 | @discardableResult 65 | public func waitForExpectations(timeout: TimeInterval = XCTestCase.waitTimeout, ignoreWaitRate: Bool) -> Error? { 66 | var error: Error? 67 | waitForExpectations(timeout: timeout * Self.waitRate) { 68 | error = $0 69 | } 70 | 71 | return error 72 | } 73 | 74 | public static func sleep(interval: TimeInterval) { 75 | Thread.sleep(forTimeInterval: interval * Self.waitRate) 76 | } 77 | 78 | public func sleep(interval: TimeInterval) { 79 | Self.sleep(interval: interval) 80 | } 81 | 82 | public func withScope(body: () throws -> R) rethrows -> R { 83 | try body() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/SpellbookTestUtils/TestError.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2021 Alkenso (Vladimir Vashurkin) 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 | 23 | import Foundation 24 | 25 | public struct TestError: Error, CustomStringConvertible { 26 | public let description: String 27 | public let underlyingError: Error? 28 | 29 | public init() { 30 | self.init("Any test error.") 31 | } 32 | 33 | public init(_ description: String, underlying underlyingError: Error? = nil) { 34 | self.description = description 35 | self.underlyingError = underlyingError 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SpellbookTestUtils/Testing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SpellbookFoundation 3 | 4 | public enum Testing { 5 | public enum Web { 6 | public static let urlPath = "/get/stuff" 7 | public static let urlScheme = "https" 8 | public static let urlHost = "some.company.com" 9 | public static let url = URL(staticString: "https://some.company.com/get/stuff") 10 | } 11 | 12 | public enum Files { 13 | public static func url(_ index: Int, isDirectory: Bool = false) -> URL { 14 | URL( 15 | fileURLWithPath: "/path/to/\(index).txt", 16 | isDirectory: isDirectory 17 | ) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/SpellbookTestUtilsTests/TestingTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookTestUtils 2 | 3 | import XCTest 4 | 5 | class TestingTests: XCTestCase { 6 | func test_web() { 7 | XCTAssertEqual( 8 | Testing.Web.url, 9 | URL(string: "\(Testing.Web.urlScheme)://\(Testing.Web.urlHost)\(Testing.Web.urlPath)") 10 | ) 11 | } 12 | 13 | func test_files() { 14 | XCTAssertEqual(Testing.Files.url(1), Testing.Files.url(1)) 15 | XCTAssertNotEqual(Testing.Files.url(1), Testing.Files.url(2)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/BinaryParsing/BinaryParsingTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookBinaryParsing 2 | import SpellbookTestUtils 3 | 4 | import XCTest 5 | 6 | class BinaryReaderTests: XCTestCase { 7 | func test() throws { 8 | let data = Data([0x01, 0x02, 0x03, 0x04, 0xaa, 0xbb, 0xcc, 0xdd]) 9 | var reader = BinaryReader(data: data) 10 | XCTAssertEqual(try reader.size(), data.count) 11 | 12 | XCTAssertEqual(try reader.read() as UInt32, 0x04030201) 13 | XCTAssertEqual(try reader.read(count: 2), Data([0xaa, 0xbb])) 14 | XCTAssertEqual(try reader.read(maxCount: 10), Data([0xcc, 0xdd])) 15 | 16 | XCTAssertEqual(try reader.peekInt8(offset: 2), 0x03) 17 | XCTAssertEqual(try reader.peek(at: Range(offset: 1, length: 3)), Data([0x02, 0x03, 0x04])) 18 | 19 | XCTAssertEqual(try reader.peek(offset: 2, while: { $0 != 0xcc }), Data([0x03, 0x04, 0xaa, 0xbb])) 20 | 21 | var resetted = try reader.resetted() 22 | XCTAssertEqual(try resetted.read(offset: 2, while: { $0 != 0xcc }), Data([0x03, 0x04, 0xaa, 0xbb])) 23 | XCTAssertEqual(try resetted.readUInt8(), 0xcc) 24 | } 25 | } 26 | 27 | class BinaryWriterTests: XCTestCase { 28 | func test() throws { 29 | let output = DataBinaryWriterOutput() 30 | var writer = BinaryWriter(output) 31 | 32 | try writer.append(Data([0x00, 0x01])) 33 | XCTAssertEqual(output.data, Data([0x00, 0x01])) 34 | 35 | try writer.append(Data([0x02])) 36 | XCTAssertEqual(output.data, Data([0x00, 0x01, 0x02])) 37 | 38 | try writer.appendZeroes(2) 39 | XCTAssertEqual(output.data, Data([0x00, 0x01, 0x02, 0x00, 0x00])) 40 | 41 | try writer.write(Data([0xff, 0xfa]), at: 2) 42 | XCTAssertEqual(output.data, Data([0x00, 0x01, 0xff, 0xfa, 0x00])) 43 | 44 | try writer.writeUInt8(0x11, at: 4) 45 | XCTAssertEqual(output.data, Data([0x00, 0x01, 0xff, 0xfa, 0x11])) 46 | 47 | // Override and extend 48 | try writer.write(Data([0xaa, 0xa1, 0xa2, 0xa3]), at: 3) 49 | XCTAssertEqual(output.data, Data([0x00, 0x01, 0xff, 0xaa, 0xa1, 0xa2, 0xa3])) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/BlockingQueueTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class BlockingQueueTests: XCTestCase { 6 | func test_enqueue_dequeue() throws { 7 | let queue = BlockingQueue() 8 | 9 | queue.enqueue(10) 10 | XCTAssertEqual(queue.approximateCount, 1) 11 | queue.enqueue(20) 12 | XCTAssertEqual(queue.approximateCount, 2) 13 | queue.enqueue(30) 14 | XCTAssertEqual(queue.approximateCount, 3) 15 | 16 | XCTAssertEqual(queue.dequeue(), 10) 17 | XCTAssertEqual(queue.approximateCount, 2) 18 | XCTAssertEqual(queue.dequeue(), 20) 19 | XCTAssertEqual(queue.approximateCount, 1) 20 | XCTAssertEqual(queue.dequeue(), 30) 21 | XCTAssertEqual(queue.approximateCount, 0) 22 | } 23 | 24 | func test_blocking() throws { 25 | let queue = BlockingQueue() 26 | 27 | let beforeDequeueExp = expectation(description: "before dequeue") 28 | @Indirect var dequeueExp = expectation(description: "should not be dequeued") 29 | dequeueExp.isInverted = true 30 | DispatchQueue.global().async { 31 | beforeDequeueExp.fulfill() 32 | XCTAssertEqual(queue.dequeue(), 10) 33 | dequeueExp.fulfill() 34 | } 35 | waitForExpectations(timeout: 0.1, ignoreWaitRate: true) 36 | 37 | dequeueExp = expectation(description: "dequeued after enqueue") 38 | queue.enqueue(10) 39 | 40 | waitForExpectations() 41 | } 42 | 43 | func test_invalidate() throws { 44 | let emptyQueue = BlockingQueue() 45 | emptyQueue.invalidate() 46 | XCTAssertNil(emptyQueue.dequeue()) 47 | 48 | let queue = BlockingQueue() 49 | queue.enqueue(10) 50 | queue.enqueue(20) 51 | queue.enqueue(30) 52 | 53 | queue.invalidate() 54 | 55 | XCTAssertNil(emptyQueue.dequeue()) 56 | } 57 | 58 | func test_invalidate_noRemoval() throws { 59 | let queue = BlockingQueue() 60 | queue.enqueue(10) 61 | queue.enqueue(20) 62 | queue.enqueue(30) 63 | 64 | queue.invalidate(removeAll: false) 65 | 66 | XCTAssertEqual(queue.dequeue(), 10) 67 | XCTAssertEqual(queue.dequeue(), 20) 68 | XCTAssertEqual(queue.dequeue(), 30) 69 | XCTAssertNil(queue.dequeue()) 70 | } 71 | 72 | func test_cancel() throws { 73 | let queue = BlockingQueue() 74 | 75 | queue.enqueue(10) 76 | queue.enqueue(20) 77 | queue.enqueue(30) 78 | 79 | var isCancelled = false 80 | XCTAssertEqual(queue.dequeue(isCancelled: &isCancelled), 10) 81 | XCTAssertEqual(isCancelled, false) 82 | 83 | queue.cancel() 84 | 85 | XCTAssertEqual(queue.dequeue(isCancelled: &isCancelled), 20) 86 | XCTAssertEqual(isCancelled, true) 87 | 88 | XCTAssertEqual(queue.dequeue(), 30) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/CancellationTokenTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import XCTest 3 | 4 | class CancellationTokenTests: XCTestCase { 5 | func test_basic() { 6 | var isCancelled = false 7 | let expCancel = expectation(description: "") 8 | let token = CancellationToken { 9 | isCancelled = true 10 | expCancel.fulfill() 11 | } 12 | 13 | XCTAssertFalse(isCancelled) 14 | 15 | token.cancel() 16 | 17 | waitForExpectations() 18 | XCTAssertTrue(isCancelled) 19 | } 20 | 21 | func test_cancelOnce() { 22 | let expCancel = expectation(description: "") 23 | let token = CancellationToken { expCancel.fulfill() } 24 | 25 | token.cancel() 26 | token.cancel() 27 | token.cancel() 28 | 29 | waitForExpectations() 30 | } 31 | 32 | func test_children() { 33 | let token = CancellationToken() 34 | 35 | var isCancelled = false 36 | let expCancel = expectation(description: "") 37 | expCancel.expectedFulfillmentCount = 2 38 | 39 | token.addChild { 40 | isCancelled = true 41 | expCancel.fulfill() 42 | } 43 | 44 | let childToken = CancellationToken { expCancel.fulfill() } 45 | token.addChild(childToken) 46 | 47 | XCTAssertFalse(isCancelled) 48 | XCTAssertFalse(childToken.isCancelled) 49 | 50 | token.cancel() 51 | waitForExpectations() 52 | 53 | XCTAssertTrue(isCancelled) 54 | XCTAssertTrue(token.isCancelled) 55 | XCTAssertTrue(childToken.isCancelled) 56 | } 57 | 58 | func test_childrenWhenCancelled() { 59 | let token = CancellationToken() 60 | token.cancel() 61 | 62 | let expCancel = expectation(description: "") 63 | token.addChild { 64 | expCancel.fulfill() 65 | } 66 | waitForExpectations() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/CollectionDiffTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import XCTest 3 | 4 | class CollectionDiffTests: XCTestCase { 5 | func test_array() { 6 | let diff = CollectionDiff(from: [1, 2, 3], to: [4, 2]) 7 | XCTAssertEqual(Set(diff.added), [4]) 8 | XCTAssertEqual(Set(diff.updated), []) 9 | XCTAssertEqual(Set(diff.removed), [1, 3]) 10 | XCTAssertEqual(Set(diff.unchanged), [2]) 11 | } 12 | 13 | func test_update() { 14 | struct Foo: Hashable { 15 | var id: Int 16 | var value: String 17 | } 18 | 19 | let diff = CollectionDiff( 20 | from: [Foo(id: 1, value: "q"), Foo(id: 2, value: "w"), Foo(id: 3, value: "e"), ], 21 | to: [Foo(id: 1, value: "q"), Foo(id: 2, value: "ww"), Foo(id: 4, value: "ee"), ], 22 | similarBy: \.id 23 | ) 24 | XCTAssertEqual(Set(diff.added), [Foo(id: 4, value: "ee")]) 25 | XCTAssertEqual(Set(diff.updated), [.unchecked(old: Foo(id: 2, value: "w"), new: Foo(id: 2, value: "ww"))]) 26 | XCTAssertEqual(Set(diff.removed), [Foo(id: 3, value: "e")]) 27 | XCTAssertEqual(Set(diff.unchanged), [Foo(id: 1, value: "q")]) 28 | } 29 | 30 | func test_dict() { 31 | let diff = DictionaryDiff(from: [1: "q", 2: "w", 3: "e"], to: [1: "q", 2: "ww", 4: "r"]) 32 | XCTAssertEqual(diff.added, [4: "r"]) 33 | XCTAssertEqual(diff.updated, [2: .unchecked(old: "w", new: "ww")]) 34 | XCTAssertEqual(diff.removed, [3: "e"]) 35 | XCTAssertEqual(diff.unchanged, [1: "q"]) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/ObjectBuilderTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class ObjectBuilderTests: XCTestCase { 6 | struct Foo: Equatable, ValueBuilder { 7 | var a = 10 8 | var b = "qwerty" 9 | } 10 | 11 | func test() { 12 | XCTAssertEqual(Foo().set(\.a, 15).set(\.b, "q").set(\.a, 1), Foo(a: 1, b: "q")) 13 | 14 | XCTAssertEqual(Foo().if(true, then: { $0.a = 1 }), Foo(a: 1)) 15 | XCTAssertEqual(Foo().if(false, then: { $0.a = 1 }), Foo()) 16 | XCTAssertEqual(Foo().if(true, then: { $0.a = 1 }, else: { $0.a = 2 }), Foo(a: 1)) 17 | XCTAssertEqual(Foo().if(false, then: { $0.a = 1 }, else: { $0.a = 2 }), Foo(a: 2)) 18 | 19 | XCTAssertEqual(Foo().ifLet(1, then: { $0.a = $1 }), Foo(a: 1)) 20 | XCTAssertEqual(Foo().ifLet(nil as Int?, then: { $0.a = $1 }), Foo()) 21 | XCTAssertEqual(Foo().ifLet(1, then: { $0.a = $1 }, else: { $0.a = 2 }), Foo(a: 1)) 22 | XCTAssertEqual(Foo().ifLet(nil as Int?, then: { $0.a = $1 }, else: { $0.a = 2 }), Foo(a: 2)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/OtherCommonTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class OtherCommonTests: XCTestCase { 6 | func test_isXCTest() { 7 | XCTAssertTrue(RunEnvironment.isXCTesting) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/SBUnitTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | import XCTest 4 | 5 | final class SBUnitTests: XCTestCase { 6 | func test() { 7 | XCTAssertEqual(SBUnitInformationStorage.convert(1, to: .kilobyte), 1.0 / 1024) 8 | XCTAssertEqual(SBUnitInformationStorage.convert(1, .kilobyte), 1.0 * 1024) 9 | XCTAssertEqual(SBUnitInformationStorage.convert(1, .kilobyte, to: .kilobyte), 1) 10 | XCTAssertEqual(SBUnitInformationStorage.convert(1, .megabyte, to: .kilobyte), 1024) 11 | 12 | XCTAssertEqual(SBUnitTime.convert(1, .minute), 60) 13 | XCTAssertEqual(SBUnitTime.convert(1, .hour), 60 * 60) 14 | XCTAssertEqual(SBUnitTime.convert(1, .day), 24 * 60 * 60) 15 | XCTAssertEqual(SBUnitTime.convert(1, .day, to: .hour), 24) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/UtilsTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class UtilsTests: XCTestCase { 6 | func test_updateSwap() { 7 | var a = 10 8 | XCTAssertEqual(updateSwap(&a, 20), 10) 9 | XCTAssertEqual(a, 20) 10 | } 11 | 12 | func test_updateValue() { 13 | struct Foo { 14 | var value = 10 15 | } 16 | XCTAssertEqual(updateValue(Foo(), at: \.value, with: 20).value, 20) 17 | XCTAssertEqual(updateValue(Foo(), using: { $0.value = 20 }).value, 20) 18 | XCTAssertEqual(updateValue(Foo(), using: { $0 = .init(value: 20) }).value, 20) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Common/WildcardExpressionTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class WildcardExpressionTests: XCTestCase { 6 | func test_WildcardExpression() throws { 7 | XCTAssertTrue(WildcardExpression(pattern: "").match("")) 8 | XCTAssertTrue(WildcardExpression(pattern: "qwerty").match("qwerty")) 9 | XCTAssertTrue(WildcardExpression(pattern: "q*y").match("qwerty")) 10 | XCTAssertTrue(WildcardExpression(pattern: "qwe?ty").match("qwerty")) 11 | XCTAssertTrue(WildcardExpression(pattern: "/path/to/*/file").match("/path/to/some/file")) 12 | } 13 | 14 | func test_WildcardExpression_caseSensitive() throws { 15 | let caseSensitive = WildcardExpression(pattern: "QwErTy") 16 | XCTAssertTrue(caseSensitive.match("QwErTy")) 17 | XCTAssertFalse(caseSensitive.match("qwerty")) 18 | 19 | let caseInsensitive = WildcardExpression.caseInsensitive(pattern: "QwErTy") 20 | XCTAssertTrue(caseInsensitive.match("QwErTy")) 21 | XCTAssertTrue(caseInsensitive.match("qwerty")) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Errors/CommonErrorTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class CommonErrorTests: XCTestCase { 6 | func test_NSError() { 7 | let err = CommonError(.invalidArgument, userInfo: [NSDebugDescriptionErrorKey: "qwerty"]) 8 | let nsErr = err as NSError 9 | 10 | XCTAssertEqual(nsErr.code, CommonError.Code.invalidArgument.rawValue) 11 | XCTAssertEqual(nsErr.userInfo[NSDebugDescriptionErrorKey] as? String, "qwerty") 12 | } 13 | 14 | func test_bridge() throws { 15 | let err = CommonError(.invalidArgument, userInfo: [NSDebugDescriptionErrorKey: "qwerty"]) 16 | let encoded = try NSKeyedArchiver.archivedData(withRootObject: err, requiringSecureCoding: true) 17 | let decoded = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: encoded) 18 | let reencoded = try XCTUnwrap(decoded as? CommonError) 19 | 20 | XCTAssertEqual(reencoded.code, .invalidArgument) 21 | XCTAssertEqual(reencoded.userInfo[NSDebugDescriptionErrorKey] as? String, "qwerty") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Errors/ErrorExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | 4 | import XCTest 5 | 6 | class ErrorExtensionsTests: XCTestCase { 7 | func test_secureCodingCompliant() { 8 | let compatibleError = NSError(domain: "test", code: 1, userInfo: [ 9 | "compatible_key": "compatible_value", 10 | "compatible_key2": ["value1", "value2"], 11 | ]) 12 | // No conversion. 13 | XCTAssertTrue(compatibleError === (compatibleError.secureCodingCompliant() as NSError)) 14 | 15 | struct SwiftType {} 16 | let error = NSError(domain: "test", code: 1, userInfo: [ 17 | "compatible_key": "compatible_value", 18 | "incompatible_key": SwiftType(), 19 | "maybe_incompatible_key": UUID(), 20 | ]) 21 | 22 | XCTAssertThrowsError(try NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true)) 23 | 24 | let xpcCompatible = error.secureCodingCompliant() 25 | XCTAssertNoThrow(try NSKeyedArchiver.archivedData(withRootObject: xpcCompatible, requiringSecureCoding: true)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Errors/NSErrorTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class NSErrorTests: XCTestCase { 6 | func test_errorBuilding_withDebugDescription() { 7 | XCTAssertEqual( 8 | NSError(posix: 0) 9 | .updatingDebugDescription("Description") 10 | .userInfo[NSDebugDescriptionErrorKey] as? String, 11 | "Description" 12 | ) 13 | XCTAssertEqual( 14 | NSError(posix: 0) 15 | .updatingDebugDescription("Description") 16 | .updatingDebugDescription("Description 2") 17 | .userInfo[NSDebugDescriptionErrorKey] as? String, 18 | "Description 2" 19 | ) 20 | } 21 | 22 | func test_errorBuilding_withUserInfoSingle() { 23 | XCTAssertEqual( 24 | NSError(posix: 0) 25 | .updatingUserInfo(10, for: "Key") 26 | .userInfo["Key"] as? Int, 27 | 10 28 | ) 29 | XCTAssertEqual( 30 | NSError(posix: 0) 31 | .updatingUserInfo(10, for: "Key") 32 | .updatingUserInfo(20, for: "Key") 33 | .userInfo["Key"] as? Int, 34 | 20 35 | ) 36 | 37 | let userInfo = NSError(posix: 0) 38 | .updatingUserInfo(10, for: "Key") 39 | .updatingUserInfo(20, for: "Key 2") 40 | .userInfo 41 | XCTAssertEqual(userInfo["Key"] as? Int, 10) 42 | XCTAssertEqual(userInfo["Key 2"] as? Int, 20) 43 | } 44 | 45 | func test_errorBuilding_withUserInfoMerged() { 46 | let error = NSError(posix: 0) 47 | .updatingUserInfo(10, for: "Key") 48 | .updatingUserInfo(20, for: "Key 2") 49 | 50 | let mergedUserInfo = error.updatingUserInfo([ 51 | "Key": "Abc", 52 | "Key 3": 30, 53 | ]).userInfo 54 | 55 | XCTAssertEqual(mergedUserInfo["Key"] as? String, "Abc") 56 | XCTAssertEqual(mergedUserInfo["Key 2"] as? Int, 20) 57 | XCTAssertEqual(mergedUserInfo["Key 3"] as? Int, 30) 58 | } 59 | 60 | func test_errorBuilding_appendingUnderlyingError() { 61 | let underlyingError1 = NSError(domain: "Test 1", code: 1) 62 | let underlyingError2 = NSError(domain: "Test 2", code: 2) 63 | let underlyingError3 = NSError(domain: "Test 3", code: 3) 64 | 65 | let error1 = NSError(posix: 0).appendingUnderlyingError(underlyingError1) 66 | XCTAssertEqual((error1.userInfo[NSUnderlyingErrorKey] as? NSError)?.domain, "Test 1") 67 | XCTAssertEqual((error1.userInfo[NSUnderlyingErrorKey] as? NSError)?.code, 1) 68 | 69 | let error2 = NSError(posix: 0) 70 | .appendingUnderlyingError(underlyingError1) 71 | .appendingUnderlyingError(underlyingError2) 72 | .appendingUnderlyingError(underlyingError3) 73 | XCTAssertEqual((error2.userInfo[NSUnderlyingErrorKey] as? NSError)?.domain, "Test 1") 74 | XCTAssertEqual((error2.userInfo[NSUnderlyingErrorKey] as? NSError)?.code, 1) 75 | if let error2UnderlyingErrors = error2.userInfo[NSError.multipleUnderlyingErrorsKey] as? [NSError], error2UnderlyingErrors.count == 2 { 76 | XCTAssertEqual(error2UnderlyingErrors[0].domain, "Test 2") 77 | XCTAssertEqual(error2UnderlyingErrors[0].code, 2) 78 | 79 | XCTAssertEqual(error2UnderlyingErrors[1].domain, "Test 3") 80 | XCTAssertEqual(error2UnderlyingErrors[1].code, 3) 81 | } else { 82 | XCTFail("Invalid underlying errors") 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Extensions Tests/CodableTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class CodableTests: XCTestCase { 6 | func test_PropertyListSerializable() throws { 7 | @PropertyListSerializable var anyDict = ["key1": "value1", "key2": 20] 8 | let data = try JSONEncoder().encode(_anyDict) 9 | 10 | let decoded = try JSONDecoder().decode(PropertyListSerializable<[String: Any]>.self, from: data) 11 | XCTAssertEqual(decoded.wrappedValue["key1"] as? String, "value1") 12 | XCTAssertEqual(decoded.wrappedValue["key2"] as? Int, 20) 13 | } 14 | 15 | func test_PropertyListSerializable_failure() throws { 16 | // Custom type is not compatible with PropertyListSerialization. 17 | struct Foo {} 18 | @PropertyListSerializable var custom: Any = Foo() 19 | XCTAssertThrowsError(try JSONEncoder().encode(_custom)) 20 | } 21 | 22 | func test_JSONSerializable() throws { 23 | @JSONSerializable var anyDict = ["key1": "value1", "key2": 20] 24 | let data = try JSONEncoder().encode(_anyDict) 25 | 26 | let decoded = try JSONDecoder().decode(JSONSerializable<[String: Any]>.self, from: data) 27 | XCTAssertEqual(decoded.wrappedValue["key1"] as? String, "value1") 28 | XCTAssertEqual(decoded.wrappedValue["key2"] as? Int, 20) 29 | } 30 | 31 | func test_JSONSerializable_failure() throws { 32 | // `Date` is not compatible with JSON. 33 | @JSONSerializable var object: Any = Date() 34 | XCTAssertThrowsError(try JSONEncoder().encode(_object)) 35 | 36 | // Custom type is not compatible with JSONSerialization. 37 | struct Foo {} 38 | @JSONSerializable var custom: Any = Foo() 39 | XCTAssertThrowsError(try JSONEncoder().encode(_custom)) 40 | } 41 | 42 | func test_NSKeyedArchiveSerializable_dict() throws { 43 | @KeyedArchiveSerializable var anyDict = ["key1": "value1", "key2": 20] as NSDictionary 44 | let data = try JSONEncoder().encode(_anyDict) 45 | 46 | let decoded = try JSONDecoder().decode(KeyedArchiveSerializable.self, from: data) 47 | XCTAssertEqual(decoded.wrappedValue["key1"] as? String, "value1") 48 | XCTAssertEqual(decoded.wrappedValue["key2"] as? Int, 20) 49 | } 50 | 51 | func test_NSKeyedArchiveSerializable_error() throws { 52 | @KeyedArchiveSerializable var error = NSError( 53 | domain: "D", 54 | code: 10, 55 | userInfo: [NSDebugDescriptionErrorKey: "test"] 56 | ) 57 | let data = try JSONEncoder().encode(_error) 58 | 59 | let decoded = try JSONDecoder().decode(KeyedArchiveSerializable.self, from: data).wrappedValue 60 | XCTAssertEqual(decoded.domain, "D") 61 | XCTAssertEqual(decoded.code, 10) 62 | XCTAssertEqual(decoded.userInfo[NSDebugDescriptionErrorKey] as? String, "test") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Filesystem & Bundle/FileEnumeratorTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | 4 | import XCTest 5 | 6 | class FileEnumeratorTests: XCTestCase { 7 | let tempDir = TemporaryDirectory.bundle 8 | 9 | override func setUpWithError() throws { 10 | continueAfterFailure = false 11 | 12 | try tempDir.setUp() 13 | try super.setUpWithError() 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | try tempDir.tearDown() 18 | try super.tearDownWithError() 19 | } 20 | 21 | func test_enumerateFiles() throws { 22 | var expectedFiles = [tempDir.location] 23 | try expectedFiles.append(tempDir.createFile(name: "file1", content: Data())) 24 | try expectedFiles.append(tempDir.createFile(name: "file2", content: Data())) 25 | try expectedFiles.append(tempDir.directory(name: "subdir").setUp().location) 26 | try expectedFiles.append(tempDir.createFile(name: "subdir/file3", content: Data())) 27 | try expectedFiles.append(tempDir.directory(name: "subdir/nested").setUp().location) 28 | try expectedFiles.append(tempDir.createFile(name: "subdir/nested/file4", content: Data())) 29 | 30 | let enumeratedFiles = Array(FileEnumerator(tempDir.location)) 31 | XCTAssertEqual( 32 | Set(enumeratedFiles.map { $0.resolvingSymlinksInPath() }), 33 | Set(expectedFiles.map { $0.resolvingSymlinksInPath() }) 34 | ) 35 | } 36 | 37 | func test_enumerateFiles_ofTypes() throws { 38 | var expectedFiles: [URL] = [] 39 | try expectedFiles.append(tempDir.createFile(name: "file1", content: Data())) 40 | try expectedFiles.append(tempDir.createFile(name: "file2", content: Data())) 41 | try tempDir.directory(name: "subdir").setUp() 42 | try expectedFiles.append(tempDir.createFile(name: "subdir/file3", content: Data())) 43 | try tempDir.directory(name: "subdir/nested").setUp() 44 | try expectedFiles.append(tempDir.createFile(name: "subdir/nested/file4", content: Data())) 45 | 46 | let enumeratedFiles = Array(FileEnumerator(types: [.regular], tempDir.location)) 47 | XCTAssertEqual( 48 | Set(enumeratedFiles.map { $0.resolvingSymlinksInPath() }), 49 | Set(expectedFiles.map { $0.resolvingSymlinksInPath() }) 50 | ) 51 | } 52 | 53 | func test_enumerateFiles_filter() throws { 54 | var expectedFiles = [tempDir.location] 55 | try expectedFiles.append(tempDir.createFile(name: "file1", content: Data())) 56 | 57 | try tempDir.directory("subdir").setUp() 58 | try expectedFiles.append(tempDir.directory("subdir/folder1").setUp().location) 59 | try expectedFiles.append(tempDir.createFile(name: "subdir/file3", content: Data())) 60 | 61 | try expectedFiles.append(tempDir.directory("subdir2").setUp().location) 62 | _ = try tempDir.createFile(name: "subdir2/file4", content: Data()) 63 | 64 | try tempDir.directory("subdir/nested").setUp() 65 | _ = try tempDir.createFile(name: "subdir/nested/file5", content: Data()) 66 | 67 | let enumerator = FileEnumerator(tempDir.location) 68 | enumerator.locationFilter = { 69 | switch $0.lastPathComponent { 70 | case "subdir": 71 | return .skip 72 | case "subdir2": 73 | return .init(current: true, children: false) 74 | case "nested": 75 | return .skipRecursive 76 | default: 77 | return .proceed 78 | } 79 | } 80 | let enumeratedFiles = Array(enumerator) 81 | XCTAssertEqual( 82 | Set(enumeratedFiles.map { $0.resolvingSymlinksInPath() }), 83 | Set(expectedFiles.map { $0.resolvingSymlinksInPath() }) 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Filesystem & Bundle/FileManagerTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | 4 | import XCTest 5 | 6 | class FileManagerExtensionsTests: XCTestCase { 7 | let tempDir = TemporaryDirectory.bundle 8 | 9 | override func setUpWithError() throws { 10 | continueAfterFailure = false 11 | 12 | try tempDir.setUp() 13 | try super.setUpWithError() 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | try tempDir.tearDown() 18 | try super.tearDownWithError() 19 | } 20 | 21 | func test_fileExistsAt() throws { 22 | let file = try tempDir.createFile(name: "testfile", content: Data()) 23 | XCTAssertTrue(FileManager.default.fileExists(atPath: file.path)) 24 | XCTAssertTrue(FileManager.default.fileExists(at: file)) 25 | } 26 | 27 | func test_directoryExistsAt() throws { 28 | let directory = tempDir.location 29 | let file = try tempDir.createFile(name: "testfile", content: Data()) 30 | 31 | var isDirectory = ObjCBool(false) 32 | XCTAssertTrue(FileManager.default.fileExists(atPath: file.path, isDirectory: &isDirectory)) 33 | XCTAssertFalse(isDirectory.boolValue) 34 | XCTAssertFalse(FileManager.default.directoryExists(at: file)) 35 | 36 | XCTAssertTrue(FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory)) 37 | XCTAssertTrue(isDirectory.boolValue) 38 | XCTAssertTrue(FileManager.default.directoryExists(at: directory)) 39 | } 40 | 41 | func test_xattr() throws { 42 | let file = try tempDir.createFile(name: "file", content: Data()) 43 | XCTAssertEqual(try FileManager.default.listXattr(at: file), []) 44 | XCTAssertThrowsError(try FileManager.default.xattr(at: file, name: "xa")) 45 | XCTAssertThrowsError(try FileManager.default.removeXattr(at: file, name: "xa")) 46 | 47 | let value1 = Data(pod: 100500) 48 | let value2 = Data("some value".utf8) 49 | XCTAssertNoThrow(try FileManager.default.setXattr(at: file, name: "xa1", value: value1)) 50 | XCTAssertNoThrow(try FileManager.default.setXattr(at: file, name: "xa2", value: value2)) 51 | 52 | let attrs = try FileManager.default.listXattr(at: file) 53 | XCTAssertTrue(attrs.contains("xa1")) 54 | XCTAssertTrue(attrs.contains("xa2")) 55 | 56 | XCTAssertEqual(try FileManager.default.xattr(at: file, name: "xa1"), value1) 57 | XCTAssertEqual(try FileManager.default.xattr(at: file, name: "xa2"), value2) 58 | 59 | try FileManager.default.removeXattr(at: file, name: "xa1") 60 | XCTAssertEqual(try FileManager.default.listXattr(at: file), ["xa2"]) 61 | } 62 | 63 | func test_uniqueFile() { 64 | let fm = FileManager.default 65 | func createFile(_ name: String) -> Bool { 66 | fm.createFile(atPath: tempDir.location.appendingPathComponent(name).path, contents: .random(20)) 67 | } 68 | 69 | XCTAssertEqual(fm.uniqueFile("test.foo", in: tempDir.location).lastPathComponent, "test.foo") 70 | // Ensure it is not created. 71 | XCTAssertEqual(fm.uniqueFile("test.foo", in: tempDir.location).lastPathComponent, "test.foo") 72 | 73 | XCTAssertTrue(createFile("test.foo")) 74 | XCTAssertEqual(fm.uniqueFile("test.foo", in: tempDir.location).lastPathComponent, "test_1.foo") 75 | 76 | XCTAssertTrue(createFile("test.foo.bar")) 77 | XCTAssertEqual(fm.uniqueFile("test.foo.bar", in: tempDir.location).lastPathComponent, "test.foo_1.bar") 78 | 79 | XCTAssertEqual(fm.uniqueFile("test", in: tempDir.location).lastPathComponent, "test") 80 | XCTAssertTrue(createFile("test")) 81 | XCTAssertTrue(createFile("test_1")) 82 | XCTAssertTrue(createFile("test_3")) 83 | XCTAssertEqual(fm.uniqueFile("test", in: tempDir.location).lastPathComponent, "test_2") 84 | 85 | XCTAssertEqual(fm.uniqueFile("dir/", in: tempDir.location).lastPathComponent, "dir") 86 | XCTAssertTrue(createFile("dir")) 87 | XCTAssertEqual(fm.uniqueFile("dir/", in: tempDir.location).lastPathComponent, "dir_1") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Filesystem & Bundle/FileStoreTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | 4 | import XCTest 5 | 6 | class FileStoreTests: XCTestCase { 7 | let tempDir = TemporaryDirectory.bundle 8 | 9 | override func setUpWithError() throws { 10 | continueAfterFailure = false 11 | 12 | try tempDir.setUp() 13 | try super.setUpWithError() 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | try tempDir.tearDown() 18 | try super.tearDownWithError() 19 | } 20 | 21 | func test_standard() throws { 22 | let url = tempDir.location.appendingPathComponent("test.file") 23 | let store = FileStore.standard() 24 | XCTAssertThrowsError(try store.read(from: url)) 25 | XCTAssertEqual(try store.read(from: url, default: Data(pod: 100500)), Data(pod: 100500)) 26 | 27 | let subdir = tempDir.location.appendingPathComponent("subdir") 28 | let fileInSubdir = subdir.appendingPathComponent("test.file") 29 | XCTAssertFalse(FileManager.default.fileExists(at: subdir)) 30 | XCTAssertThrowsError(try store.write(Data(pod: 100500), to: fileInSubdir)) 31 | XCTAssertFalse(FileManager.default.fileExists(at: subdir)) 32 | XCTAssertNoThrow(try store.write(Data(pod: 100500), to: fileInSubdir, createDirectories: true)) 33 | XCTAssertTrue(FileManager.default.fileExists(at: subdir)) 34 | XCTAssertTrue(FileManager.default.fileExists(at: fileInSubdir)) 35 | } 36 | 37 | func test_exact() throws { 38 | let url = tempDir.location.appendingPathComponent("test.file") 39 | let store = FileStore.standard().exact(url) 40 | XCTAssertThrowsError(try store.read()) 41 | XCTAssertEqual(try store.read(default: Data(pod: 20)), Data(pod: 20)) 42 | 43 | let storeWithDefault = FileStore.standard().exact(url, default: Data(pod: 10)) 44 | XCTAssertEqual(try storeWithDefault.read(), Data(pod: 10)) 45 | XCTAssertEqual(try storeWithDefault.read(default: Data(pod: 20)), Data(pod: 20)) 46 | 47 | XCTAssertFalse(FileManager.default.fileExists(at: url)) 48 | } 49 | 50 | func test_codable() throws { 51 | let url = tempDir.location.appendingPathComponent("test.file") 52 | let store = FileStore.standard().codable(Int.self, using: .json()).exact(url) 53 | XCTAssertThrowsError(try store.read()) 54 | XCTAssertEqual(try store.read(default: 20), 20) 55 | XCTAssertFalse(FileManager.default.fileExists(at: url)) 56 | 57 | XCTAssertNoThrow(try store.write(10)) 58 | XCTAssertEqual(try store.read(default: 20), 10) 59 | XCTAssertTrue(FileManager.default.fileExists(at: url)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Filesystem & Bundle/TemporaryDirectoryTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class TemporaryDirectoryTests: XCTestCase { 6 | let tempDir = TemporaryDirectory.bundle 7 | 8 | override func setUpWithError() throws { 9 | continueAfterFailure = false 10 | 11 | try tempDir.setUp() 12 | try super.setUpWithError() 13 | } 14 | 15 | override func tearDownWithError() throws { 16 | try tempDir.tearDown() 17 | try super.tearDownWithError() 18 | } 19 | 20 | func test_multipleDirectories() throws { 21 | let dirs = [nil, nil, "prefix", "prefix"] 22 | .map { TemporaryDirectory(prefix: $0) } 23 | try dirs.forEach { try $0.setUp() } 24 | try dirs.forEach { try $0.tearDown() } 25 | } 26 | 27 | func test_manualSetupTearDown() throws { 28 | let fm = FileManager.default 29 | let tempDir = TemporaryDirectory() 30 | 31 | XCTAssertFalse(fm.fileExists(at: tempDir.location)) 32 | try tempDir.setUp() 33 | XCTAssertTrue(fm.directoryExists(at: tempDir.location)) 34 | 35 | let file = try tempDir.createFile(name: "content", content: Data()) 36 | XCTAssertTrue(fm.fileExists(at: file)) 37 | let subdir = try tempDir.directory("subdir").setUp() 38 | XCTAssertTrue(fm.directoryExists(at: subdir.location)) 39 | 40 | try tempDir.tearDown() 41 | 42 | XCTAssertFalse(fm.fileExists(at: file)) 43 | XCTAssertFalse(fm.directoryExists(at: subdir.location)) 44 | XCTAssertFalse(fm.directoryExists(at: tempDir.location)) 45 | } 46 | 47 | func test_createSubdirectory() throws { 48 | // simple subpath 49 | let simpleSubdir = try tempDir.directory("Subdir").setUp() 50 | XCTAssertEqual(simpleSubdir.location.lastPathComponent, "Subdir") 51 | XCTAssertTrue(FileManager.default.directoryExists(at: simpleSubdir.location)) 52 | 53 | // complex subpath 54 | let complexSubdir = try tempDir.directory("Subdir2/Subsubdir").setUp() 55 | XCTAssertEqual(complexSubdir.location.lastPathComponent, "Subsubdir") 56 | XCTAssertEqual(complexSubdir.location.deletingLastPathComponent().lastPathComponent, "Subdir2") 57 | XCTAssertTrue(FileManager.default.directoryExists(at: complexSubdir.location)) 58 | } 59 | 60 | func test_createFile() throws { 61 | let emptyFile = try tempDir.createFile(name: "Dup.txt", content: "q".utf8Data) 62 | XCTAssertEqual(try Data(contentsOf: emptyFile), "q".utf8Data) 63 | 64 | // Overwrite file if trying to create file that already exists. 65 | XCTAssertEqual(try tempDir.createFile(name: "Dup.txt", content: "w".utf8Data), emptyFile) 66 | XCTAssertEqual(try Data(contentsOf: emptyFile), "w".utf8Data) 67 | 68 | let content = Data("content".utf8) 69 | let nonEmptyFile = try tempDir.createFile(name: "NonEmpty.txt", content: content) 70 | XCTAssertTrue(FileManager.default.fileExists(at: nonEmptyFile)) 71 | XCTAssertEqual(try Data(contentsOf: nonEmptyFile), content) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/GUITests/GUITests.swift: -------------------------------------------------------------------------------- 1 | @testable import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class GUITests: XCTestCase { 6 | func test_CGRect_center() { 7 | XCTAssertEqual( 8 | CGRect(x: 20, y: 40, width: 100, height: 200) 9 | .centered(against: CGRect(x: 60, y: 80, width: 400, height: 600)), 10 | CGRect(x: 210, y: 280, width: 100, height: 200) 11 | ) 12 | 13 | XCTAssertEqual( 14 | CGRect(x: 20, y: 40, width: 400, height: 600) 15 | .centered(against: CGRect(x: 60, y: 80, width: 100, height: 200)), 16 | CGRect(x: -90, y: -120, width: 400, height: 600) 17 | ) 18 | } 19 | 20 | func test_CGRect_flip() { 21 | let rect = CGRect(x: 20, y: 40, width: 100, height: 200) 22 | XCTAssertEqual( 23 | rect.flippedY(fullHeight: 400), 24 | CGRect(x: 20, y: 160, width: 100, height: 200) 25 | ) 26 | XCTAssertEqual( 27 | rect.flippedY(fullHeight: 100), 28 | CGRect(x: 20, y: -140, width: 100, height: 200) 29 | ) 30 | 31 | XCTAssertEqual(rect.flippedY(fullHeight: 400).flippedY(fullHeight: 400), rect) 32 | } 33 | 34 | func test_RGBColor() { 35 | XCTAssertEqual(RGBColor(hex: ""), nil) 36 | XCTAssertEqual(RGBColor(hex: "#"), nil) 37 | XCTAssertEqual(RGBColor(hex: "#FFF"), nil) 38 | XCTAssertEqual(RGBColor(hex: "#AABBCCDDE"), nil) 39 | XCTAssertEqual(RGBColor(hex: "#AABBCCE"), nil) 40 | 41 | XCTAssertEqual(RGBColor(hex: "#66FFCC"), RGBColor(red: 0x66 / 255.0, green: 1.0, blue: 0xCC / 255.0)) 42 | XCTAssertEqual(RGBColor(hex: "66FFCC"), RGBColor(red: 0x66 / 255.0, green: 1.0, blue: 0xCC / 255.0)) 43 | XCTAssertEqual(RGBColor(hex: "#66FFCC", alphaFirst: true), RGBColor(red: 0x66 / 255.0, green: 1.0, blue: 0xCC / 255.0)) 44 | 45 | XCTAssertEqual(RGBColor(hex: "#66FFCC80"), RGBColor(red: 0x66 / 255.0, green: 1.0, blue: 0xCC / 255.0, alpha: 0x80 / 255.0)) 46 | XCTAssertEqual(RGBColor(hex: "66FFCC80"), RGBColor(red: 0x66 / 255.0, green: 1.0, blue: 0xCC / 255.0, alpha: 0x80 / 255.0)) 47 | XCTAssertEqual(RGBColor(hex: "#8066FFCC", alphaFirst: true), RGBColor(red: 0x66 / 255.0, green: 1.0, blue: 0xCC / 255.0, alpha: 0x80 / 255.0)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/LowLevel/AuditTokenTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import XCTest 3 | 4 | #if os(macOS) 5 | 6 | class AuditTokenTests: XCTestCase { 7 | func test_currentTaskAuditToken() throws { 8 | let token = try audit_token_t.current() 9 | 10 | XCTAssertEqual(audit_token_to_ruid(token), getuid()) 11 | XCTAssertEqual(audit_token_to_rgid(token), getgid()) 12 | XCTAssertEqual(audit_token_to_euid(token), geteuid()) 13 | XCTAssertEqual(audit_token_to_egid(token), getegid()) 14 | XCTAssertEqual(audit_token_to_pid(token), getpid()) 15 | XCTAssertNotEqual(audit_token_to_auid(token), 0) 16 | XCTAssertNotEqual(audit_token_to_asid(token), 0) 17 | XCTAssertNotEqual(audit_token_to_pidversion(token), 0) 18 | } 19 | 20 | func test_equals() { 21 | let token1 = audit_token_t(val: (UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1))) 22 | let token2 = audit_token_t(val: (UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1))) 23 | let token3 = audit_token_t(val: (UInt32(2), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1), UInt32(1))) 24 | 25 | XCTAssertEqual(token1, token2) 26 | XCTAssertNotEqual(token1, token3) 27 | } 28 | 29 | func test_codable() throws { 30 | let token = audit_token_t(val: (UInt32(1), UInt32(2), UInt32(3), UInt32(4), UInt32(5), UInt32(6), UInt32(7), UInt32(8))) 31 | 32 | let data = try JSONEncoder().encode(token) 33 | let decodedToken = try JSONDecoder().decode(audit_token_t.self, from: data) 34 | XCTAssertEqual(decodedToken, token) 35 | } 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/LowLevel/MachTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class MachTests: XCTestCase { 6 | private let ticksAccuracy: UInt64 = UInt64(Double(700) * XCTestCase.waitRate) 7 | 8 | func test_machTime() throws { 9 | let currentMach = mach_absolute_time() 10 | let calculated = try XCTUnwrap(Date().machTime) 11 | XCTAssertLessThanOrEqual(calculated - currentMach, ticksAccuracy) 12 | 13 | let currentDate = try XCTUnwrap(Date(machTime: mach_absolute_time())) 14 | XCTAssertEqual(currentDate.timeIntervalSince1970, Date().timeIntervalSince1970, accuracy: 0.001) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/LowLevel/UnsafeTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class UnsafeTests: XCTestCase { 6 | func test_bzero() { 7 | let zeroedPtr = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1) 8 | defer { zeroedPtr.deallocate() } 9 | bzero(zeroedPtr, 4) 10 | 11 | let mutablePtr = UnsafeMutablePointer.allocate(capacity: 1) 12 | defer { mutablePtr.deallocate() } 13 | mutablePtr.pointee = 0xFF_FF_FF_FF 14 | mutablePtr.bzero() 15 | XCTAssertEqual(memcmp(mutablePtr, zeroedPtr, 4), 0) 16 | 17 | let mutableRawPtr = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1) 18 | defer { mutableRawPtr.deallocate() } 19 | mutableRawPtr.bindMemory(to: UInt32.self, capacity: 1).pointee = 0xFF_FF_FF_FF 20 | mutableRawPtr.bzero(MemoryLayout.size) 21 | XCTAssertEqual(memcmp(mutableRawPtr, zeroedPtr, 4), 0) 22 | 23 | let mutableBufferPtr = UnsafeMutableBufferPointer.allocate(capacity: 1) 24 | defer { mutableBufferPtr.deallocate() } 25 | mutableBufferPtr[0] = 0xFF_FF_FF_FF 26 | mutableBufferPtr.bzero() 27 | XCTAssertEqual(memcmp(mutableBufferPtr.baseAddress, zeroedPtr, 4), 0) 28 | 29 | let mutableRawBufferPtr = UnsafeMutableRawBufferPointer.allocate(byteCount: 4, alignment: 1) 30 | defer { mutableRawBufferPtr.deallocate() } 31 | mutableRawBufferPtr[0] = 0xFF 32 | mutableRawBufferPtr[1] = 0xFF 33 | mutableRawBufferPtr[2] = 0xFF 34 | mutableRawBufferPtr[3] = 0xFF 35 | mutableRawBufferPtr.bzero() 36 | XCTAssertEqual(memcmp(mutableRawBufferPtr.baseAddress, zeroedPtr, 4), 0) 37 | 38 | let autoreleasingMutableBackingPtr = UnsafeMutablePointer.allocate(capacity: 1) 39 | defer { autoreleasingMutableBackingPtr.deallocate() } 40 | let autoreleasingMutablePtr = AutoreleasingUnsafeMutablePointer(autoreleasingMutableBackingPtr) 41 | autoreleasingMutablePtr.pointee = 0xFF_FF_FF_FF 42 | autoreleasingMutablePtr.bzero() 43 | XCTAssertEqual(memcmp(autoreleasingMutablePtr, zeroedPtr, 4), 0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Observing/EventNotifyTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import XCTest 4 | 5 | class EventNotifyTests: XCTestCase { 6 | func test_receiveValue() { 7 | let event = EventNotify() 8 | var subscriptions: [SubscriptionToken] = [] 9 | defer { withExtendedLifetime(subscriptions) {} } 10 | 11 | var expectedValues = [10, 20, 30] 12 | 13 | let exp = expectation(description: "notify called") 14 | exp.expectedFulfillmentCount = expectedValues.count 15 | 16 | event.subscribe(suppressInitialNotify: false) { 17 | XCTAssertEqual($0, expectedValues.popFirst()) 18 | exp.fulfill() 19 | }.store(in: &subscriptions) 20 | 21 | /// No matter of `initialNotify` value, if `initialValue` not provided, 22 | /// `receiveValue` is called only on value update. 23 | let exp2 = expectation(description: "notify called 2") 24 | exp2.expectedFulfillmentCount = expectedValues.count 25 | event.subscribe(suppressInitialNotify: true) { _ in 26 | exp2.fulfill() 27 | }.store(in: &subscriptions) 28 | 29 | for value in expectedValues { 30 | event.notify(value) 31 | } 32 | 33 | waitForExpectations() 34 | } 35 | 36 | func test_receiveValue_initialValue() { 37 | let event = EventNotify(initialValue: 0) 38 | var subscriptions: [SubscriptionToken] = [] 39 | defer { withExtendedLifetime(subscriptions) {} } 40 | 41 | let testValues = [10, 20, 30] 42 | var expectedValues = [0] + testValues // include `initialValue`. 43 | 44 | let exp = expectation(description: "notify called") 45 | exp.expectedFulfillmentCount = expectedValues.count 46 | 47 | event.subscribe(suppressInitialNotify: false) { 48 | XCTAssertEqual($0, expectedValues.popFirst()) 49 | exp.fulfill() 50 | }.store(in: &subscriptions) 51 | 52 | /// Because `initialValue` is provided, number of `receiveValue` calls depends on 53 | /// `initialNotify` parameter. 54 | let exp2 = expectation(description: "notify called 2") 55 | exp2.expectedFulfillmentCount = testValues.count 56 | event.subscribe(suppressInitialNotify: true) { _ in 57 | exp2.fulfill() 58 | }.store(in: &subscriptions) 59 | 60 | for value in testValues { 61 | event.notify(value) 62 | } 63 | 64 | waitForExpectations() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Observing/ObservableTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import Combine 4 | import XCTest 5 | 6 | class ObservableTests: XCTestCase { 7 | var cancellables: [AnyCancellable] = [] 8 | 9 | override func setUp() { 10 | cancellables.removeAll() 11 | } 12 | 13 | func test() { 14 | var value = 10 15 | let event = EventNotify() 16 | let observable = ValueObservable( 17 | view: .init { value }, 18 | subscribeReceiveValue: { 19 | let token = event.subscribe(receiveValue: $1) 20 | $1(value, nil) 21 | return token 22 | } 23 | ) 24 | XCTAssertEqual(observable.value, 10) 25 | 26 | let exp = expectation(description: "OnChange called") 27 | 28 | observable.subscribeChange { change in 29 | XCTAssertEqual(change.old, 10) 30 | XCTAssertEqual(change.new, 20) 31 | 32 | XCTAssertEqual(observable.value, 20) 33 | exp.fulfill() 34 | }.store(in: &cancellables) 35 | 36 | value = 20 37 | event.notify(20) 38 | 39 | waitForExpectations() 40 | } 41 | 42 | func test_constant() { 43 | let observable = ValueObservable.constant(10) 44 | var value: Int? 45 | 46 | observable 47 | .subscribe(suppressInitialNotify: true) { value = $0 } 48 | .store(in: &cancellables) 49 | XCTAssertNil(value) 50 | 51 | observable 52 | .subscribe(suppressInitialNotify: false) { value = $0 } 53 | .store(in: &cancellables) 54 | XCTAssertEqual(value, 10) 55 | } 56 | 57 | func test_scope() { 58 | var value = 10 59 | let event = EventNotify() 60 | let observable = ValueObservable( 61 | view: .init { value }, 62 | subscribeReceiveValue: { 63 | let token = event.subscribe(receiveValue: $1) 64 | $1(value, nil) 65 | return token 66 | } 67 | ) 68 | 69 | let exp = expectation(description: "OnChange called") 70 | exp.expectedFulfillmentCount = 4 71 | 72 | observable.subscribeChange { change in 73 | XCTAssertEqual(change.old, 10) 74 | XCTAssertEqual(change.new, 200) 75 | exp.fulfill() 76 | }.store(in: &cancellables) 77 | 78 | let stringObservable = observable.scope(String.init) 79 | stringObservable.subscribeChange { change in 80 | XCTAssertEqual(change.old, "10") 81 | XCTAssertEqual(change.new, "200") 82 | exp.fulfill() 83 | }.store(in: &cancellables) 84 | 85 | let countObservable = stringObservable.scope(\.count) 86 | countObservable.subscribeChange { change in 87 | XCTAssertEqual(change.old, 2) 88 | XCTAssertEqual(change.new, 3) 89 | exp.fulfill() 90 | }.store(in: &cancellables) 91 | 92 | // anonymous. 93 | stringObservable.scope(\.count).subscribeChange { change in 94 | XCTAssertEqual(change.old, 2) 95 | XCTAssertEqual(change.new, 3) 96 | exp.fulfill() 97 | }.store(in: &cancellables) 98 | 99 | value = 200 100 | event.notify(200) 101 | 102 | waitForExpectations() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Observing/ValueObservingTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import Combine 4 | import XCTest 5 | 6 | private class ValueWrapper: ValueObserving { 7 | private var subscriptions: [(suppressInitialNotify: Bool, receiveValue: (T, Any?) -> Void)] = [] 8 | 9 | init(value: T) { 10 | self.value = value 11 | } 12 | 13 | var value: T { 14 | didSet { subscriptions.forEach { $0.receiveValue(value, nil) } } 15 | } 16 | 17 | func subscribe(suppressInitialNotify: Bool, receiveValue: @escaping (T, Any?) -> Void) -> SubscriptionToken { 18 | subscriptions.append((suppressInitialNotify, receiveValue)) 19 | if !suppressInitialNotify { 20 | receiveValue(value, nil) 21 | } 22 | return .init {} 23 | } 24 | } 25 | 26 | class ValueObservingTests: XCTestCase { 27 | private var cancellables: [AnyCancellable] = [] 28 | 29 | override func tearDown() { 30 | cancellables.removeAll() 31 | } 32 | 33 | func test_receiveValue() { 34 | let wrapper = ValueStore(initialValue: 10) 35 | XCTAssertEqual(wrapper.value, 10) 36 | 37 | var receivedValue: Int? 38 | wrapper.subscribe { 39 | receivedValue = $0 40 | }.store(in: &cancellables) 41 | 42 | XCTAssertEqual(receivedValue, 10) 43 | 44 | wrapper.update(20) 45 | XCTAssertEqual(receivedValue, 20) 46 | 47 | wrapper.update(30) 48 | XCTAssertEqual(receivedValue, 30) 49 | } 50 | 51 | func test_receiveChange() { 52 | let wrapper = ValueWrapper(value: 10) 53 | XCTAssertEqual(wrapper.value, 10) 54 | 55 | var receivedChange: Change? 56 | wrapper.subscribeChange { 57 | receivedChange = $0 58 | }.store(in: &cancellables) 59 | 60 | XCTAssertEqual(receivedChange, nil) 61 | 62 | wrapper.value = 20 63 | XCTAssertEqual(receivedChange, .unchecked(old: 10, new: 20)) 64 | 65 | wrapper.value = 30 66 | XCTAssertEqual(receivedChange, .unchecked(old: 20, new: 30)) 67 | } 68 | 69 | func test_publisher() { 70 | let wrapper = ValueWrapper(value: 10) 71 | let exp = expectation(description: "Value published") 72 | wrapper.publisher().sink { 73 | XCTAssertEqual($0.0, 10) 74 | exp.fulfill() 75 | }.store(in: &cancellables) 76 | 77 | waitForExpectations() 78 | } 79 | 80 | func test_stream() { 81 | let wrapper = ValueWrapper(value: 10) 82 | let exp = expectation(description: "Value async delivered") 83 | Task { 84 | for await value in wrapper.stream() { 85 | XCTAssertEqual(value.0, 10) 86 | exp.fulfill() 87 | } 88 | }.store(in: &cancellables) 89 | 90 | waitForExpectations() 91 | } 92 | 93 | func test_stream_suppressInitial() { 94 | let wrapper = ValueWrapper(value: 10) 95 | let exp = expectation(description: "Value async delivered") 96 | let stream = wrapper.stream(suppressInitialNotify: true) 97 | Task { 98 | for await value in stream { 99 | XCTAssertEqual(value.0, 20) 100 | exp.fulfill() 101 | } 102 | }.store(in: &cancellables) 103 | wrapper.value = 20 104 | 105 | waitForExpectations() 106 | } 107 | 108 | func test_stream_suppressInitial_noValue() { 109 | let wrapper = ValueWrapper(value: 10) 110 | let exp = expectation(description: "No value async delivered") 111 | exp.isInverted = true 112 | let stream = wrapper.stream(suppressInitialNotify: true) 113 | Task { 114 | for await _ in stream { 115 | exp.fulfill() 116 | } 117 | }.store(in: &cancellables) 118 | 119 | waitForExpectations(timeout: 0.05) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Other/ObjCTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | import XCTest 4 | 5 | class ObjCTests: XCTestCase { 6 | private let objcExeption = NSException(name: .genericException, reason: "Test Obj-C ex", userInfo: nil) 7 | private let cppException = CppException(what: "Test c++ ex") 8 | private let swiftError = TestError("Test error") 9 | 10 | func test_NSException_catching() { 11 | XCTAssertEqual(NSException.catching { 10 }.success, 10) 12 | 13 | let failure = NSException.catching { objcExeption.raise() }.failure 14 | XCTAssertEqual(failure?.exception.name, objcExeption.name) 15 | XCTAssertEqual(failure?.exception.reason, objcExeption.reason) 16 | } 17 | 18 | func test_NSException_catchingAll() { 19 | XCTAssertNoThrow(try NSException.catchingAll { 10 }) 20 | XCTAssertThrowsError(try NSException.catchingAll { objcExeption.raise() }) 21 | XCTAssertThrowsError(try NSException.catchingAll { throw swiftError }) 22 | } 23 | 24 | func test_StdException() { 25 | XCTAssertEqual(NSException.catching { 10 }.success, 10) 26 | 27 | let failure = CppException.catching { cppException.raise() }.failure 28 | XCTAssertEqual(failure?.what, cppException.what) 29 | } 30 | 31 | func test_catchingAny() { 32 | let nsEx = Result { try catchingAny { objcExeption.raise() } }.failure as? NSExceptionError 33 | let stdEx = Result { try catchingAny { cppException.raise() } }.failure as? CppException 34 | 35 | XCTAssertEqual(nsEx?.exception.reason, objcExeption.reason) 36 | XCTAssertEqual(stdEx?.what, cppException.what) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Threading & Concurrency/ConcurrentBlockOperationTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import Foundation 4 | import XCTest 5 | 6 | class ConcurrentBlockOperationTests: XCTestCase { 7 | func test() throws { 8 | let interval = 0.1 9 | let op = ConcurrentBlockOperation { isCancelled, completion in 10 | Self.sleep(interval: interval) 11 | completion() 12 | } 13 | let queue = OperationQueue() 14 | queue.addOperation(op) 15 | 16 | Self.sleep(interval: 0.05) 17 | 18 | XCTAssertTrue(op.isAsynchronous) 19 | XCTAssertTrue(op.isReady) 20 | XCTAssertTrue(op.isExecuting) 21 | XCTAssertFalse(op.isFinished) 22 | 23 | Self.sleep(interval: interval) 24 | 25 | XCTAssertFalse(op.isExecuting) 26 | XCTAssertTrue(op.isFinished) 27 | } 28 | 29 | func test_cancel() throws { 30 | let exp = expectation(description: "finished") 31 | let op = ConcurrentBlockOperation { isCancelled, completion in 32 | while !isCancelled.value { 33 | Thread.sleep(forTimeInterval: 0.01) 34 | } 35 | completion() 36 | exp.fulfill() 37 | } 38 | let queue = OperationQueue() 39 | queue.addOperation(op) 40 | 41 | DispatchQueue.global().asyncAfter(delay: 0.1) { 42 | op.cancel() 43 | } 44 | 45 | waitForExpectations(timeout: 0.2) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Threading & Concurrency/DispatchQueueExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | 4 | import Foundation 5 | import XCTest 6 | 7 | class DispatchQueueExtensionsTests: XCTestCase { 8 | func test_asyncPeriodically() { 9 | var count: Int = 0 10 | let limit = 5 11 | let exp = expectation(description: "Repeated action") 12 | exp.expectedFulfillmentCount = limit 13 | DispatchQueue.global().asyncPeriodically(interval: 0.01, immediately: true) { 14 | count += 1 15 | exp.fulfill() 16 | return count < limit 17 | } 18 | Thread.sleep(forTimeInterval: 0.1 * Self.waitRate) 19 | XCTAssertEqual(count, limit) 20 | waitForExpectations() 21 | } 22 | 23 | func test_asyncPeriodically_async() { 24 | var count: Int = 0 25 | let limit = 5 26 | let exp = expectation(description: "Repeated action") 27 | exp.expectedFulfillmentCount = limit 28 | DispatchQueue.global().asyncPeriodically(interval: 0.01, immediately: true) { 29 | count += 1 30 | exp.fulfill() 31 | if count < limit { 32 | $0() 33 | } 34 | } 35 | Thread.sleep(forTimeInterval: 0.1 * Self.waitRate) 36 | XCTAssertEqual(count, limit) 37 | waitForExpectations() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Threading & Concurrency/SynchronousExecutorTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SpellbookFoundation 2 | 3 | import Foundation 4 | import XCTest 5 | 6 | class SynchronousExecutorTests: XCTestCase { 7 | func test() throws { 8 | let infiniteExecutor = SynchronousExecutor(timeout: nil) 9 | let dummyValue = Dummy(value: 10, timeout: 0.05) 10 | XCTAssertEqual(try infiniteExecutor.sync(dummyValue.value), 10) 11 | XCTAssertEqual(try infiniteExecutor.sync(dummyValue.resultValue), 10) 12 | XCTAssertEqual(try infiniteExecutor.sync(dummyValue.optionalValue), 10) 13 | XCTAssertEqual(try infiniteExecutor.sync { dummyValue.multiReplyValue(count: 10, reply: $0) }, 10) 14 | 15 | let dummyError = Dummy(value: nil, timeout: 0.05) 16 | XCTAssertThrowsError(try infiniteExecutor.sync(dummyError.error)) 17 | XCTAssertThrowsError(try infiniteExecutor.sync(dummyError.resultValue)) 18 | XCTAssertEqual(try infiniteExecutor.sync(dummyError.optionalValue), nil) 19 | 20 | if #available(macOS 10.15, iOS 13, tvOS 13.0, watchOS 6.0, *) { 21 | XCTAssertEqual(try infiniteExecutor.sync(dummyValue.asyncValue), 10) 22 | XCTAssertThrowsError(try infiniteExecutor.sync(dummyError.asyncError)) 23 | } 24 | } 25 | 26 | func test_timeout() throws { 27 | let timedExecutor = SynchronousExecutor(timeout: 0.05) 28 | let dummyValue = Dummy(value: 10, timeout: 0.1) 29 | XCTAssertThrowsError(try timedExecutor.sync(dummyValue.value)) 30 | XCTAssertThrowsError(try timedExecutor.sync(dummyValue.resultValue)) 31 | XCTAssertThrowsError(try timedExecutor.sync(dummyValue.optionalValue)) 32 | 33 | let dummyError = Dummy(value: nil, timeout: 0.1) 34 | XCTAssertThrowsError(try timedExecutor.sync(dummyError.error)) 35 | XCTAssertThrowsError(try timedExecutor.sync(dummyError.resultValue)) 36 | XCTAssertThrowsError(try timedExecutor.sync(dummyError.optionalValue)) 37 | 38 | if #available(macOS 10.15, iOS 13, tvOS 13.0, watchOS 6.0, *) { 39 | XCTAssertThrowsError(try timedExecutor.sync(dummyValue.asyncValue)) 40 | XCTAssertThrowsError(try timedExecutor.sync(dummyError.asyncError)) 41 | } 42 | } 43 | } 44 | 45 | private struct Dummy { 46 | var value: T! 47 | var timeout: TimeInterval? 48 | 49 | func value(reply: @escaping (T) -> Void) { 50 | execute { reply(value) } 51 | } 52 | 53 | func optionalValue(reply: @escaping (T?) -> Void) { 54 | execute { reply(value) } 55 | } 56 | 57 | func resultValue(reply: @escaping (Result) -> Void) { 58 | execute { reply(Result { try value.get() }) } 59 | } 60 | 61 | func error(reply: @escaping (Error?) -> Void) { 62 | execute { reply(Result { try value.get() }.failure) } 63 | } 64 | 65 | func multiReplyValue(count: Int, reply: @escaping (T) -> Void) { 66 | execute { 67 | for _ in 0.. Void) { 74 | DispatchQueue.global().async { 75 | timeout.flatMap(Thread.sleep(forTimeInterval:)) 76 | action() 77 | } 78 | } 79 | } 80 | 81 | extension Dummy { 82 | func asyncValue() async -> T { 83 | await withCheckedContinuation { continuation in 84 | value { continuation.resume(returning: $0) } 85 | } 86 | } 87 | 88 | func asyncError() async throws { 89 | let _: Void = try await withCheckedThrowingContinuation { continuation in 90 | error { 91 | if let error = $0 { 92 | continuation.resume(throwing: error) 93 | } else { 94 | continuation.resume(returning: ()) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Types & PropertyWrappers/PropertyWrapperTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | 3 | import Foundation 4 | import XCTest 5 | 6 | class PropertyWrapperTests: XCTestCase { 7 | func test_clamping() { 8 | @Clamped(0 ... 10) var a = 15 9 | XCTAssertEqual(a, 10) 10 | 11 | a = 0 12 | XCTAssertEqual(a, 0) 13 | 14 | a = -5 15 | XCTAssertEqual(a, 0) 16 | 17 | a = 3 18 | XCTAssertEqual(a, 3) 19 | } 20 | 21 | func test_Indirect_valueType() throws { 22 | var a = Indirect(wrappedValue: 123) 23 | var b = a 24 | 25 | a.wrappedValue = 10 26 | b.wrappedValue = 20 27 | 28 | XCTAssertEqual(a.wrappedValue, 10) 29 | XCTAssertEqual(b.wrappedValue, 20) 30 | } 31 | 32 | func test_Indirect_codable() throws { 33 | struct Test: Codable { 34 | @Indirect var value = 123 35 | } 36 | let data = try JSONEncoder().encode(Test()) 37 | let string = try String(data: data, encoding: .utf8).get() 38 | XCTAssertEqual(string, #"{"value":123}"#) 39 | 40 | XCTAssertEqual(try JSONDecoder().decode(Test.self, from: data).value, 123) 41 | } 42 | 43 | func test_Indirect_codable_optional() throws { 44 | struct Test: Codable { 45 | @Indirect var value: Int? 46 | } 47 | let jsonValue = "{}" 48 | XCTAssertEqual(try JSONDecoder().decode(Test.self, from: Data(jsonValue.utf8)).value, nil) 49 | XCTAssertEqual(try JSONEncoder().encode(Test()), Data(jsonValue.utf8)) 50 | 51 | let jsonArray = #"[{},{},{}]"# 52 | XCTAssertEqual(try JSONDecoder().decode([Test].self, from: Data(jsonArray.utf8)).count, 3) 53 | XCTAssertEqual(try JSONEncoder().encode([Test](repeating: Test(), count: 3)), Data(jsonArray.utf8)) 54 | } 55 | 56 | func test_ValueView() { 57 | XCTAssertEqual(ValueView.constant(10).value, 10) 58 | 59 | var value1 = 1 60 | @ValueViewed var view1 = value1 61 | 62 | XCTAssertEqual(view1, value1) 63 | value1 = 10 64 | XCTAssertEqual(view1, value1) 65 | value1 = 20 66 | XCTAssertEqual($view1.value, value1) 67 | 68 | var value2 = 1 69 | let view2 = ValueView { value2 } 70 | 71 | XCTAssertEqual(view2.value, value2) 72 | value2 = 10 73 | XCTAssertEqual(view2.value, value2) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Types & PropertyWrappers/RefreshableTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import XCTest 3 | 4 | class RefreshableTests: XCTestCase { 5 | func test_basic() { 6 | var expired = false 7 | var newValue = 1 8 | var value = Refreshable( 9 | wrappedValue: 10, 10 | expire: .init(checkExpired: { _ in expired }, onUpdate: { _ in expired = false }), 11 | source: .init(newValue: { _ in newValue }) 12 | ) 13 | 14 | // Refreshable holds value it initialized with 15 | XCTAssertEqual(value.wrappedValue, 10) 16 | 17 | // Mark Refreshable as expired. Value should be updated, expired state reset 18 | expired = true 19 | XCTAssertEqual(value.wrappedValue, 1) 20 | XCTAssertEqual(expired, false) 21 | 22 | // New value will be picked up only when previous is expired 23 | newValue = 20 24 | XCTAssertEqual(value.wrappedValue, 1) 25 | expired = true 26 | XCTAssertEqual(value.wrappedValue, 20) 27 | } 28 | 29 | func test_ttl() { 30 | var value = Refreshable( 31 | wrappedValue: 10, 32 | expire: .ttl(0.05), 33 | source: .defaultValue(1) 34 | ) 35 | 36 | XCTAssertEqual(value.wrappedValue, 10) 37 | 38 | // Value is expired after 0.1 and reset to default 39 | Thread.sleep(forTimeInterval: 0.1) 40 | XCTAssertEqual(value.wrappedValue, 1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Types & PropertyWrappers/ResourceTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | import XCTest 4 | 5 | class ResourceTests: XCTestCase { 6 | func test_accessValue() { 7 | let resource = Resource.stub(10) 8 | 9 | XCTAssertEqual(resource.wrappedValue, 10) 10 | resource.withValue { XCTAssertEqual($0, 10) } 11 | } 12 | 13 | func test_reset() { 14 | func test(free: Bool, newValue: Int?, cleanupCalls: Int) { 15 | withScope { 16 | let expectation = expectation(description: "Cleanup should be called \(cleanupCalls) times only") 17 | if cleanupCalls > 0 { 18 | expectation.expectedFulfillmentCount = cleanupCalls 19 | } else { 20 | expectation.isInverted = true 21 | } 22 | 23 | let resource = Resource(10) { _ in 24 | expectation.fulfill() 25 | } 26 | 27 | XCTAssertEqual(resource.reset(free: free, to: newValue), 10) 28 | XCTAssertEqual(resource.wrappedValue, newValue ?? 10) 29 | } 30 | 31 | waitForExpectations() 32 | } 33 | 34 | test(free: true, newValue: nil, cleanupCalls: 1) 35 | test(free: false, newValue: nil, cleanupCalls: 0) 36 | test(free: true, newValue: 20, cleanupCalls: 2) 37 | test(free: false, newValue: 20, cleanupCalls: 1) 38 | } 39 | 40 | func test_DeinitAction() { 41 | let exp = expectation(description: "Action on deinit.") 42 | DispatchQueue.global().async { 43 | _ = DeinitAction { exp.fulfill() } 44 | } 45 | 46 | waitForExpectations() 47 | } 48 | 49 | func test_pointer() { 50 | let exp = expectation(description: "Class deinited.") 51 | let ptr = UnsafeMutablePointer.allocate(capacity: 1) 52 | ptr.initialize(to: .init(exp: exp)) 53 | 54 | let resource = Resource.pointer(ptr) 55 | resource.reset() 56 | 57 | waitForExpectations() 58 | } 59 | 60 | func test_pointerFromValue() { 61 | let exp = expectation(description: "Class deinited.") 62 | let resource = Resource>.pointer(value: Fulfill(exp: exp)) 63 | resource.reset() 64 | 65 | waitForExpectations() 66 | } 67 | 68 | func test_bufferPointer() { 69 | let exp = expectation(description: "Class deinited.") 70 | exp.expectedFulfillmentCount = 3 71 | let ptr = UnsafeMutableBufferPointer.allocate(capacity: 3) 72 | _ = ptr.initialize(from: [Fulfill(exp: exp), Fulfill(exp: exp), Fulfill(exp: exp)]) 73 | 74 | let resource = Resource.pointer(ptr) 75 | resource.reset() 76 | 77 | waitForExpectations() 78 | } 79 | 80 | func test_bufferPointer_fromValue() { 81 | let exp = expectation(description: "Class deinited.") 82 | exp.expectedFulfillmentCount = 3 83 | let resource = Resource.pointer(values: [Fulfill(exp: exp), Fulfill(exp: exp), Fulfill(exp: exp)]) 84 | resource.reset() 85 | 86 | waitForExpectations() 87 | } 88 | } 89 | 90 | private class Fulfill { 91 | let exp: XCTestExpectation 92 | init(exp: XCTestExpectation) { self.exp = exp } 93 | deinit { exp.fulfill() } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/SpellbookTests/Types & PropertyWrappers/TypesTests.swift: -------------------------------------------------------------------------------- 1 | import SpellbookFoundation 2 | import SpellbookTestUtils 3 | import XCTest 4 | 5 | class TypesTests: XCTestCase { 6 | func test_ProgressValue() { 7 | var value = ProgressValue(current: 1, total: 5) 8 | XCTAssertEqual(value.ratio, 0.2) 9 | 10 | value.increment() 11 | XCTAssertEqual(value.current, 2) 12 | XCTAssertEqual(value.total, 5) 13 | 14 | value.increment(by: 10) 15 | XCTAssertEqual(value.current, 5) 16 | XCTAssertEqual(value.total, 5) 17 | 18 | value.increment(by: 5, unsafe: true) 19 | XCTAssertEqual(value.current, 10) 20 | XCTAssertEqual(value.total, 5) 21 | XCTAssertEqual(value.ratio, 1) 22 | XCTAssertEqual(value.unsafeRatio, 2) 23 | 24 | value.increment(by: -15) 25 | XCTAssertEqual(value.current, 0) 26 | XCTAssertEqual(value.total, 5) 27 | 28 | value.increment(by: -5, unsafe: true) 29 | XCTAssertEqual(value.current, -5) 30 | XCTAssertEqual(value.total, 5) 31 | } 32 | } 33 | --------------------------------------------------------------------------------