├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ └── build.yml ├── .gitignore ├── .swift-version ├── .swiftformat ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── PrefsKit-CI.xcscheme ├── Images └── prefskit-banner.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── PrefsKit │ └── PrefsKit.swift ├── PrefsKitCore │ ├── Macro Declarations.swift │ └── PrefsKitCore.swift ├── PrefsKitMacrosImplementation │ ├── PrefMacro │ │ ├── AtomicPrefMacro.swift │ │ ├── CodingPrefMacro.swift │ │ ├── InlinePrefMacro.swift │ │ ├── JSONDataCodablePrefMacro.swift │ │ ├── JSONStringCodablePrefMacro.swift │ │ ├── PrefMacro.swift │ │ ├── PrefMacroError.swift │ │ └── RawRepresentablePrefMacro.swift │ ├── PrefsKitMacrosPlugin.swift │ ├── PrefsSchemaMacro │ │ ├── PrefsSchemaMacro.swift │ │ └── PrefsSchemaMacroError.swift │ ├── RawPrefMacro │ │ └── RawPrefMacro.swift │ └── Support │ │ ├── Internal Types.swift │ │ └── PrefMacroUtils.swift ├── PrefsKitTypes │ ├── PrefsCodable Prototypes │ │ ├── AtomicPrefsCoding.swift │ │ ├── CodableArrayPrefsCoding.swift │ │ ├── CodableDictionaryPrefsCoding.swift │ │ ├── CodablePrefsCoding.swift │ │ ├── JSONDataCodablePrefsCoding.swift │ │ ├── JSONStringCodablePrefsCoding.swift │ │ ├── PrefsCoding.swift │ │ ├── PrefsCodingTuple.swift │ │ ├── RawRepresentableArrayPrefsCoding.swift │ │ ├── RawRepresentableDictionaryPrefsCoding.swift │ │ └── RawRepresentablePrefsCoding.swift │ ├── PrefsCodable Strategies │ │ ├── Base64StringDataPrefsCoding.swift │ │ ├── BoolIntegerPrefsCoding.swift │ │ ├── BoolStringPrefsCoding.swift │ │ ├── CompressedDataPrefsCoding.swift │ │ ├── ISO8601StringDatePrefsCoding.swift │ │ ├── IntegerPrefsCoding.swift │ │ ├── IntegerStringPrefsCoding.swift │ │ └── URLStringPrefsCoding.swift │ ├── PrefsCodable │ │ ├── Atomic │ │ │ └── AtomicPrefsCodable.swift │ │ ├── Codable │ │ │ ├── CodablePrefsCodable.swift │ │ │ └── JSON │ │ │ │ ├── JSONDataCodablePrefsCodable.swift │ │ │ │ └── JSONStringCodablePrefsCodable.swift │ │ ├── PrefsCodable.swift │ │ └── RawRepresentable │ │ │ ├── PrefsCodable+RawRepresentable.swift │ │ │ └── RawRepresentablePrefsCodable.swift │ ├── PrefsKey Prototypes │ │ └── PrefsKey Prototypes.swift │ ├── PrefsKey │ │ ├── DefaultedPrefsKey.swift │ │ └── PrefsKey.swift │ ├── PrefsSchema │ │ ├── AnyPrefsSchema.swift │ │ ├── PrefsSchema.swift │ │ └── PropertyWrappers │ │ │ ├── PrefsStorageModeWrapper.swift │ │ │ └── PrefsStorageWrapper.swift │ ├── PrefsStorage Prototypes │ │ ├── AnyPrefsStorage+PrefsStorageExportable.swift │ │ ├── AnyPrefsStorage+PrefsStorageImportable.swift │ │ ├── AnyPrefsStorage.swift │ │ ├── DictionaryPrefsStorage+PrefsStorageExportable.swift │ │ ├── DictionaryPrefsStorage+PrefsStorageImportable.swift │ │ ├── DictionaryPrefsStorage+PrefsStorageInitializable.swift │ │ ├── DictionaryPrefsStorage.swift │ │ ├── UserDefaultsPrefsStorage+PrefsStorageExportable.swift │ │ ├── UserDefaultsPrefsStorage+PrefsStorageImportable.swift │ │ ├── UserDefaultsPrefsStorage+Utilities.swift │ │ └── UserDefaultsPrefsStorage.swift │ ├── PrefsStorage Traits Prototypes │ │ ├── JSON │ │ │ ├── JSON Utilities.swift │ │ │ ├── JSONPrefsStorageExportFormat.swift │ │ │ └── JSONPrefsStorageImportFormat.swift │ │ └── PList │ │ │ ├── PList Utilities.swift │ │ │ ├── PListPrefsStorageExportFormat.swift │ │ │ ├── PListPrefsStorageExportStrategy.swift │ │ │ ├── PListPrefsStorageImportFormat.swift │ │ │ └── PListPrefsStorageImportStrategy.swift │ ├── PrefsStorage Traits │ │ ├── PrefsStorageExportable │ │ │ ├── PrefsStorageExportFormat │ │ │ │ └── PrefsStorageExportFormat.swift │ │ │ ├── PrefsStorageExportStrategy │ │ │ │ ├── PrefsStorageExportStrategy.swift │ │ │ │ ├── PrefsStorageMappingExportStrategy.swift │ │ │ │ └── PrefsStoragePassthroughExportStrategy.swift │ │ │ └── PrefsStorageExportable.swift │ │ ├── PrefsStorageImportable │ │ │ ├── PrefsStorageImportFormat │ │ │ │ └── PrefsStorageImportFormat.swift │ │ │ ├── PrefsStorageImportStrategy │ │ │ │ ├── PrefsStorageImportStrategy.swift │ │ │ │ ├── PrefsStorageMappingImportStrategy.swift │ │ │ │ ├── PrefsStoragePassthroughImportStrategy.swift │ │ │ │ └── PrefsStorageTypedImportStrategy.swift │ │ │ ├── PrefsStorageImportable.swift │ │ │ └── PrefsStorageUpdateStrategy.swift │ │ └── PrefsStorageInitializable │ │ │ └── PrefsStorageInitializable.swift │ ├── PrefsStorage │ │ ├── PrefsStorage+DefaultedPrefsKey.swift │ │ ├── PrefsStorage+PrefsCodable.swift │ │ ├── PrefsStorage+PrefsKey.swift │ │ ├── PrefsStorage+Static.swift │ │ ├── PrefsStorage.swift │ │ ├── PrefsStorageError.swift │ │ └── PrefsStorageMode.swift │ ├── PrefsStorageValue │ │ ├── PrefsStorageValue Types.swift │ │ └── PrefsStorageValue.swift │ └── Utilities │ │ ├── Concurrency.swift │ │ ├── NSNumber.swift │ │ ├── Outsourced │ │ └── UserDefaults Outsourced.swift │ │ └── UserDefaults.swift └── PrefsKitUI │ ├── MultiplatformSection.swift │ ├── PrefsKitUI.swift │ ├── SectionFooterView.swift │ └── SystemSettings │ ├── SystemSettings Panel.swift │ └── SystemSettings.swift └── Tests ├── PrefsKitCoreTests ├── ActorTests.swift ├── ChainingEncodingStrategiesTests.swift ├── CodableArrayTests.swift ├── CodableDictionaryTests.swift ├── CustomEncodingTests.swift ├── MacroTests.swift ├── PrefsCodable Strategies │ ├── Base64StringDataPrefsCodingTests.swift │ ├── BoolIntegerPrefCodingTests.swift │ ├── BoolStringPrefsCodingTests.swift │ ├── CompressedDataPrefsCodingTests.swift │ ├── ISO8601StringDatePrefsCodingTests.swift │ ├── IntegerPrefsCodingTests.swift │ ├── IntegerStringPrefsCodingTests.swift │ └── URLStringPrefsCodingTests.swift ├── PrefsSchemaPrefsStorageTests.swift ├── RawPrefsKeyTests.swift ├── RawRepresentableArrayTests.swift ├── RawRepresentableDictionaryTests.swift └── UserDefaultsPrefsSchemaTests.swift └── PrefsKitTypesTests ├── PrefsStorage Traits ├── PrefsStorageExportableTests.swift ├── PrefsStorageImportableLoadTests.swift ├── PrefsStorageImportableTests.swift └── PrefsStorageInitializableTests.swift ├── PrefsStorage ├── PrefsStorage+Static Tests.swift ├── PrefsStorageArrayTests.swift └── PrefsStorageDictionaryTests.swift └── TestContent ├── TestContent Basic.swift └── TestContent.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: orchetect 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report about a reproducible problem. 3 | labels: bug 4 | body: 5 | - type: textarea 6 | id: bug-description 7 | attributes: 8 | label: Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc. 9 | description: "A clear and concise description of the bug and steps to reproduce. Include system details (OS version) and build environment particulars (Xcode version, etc.)." 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature request 4 | url: https://github.com/orchetect/PrefsKit/discussions 5 | about: Suggest new features or improvements. 6 | - name: I need help setting up or troubleshooting 7 | url: https://github.com/orchetect/PrefsKit/discussions 8 | about: Questions not answered in the documentation, discussions forum, or example projects. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | [Dd]ev/ 3 | 4 | # Xcode 5 | 6 | # macOS 7 | .DS_Store 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | ## SPM support in Xcode 40 | # .swiftpm - for shared CI schemes we need these checked in: 41 | # -> .swiftpm/xcode/package.xcworkspace 42 | # -> .swiftpm/xcode/xcshareddata/xcschemes/*.* 43 | 44 | # Swift Package Manager 45 | # 46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 47 | Packages/ 48 | Package.pins 49 | Package.resolved 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | 58 | Pods/ 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots/**/*.png 77 | fastlane/test_output 78 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 6.0 -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --acronyms ID,URL,UUID 2 | --allman false 3 | --assetliterals visual-width 4 | --beforemarks 5 | --binarygrouping 8,8 6 | --categorymark "MARK: %c" 7 | --classthreshold 0 8 | --closingparen balanced 9 | --closurevoid remove 10 | --commas inline 11 | --conflictmarkers reject 12 | --decimalgrouping ignore 13 | --elseposition same-line 14 | --emptybraces spaced 15 | --enumthreshold 0 16 | --exponentcase lowercase 17 | --exponentgrouping disabled 18 | --extensionacl on-declarations 19 | --extensionlength 0 20 | --extensionmark "MARK: - %t + %c" 21 | --fractiongrouping disabled 22 | --fragment false 23 | --funcattributes prev-line 24 | --groupedextension "MARK: %c" 25 | --guardelse auto 26 | --header "\n {file}\n PrefsKit • https://github.com/orchetect/PrefsKit\n © {year} Steffan Andrews • Licensed under MIT License\n" 27 | --hexgrouping 8,8 28 | --hexliteralcase uppercase 29 | --ifdef no-indent 30 | --importgrouping alpha 31 | --indent 4 32 | --indentcase false 33 | --indentstrings true 34 | --lifecycle 35 | --lineaftermarks true 36 | --linebreaks lf 37 | --markcategories true 38 | --markextensions always 39 | --marktypes always 40 | --maxwidth 140 41 | --modifierorder 42 | --nevertrailing 43 | --nospaceoperators 44 | --nowrapoperators 45 | --octalgrouping 4,8 46 | --someany false 47 | --operatorfunc spaced 48 | --organizetypes actor,class,enum,struct 49 | --patternlet hoist 50 | --ranges spaced 51 | --redundanttype infer-locals-only 52 | --self remove 53 | --selfrequired 54 | --semicolons inline 55 | --shortoptionals always 56 | --smarttabs enabled 57 | --stripunusedargs always 58 | --structthreshold 0 59 | --tabwidth unspecified 60 | --trailingclosures 61 | --trimwhitespace nonblank-lines 62 | --typeattributes preserve 63 | --typemark "MARK: - %t" 64 | --varattributes preserve 65 | --voidtype void 66 | --wraparguments before-first 67 | --wrapcollections before-first 68 | --wrapconditions after-first 69 | --wrapparameters before-first 70 | --wrapreturntype preserve 71 | --wrapternary before-operators 72 | --wraptypealiases before-first 73 | --xcodeindentation enabled 74 | --yodaswap always 75 | --disable blankLinesAroundMark,consecutiveSpaces,preferKeyPath,redundantParens,sortDeclarations,sortedImports,unusedArguments 76 | --enable blankLinesBetweenImports,blockComments,isEmpty,wrapEnumCases 77 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/PrefsKit-CI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 63 | 69 | 70 | 71 | 73 | 79 | 80 | 81 | 82 | 83 | 94 | 95 | 99 | 100 | 101 | 102 | 108 | 109 | 115 | 116 | 117 | 118 | 120 | 121 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /Images/prefskit-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orchetect/PrefsKit/c496e0c82e186db2450eb33a9f65343023001224/Images/prefskit-banner.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Steffan Andrews - https://github.com/orchetect 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: 6.0 2 | 3 | import CompilerPluginSupport 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PrefsKit", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], 9 | products: [ 10 | .library(name: "PrefsKit", targets: ["PrefsKit"]), 11 | .library(name: "PrefsKitCore", targets: ["PrefsKitCore"]), 12 | .library(name: "PrefsKitUI", targets: ["PrefsKitUI"]) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "PrefsKit", 20 | dependencies: ["PrefsKitCore", "PrefsKitUI"] 21 | ), 22 | .target( 23 | name: "PrefsKitCore", 24 | dependencies: ["PrefsKitTypes", "PrefsKitMacrosImplementation"] 25 | ), 26 | .target( 27 | name: "PrefsKitTypes", 28 | dependencies: [] 29 | ), 30 | .macro( 31 | name: "PrefsKitMacrosImplementation", 32 | dependencies: [ 33 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 34 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 35 | ] 36 | ), 37 | .target( 38 | name: "PrefsKitUI", 39 | dependencies: ["PrefsKitCore"] 40 | ), 41 | .testTarget( 42 | name: "PrefsKitCoreTests", 43 | dependencies: [ 44 | "PrefsKitCore", 45 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") 46 | ] 47 | ), 48 | .testTarget( 49 | name: "PrefsKitTypesTests", 50 | dependencies: ["PrefsKitTypes"] 51 | ) 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /Sources/PrefsKit/PrefsKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsKit.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | @_exported import PrefsKitCore 8 | @_exported import PrefsKitTypes 9 | @_exported import PrefsKitUI 10 | -------------------------------------------------------------------------------- /Sources/PrefsKitCore/Macro Declarations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Macro Declarations.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitTypes 9 | 10 | // MARK: - PrefsSchema (Class) 11 | 12 | @attached(member, names: named(_$observationRegistrar)) 13 | @attached(extension, names: named(access), named(withMutation), conformances: Observable & PrefsSchema) 14 | public macro PrefsSchema() 15 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "PrefsSchemaMacro") 16 | 17 | // MARK: - Pref 18 | 19 | @attached(accessor, names: named(get), named(set), named(_modify)) 20 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_)) 21 | public macro Pref(key: String? = nil) 22 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "AtomicPrefMacro") 23 | 24 | @attached(accessor, names: named(get), named(set), named(_modify)) 25 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_)) 26 | public macro Pref(key: String? = nil, coding: Coding) 27 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "CodingPrefMacro") 28 | 29 | @attached(accessor, names: named(get), named(set), named(_modify)) 30 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_)) 31 | public macro Pref( 32 | key: String? = nil, 33 | encode: (Value) -> StorageValue?, 34 | decode: (StorageValue) -> Value? 35 | ) 36 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "InlinePrefMacro") 37 | 38 | // MARK: - RawRepresentablePref 39 | 40 | @attached(accessor, names: named(get), named(set), named(_modify)) 41 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_)) 42 | public macro RawRepresentablePref(key: String? = nil) 43 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "RawRepresentablePrefMacro") 44 | 45 | // MARK: - JSONDataCodablePref 46 | 47 | @attached(accessor, names: named(get), named(set), named(_modify)) 48 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_)) 49 | public macro JSONDataCodablePref(key: String? = nil) 50 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "JSONDataCodablePrefMacro") 51 | 52 | // MARK: - JSONStringCodablePref 53 | 54 | @attached(accessor, names: named(get), named(set), named(_modify)) 55 | @attached(peer, names: /* arbitrary */ prefixed(__PrefCoding_), prefixed(__PrefValue_)) 56 | public macro JSONStringCodablePref(key: String? = nil) 57 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "JSONStringCodablePrefMacro") 58 | 59 | // MARK: - RawPref 60 | 61 | @attached(accessor, names: named(get), named(set), named(_modify)) 62 | @attached(peer, names: /* arbitrary */ prefixed(__PrefValue_)) 63 | public macro RawPref(key: String? = nil) 64 | = #externalMacro(module: "PrefsKitMacrosImplementation", type: "RawPrefMacro") 65 | -------------------------------------------------------------------------------- /Sources/PrefsKitCore/PrefsKitCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsKitCore.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | @_exported import PrefsKitTypes 8 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/AtomicPrefMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicPrefMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public struct AtomicPrefMacro: PrefMacro { 8 | public static let keyStructName: String = "AnyAtomicPrefsKey" 9 | public static let defaultedKeyStructName: String = "AnyDefaultedAtomicPrefsKey" 10 | public static let hasCustomCoding: Bool = false 11 | public static let hasInlineCoding: Bool = false 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/CodingPrefMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodingPrefMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public struct CodingPrefMacro: PrefMacro { 8 | public static let keyStructName: String = "AnyPrefsKey" 9 | public static let defaultedKeyStructName: String = "AnyDefaultedPrefsKey" 10 | public static let hasCustomCoding: Bool = true 11 | public static let hasInlineCoding: Bool = false 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/InlinePrefMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlinePrefMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public struct InlinePrefMacro: PrefMacro { 8 | public static let keyStructName: String = "AnyPrefsKey" 9 | public static let defaultedKeyStructName: String = "AnyDefaultedPrefsKey" 10 | public static let hasCustomCoding: Bool = false 11 | public static let hasInlineCoding: Bool = true 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/JSONDataCodablePrefMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDataCodablePrefMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public struct JSONDataCodablePrefMacro: PrefMacro { 8 | public static let keyStructName: String = "AnyJSONDataCodablePrefsKey" 9 | public static let defaultedKeyStructName: String = "AnyDefaultedJSONDataCodablePrefsKey" 10 | public static let hasCustomCoding: Bool = false 11 | public static let hasInlineCoding: Bool = false 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/JSONStringCodablePrefMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONStringCodablePrefMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public struct JSONStringCodablePrefMacro: PrefMacro { 8 | public static let keyStructName: String = "AnyJSONStringCodablePrefsKey" 9 | public static let defaultedKeyStructName: String = "AnyDefaultedJSONStringCodablePrefsKey" 10 | public static let hasCustomCoding: Bool = false 11 | public static let hasInlineCoding: Bool = false 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/PrefMacroError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefMacroError.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | public enum PrefMacroError: LocalizedError { 10 | case missingKeyArgument 11 | case missingCodingArgument 12 | case missingDecodeArgument 13 | case incorrectSyntax 14 | case invalidArgumentLabel 15 | case invalidKeyArgumentType 16 | case invalidVariableName 17 | case notVarDeclaration 18 | case missingDefaultValue 19 | case missingOrInvalidTypeAnnotation 20 | case modifiersNotAllowed 21 | case noDefaultValueAllowed 22 | case tooManyArguments 23 | 24 | public var errorDescription: String? { 25 | switch self { 26 | case .missingKeyArgument: 27 | "Missing value for key argument." 28 | case .missingCodingArgument: 29 | "Missing value for coding argument." 30 | case .missingDecodeArgument: 31 | "Missing value for decode argument." 32 | case .incorrectSyntax: 33 | "Incorrect syntax." 34 | case .invalidArgumentLabel: 35 | "Invalid argument label." 36 | case .invalidKeyArgumentType: 37 | "Invalid key argument type." 38 | case .invalidVariableName: 39 | "Invalid variable name." 40 | case .notVarDeclaration: 41 | "Must be a var declaration." 42 | case .missingDefaultValue: 43 | "Missing default value." 44 | case .missingOrInvalidTypeAnnotation: 45 | "Missing or invalid type annotation." 46 | case .modifiersNotAllowed: 47 | "Modifiers are not allowed." 48 | case .noDefaultValueAllowed: 49 | "No default value allowed." 50 | case .tooManyArguments: 51 | "Too many arguments." 52 | } 53 | } 54 | } 55 | 56 | extension PrefMacroError: CustomStringConvertible { 57 | public var description: String { errorDescription ?? localizedDescription } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefMacro/RawRepresentablePrefMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentablePrefMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public struct RawRepresentablePrefMacro: PrefMacro { 8 | public static let keyStructName: String = "AnyRawRepresentablePrefsKey" 9 | public static let defaultedKeyStructName: String = "AnyDefaultedRawRepresentablePrefsKey" 10 | public static let hasCustomCoding: Bool = false 11 | public static let hasInlineCoding: Bool = false 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefsKitMacrosPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsKitMacrosPlugin.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftCompilerPlugin 8 | import SwiftSyntax 9 | import SwiftSyntaxBuilder 10 | import SwiftSyntaxMacros 11 | 12 | @main 13 | struct PrefsKitMacrosPlugin: CompilerPlugin { 14 | let providingMacros: [Macro.Type] = [ 15 | PrefsSchemaMacro.self, 16 | AtomicPrefMacro.self, 17 | CodingPrefMacro.self, 18 | InlinePrefMacro.self, 19 | RawRepresentablePrefMacro.self, 20 | JSONDataCodablePrefMacro.self, 21 | JSONStringCodablePrefMacro.self, 22 | RawPrefMacro.self 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefsSchemaMacro/PrefsSchemaMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsSchemaMacro.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import SwiftCompilerPlugin 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | 13 | public struct PrefsSchemaMacro { } 14 | 15 | extension PrefsSchemaMacro: MemberMacro { 16 | public static func expansion( 17 | of node: AttributeSyntax, 18 | providingMembersOf declaration: some DeclGroupSyntax, 19 | conformingTo protocols: [TypeSyntax], 20 | in context: some MacroExpansionContext 21 | ) throws -> [DeclSyntax] { 22 | [ 23 | """ 24 | @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar() 25 | """ 26 | ] 27 | } 28 | } 29 | 30 | extension PrefsSchemaMacro: ExtensionMacro { 31 | public static func expansion( 32 | of node: SwiftSyntax.AttributeSyntax, 33 | attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, 34 | providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, 35 | conformingTo protocols: [SwiftSyntax.TypeSyntax], 36 | in context: some SwiftSyntaxMacros.MacroExpansionContext 37 | ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { 38 | let prefsSchemaExtension = try ExtensionDeclSyntax( 39 | "extension \(type.trimmed): PrefsSchema { }" 40 | ) 41 | let observableExtension = try ExtensionDeclSyntax( 42 | """ 43 | extension \(type.trimmed): Observable { 44 | internal nonisolated func access( 45 | keyPath: KeyPath<\(type.trimmed), Member> 46 | ) { 47 | _$observationRegistrar.access(self, keyPath: keyPath) 48 | } 49 | 50 | internal nonisolated func withMutation( 51 | keyPath: KeyPath<\(type.trimmed), Member>, 52 | _ mutation: () throws -> MutationResult 53 | ) rethrows -> MutationResult { 54 | try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) 55 | } 56 | } 57 | """ 58 | ) 59 | 60 | return [prefsSchemaExtension, observableExtension] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/PrefsSchemaMacro/PrefsSchemaMacroError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsSchemaMacroError.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension PrefsSchemaMacro { 10 | public enum PrefsSchemaMacroError: LocalizedError { 11 | case incorrectSyntax 12 | 13 | public var errorDescription: String? { 14 | switch self { 15 | case .incorrectSyntax: 16 | "Incorrect syntax." 17 | } 18 | } 19 | } 20 | } 21 | 22 | extension PrefsSchemaMacro.PrefsSchemaMacroError: CustomStringConvertible { 23 | public var description: String { errorDescription ?? localizedDescription } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PrefsKitMacrosImplementation/Support/Internal Types.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Internal Types.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import SwiftCompilerPlugin 9 | import SwiftSyntax 10 | import SwiftSyntaxBuilder 11 | import SwiftSyntaxMacros 12 | 13 | enum TypeBinding { 14 | case nonOptional(TypeSyntax) 15 | case optional(TypeSyntax) 16 | 17 | var isOptional: Bool { 18 | switch self { 19 | case .nonOptional: false 20 | case .optional: true 21 | } 22 | } 23 | 24 | var description: String { 25 | switch self { 26 | case let .nonOptional(typeSyntax): 27 | typeSyntax.trimmedDescription 28 | case let .optional(typeSyntax): 29 | typeSyntax.trimmedDescription 30 | } 31 | } 32 | } 33 | 34 | struct TypeBindingInfo { 35 | let isOptional: Bool 36 | let typeName: String 37 | let keyAndCodingStructName: String 38 | let keyAndCodingStructDeclaration: String 39 | let privateKeyVarDeclaration: String 40 | let privateValueVarDeclaration: String 41 | 42 | init( 43 | for macro: PrefMacro.Type, 44 | from declaration: some DeclSyntaxProtocol, 45 | keyName: String, 46 | privateKeyVarName: String, 47 | privateValueVarName: String 48 | ) throws { 49 | guard let varDec = declaration.as(VariableDeclSyntax.self) 50 | else { 51 | throw PrefMacroError.incorrectSyntax 52 | } 53 | try self.init( 54 | for: macro, 55 | from: varDec, 56 | keyName: keyName, 57 | privateKeyVarName: privateKeyVarName, 58 | privateValueVarName: privateValueVarName 59 | ) 60 | } 61 | 62 | init( 63 | for macro: PrefMacro.Type, 64 | from varDec: VariableDeclSyntax, 65 | keyName: String, 66 | privateKeyVarName: String, 67 | privateValueVarName: String, 68 | customCodingDecl: String? 69 | ) throws { 70 | let typeBinding = try PrefMacroUtils.typeBinding(from: varDec) 71 | typeName = typeBinding.description 72 | 73 | let useCustomCodingDecl = macro.hasCustomCoding || macro.hasInlineCoding 74 | 75 | isOptional = typeBinding.isOptional 76 | if isOptional { 77 | // must not have a default value 78 | guard (try? PrefMacroUtils.defaultValue(from: varDec)) == nil else { 79 | throw PrefMacroError.noDefaultValueAllowed 80 | } 81 | keyAndCodingStructName = "\(PrefMacroUtils.moduleNamePrefix)\(macro.keyStructName)\(useCustomCodingDecl ? "" : "<\(typeName)>")" 82 | keyAndCodingStructDeclaration = keyAndCodingStructName + 83 | "(key: \(keyName)\(useCustomCodingDecl ? ", coding: \(customCodingDecl ?? "nil")" : ""))" 84 | privateKeyVarDeclaration = "private let \(privateKeyVarName) = \(keyAndCodingStructDeclaration)" 85 | privateValueVarDeclaration = "private var \(privateValueVarName): \(typeName)?" 86 | } else { 87 | // must have a default value 88 | let defaultValue = try PrefMacroUtils.defaultValue(from: varDec) 89 | keyAndCodingStructName = 90 | "\(PrefMacroUtils.moduleNamePrefix)\(macro.defaultedKeyStructName)\(useCustomCodingDecl ? "" : "<\(typeName)>")" 91 | keyAndCodingStructDeclaration = keyAndCodingStructName + 92 | "(key: \(keyName), defaultValue: \(defaultValue)\(useCustomCodingDecl ? ", coding: \(customCodingDecl ?? "nil")" : ""))" 93 | privateKeyVarDeclaration = "private let \(privateKeyVarName) = \(keyAndCodingStructDeclaration)" 94 | privateValueVarDeclaration = "private var \(privateValueVarName): \(typeName)?" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/AtomicPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// A basic prefs value coding strategy that stores a standard atomic value type directly without any additional 8 | /// processing. 9 | public struct AtomicPrefsCoding: AtomicPrefsCodable where Value: PrefsStorageValue { 10 | public typealias Value = Value 11 | 12 | public init() { } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/CodableArrayPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableArrayPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes an array of a `Codable` type to/from raw storage. 11 | public struct CodableArrayPrefsCoding: PrefsCodable where Element: CodablePrefsCodable { 12 | public typealias Value = [Element.Value] 13 | public typealias StorageValue = [Element.StorageValue] 14 | public let elementCoding: Element 15 | 16 | public init(element: Element) { 17 | self.elementCoding = element 18 | } 19 | 20 | public func decode(prefsValue: StorageValue) -> Value? { 21 | // TODO: should assert or throw on elements that return nil? 22 | prefsValue.compactMap { elementCoding.decode(prefsValue: $0) } 23 | } 24 | 25 | public func encode(prefsValue: Value) -> StorageValue? { 26 | // TODO: should assert or throw on elements that return nil? 27 | prefsValue.compactMap { elementCoding.encode(prefsValue: $0) } 28 | } 29 | } 30 | 31 | // MARK: - Static Constructor 32 | 33 | extension Array where Element: Codable, Element: Sendable { 34 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON 35 | /// `Data` element storage with default options. 36 | public static var jsonDataArrayPrefsCoding: CodableArrayPrefsCoding> { 37 | .init(element: JSONDataCodablePrefsCoding()) 38 | } 39 | 40 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON 41 | /// `String` (UTF-8) element storage with default options. 42 | public static var jsonStringArrayPrefsCoding: CodableArrayPrefsCoding> { 43 | .init(element: JSONStringCodablePrefsCoding()) 44 | } 45 | } 46 | 47 | // MARK: - Chaining Constructor 48 | 49 | extension PrefsCodable where StorageValue == [JSONStringCodablePrefsCoding.Value] { 50 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON 51 | /// `Data` element storage with default options. 52 | public var jsonDataArrayPrefsCoding: PrefsCodingTuple< 53 | Self, 54 | CodableArrayPrefsCoding> 55 | > { 56 | PrefsCodingTuple( 57 | self, 58 | .init(element: JSONDataCodablePrefsCoding()) 59 | ) 60 | } 61 | 62 | /// A prefs value coding strategy that encodes and decodes an array of a `Codable` type to/from an array of raw JSON 63 | /// `String` (UTF-8) element storage with default options. 64 | public var jsonStringArrayPrefsCoding: PrefsCodingTuple< 65 | Self, 66 | CodableArrayPrefsCoding> 67 | > { 68 | PrefsCodingTuple( 69 | self, 70 | .init(element: JSONStringCodablePrefsCoding()) 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/CodableDictionaryPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableDictionaryPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes a dictionary keyed by `String` and containing values of a `Codable` type 11 | /// to/from raw storage. 12 | public struct CodableDictionaryPrefsCoding: PrefsCodable where Element: CodablePrefsCodable { 13 | public typealias Value = [String: Element.Value] 14 | public typealias StorageValue = [String: Element.StorageValue] 15 | public let elementCoding: Element 16 | 17 | public init(element: Element) { 18 | self.elementCoding = element 19 | } 20 | 21 | public func decode(prefsValue: StorageValue) -> Value? { 22 | // TODO: should assert or throw on elements that return nil? 23 | prefsValue.compactMapValues { elementCoding.decode(prefsValue: $0) } 24 | } 25 | 26 | public func encode(prefsValue: Value) -> StorageValue? { 27 | // TODO: should assert or throw on elements that return nil? 28 | prefsValue.compactMapValues { elementCoding.encode(prefsValue: $0) } 29 | } 30 | } 31 | 32 | // MARK: - Static Constructor 33 | 34 | extension Dictionary where Key == String, Value: Codable, Value: Sendable { 35 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable` 36 | /// value type to/from a dictionary of raw JSON `Data` value storage storage with default options. 37 | public static var jsonDataDictionaryPrefsCoding: CodableDictionaryPrefsCoding> { 38 | .init(element: JSONDataCodablePrefsCoding()) 39 | } 40 | 41 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable` 42 | /// value type to/from a dictionary of raw JSON `String` (UTF-8) value storage storage with default options. 43 | public static var jsonStringDictionaryPrefsCoding: CodableDictionaryPrefsCoding< 44 | JSONStringCodablePrefsCoding 45 | > { 46 | .init(element: JSONStringCodablePrefsCoding()) 47 | } 48 | } 49 | 50 | // MARK: - Chaining Constructor 51 | 52 | extension PrefsCodable where StorageValue == [String: JSONStringCodablePrefsCoding.Value] { 53 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable` 54 | /// value type to/from a dictionary of raw JSON `Data` value storage storage with default options. 55 | public var jsonDataDictionaryPrefsCoding: PrefsCodingTuple< 56 | Self, 57 | CodableDictionaryPrefsCoding> 58 | > { 59 | PrefsCodingTuple( 60 | self, 61 | .init(element: JSONDataCodablePrefsCoding()) 62 | ) 63 | } 64 | 65 | /// A prefs value coding strategy that encodes and decodes a dictionary keyed by `String` and containing a `Codable` 66 | /// value type to/from a dictionary of raw JSON `String` (UTF-8) value storage storage with default options. 67 | public var jsonStringDictionaryPrefsCoding: PrefsCodingTuple< 68 | Self, 69 | CodableDictionaryPrefsCoding> 70 | > { 71 | PrefsCodingTuple( 72 | self, 73 | .init(element: JSONStringCodablePrefsCoding()) 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/CodablePrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A generic prefs value coding strategy that allows the encoding and decoding logic for a `Codable` type to be 11 | /// conveniently supplied as closures, alleviating the need to create a new ``CodablePrefsCodable``-conforming type for 12 | /// basic coding logic. 13 | public struct CodablePrefsCoding: CodablePrefsCodable 14 | where Value: Codable, Value: Sendable, 15 | StorageValue: PrefsStorageValue, StorageValue == Encoder.Output, 16 | Encoder: TopLevelEncoder, Encoder: Sendable, Encoder.Output: PrefsStorageValue, 17 | Decoder: TopLevelDecoder, Decoder: Sendable, Decoder.Input: PrefsStorageValue, 18 | Encoder.Output == Decoder.Input 19 | { 20 | public typealias Value = Value 21 | public typealias StorageValue = StorageValue 22 | public typealias Encoder = Encoder 23 | public typealias Decoder = Decoder 24 | let encoder: Encoder 25 | let decoder: Decoder 26 | 27 | public init( 28 | value: Value.Type, 29 | storageValue: StorageValue.Type, 30 | encoder: @escaping @Sendable @autoclosure () -> Encoder, 31 | decoder: @escaping @Sendable @autoclosure () -> Decoder 32 | ) { 33 | self.encoder = encoder() 34 | self.decoder = decoder() 35 | } 36 | 37 | public func prefsEncoder() -> Encoder { encoder } 38 | public func prefsDecoder() -> Decoder { decoder } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/JSONDataCodablePrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDataCodablePrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with default 10 | /// options. 11 | /// 12 | /// > Note: 13 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of 14 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured. 15 | public struct JSONDataCodablePrefsCoding: JSONDataCodablePrefsCodable 16 | where Value: Codable, Value: Sendable 17 | { 18 | public typealias Value = Value 19 | 20 | public init() { } 21 | } 22 | 23 | // MARK: - Static Constructor 24 | 25 | extension /* Codable */ Encodable where Self: Decodable, Self: Sendable { 26 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with 27 | /// default options. 28 | public static var jsonDataPrefsCoding: JSONDataCodablePrefsCoding { 29 | JSONDataCodablePrefsCoding() 30 | } 31 | } 32 | 33 | // MARK: - Chaining Constructor 34 | 35 | extension PrefsCodable where StorageValue == JSONDataCodablePrefsCoding.Value { 36 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with 37 | /// default options. 38 | public var jsonDataPrefsCoding: PrefsCodingTuple> { 39 | PrefsCodingTuple( 40 | self, 41 | JSONDataCodablePrefsCoding() 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/JSONStringCodablePrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONStringCodablePrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8) storage 10 | /// with default options. 11 | /// 12 | /// > Note: 13 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of 14 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured. 15 | public struct JSONStringCodablePrefsCoding: JSONStringCodablePrefsCodable 16 | where Value: Codable, Value: Sendable 17 | { 18 | public typealias Value = Value 19 | 20 | public init() { } 21 | } 22 | 23 | // MARK: - Static Constructor 24 | 25 | extension /* Codable */ Encodable where Self: Decodable, Self: Sendable { 26 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8) 27 | /// storage with default options. 28 | public static var jsonStringPrefsCoding: JSONStringCodablePrefsCoding { 29 | JSONStringCodablePrefsCoding() 30 | } 31 | } 32 | 33 | // MARK: - Chaining Constructor 34 | 35 | extension PrefsCodable where StorageValue == JSONStringCodablePrefsCoding.Value { 36 | /// A prefs value coding strategy that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8) 37 | /// storage with default options. 38 | public var jsonStringPrefsCoding: PrefsCodingTuple> { 39 | PrefsCodingTuple( 40 | self, 41 | JSONStringCodablePrefsCoding() 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/PrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// A generic prefs value coding strategy that allows the encoding and decoding logic to be conveniently supplied as 8 | /// closures, alleviating the need to create a new ``PrefsCodable``-conforming type for basic coding logic. 9 | public struct PrefsCoding: PrefsCodable 10 | where Value: Sendable, StorageValue: PrefsStorageValue 11 | { 12 | let encodeBlock: @Sendable (Value) -> StorageValue? 13 | let decodeBlock: @Sendable (StorageValue) -> Value? 14 | 15 | public init( 16 | encode: @escaping @Sendable (Value) -> StorageValue?, 17 | decode: @escaping @Sendable (StorageValue) -> Value? 18 | ) { 19 | encodeBlock = encode 20 | decodeBlock = decode 21 | } 22 | 23 | public func decode(prefsValue: StorageValue) -> Value? { 24 | decodeBlock(prefsValue) 25 | } 26 | 27 | public func encode(prefsValue: Value) -> StorageValue? { 28 | encodeBlock(prefsValue) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/PrefsCodingTuple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsCodingTuple.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// A container for two ``PrefsCodable`` coding strategies in series. 8 | public struct PrefsCodingTuple: PrefsCodable 9 | where First: PrefsCodable, Second: PrefsCodable, First.StorageValue == Second.Value 10 | { 11 | public typealias Value = First.Value 12 | public typealias StorageValue = Second.StorageValue 13 | 14 | public let first: First 15 | public let second: Second 16 | 17 | public init(_ first: First, _ second: Second) { 18 | self.first = first 19 | self.second = second 20 | } 21 | 22 | public func decode(prefsValue: Second.StorageValue) -> First.Value? { 23 | guard let secondDecoded = second.decode(prefsValue: prefsValue) else { return nil } 24 | let firstDecoded = first.decode(prefsValue: secondDecoded) 25 | return firstDecoded 26 | } 27 | 28 | public func encode(prefsValue: First.Value) -> Second.StorageValue? { 29 | guard let firstEncoded = first.encode(prefsValue: prefsValue) else { return nil } 30 | let secondEncoded = second.encode(prefsValue: firstEncoded) 31 | return secondEncoded 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/RawRepresentableArrayPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentableArrayPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes an array of a `RawRepresentable` type to/from raw storage using the element's 11 | /// `RawValue` as its storage value. 12 | public struct RawRepresentableArrayPrefsCoding: PrefsCodable where Element: RawRepresentablePrefsCodable { 13 | public typealias Value = [Element.Value] 14 | public typealias StorageValue = [Element.StorageValue] 15 | public let elementCoding: Element 16 | 17 | public init(element: Element) { 18 | self.elementCoding = element 19 | } 20 | 21 | public func decode(prefsValue: StorageValue) -> Value? { 22 | // TODO: should assert or throw on elements that return nil? 23 | prefsValue.compactMap { elementCoding.decode(prefsValue: $0) } 24 | } 25 | 26 | public func encode(prefsValue: Value) -> StorageValue? { 27 | // TODO: should assert or throw on elements that return nil? 28 | prefsValue.compactMap { elementCoding.encode(prefsValue: $0) } 29 | } 30 | } 31 | 32 | // MARK: - Static Constructor 33 | 34 | extension Array where Element: RawRepresentable, Element.RawValue: PrefsStorageValue, Element: Sendable { 35 | /// A prefs key that encodes and decodes an array of a `RawRepresentable` type to/from raw storage using the 36 | /// element's `RawValue` as its storage value. 37 | public static var rawRepresentableArrayPrefsCoding: RawRepresentableArrayPrefsCoding> { 38 | .init(element: RawRepresentablePrefsCoding()) 39 | } 40 | } 41 | 42 | // MARK: - Chaining Constructor 43 | 44 | // there does not seem to be a reasonable way to implement a chaining constructor 45 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/RawRepresentableDictionaryPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentableDictionaryPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes a dictionary of a `RawRepresentable` type to/from raw storage using the element's 11 | /// `RawValue` as its storage value. 12 | public struct RawRepresentableDictionaryPrefsCoding: PrefsCodable where Element: RawRepresentablePrefsCodable { 13 | public typealias Value = [String: Element.Value] 14 | public typealias StorageValue = [String: Element.StorageValue] 15 | public let elementCoding: Element 16 | 17 | public init(element: Element) { 18 | self.elementCoding = element 19 | } 20 | 21 | public func decode(prefsValue: StorageValue) -> Value? { 22 | // TODO: should assert or throw on elements that return nil? 23 | prefsValue.compactMapValues { elementCoding.decode(prefsValue: $0) } 24 | } 25 | 26 | public func encode(prefsValue: Value) -> StorageValue? { 27 | // TODO: should assert or throw on elements that return nil? 28 | prefsValue.compactMapValues { elementCoding.encode(prefsValue: $0) } 29 | } 30 | } 31 | 32 | // MARK: - Static Constructor 33 | 34 | extension Dictionary where Key == String, Value: RawRepresentable, Value.RawValue: PrefsStorageValue, Value: Sendable { 35 | /// A prefs key that encodes and decodes a dictionary of a `RawRepresentable` type to/from raw storage using the 36 | /// element's `RawValue` as its storage value. 37 | public static var rawRepresentableDictionaryPrefsCoding: RawRepresentableDictionaryPrefsCoding> { 38 | .init(element: RawRepresentablePrefsCoding()) 39 | } 40 | } 41 | 42 | // MARK: - Chaining Constructor 43 | 44 | // there does not seem to be a reasonable way to implement a chaining constructor 45 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Prototypes/RawRepresentablePrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentablePrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// A prefs value coding strategy which uses a `RawRepresentable` type's `RawValue` as its storage value. 10 | public struct RawRepresentablePrefsCoding: RawRepresentablePrefsCodable 11 | where Value: Sendable, Value: RawRepresentable, Value.RawValue: PrefsStorageValue 12 | { 13 | public typealias Value = Value 14 | public typealias StorageValue = Value.RawValue 15 | 16 | public init() { } 17 | } 18 | 19 | // MARK: - Static Constructor 20 | 21 | extension RawRepresentable where Self: Sendable, Self.RawValue: PrefsStorageValue { 22 | /// A prefs value coding strategy which uses a `RawRepresentable` type's `RawValue` as its storage value. 23 | public static var rawRepresentablePrefsCoding: RawRepresentablePrefsCoding { 24 | RawRepresentablePrefsCoding() 25 | } 26 | } 27 | 28 | // MARK: - Chaining Constructor 29 | 30 | extension PrefsCodable where StorageValue: RawRepresentable, StorageValue.RawValue: PrefsStorageValue { 31 | // (Note that the availability of this chaining property is very rare, but still technically possible) 32 | 33 | /// A prefs value coding strategy which uses a `RawRepresentable` type's `RawValue` as its storage value. 34 | public var rawRepresentablePrefsCoding: RawRepresentablePrefsCoding { 35 | RawRepresentablePrefsCoding() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/Base64StringDataPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64StringDataPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for `Data` using base-64 encoded `String` as the encoded storage value. 10 | public struct Base64StringDataPrefsCoding: PrefsCodable { 11 | public let encodingOptions: Data.Base64EncodingOptions 12 | public let decodingOptions: Data.Base64DecodingOptions 13 | 14 | public init(encodingOptions: Data.Base64EncodingOptions, decodingOptions: Data.Base64DecodingOptions) { 15 | self.encodingOptions = encodingOptions 16 | self.decodingOptions = decodingOptions 17 | } 18 | 19 | public func encode(prefsValue: Data) -> String? { 20 | prefsValue.base64EncodedString(options: encodingOptions) 21 | } 22 | 23 | public func decode(prefsValue: String) -> Data? { 24 | Data(base64Encoded: prefsValue, options: decodingOptions) 25 | } 26 | } 27 | 28 | // MARK: - Static Constructor 29 | 30 | extension PrefsCodable where Self == Base64StringDataPrefsCoding { 31 | /// Coding strategy for `Data` using base-64 encoded `String` as the encoded storage value. 32 | public static func base64DataString( 33 | encodingOptions: Data.Base64EncodingOptions = [], 34 | decodingOptions: Data.Base64DecodingOptions = [] 35 | ) -> Base64StringDataPrefsCoding { 36 | Base64StringDataPrefsCoding(encodingOptions: encodingOptions, decodingOptions: decodingOptions) 37 | } 38 | } 39 | 40 | // MARK: - Chaining Constructor 41 | 42 | extension PrefsCodable where StorageValue == Base64StringDataPrefsCoding.Value { 43 | /// Coding strategy for `Data` using base-64 encoded `String` as the encoded storage value. 44 | public func base64DataString( 45 | encodingOptions: Data.Base64EncodingOptions = [], 46 | decodingOptions: Data.Base64DecodingOptions = [] 47 | ) -> PrefsCodingTuple { 48 | PrefsCodingTuple( 49 | self, 50 | Base64StringDataPrefsCoding(encodingOptions: encodingOptions, decodingOptions: decodingOptions) 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/BoolIntegerPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolIntegerPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for `Bool` using `Int` as the encoded storage value (`1` or `0`). 10 | public struct BoolIntegerPrefsCoding: PrefsCodable { 11 | public let decodingStrategy: DecodingStrategy 12 | 13 | public init(decodingStrategy: DecodingStrategy) { 14 | self.decodingStrategy = decodingStrategy 15 | } 16 | 17 | public func encode(prefsValue: Bool) -> Int? { 18 | prefsValue ? 1 : 0 19 | } 20 | 21 | public func decode(prefsValue: Int) -> Bool? { 22 | switch decodingStrategy { 23 | case .strict: 24 | switch prefsValue { 25 | case 1: true 26 | case 0: false 27 | default: nil 28 | } 29 | case .nearest: 30 | prefsValue >= 1 31 | } 32 | } 33 | } 34 | 35 | extension BoolIntegerPrefsCoding { 36 | /// Integer decoding strategy for ``BoolIntegerPrefsCoding``. 37 | public enum DecodingStrategy: Equatable, Hashable, Sendable { 38 | /// Strict decoding of a stored integer. 39 | /// Only `1` will be interpreted as `true` and `0` as `false`. 40 | /// Any other value will return `nil`. 41 | case strict 42 | 43 | /// Use the value nearest to `0` or `1` when reading a stored integer. 44 | /// `1` or greater will be interpreted as `true`. 45 | /// `0` or less will be interpreted as `false`. 46 | case nearest 47 | } 48 | } 49 | 50 | // MARK: - Static Constructor 51 | 52 | extension PrefsCodable where Self == BoolIntegerPrefsCoding { 53 | /// Coding strategy for `Bool` using `Int` as the encoded storage value (`1` or `0`). 54 | public static func boolAsInteger( 55 | decodingStrategy: BoolIntegerPrefsCoding.DecodingStrategy = .nearest 56 | ) -> Self { 57 | BoolIntegerPrefsCoding(decodingStrategy: decodingStrategy) 58 | } 59 | } 60 | 61 | // MARK: - Chaining Constructor 62 | 63 | extension PrefsCodable where StorageValue == Bool { 64 | /// Coding strategy for `Bool` using `Int` as the encoded storage value (`1` or `0`). 65 | public func boolAsInteger( 66 | decodingStrategy: BoolIntegerPrefsCoding.DecodingStrategy = .nearest 67 | ) -> PrefsCodingTuple { 68 | PrefsCodingTuple(self, BoolIntegerPrefsCoding(decodingStrategy: decodingStrategy)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/BoolStringPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolStringPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for `Bool` using `String` as the encoded storage value (`true`/`false` or `yes`/`no`). 10 | public struct BoolStringPrefsCoding: PrefsCodable { 11 | public let encodingStrategy: EncodingStrategy 12 | 13 | public init(encodingStrategy: EncodingStrategy) { 14 | self.encodingStrategy = encodingStrategy 15 | } 16 | 17 | public func encode(prefsValue: Bool) -> String? { 18 | switch encodingStrategy { 19 | case let .trueFalse(textCase): 20 | let text = prefsValue ? "true" : "false" 21 | return textCase.process(text) 22 | 23 | case let .yesNo(textCase): 24 | let text = prefsValue ? "yes" : "no" 25 | return textCase.process(text) 26 | 27 | case let .custom(true: trueValue, false: falseValue, caseInsensitive: _): 28 | return prefsValue ? trueValue : falseValue 29 | } 30 | } 31 | 32 | public func decode(prefsValue: String) -> Bool? { 33 | switch encodingStrategy { 34 | case .trueFalse, .yesNo: 35 | switch prefsValue.trimmingCharacters(in: .whitespacesAndNewlines) { 36 | case let v where v.caseInsensitiveCompare("true") == .orderedSame: true 37 | case let v where v.caseInsensitiveCompare("false") == .orderedSame: false 38 | case let v where v.caseInsensitiveCompare("yes") == .orderedSame: true 39 | case let v where v.caseInsensitiveCompare("no") == .orderedSame: false 40 | default: nil 41 | } 42 | case let .custom(true: trueValue, false: falseValue, caseInsensitive: isCaseInsensitive): 43 | if isCaseInsensitive { 44 | switch prefsValue.trimmingCharacters(in: .whitespacesAndNewlines) { 45 | case let v where v.caseInsensitiveCompare(trueValue) == .orderedSame: true 46 | case let v where v.caseInsensitiveCompare(falseValue) == .orderedSame: false 47 | default: nil 48 | } 49 | } else { 50 | switch prefsValue.trimmingCharacters(in: .whitespacesAndNewlines) { 51 | case trueValue: true 52 | case falseValue: false 53 | default: nil 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | extension BoolStringPrefsCoding { 61 | /// String encoding strategy for ``BoolStringPrefsCoding``. 62 | public enum EncodingStrategy: Equatable, Hashable, Sendable { 63 | /// True or False. 64 | case trueFalse(_ textCase: TextCase = .lowercase) 65 | 66 | /// Yes or No. 67 | case yesNo(_ textCase: TextCase = .lowercase) 68 | 69 | /// Custom string values for `true` and `false` states. 70 | case custom(true: String, false: String, caseInsensitive: Bool = true) 71 | } 72 | } 73 | 74 | extension BoolStringPrefsCoding.EncodingStrategy { 75 | public enum TextCase: Equatable, Hashable, Sendable { 76 | /// Capitalized text. 77 | case capitalized 78 | 79 | /// Lowercase text. 80 | case lowercase 81 | 82 | /// Uppercase text. 83 | case uppercase 84 | 85 | /// Process an input string based on the enumeration case. 86 | public func process(_ string: String) -> String { 87 | switch self { 88 | case .capitalized: string.capitalized 89 | case .lowercase: string.lowercased() 90 | case .uppercase: string.uppercased() 91 | } 92 | } 93 | } 94 | } 95 | 96 | // MARK: - Static Constructor 97 | 98 | extension PrefsCodable where Self == BoolStringPrefsCoding { 99 | /// Coding strategy for `Bool` using `String` as the encoded storage value (`true`/`false` or `yes`/`no`). 100 | public static func boolAsString( 101 | _ encodingStrategy: BoolStringPrefsCoding.EncodingStrategy = .trueFalse(.lowercase) 102 | ) -> Self { 103 | BoolStringPrefsCoding(encodingStrategy: encodingStrategy) 104 | } 105 | } 106 | 107 | // MARK: - Chaining Constructor 108 | 109 | extension PrefsCodable where StorageValue == Bool { 110 | /// Coding strategy for `Bool` using `String` as the encoded storage value (`true`/`false` or `yes`/`no`). 111 | public func boolAsString( 112 | _ encodingStrategy: BoolStringPrefsCoding.EncodingStrategy = .trueFalse(.lowercase) 113 | ) -> PrefsCodingTuple { 114 | PrefsCodingTuple(self, BoolStringPrefsCoding(encodingStrategy: encodingStrategy)) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/CompressedDataPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressedDataPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for `Data` using data compression. Compresses when storing and decompresses when reading. 10 | /// 11 | /// > Note: 12 | /// > 13 | /// > Due to inherent computational overhead with compression and decompression, this strategy is not recommended 14 | /// > for use with data that has frequent access or requires low-latency access times. 15 | public struct CompressedDataPrefsCoding: PrefsCodable { 16 | let algorithm: NSData.CompressionAlgorithm 17 | 18 | public init(algorithm: NSData.CompressionAlgorithm) { 19 | self.algorithm = algorithm 20 | } 21 | 22 | public func encode(prefsValue: Data) -> Data? { 23 | try? (prefsValue as NSData) 24 | .compressed(using: algorithm) as Data 25 | } 26 | 27 | public func decode(prefsValue: Data) -> Data? { 28 | try? (prefsValue as NSData) 29 | .decompressed(using: algorithm) as Data 30 | } 31 | } 32 | 33 | // MARK: - Static Constructor 34 | 35 | extension PrefsCodable where Self == CompressedDataPrefsCoding { 36 | /// Coding strategy for `Data` using data compression. Compresses when storing and decompresses when reading. 37 | /// 38 | /// > Note: 39 | /// > 40 | /// > Due to inherent computational overhead with compression and decompression, this strategy is not recommended 41 | /// > for use with data that has frequent access or requires low-latency access times. 42 | public static func compressedData( 43 | algorithm: NSData.CompressionAlgorithm 44 | ) -> CompressedDataPrefsCoding { 45 | CompressedDataPrefsCoding(algorithm: algorithm) 46 | } 47 | } 48 | 49 | // MARK: - Chaining Constructor 50 | 51 | extension PrefsCodable where StorageValue == CompressedDataPrefsCoding.Value { 52 | /// Coding strategy for `Data` using data compression. Compresses when storing and decompresses when reading. 53 | /// 54 | /// > Note: 55 | /// > 56 | /// > Due to inherent computational overhead with compression and decompression, this strategy is not recommended 57 | /// > for use with data that has frequent access or requires low-latency access times. 58 | public func compressedData( 59 | algorithm: NSData.CompressionAlgorithm 60 | ) -> PrefsCodingTuple { 61 | PrefsCodingTuple( 62 | self, 63 | CompressedDataPrefsCoding(algorithm: algorithm) 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/ISO8601StringDatePrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ISO8601StringDatePrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for `Date` using standard ISO-8601 format `String` as the encoded storage value. 10 | /// 11 | /// For example: 12 | /// 13 | /// ``` 14 | /// 2024-12-31T21:30:35Z 15 | /// ``` 16 | /// 17 | /// > Important: 18 | /// > 19 | /// > This format includes date and time with a resolution of 1 second. Any sub-second time information is truncated 20 | /// > and discarded. 21 | /// 22 | /// > Tip: 23 | /// > 24 | /// > `Date` has native `Codable` conformance, which means it may also be used directly with 25 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`. 26 | public struct ISO8601DateStringPrefsCoding: PrefsCodable { 27 | public init() { } 28 | 29 | public func encode(prefsValue: Date) -> String? { 30 | ISO8601DateFormatter().string(from: prefsValue) 31 | } 32 | 33 | public func decode(prefsValue: String) -> Date? { 34 | ISO8601DateFormatter().date(from: prefsValue) 35 | } 36 | } 37 | 38 | // MARK: - Static Constructor 39 | 40 | extension PrefsCodable where Self == ISO8601DateStringPrefsCoding { 41 | /// Coding strategy for `Date` using standard ISO-8601 format `String` as the encoded storage value. 42 | /// 43 | /// For example: 44 | /// 45 | /// ``` 46 | /// 2024-12-31T21:30:35Z 47 | /// ``` 48 | /// 49 | /// > Important: 50 | /// > 51 | /// > This format includes date and time with a resolution of 1 second. Any sub-second time information is truncated 52 | /// > and discarded. 53 | /// 54 | /// > Tip: 55 | /// > 56 | /// > `Date` has native `Codable` conformance, which means it may also be used directly with 57 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`. 58 | public static var iso8601DateString: ISO8601DateStringPrefsCoding { 59 | ISO8601DateStringPrefsCoding() 60 | } 61 | } 62 | 63 | // MARK: - Chaining Constructor 64 | 65 | // note: `Date` does not conform to PrefsStorageValue so we can't offer a coding strategy chaining method. 66 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/IntegerPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntegerPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for a concrete type conforming to `BinaryInteger` using `Int` as the encoded storage value. 10 | public struct IntegerPrefsCoding: PrefsCodable where Value: BinaryInteger, Value: Sendable { 11 | public init() { } 12 | 13 | public func encode(prefsValue: Value) -> Int? { 14 | Int(exactly: prefsValue) 15 | } 16 | 17 | public func decode(prefsValue: Int) -> Value? { 18 | Value(exactly: prefsValue) 19 | } 20 | } 21 | 22 | // MARK: - Static Constructor 23 | 24 | // Note: do not offer `Int` since it is already an atomic prefs storage value type 25 | 26 | extension PrefsCodable where Self == IntegerPrefsCoding { 27 | /// Coding strategy for `UInt` using `Int` as the encoded storage value. 28 | /// 29 | /// > Important: 30 | /// > 31 | /// > Values above `Int.max` will silently fail to be stored. 32 | /// > Consider encoding as `String` or raw `Data` to ensure lossless storage of very large `UInt` values. 33 | public static var uIntAsInt: Self { .init() } 34 | } 35 | 36 | extension PrefsCodable where Self == IntegerPrefsCoding { 37 | /// Coding strategy for `Int8` using `Int` as the encoded storage value. 38 | public static var int8AsInt: Self { .init() } 39 | } 40 | 41 | extension PrefsCodable where Self == IntegerPrefsCoding { 42 | /// Coding strategy for `UInt8` using `Int` as the encoded storage value. 43 | public static var uInt8AsInt: Self { .init() } 44 | } 45 | 46 | extension PrefsCodable where Self == IntegerPrefsCoding { 47 | /// Coding strategy for `Int16` using `Int` as the encoded storage value. 48 | public static var int16AsInt: Self { .init() } 49 | } 50 | 51 | extension PrefsCodable where Self == IntegerPrefsCoding { 52 | /// Coding strategy for `UInt16` using `Int` as the encoded storage value. 53 | public static var uInt16AsInt: Self { .init() } 54 | } 55 | 56 | extension PrefsCodable where Self == IntegerPrefsCoding { 57 | /// Coding strategy for `Int32` using `Int` as the encoded storage value. 58 | public static var int32AsInt: Self { .init() } 59 | } 60 | 61 | extension PrefsCodable where Self == IntegerPrefsCoding { 62 | /// Coding strategy for `UInt32` using `Int` as the encoded storage value. 63 | public static var uInt32AsInt: Self { .init() } 64 | } 65 | 66 | extension PrefsCodable where Self == IntegerPrefsCoding { 67 | /// Coding strategy for `Int64` using `Int` as the encoded storage value. 68 | public static var int64AsInt: Self { .init() } 69 | } 70 | 71 | extension PrefsCodable where Self == IntegerPrefsCoding { 72 | /// Coding strategy for `UInt64` using `Int` as the encoded storage value. 73 | /// 74 | /// > Important: 75 | /// > 76 | /// > Values above `Int.max` will silently fail to be stored. 77 | /// > Consider encoding as `String` or raw `Data` to ensure lossless storage of very large `UInt64` values. 78 | public static var uInt64AsInt: Self { .init() } 79 | } 80 | 81 | // MARK: - Chaining Constructor 82 | 83 | // note: non-Int integers do not conform to PrefsStorageValue so we can't offer coding strategy chaining methods. 84 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/IntegerStringPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntegerStringPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for a concrete type conforming to `FixedWidthInteger` using `String` as the encoded storage value. 10 | public struct IntegerStringPrefsCoding: PrefsCodable where Value: FixedWidthInteger, Value: Sendable { 11 | public init() { } 12 | 13 | public func encode(prefsValue: Value) -> String? { 14 | String(prefsValue) 15 | } 16 | 17 | public func decode(prefsValue: String) -> Value? { 18 | Value(prefsValue) 19 | } 20 | } 21 | 22 | // MARK: - Static Constructors 23 | 24 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 25 | /// Coding strategy for `Int` using `String` as the encoded storage value. 26 | public static var intAsString: Self { .init() } 27 | } 28 | 29 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 30 | /// Coding strategy for `UInt` using `String` as the encoded storage value. 31 | public static var uIntAsString: Self { .init() } 32 | } 33 | 34 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 35 | /// Coding strategy for `Int8` using `String` as the encoded storage value. 36 | public static var int8AsString: Self { .init() } 37 | } 38 | 39 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 40 | /// Coding strategy for `UInt8` using `String` as the encoded storage value. 41 | public static var uInt8AsString: Self { .init() } 42 | } 43 | 44 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 45 | /// Coding strategy for `Int16` using `String` as the encoded storage value. 46 | public static var int16AsString: Self { .init() } 47 | } 48 | 49 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 50 | /// Coding strategy for `UInt16` using `String` as the encoded storage value. 51 | public static var uInt16AsString: Self { .init() } 52 | } 53 | 54 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 55 | /// Coding strategy for `Int32` using `String` as the encoded storage value. 56 | public static var int32AsString: Self { .init() } 57 | } 58 | 59 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 60 | /// Coding strategy for `UInt32` using `String` as the encoded storage value. 61 | public static var uInt32AsString: Self { .init() } 62 | } 63 | 64 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 65 | /// Coding strategy for `Int64` using `String` as the encoded storage value. 66 | public static var int64AsString: Self { .init() } 67 | } 68 | 69 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 70 | /// Coding strategy for `UInt64` using `String` as the encoded storage value. 71 | public static var uInt64AsString: Self { .init() } 72 | } 73 | 74 | @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 75 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 76 | /// Coding strategy for `Int128` using `String` as the encoded storage value. 77 | public static var int128AsString: Self { .init() } 78 | } 79 | 80 | @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 81 | extension PrefsCodable where Self == IntegerStringPrefsCoding { 82 | /// Coding strategy for `UInt128` using `String` as the encoded storage value. 83 | public static var uInt128AsString: Self { .init() } 84 | } 85 | 86 | // MARK: - Chaining Constructor 87 | 88 | extension PrefsCodable where StorageValue == Int { 89 | /// Coding strategy for `Int` using `String` as the encoded storage value. 90 | public var intAsString: PrefsCodingTuple> { 91 | PrefsCodingTuple(self, IntegerStringPrefsCoding()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable Strategies/URLStringPrefsCoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLStringPrefsCoding.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Coding strategy for `URL` using absolute `String` as the encoded storage value type. 10 | /// 11 | /// > Tip: 12 | /// > 13 | /// > `URL` has native `Codable` conformance, which means it may also be used directly with 14 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`. 15 | public struct URLStringPrefsCoding: PrefsCodable { 16 | public init() { } 17 | 18 | public func encode(prefsValue: URL) -> String? { 19 | prefsValue.absoluteString 20 | } 21 | 22 | public func decode(prefsValue: String) -> URL? { 23 | URL(string: prefsValue) 24 | } 25 | } 26 | 27 | // MARK: - Static Constructor 28 | 29 | extension PrefsCodable where Self == URLStringPrefsCoding { 30 | /// Coding strategy for `URL` using absolute `String` as the encoded storage value type. 31 | /// 32 | /// > Tip: 33 | /// > 34 | /// > `URL` has native `Codable` conformance, which means it may also be used directly with 35 | /// > `@JSONDataCodablePref` or `@JSONStringCodablePref`. 36 | public static var urlString: URLStringPrefsCoding { .init() } 37 | } 38 | 39 | // MARK: - Chaining Constructor 40 | 41 | // note: `URL` does not conform to PrefsStorageValue so we can't offer a coding strategy chaining method. 42 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/Atomic/AtomicPrefsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicPrefsCodable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// A basic prefs value coding protocol for storing a standard atomic value type. 8 | public protocol AtomicPrefsCodable: PrefsCodable where Value == StorageValue { } 9 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/Codable/CodablePrefsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePrefsCodable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes a `Codable` type to/from raw storage. 11 | /// 12 | /// > Tip: 13 | /// > 14 | /// > It is suggested that if multiple `Codable` types that stored in prefs storage use the same 15 | /// > underlying encoder/decoder, that you create a protocol that inherits from ``CodablePrefsCodable`` 16 | /// > for all non-defaulted `Codable` prefs and then implement `prefEncoder()` and `prefDecoder()` to return 17 | /// > the same instances. These types can then adopt this new protocol. 18 | public protocol CodablePrefsCodable: PrefsCodable 19 | where Value: Codable 20 | { 21 | associatedtype Encoder: TopLevelEncoder 22 | associatedtype Decoder: TopLevelDecoder 23 | 24 | /// Return a new instance of the encoder used to encode the type for prefs storage. 25 | func prefsEncoder() -> Encoder 26 | 27 | /// Return a new instance of the decoder used to decode the type from prefs storage. 28 | func prefsDecoder() -> Decoder 29 | } 30 | 31 | extension CodablePrefsCodable where StorageValue == Encoder.Output, Encoder.Output == Decoder.Input { 32 | public func decode(prefsValue: StorageValue) -> Value? { 33 | let decoder = prefsDecoder() 34 | guard let value = try? decoder.decode(Value.self, from: prefsValue) else { return nil } 35 | return value 36 | } 37 | 38 | public func encode(prefsValue: Value) -> StorageValue? { 39 | let encoder = prefsEncoder() 40 | guard let encoded = try? encoder.encode(prefsValue) else { return nil } 41 | return encoded 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/Codable/JSON/JSONDataCodablePrefsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDataCodablePrefsCodable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes a `Codable` type to/from raw JSON `Data` storage with default options. 11 | /// 12 | /// > Note: 13 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of 14 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured. 15 | public protocol JSONDataCodablePrefsCodable: CodablePrefsCodable 16 | where Encoder == JSONEncoder, Decoder == JSONDecoder, StorageValue == Data { } 17 | 18 | extension JSONDataCodablePrefsCodable { 19 | public func prefsEncoder() -> JSONEncoder { 20 | JSONEncoder() 21 | } 22 | 23 | public func prefsDecoder() -> JSONDecoder { 24 | JSONDecoder() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/Codable/JSON/JSONStringCodablePrefsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONStringCodablePrefsCodable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// A prefs key that encodes and decodes a `Codable` type to/from raw JSON `String` (UTF-8) storage with default 11 | /// options. 12 | /// 13 | /// > Note: 14 | /// > If custom `JSONEncoder`/`JSONDecoder` options are required, override the default implementation(s) of 15 | /// > `prefEncoder()` and/or `prefDecoder()` methods to return an encoder/decoder with necessary options configured. 16 | public protocol JSONStringCodablePrefsCodable: CodablePrefsCodable 17 | where Encoder == JSONEncoder, Decoder == JSONDecoder, StorageValue == String { } 18 | 19 | extension JSONStringCodablePrefsCodable { 20 | public func prefsEncoder() -> JSONEncoder { 21 | JSONEncoder() 22 | } 23 | 24 | public func prefsDecoder() -> JSONDecoder { 25 | JSONDecoder() 26 | } 27 | } 28 | 29 | extension JSONStringCodablePrefsCodable { 30 | public func decode(prefsValue: StorageValue) -> Value? { 31 | let decoder = prefsDecoder() 32 | guard let data = prefsValue.data(using: .utf8), 33 | let value = try? decoder.decode(Value.self, from: data) 34 | else { return nil } 35 | return value 36 | } 37 | 38 | public func encode(prefsValue: Value) -> StorageValue? { 39 | let encoder = prefsEncoder() 40 | guard let data = try? encoder.encode(prefsValue), 41 | let string = String(data: data, encoding: .utf8) 42 | else { return nil } 43 | return string 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/PrefsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsCodable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Defines value encoding and decoding for reading and writing a value to prefs storage. 10 | public protocol PrefsCodable: Sendable 11 | where Value: Sendable, StorageValue: PrefsStorageValue { 12 | associatedtype Value 13 | associatedtype StorageValue 14 | 15 | /// Decodes a raw atomic prefs storage value to a value. 16 | func decode(prefsValue: StorageValue) -> Value? 17 | 18 | /// Encodes a value to a raw atomic prefs storage value. 19 | func encode(prefsValue: Value) -> StorageValue? 20 | } 21 | 22 | extension PrefsCodable where Value == StorageValue { 23 | public func decode(prefsValue: StorageValue) -> Value? { 24 | prefsValue 25 | } 26 | 27 | public func encode(prefsValue: Value) -> StorageValue? { 28 | prefsValue 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/RawRepresentable/PrefsCodable+RawRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsCodable+RawRepresentable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension PrefsCodable where Value: RawRepresentable, Value.RawValue == StorageValue { 10 | public func decode(prefsValue: StorageValue) -> Value? { 11 | Value(rawValue: prefsValue) 12 | } 13 | 14 | public func encode(prefsValue: Value) -> StorageValue? { 15 | prefsValue.rawValue 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsCodable/RawRepresentable/RawRepresentablePrefsCodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentablePrefsCodable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Defines value encoding and decoding for reading and writing a `RawRepresentable` value to prefs storage. 8 | public protocol RawRepresentablePrefsCodable: PrefsCodable where Value: RawRepresentable, Value.RawValue == StorageValue { } 9 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsKey/DefaultedPrefsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultedPrefsKey.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public protocol DefaultedPrefsKey: PrefsKey { 8 | var defaultValue: Value { get } 9 | } 10 | 11 | extension DefaultedPrefsKey { 12 | public func decodeDefaulted(_ storageValue: StorageValue?) -> Value { 13 | decode(storageValue) ?? defaultValue 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsKey/PrefsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsKey.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | public protocol PrefsKey where Self: Sendable, Coding.Value == Value, Coding.StorageValue == StorageValue { 8 | associatedtype Value: Sendable 9 | associatedtype StorageValue: PrefsStorageValue 10 | associatedtype Coding: PrefsCodable 11 | 12 | var key: String { get } 13 | var coding: Coding { get } 14 | func encode(_ value: Value) -> StorageValue? 15 | func decode(_ storageValue: StorageValue) -> Value? 16 | } 17 | 18 | extension PrefsKey { 19 | public func encode(_ value: Value?) -> StorageValue? { 20 | guard let value else { return nil } 21 | return encode(value) 22 | } 23 | 24 | public func decode(_ storageValue: StorageValue?) -> Value? { 25 | guard let storageValue else { return nil } 26 | return decode(storageValue) 27 | } 28 | } 29 | 30 | extension PrefsKey where Value == StorageValue { 31 | public func encode(_ value: Value) -> StorageValue? { value } 32 | public func decode(_ storageValue: StorageValue) -> Value? { storageValue } 33 | } 34 | 35 | extension PrefsKey { 36 | public func encode(_ value: Value) -> StorageValue? { 37 | coding.encode(prefsValue: value) 38 | } 39 | 40 | public func decode(_ storageValue: StorageValue) -> Value? { 41 | coding.decode(prefsValue: storageValue) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsSchema/AnyPrefsSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyPrefsSchema.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol for prefs schema containing type-erased prefs storage. 10 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 11 | public protocol AnyPrefsSchema: PrefsSchema where SchemaStorage == AnyPrefsStorage { } 12 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsSchema/PrefsSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsSchema.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Combine 8 | import Foundation 9 | 10 | /// Protocol for prefs schema. 11 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 12 | public protocol PrefsSchema /* where Self: Sendable */ { 13 | /// Storage provider type for prefs. 14 | associatedtype SchemaStorage: PrefsStorage 15 | 16 | /// Storage provider for prefs. 17 | var storage: SchemaStorage { get } 18 | 19 | /// Storage mode for prefs. 20 | var storageMode: PrefsStorageMode { get } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsSchema/PropertyWrappers/PrefsStorageModeWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageModeWrapper.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Pref schema property access storage mode. 8 | @propertyWrapper 9 | public struct PrefsStorageModeWrapper { 10 | public var wrappedValue: PrefsStorageMode 11 | 12 | public init(wrappedValue: PrefsStorageMode) { 13 | self.wrappedValue = wrappedValue 14 | } 15 | 16 | public init(_ mode: PrefsStorageMode) { 17 | wrappedValue = mode 18 | } 19 | } 20 | 21 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 22 | extension PrefsSchema { 23 | /// Pref schema property access storage mode. 24 | public typealias StorageMode = PrefsStorageModeWrapper 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsSchema/PropertyWrappers/PrefsStorageWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageWrapper.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Prefs schema storage. 8 | @propertyWrapper 9 | public struct PrefsStorageWrapper where S: PrefsStorage { 10 | public var wrappedValue: S 11 | 12 | public init(wrappedValue: S) { 13 | self.wrappedValue = wrappedValue 14 | } 15 | 16 | public init(_ storage: S) { 17 | wrappedValue = storage 18 | } 19 | } 20 | 21 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 22 | extension PrefsSchema { 23 | /// Prefs schema storage. 24 | public typealias Storage = PrefsStorageWrapper 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/AnyPrefsStorage+PrefsStorageExportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyPrefsStorage+PrefsStorageExportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension AnyPrefsStorage: PrefsStorageExportable { 10 | public func dictionaryRepresentation() throws -> [String: Any] { 11 | guard let wrapped = wrapped as? PrefsStorageExportable else { 12 | throw PrefsStorageError.contentExportingNotSupported 13 | } 14 | return try wrapped.dictionaryRepresentation() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/AnyPrefsStorage+PrefsStorageImportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyPrefsStorage+PrefsStorageImportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension AnyPrefsStorage: PrefsStorageImportable { 10 | @discardableResult 11 | public func load( 12 | from contents: [String: any PrefsStorageValue], 13 | by behavior: PrefsStorageUpdateStrategy 14 | ) throws -> Set { 15 | guard let wrapped = wrapped as? PrefsStorageImportable else { 16 | throw PrefsStorageError.contentLoadingNotSupported 17 | } 18 | return try wrapped.load(from: contents, by: behavior) 19 | } 20 | 21 | @discardableResult 22 | public func load( 23 | unsafe contents: [String: Any], 24 | by behavior: PrefsStorageUpdateStrategy 25 | ) throws -> Set { 26 | guard let wrapped = wrapped as? PrefsStorageImportable else { 27 | throw PrefsStorageError.contentLoadingNotSupported 28 | } 29 | return try wrapped.load(unsafe: contents, by: behavior) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/AnyPrefsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyPrefsStorage.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Type-erased box containing an instance of a concrete class conforming to ``PrefsStorage``. 10 | public final class AnyPrefsStorage: PrefsStorage { 11 | public let wrapped: any PrefsStorage 12 | 13 | public init(_ wrapped: any PrefsStorage) { 14 | self.wrapped = wrapped 15 | } 16 | } 17 | 18 | extension AnyPrefsStorage { 19 | // MARK: - Set 20 | 21 | public func setStorageValue(forKey key: String, to value: StorageValue?) { 22 | wrapped.setStorageValue(forKey: key, to: value) 23 | } 24 | 25 | public func setUnsafeStorageValue(forKey key: String, to value: Any?) { 26 | wrapped.setUnsafeStorageValue(forKey: key, to: value) 27 | } 28 | 29 | // MARK: - Get 30 | 31 | public func storageValue(forKey key: String) -> Int? { 32 | wrapped.storageValue(forKey: key) 33 | } 34 | 35 | public func storageValue(forKey key: String) -> String? { 36 | wrapped.storageValue(forKey: key) 37 | } 38 | 39 | public func storageValue(forKey key: String) -> Bool? { 40 | wrapped.storageValue(forKey: key) 41 | } 42 | 43 | public func storageValue(forKey key: String) -> Double? { 44 | wrapped.storageValue(forKey: key) 45 | } 46 | 47 | public func storageValue(forKey key: String) -> Float? { 48 | wrapped.storageValue(forKey: key) 49 | } 50 | 51 | public func storageValue(forKey key: String) -> Data? { 52 | wrapped.storageValue(forKey: key) 53 | } 54 | 55 | public func storageValue(forKey key: String) -> Date? { 56 | wrapped.storageValue(forKey: key) 57 | } 58 | 59 | public func storageValue(forKey key: String) -> [Element]? { 60 | wrapped.storageValue(forKey: key) 61 | } 62 | 63 | public func storageValue(forKey key: String) -> [String: Element]? { 64 | wrapped.storageValue(forKey: key) 65 | } 66 | 67 | public func unsafeStorageValue(forKey key: String) -> Any? { 68 | wrapped.unsafeStorageValue(forKey: key) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage+PrefsStorageExportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryPrefsStorage+PrefsStorageExportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension DictionaryPrefsStorage: PrefsStorageExportable { 10 | public func dictionaryRepresentation() throws -> [String: Any] { 11 | storage 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage+PrefsStorageImportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryPrefsStorage+PrefsStorageImportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension DictionaryPrefsStorage: PrefsStorageImportable { 10 | @discardableResult 11 | public func load( 12 | from contents: [String: any PrefsStorageValue], 13 | by behavior: PrefsStorageUpdateStrategy 14 | ) throws -> Set { 15 | switch behavior { 16 | case .reinitializing: 17 | storage = contents 18 | return Set(contents.keys) 19 | case .updating: 20 | storage.merge(contents) { old, new in new } 21 | return Set(contents.keys) 22 | case let .updatingWithPredicate(predicate): 23 | return try load(contents: contents, updatingWithPredicate: predicate) 24 | } 25 | } 26 | 27 | @discardableResult 28 | public func load( 29 | unsafe contents: [String: Any], 30 | by behavior: PrefsStorageUpdateStrategy 31 | ) throws -> Set { 32 | switch behavior { 33 | case .reinitializing: 34 | storage = contents 35 | return Set(contents.keys) 36 | case .updating: 37 | storage.merge(contents) { old, new in new } 38 | return Set(contents.keys) 39 | case let .updatingWithPredicate(predicate): 40 | return try load(contents: contents, updatingWithPredicate: predicate) 41 | } 42 | } 43 | } 44 | 45 | // MARK: - Utilities 46 | 47 | extension DictionaryPrefsStorage { 48 | func load( 49 | contents: [String: Any], 50 | updatingWithPredicate predicate: PrefsStorageUpdateStrategy.UpdatePredicate 51 | ) throws -> Set { 52 | var updatedKeys: Set = [] 53 | 54 | for (key, newValue) in contents { 55 | if let existingValue = storage[key] { 56 | let result = try predicate(key, existingValue, newValue) 57 | switch result { 58 | case .preserveOldValue: 59 | break 60 | case .takeNewValue: 61 | storage[key] = newValue 62 | updatedKeys.insert(key) 63 | } 64 | } else { 65 | storage[key] = newValue 66 | updatedKeys.insert(key) 67 | } 68 | } 69 | return updatedKeys 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage+PrefsStorageInitializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryPrefsStorage+PrefsStorageInitializable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // Note: 10 | // 11 | // `PrefsStorageInitializable` conformance is in class definition, as 12 | // `open class` requires protocol-required inits to be defined there and not in an extension. 13 | // 14 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/DictionaryPrefsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryPrefsStorage.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Dictionary-backed ``PrefsStorage`` for use in ``PrefsSchema`` with internally-synchronized local access. 10 | /// 11 | /// This class may be used as-is, or subclassed to add additional functionality to dictionary-backed storage as an 12 | /// alternative to implementing a custom ``PrefsStorage`` type. 13 | open class DictionaryPrefsStorage: PrefsStorageInitializable { 14 | @SynchronizedLock 15 | var storage: [String: Any] 16 | 17 | /// Initialize from type-safe dictionary content. 18 | public init(root: [String: any PrefsStorageValue] = [:]) { 19 | storage = root 20 | } 21 | 22 | /// Initialize from raw untyped dictionary content. 23 | /// You are responsible for ensuring value types are compatible with related methods such as plist conversion. 24 | public required init(unsafe storage: [String: Any]) { 25 | self.storage = storage 26 | } 27 | 28 | // MARK: PrefsStorageInitializable inits 29 | 30 | // Note: 31 | // 32 | // `PrefsStorageInitializable` conformance is in class definition, as 33 | // `open class` requires protocol-required inits to be defined there and not in an extension. 34 | // 35 | 36 | required public convenience init( 37 | from url: URL, 38 | format: Format 39 | ) throws where Format: PrefsStorageImportFormatFileImportable { 40 | self.init() 41 | try load(from: url, format: format, by: .reinitializing) 42 | } 43 | 44 | required public convenience init( 45 | from data: Data, 46 | format: Format 47 | ) throws where Format: PrefsStorageImportFormatDataImportable { 48 | self.init() 49 | try load(from: data, format: format, by: .reinitializing) 50 | } 51 | 52 | required public convenience init( 53 | from string: String, 54 | format: Format 55 | ) throws where Format: PrefsStorageImportFormatStringImportable { 56 | self.init() 57 | try load(from: string, format: format, by: .reinitializing) 58 | } 59 | } 60 | 61 | extension DictionaryPrefsStorage: @unchecked Sendable { } 62 | 63 | extension DictionaryPrefsStorage: PrefsStorage { 64 | // MARK: - Set 65 | 66 | public func setStorageValue(forKey key: String, to value: StorageValue?) { 67 | storage[key] = value 68 | } 69 | 70 | public func setUnsafeStorageValue(forKey key: String, to value: Any?) { 71 | storage[key] = value 72 | } 73 | 74 | // MARK: - Get 75 | 76 | public func storageValue(forKey key: String) -> Int? { 77 | storage[key] as? Int 78 | } 79 | 80 | public func storageValue(forKey key: String) -> String? { 81 | storage[key] as? String 82 | } 83 | 84 | public func storageValue(forKey key: String) -> Bool? { 85 | storage[key] as? Bool 86 | } 87 | 88 | public func storageValue(forKey key: String) -> Double? { 89 | storage[key] as? Double 90 | } 91 | 92 | public func storageValue(forKey key: String) -> Float? { 93 | storage[key] as? Float 94 | } 95 | 96 | public func storageValue(forKey key: String) -> Data? { 97 | storage[key] as? Data 98 | } 99 | 100 | public func storageValue(forKey key: String) -> Date? { 101 | storage[key] as? Date 102 | } 103 | 104 | public func storageValue(forKey key: String) -> [Element]? { 105 | storage[key] as? [Element] 106 | } 107 | 108 | public func storageValue(forKey key: String) -> [String: Element]? { 109 | storage[key] as? [String: Element] 110 | } 111 | 112 | public func unsafeStorageValue(forKey key: String) -> Any? { 113 | storage[key] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage+PrefsStorageExportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsPrefsStorage+PrefsStorageExportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension UserDefaultsPrefsStorage: PrefsStorageExportable { 10 | public func dictionaryRepresentation() throws -> [String: Any] { 11 | // UserDefaults suite includes all search lists when requesting its `dictionaryRepresentation()`, 12 | // which means a lot more keys than expected may be included. 13 | suite.dictionaryRepresentation() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage+PrefsStorageImportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsPrefsStorage+PrefsStorageImportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension UserDefaultsPrefsStorage: PrefsStorageImportable { 10 | @discardableResult 11 | public func load( 12 | from contents: [String: any PrefsStorageValue], 13 | by behavior: PrefsStorageUpdateStrategy 14 | ) throws -> Set { 15 | switch behavior { 16 | case .reinitializing: 17 | suite.removeAllKeys() 18 | suite.merge(contents) 19 | return Set(contents.keys) 20 | case .updating: 21 | suite.merge(contents) 22 | return Set(contents.keys) 23 | case let .updatingWithPredicate(predicate): 24 | return try load(contents: contents, updatingWithPredicate: predicate) 25 | } 26 | } 27 | 28 | @discardableResult 29 | public func load( 30 | unsafe contents: [String: Any], 31 | by behavior: PrefsStorageUpdateStrategy 32 | ) throws -> Set { 33 | switch behavior { 34 | case .reinitializing: 35 | suite.removeAllKeys() 36 | suite.merge(contents) 37 | return Set(contents.keys) 38 | case .updating: 39 | suite.merge(contents) 40 | return Set(contents.keys) 41 | case let .updatingWithPredicate(predicate): 42 | return try load(contents: contents, updatingWithPredicate: predicate) 43 | } 44 | } 45 | } 46 | 47 | // MARK: - Utilities 48 | 49 | extension UserDefaultsPrefsStorage { 50 | func load( 51 | contents: [String: Any], 52 | updatingWithPredicate predicate: PrefsStorageUpdateStrategy.UpdatePredicate 53 | ) throws -> Set { 54 | var updatedKeys: Set = [] 55 | 56 | for (key, newValue) in contents { 57 | if let existingValue = suite.object(forKey: key) { 58 | let result = try predicate(key, existingValue, newValue) 59 | switch result { 60 | case .preserveOldValue: 61 | break 62 | case .takeNewValue: 63 | suite.set(newValue, forKey: key) 64 | updatedKeys.insert(key) 65 | } 66 | } else { 67 | suite.set(newValue, forKey: key) 68 | updatedKeys.insert(key) 69 | } 70 | } 71 | return updatedKeys 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage+Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsPrefsStorage+Utilities.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension UserDefaults { 10 | @inlinable 11 | package static func castAsPrefsStorageCompatible(value: Any) -> Any { 12 | // Note that underlying number format of NSNumber can't easily be determined 13 | // so the cleanest solution is to make NSNumber `PrefsStorageValue` and allow 14 | // the user to conditionally cast it as the number type they desire. 15 | 16 | switch value { 17 | // MARK: Atomic 18 | case let value as NSString: 19 | return value as String 20 | case let value as Bool where "\(type(of: value))" == "__NSCFBoolean": 21 | return value 22 | case let value as NSNumber: 23 | return value 24 | case let value as NSData: 25 | return value as Data 26 | case let value as NSDate: 27 | return value as Date 28 | // MARK: Arrays 29 | case let value as [NSString]: 30 | return value as [String] 31 | case let value as [Bool] where value.allSatisfy { "\(type(of: $0))" == "__NSCFBoolean" }: 32 | return value 33 | case let value as [NSNumber]: 34 | return value 35 | case let value as [NSData]: 36 | return value as [Data] 37 | case let value as [NSDate]: 38 | return value as [Date] 39 | case let value as [Any]: 40 | return value.map(castAsPrefsStorageCompatible(value:)) 41 | // MARK: Dictionaries 42 | case let value as [NSString: NSString]: 43 | return value as [String: String] 44 | case let value as [NSString: Bool] where value.values.allSatisfy { "\(type(of: $0))" == "__NSCFBoolean" }: 45 | return value as [String: Bool] 46 | case let value as [NSString: NSNumber]: 47 | return value as [String: NSNumber] 48 | case let value as [NSString: NSData]: 49 | return value as [String: Data] 50 | case let value as [NSString: NSDate]: 51 | return value as [String: Date] 52 | case let value as [String: Any]: 53 | return value.mapValues(castAsPrefsStorageCompatible(value:)) 54 | // MARK: Default 55 | default: 56 | assertionFailure("Unhandled UserDefaults pref storage value type: \(type(of: value))") 57 | return value 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Prototypes/UserDefaultsPrefsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsPrefsStorage.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// UserDefaults-backed ``PrefsStorage`` for use in ``PrefsSchema``. 10 | open class UserDefaultsPrefsStorage { 11 | public let suite: UserDefaults 12 | 13 | public init(suite: UserDefaults = .standard) { 14 | self.suite = suite 15 | } 16 | } 17 | 18 | extension UserDefaultsPrefsStorage: @unchecked Sendable { } 19 | 20 | extension UserDefaultsPrefsStorage: PrefsStorage { 21 | // MARK: - Set 22 | 23 | public func setStorageValue(forKey key: String, to value: StorageValue?) { 24 | suite.set(value, forKey: key) 25 | } 26 | 27 | public func setUnsafeStorageValue(forKey key: String, to value: Any?) { 28 | suite.set(value, forKey: key) 29 | } 30 | 31 | // MARK: - Get 32 | 33 | public func storageValue(forKey key: String) -> Int? { 34 | suite.integerOptional(forKey: key) 35 | } 36 | 37 | public func storageValue(forKey key: String) -> String? { 38 | suite.string(forKey: key) 39 | } 40 | 41 | public func storageValue(forKey key: String) -> Bool? { 42 | suite.boolOptional(forKey: key) 43 | } 44 | 45 | public func storageValue(forKey key: String) -> Double? { 46 | suite.doubleOptional(forKey: key) 47 | } 48 | 49 | public func storageValue(forKey key: String) -> Float? { 50 | suite.floatOptional(forKey: key) 51 | } 52 | 53 | public func storageValue(forKey key: String) -> Data? { 54 | suite.data(forKey: key) 55 | } 56 | 57 | public func storageValue(forKey key: String) -> Date? { 58 | (suite.object(forKey: key) as? NSDate) as Date? 59 | } 60 | 61 | public func storageValue(forKey key: String) -> [Element]? { 62 | guard let rawArray = suite.array(forKey: key) else { return nil } 63 | if let typedArray = rawArray as? [Element] { 64 | return typedArray 65 | } else if let typedArray = rawArray.map(UserDefaults.castAsPrefsStorageCompatible(value:)) as? [Element] { 66 | return typedArray 67 | } else { 68 | return nil 69 | } 70 | } 71 | 72 | public func storageValue(forKey key: String) -> [String: Element]? { 73 | guard let rawDict = suite.dictionary(forKey: key) else { return nil } 74 | if let typedDict = rawDict as? [String: Element] { 75 | return typedDict 76 | } else if let typedDict = rawDict.mapValues(UserDefaults.castAsPrefsStorageCompatible(value:)) as? [String: Element] { 77 | return typedDict 78 | } else { 79 | return nil 80 | } 81 | } 82 | 83 | public func unsafeStorageValue(forKey key: String) -> Any? { 84 | suite.object(forKey: key) 85 | } 86 | } 87 | 88 | // MARK: - Additional Storage Access Methods 89 | 90 | extension UserDefaultsPrefsStorage { 91 | public func unsafeStorageValue(forKey key: String) -> [Any]? { 92 | guard let rawArray = suite.array(forKey: key) else { return nil } 93 | let typedArray = rawArray 94 | .map(UserDefaults.castAsPrefsStorageCompatible(value:)) // TODO: may not be necessary 95 | return typedArray 96 | } 97 | 98 | public func unsafeStorageValue(forKey key: String) -> [String: Any]? { 99 | guard let rawDict = suite.dictionary(forKey: key) else { return nil } 100 | let typedDict = rawDict 101 | .mapValues(UserDefaults.castAsPrefsStorageCompatible(value:)) // TODO: may not be necessary 102 | return typedDict 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/JSON/JSON Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON Utilities.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - Import 10 | 11 | extension [String: Any] { 12 | package init(json url: URL, options: JSONSerialization.ReadingOptions = []) throws { 13 | let fileData = try Data(contentsOf: url) 14 | try self.init(json: fileData, options: options) 15 | } 16 | 17 | package init(json data: Data, options: JSONSerialization.ReadingOptions = []) throws { 18 | let object = try JSONSerialization.jsonObject(with: data, options: options) 19 | guard let dictionary = object as? [String: Any] else { 20 | throw PrefsStorageError.jsonFormatNotSupported 21 | } 22 | self = dictionary 23 | } 24 | 25 | package init(json string: String, options: JSONSerialization.ReadingOptions = []) throws { 26 | guard let data = string.data(using: .utf8) else { 27 | throw PrefsStorageError.jsonFormatNotSupported 28 | } 29 | try self.init(json: data, options: options) 30 | } 31 | } 32 | 33 | // MARK: - Export 34 | 35 | extension [String: Any] { 36 | package func jsonData(options: JSONSerialization.WritingOptions = []) throws -> Data { 37 | try JSONSerialization 38 | .data(withJSONObject: self, options: options) 39 | } 40 | 41 | package func jsonString( 42 | options: JSONSerialization.WritingOptions = [], 43 | encoding: String.Encoding = .utf8 44 | ) throws -> String { 45 | let data = try jsonData(options: options) 46 | guard let string = String(data: data, encoding: encoding) else { 47 | throw PrefsStorageError.jsonExportError 48 | } 49 | return string 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/JSON/JSONPrefsStorageExportFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONPrefsStorageExportFormat.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Prefs storage export format to export storage as JSON. 10 | /// 11 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create and 12 | /// supply your own export strategy. 13 | public struct JSONPrefsStorageExportFormat: PrefsStorageExportFormat { 14 | public var options: JSONSerialization.WritingOptions 15 | public var strategy: any PrefsStorageExportStrategy 16 | 17 | public init( 18 | options: JSONSerialization.WritingOptions = [], 19 | strategy: any PrefsStorageExportStrategy 20 | ) { 21 | self.options = options 22 | self.strategy = strategy 23 | } 24 | } 25 | 26 | // MARK: - Static Constructor 27 | 28 | extension PrefsStorageExportFormat where Self == JSONPrefsStorageExportFormat { 29 | /// Prefs storage export format to export storage as JSON. 30 | /// 31 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create 32 | /// and supply your own export strategy. 33 | public static func json( 34 | options: JSONSerialization.WritingOptions = [], 35 | strategy: any PrefsStorageExportStrategy 36 | ) -> JSONPrefsStorageExportFormat { 37 | JSONPrefsStorageExportFormat(options: options, strategy: strategy) 38 | } 39 | } 40 | 41 | // MARK: - Format Traits 42 | 43 | extension JSONPrefsStorageExportFormat: PrefsStorageExportFormatFileExportable { 44 | // Note: 45 | // default implementation is provided when we conform to both 46 | // PrefsStorageExportFormatFileExportable & PrefsStorageExportFormatDataExportable 47 | } 48 | 49 | extension JSONPrefsStorageExportFormat: PrefsStorageExportFormatDataExportable { 50 | public func exportData(storage: [String: Any]) throws -> Data { 51 | let prepared = try strategy.prepareForExport(storage: storage) 52 | let data = try prepared.jsonData(options: options) 53 | return data 54 | } 55 | } 56 | 57 | extension JSONPrefsStorageExportFormat: PrefsStorageExportFormatStringExportable { 58 | public func exportString(storage: [String: Any]) throws -> String { 59 | let prepared = try strategy.prepareForExport(storage: storage) 60 | let string = try prepared.jsonString(options: options, encoding: .utf8) 61 | return string 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/JSON/JSONPrefsStorageImportFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONPrefsStorageImportFormat.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Prefs storage import format to import JSON contents. 10 | /// 11 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create and 12 | /// supply your own import strategy. 13 | public struct JSONPrefsStorageImportFormat: PrefsStorageImportFormat { 14 | public var options: JSONSerialization.ReadingOptions 15 | 16 | public var strategy: any PrefsStorageImportStrategy 17 | 18 | public init( 19 | options: JSONSerialization.ReadingOptions = [], 20 | strategy: any PrefsStorageImportStrategy 21 | ) { 22 | self.options = options 23 | self.strategy = strategy 24 | } 25 | } 26 | 27 | // MARK: - Static Constructor 28 | 29 | extension PrefsStorageImportFormat where Self == JSONPrefsStorageImportFormat { 30 | /// Prefs storage import format to import JSON contents. 31 | /// 32 | /// Since JSON is a does not support all atomic types that ``PrefsStorage`` supports, it is required that you create 33 | /// and supply your own import strategy. 34 | public static func json( 35 | options: JSONSerialization.ReadingOptions = [], 36 | strategy: any PrefsStorageImportStrategy 37 | ) -> JSONPrefsStorageImportFormat { 38 | JSONPrefsStorageImportFormat(options: options, strategy: strategy) 39 | } 40 | } 41 | 42 | // MARK: - Format Traits 43 | 44 | extension JSONPrefsStorageImportFormat: PrefsStorageImportFormatFileImportable { 45 | // Note: 46 | // default implementation is provided when we conform to both 47 | // PrefsStorageImportFormatFileImportable & PrefsStorageImportFormatDataImportable 48 | } 49 | 50 | extension JSONPrefsStorageImportFormat: PrefsStorageImportFormatDataImportable { 51 | public func load(from data: Data) throws -> [String: Any] { 52 | let loaded: [String: Any] = try .init(json: data, options: options) 53 | let prepared = try strategy.prepareForImport(storage: loaded) 54 | return prepared 55 | } 56 | } 57 | 58 | extension JSONPrefsStorageImportFormat: PrefsStorageImportFormatStringImportable { 59 | public func load(from string: String) throws -> [String: Any] { 60 | let loaded: [String: Any] = try .init(json: string, options: options) 61 | let prepared = try strategy.prepareForImport(storage: loaded) 62 | return prepared 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PList Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PList Utilities.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - Import 10 | 11 | extension [String: Any] { 12 | package init(plist url: URL) throws { 13 | let fileData = try Data(contentsOf: url) 14 | try self.init(plist: fileData) 15 | } 16 | 17 | package init(plist data: Data) throws { 18 | var fmt: PropertyListSerialization.PropertyListFormat = .xml // will be overwritten 19 | let dict = try PropertyListSerialization.propertyList(from: data, format: &fmt) 20 | guard let nsDict = dict as? NSDictionary else { 21 | throw CocoaError(.coderReadCorrupt) 22 | } 23 | try self.init(plist: nsDict) 24 | } 25 | 26 | package init(plist string: String) throws { 27 | guard let data = string.data(using: .utf8) else { 28 | throw CocoaError(.coderReadCorrupt) 29 | } 30 | try self.init(plist: data) 31 | } 32 | 33 | package init(plist dictionary: NSDictionary) throws { 34 | // let mappedDict = try convertToPrefDict(plist: dictionary) 35 | guard let mappedDict = dictionary as? [String: Any] else { 36 | throw CocoaError(.coderReadCorrupt) 37 | } 38 | self = mappedDict 39 | } 40 | } 41 | 42 | // MARK: - Export 43 | 44 | extension [String: Any] { 45 | package func plistData(format: PropertyListSerialization.PropertyListFormat = .xml) throws -> Data { 46 | try PropertyListSerialization 47 | .data(fromPropertyList: self, format: format, options: .init()) 48 | } 49 | 50 | package func plistString(encoding: String.Encoding = .utf8) throws -> String { 51 | let data = try plistData(format: .xml) 52 | guard let string = String(data: data, encoding: encoding) else { 53 | throw PrefsStorageError.plistExportError 54 | } 55 | return string 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageExportFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PListPrefsStorageExportFormat.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Prefs storage export format to export storage as plist (property list). 10 | public struct PListPrefsStorageExportFormat: PrefsStorageExportFormat { 11 | public var format: PropertyListSerialization.PropertyListFormat 12 | public var strategy: any PrefsStorageExportStrategy 13 | 14 | public init( 15 | format: PropertyListSerialization.PropertyListFormat, 16 | strategy: some PrefsStorageExportStrategy 17 | ) { 18 | self.format = format 19 | self.strategy = strategy 20 | } 21 | } 22 | 23 | // MARK: - Static Constructor 24 | 25 | extension PrefsStorageExportFormat where Self == PListPrefsStorageExportFormat { 26 | /// Prefs storage export format to export storage as plist (property list). 27 | public static func plist( 28 | format: PropertyListSerialization.PropertyListFormat = .xml, 29 | strategy: some PrefsStorageExportStrategy = .plist 30 | ) -> PListPrefsStorageExportFormat { 31 | PListPrefsStorageExportFormat(format: format, strategy: strategy) 32 | } 33 | } 34 | 35 | // MARK: - Format Traits 36 | 37 | extension PListPrefsStorageExportFormat: PrefsStorageExportFormatFileExportable { 38 | // Note: 39 | // default implementation is provided when we conform to both 40 | // PrefsStorageExportFormatFileExportable & PrefsStorageExportFormatDataExportable 41 | } 42 | 43 | extension PListPrefsStorageExportFormat: PrefsStorageExportFormatDataExportable { 44 | public func exportData(storage: [String: Any]) throws -> Data { 45 | let prepared = try strategy.prepareForExport(storage: storage) 46 | let data = try prepared.plistData(format: format) 47 | return data 48 | } 49 | } 50 | 51 | extension PListPrefsStorageExportFormat: PrefsStorageExportFormatStringExportable { 52 | public func exportString(storage: [String: Any]) throws -> String { 53 | let prepared = try strategy.prepareForExport(storage: storage) 54 | let string = try prepared.plistString(encoding: .utf8) 55 | return string 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageExportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PListPrefsStorageExportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Prefs storage export strategy to export storage as plist (property list). 10 | public struct PListPrefsStorageExportStrategy { 11 | public init() { } 12 | } 13 | 14 | extension PListPrefsStorageExportStrategy: PrefsStorageExportStrategy { 15 | public func prepareForExport(storage: [String: Any]) throws -> [String: Any] { 16 | // pass storage through as-is, no casting or conversions necessary 17 | storage 18 | } 19 | } 20 | 21 | // MARK: - Static Constructor 22 | 23 | extension PrefsStorageExportStrategy where Self == PListPrefsStorageExportStrategy { 24 | /// Prefs storage export strategy to export storage as plist (property list). 25 | public static var plist: PListPrefsStorageExportStrategy { 26 | PListPrefsStorageExportStrategy() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageImportFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PListPrefsStorageImportFormat.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Prefs storage import format to import plist (property list) contents. 10 | public struct PListPrefsStorageImportFormat: PrefsStorageImportFormat { 11 | public var strategy: any PrefsStorageImportStrategy 12 | 13 | public init( 14 | strategy: any PrefsStorageImportStrategy 15 | ) { 16 | self.strategy = strategy 17 | } 18 | } 19 | 20 | // MARK: - Static Constructor 21 | 22 | extension PrefsStorageImportFormat where Self == PListPrefsStorageImportFormat { 23 | /// Prefs storage import format to import plist (property list) contents. 24 | public static func plist( 25 | strategy: any PrefsStorageImportStrategy = .plist 26 | ) -> PListPrefsStorageImportFormat { 27 | PListPrefsStorageImportFormat(strategy: strategy) 28 | } 29 | } 30 | 31 | // MARK: - Format Traits 32 | 33 | extension PListPrefsStorageImportFormat: PrefsStorageImportFormatFileImportable { 34 | // Note: 35 | // default implementation is provided when we conform to both 36 | // PrefsStorageImportFormatFileImportable & PrefsStorageImportFormatDataImportable 37 | } 38 | 39 | extension PListPrefsStorageImportFormat: PrefsStorageImportFormatDataImportable { 40 | public func load(from data: Data) throws -> [String: Any] { 41 | let loaded: [String: Any] = try .init(plist: data) 42 | let prepared = try strategy.prepareForImport(storage: loaded) 43 | return prepared 44 | } 45 | } 46 | 47 | extension PListPrefsStorageImportFormat: PrefsStorageImportFormatStringImportable { 48 | public func load(from string: String) throws -> [String: Any] { 49 | let loaded: [String: Any] = try .init(plist: string) 50 | let prepared = try strategy.prepareForImport(storage: loaded) 51 | return prepared 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits Prototypes/PList/PListPrefsStorageImportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PListPrefsStorageImportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Prefs storage import strategy to import storage as plist (property list). 10 | public typealias PListPrefsStorageImportStrategy = PrefsStoragePassthroughImportStrategy 11 | 12 | // MARK: - Static Constructor 13 | 14 | extension PrefsStorageImportStrategy where Self == PListPrefsStorageImportStrategy { 15 | /// Prefs storage import strategy to import storage as plist (property list). 16 | public static var plist: PListPrefsStorageImportStrategy { 17 | PListPrefsStorageImportStrategy() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportFormat/PrefsStorageExportFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageExportFormat.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol that allows creating a type that implements storage content exporting to a particular data serialization 10 | /// format. 11 | public protocol PrefsStorageExportFormat { } 12 | 13 | // MARK: - Format Traits 14 | 15 | /// Trait for ``PrefsStorageExportFormat`` that enables exporting storage to a file on disk. 16 | public protocol PrefsStorageExportFormatFileExportable where Self: PrefsStorageExportFormat { 17 | func export(storage: [String: Any], to file: URL) throws 18 | } 19 | 20 | /// Trait for ``PrefsStorageExportFormat`` that enables exporting storage as raw data. 21 | public protocol PrefsStorageExportFormatDataExportable where Self: PrefsStorageExportFormat { 22 | func exportData(storage: [String: Any]) throws -> Data 23 | } 24 | 25 | /// Trait for ``PrefsStorageExportFormat`` that enables exporting storage as string encoding/markup. 26 | public protocol PrefsStorageExportFormatStringExportable where Self: PrefsStorageExportFormat { 27 | func exportString(storage: [String: Any]) throws -> String 28 | } 29 | 30 | // MARK: - Default Implementation 31 | 32 | extension PrefsStorageExportFormat where Self: PrefsStorageExportFormatDataExportable, Self: PrefsStorageExportFormatFileExportable { 33 | public func export(storage: [String: Any], to file: URL) throws { 34 | let data = try exportData(storage: storage) 35 | try data.write(to: file) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportStrategy/PrefsStorageExportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageExportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Export strategy used by prefs export formats. 8 | public protocol PrefsStorageExportStrategy { 9 | /// The main method used to prepare local storage for export. 10 | func prepareForExport(storage: [String: Any]) throws -> [String: Any] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportStrategy/PrefsStorageMappingExportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageMappingExportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | public protocol PrefsStorageMappingExportStrategy: PrefsStorageExportStrategy { 10 | func exportValue(forKeyPath keyPath: [String], value: Int) throws -> Any 11 | func exportValue(forKeyPath keyPath: [String], value: String) throws -> Any 12 | func exportValue(forKeyPath keyPath: [String], value: Bool) throws -> Any 13 | func exportValue(forKeyPath keyPath: [String], value: Double) throws -> Any 14 | func exportValue(forKeyPath keyPath: [String], value: Float) throws -> Any 15 | func exportValue(forKeyPath keyPath: [String], value: NSNumber) throws -> Any 16 | func exportValue(forKeyPath keyPath: [String], value: Data) throws -> Any 17 | func exportValue(forKeyPath keyPath: [String], value: Date) throws -> Any 18 | } 19 | 20 | // MARK: - Default Implementation 21 | 22 | extension PrefsStorageExportStrategy where Self: PrefsStorageMappingExportStrategy { 23 | public func prepareForExport(storage: [String: Any]) throws -> [String: Any] { 24 | // start recursive call at root 25 | try prepareForExport(keyPath: [], dict: storage) 26 | } 27 | 28 | func prepareForExport(keyPath: [String], dict: [String: Any]) throws -> [String: Any] { 29 | var copy = dict 30 | 31 | for (key, value) in copy { 32 | var keyPath = keyPath 33 | keyPath.append(key) 34 | copy[key] = try prepareForExport(keyPath: keyPath, element: value) 35 | } 36 | 37 | return copy 38 | } 39 | 40 | func prepareForExport(keyPath: [String], array: [Any]) throws -> Any { 41 | try array.map { try prepareForExport(keyPath: keyPath, element: $0) } 42 | } 43 | 44 | func prepareForExport(keyPath: [String], element: Any) throws -> Any { 45 | switch element { 46 | case let v as String: 47 | try exportValue(forKeyPath: keyPath, value: v) 48 | case let v as NSNumber where ["__NSCFNumber", "__NSCFBoolean"].contains("\(type(of: element))"): 49 | try prepareForExport(keyPath: keyPath, number: v) 50 | case let v as Int: 51 | try exportValue(forKeyPath: keyPath, value: v) 52 | case let v as Bool: 53 | try exportValue(forKeyPath: keyPath, value: v) 54 | case let v as Double: 55 | try exportValue(forKeyPath: keyPath, value: v) 56 | case let v as Float: 57 | try exportValue(forKeyPath: keyPath, value: v) 58 | case let v as Bool: 59 | try exportValue(forKeyPath: keyPath, value: v) 60 | case let v as Data: 61 | try exportValue(forKeyPath: keyPath, value: v) 62 | case let v as Date: 63 | try exportValue(forKeyPath: keyPath, value: v) 64 | case let v as [Any]: 65 | try prepareForExport(keyPath: keyPath, array: v) 66 | case let v as [String: Any]: 67 | try prepareForExport(keyPath: keyPath, dict: v) 68 | default: 69 | element 70 | } 71 | } 72 | 73 | func prepareForExport( 74 | keyPath: [String], 75 | number: NSNumber, 76 | typeEraseFloatingPoint: Bool = false 77 | ) throws -> Any { 78 | switch number { 79 | case let v as Bool where number.potentialNumberType == .int8_bool 80 | && "\(type(of: number))" == "__NSCFBoolean": 81 | try exportValue(forKeyPath: keyPath, value: v) 82 | case let v as Int where number.potentialNumberType == .int_uInt_uInt32_uInt64_uInt16: 83 | try exportValue(forKeyPath: keyPath, value: v) 84 | case let v as Double where number.potentialNumberType == .double: 85 | typeEraseFloatingPoint 86 | ? try exportValue(forKeyPath: keyPath, value: number) 87 | : try exportValue(forKeyPath: keyPath, value: v) 88 | case let v as Float where number.potentialNumberType == .float: 89 | typeEraseFloatingPoint 90 | ? try exportValue(forKeyPath: keyPath, value: number) 91 | : try exportValue(forKeyPath: keyPath, value: v) 92 | default: 93 | number 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportStrategy/PrefsStoragePassthroughExportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStoragePassthroughExportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// A prefs storage export strategy that passes local storage data through without any modification, conversion, or 8 | /// casting. 9 | public struct PrefsStoragePassthroughExportStrategy { 10 | public init() { } 11 | } 12 | 13 | extension PrefsStoragePassthroughExportStrategy: PrefsStorageExportStrategy { 14 | public func prepareForExport(storage: [String: Any]) throws -> [String: Any] { 15 | // pass storage through as-is, no casting or conversions necessary 16 | storage 17 | } 18 | } 19 | 20 | // MARK: - Static Constructor 21 | 22 | extension PrefsStorageExportStrategy where Self == PrefsStoragePassthroughExportStrategy { 23 | /// A prefs storage export strategy that passes local storage data through without any modification, conversion, 24 | /// or casting. 25 | public static var passthrough: PrefsStoragePassthroughExportStrategy { 26 | PrefsStoragePassthroughExportStrategy() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageExportable/PrefsStorageExportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageExportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Trait for ``PrefsStorage`` that enables exporting storage contents. 10 | public protocol PrefsStorageExportable where Self: PrefsStorage { 11 | /// Returns the storage contents as a dictionary. 12 | /// This method is a required implementation detail for storage export methods provided by 13 | /// ``PrefsStorageExportable`` and is not meant to be used directly. 14 | func dictionaryRepresentation() throws -> [String: Any] 15 | 16 | /// Export the storage contents to a file on disk. 17 | func export( 18 | format: Format, 19 | to file: URL 20 | ) throws where Format: PrefsStorageExportFormatFileExportable 21 | 22 | /// Export the storage contents as raw data. 23 | func exportData( 24 | format: Format 25 | ) throws -> Data where Format: PrefsStorageExportFormatDataExportable 26 | 27 | /// Export the storage contents encoded in a format that supports string encoding/markup. 28 | func exportString( 29 | format: Format 30 | ) throws -> String where Format: PrefsStorageExportFormatStringExportable 31 | } 32 | 33 | // MARK: - Default Implementation 34 | 35 | extension PrefsStorage where Self: PrefsStorageExportable { 36 | public func export( 37 | format: Format, 38 | to file: URL 39 | ) throws where Format: PrefsStorageExportFormatFileExportable { 40 | try format.export(storage: dictionaryRepresentation(), to: file) 41 | } 42 | 43 | public func exportData( 44 | format: Format 45 | ) throws -> Data where Format: PrefsStorageExportFormatDataExportable { 46 | try format.exportData(storage: dictionaryRepresentation()) 47 | } 48 | 49 | public func exportString( 50 | format: Format 51 | ) throws -> String where Format: PrefsStorageExportFormatStringExportable { 52 | try format.exportString(storage: dictionaryRepresentation()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportFormat/PrefsStorageImportFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageImportFormat.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol that allows creating a type that implements storage content importing from a particular data serialization 10 | /// format. 11 | public protocol PrefsStorageImportFormat { } 12 | 13 | // MARK: - Format Traits 14 | 15 | /// Trait for ``PrefsStorageImportFormat`` that enables importing serialized storage content from a file on disk. 16 | public protocol PrefsStorageImportFormatFileImportable where Self: PrefsStorageImportFormat { 17 | /// Read data into a raw dictionary, ready to be processed by the import strategy. 18 | func load(from file: URL) throws -> [String: Any] 19 | } 20 | 21 | /// Trait for ``PrefsStorageImportFormat`` that enables importing serialized storage content from raw data. 22 | public protocol PrefsStorageImportFormatDataImportable where Self: PrefsStorageImportFormat { 23 | /// Read data into a raw dictionary, ready to be processed by the import strategy. 24 | func load(from data: Data) throws -> [String: Any] 25 | } 26 | 27 | /// Trait for ``PrefsStorageImportFormat`` that enables importing serialized storage content from string encoding/markup. 28 | public protocol PrefsStorageImportFormatStringImportable where Self: PrefsStorageImportFormat { 29 | /// Read data into a raw dictionary, ready to be processed by the import strategy. 30 | func load(from string: String) throws -> [String: Any] 31 | } 32 | 33 | // MARK: - Default Implementation 34 | 35 | extension PrefsStorageImportFormat where Self: PrefsStorageImportFormatDataImportable, Self: PrefsStorageImportFormatFileImportable { 36 | public func load(from file: URL) throws -> [String: Any] { 37 | let data = try Data(contentsOf: file) 38 | let dict = try load(from: data) 39 | return dict 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportStrategy/PrefsStorageImportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageImportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Import strategy used by prefs import formats. 8 | public protocol PrefsStorageImportStrategy { 9 | /// The main method used to prepare imported data for merging into local storage. 10 | func prepareForImport(storage: [String: Any]) throws -> [String: Any] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportStrategy/PrefsStoragePassthroughImportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStoragePassthroughImportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// A prefs storage import strategy that passes imported data through without any modification, conversion, or 10 | /// casting. 11 | public struct PrefsStoragePassthroughImportStrategy { 12 | public init() { } 13 | } 14 | 15 | extension PrefsStoragePassthroughImportStrategy: PrefsStorageImportStrategy { 16 | public func prepareForImport(storage: [String: Any]) throws -> [String: Any] { 17 | // pass storage through as-is, no casting or conversions necessary 18 | storage 19 | } 20 | } 21 | 22 | // MARK: - Static Constructor 23 | 24 | extension PrefsStorageImportStrategy where Self == PrefsStoragePassthroughImportStrategy { 25 | /// A prefs storage import strategy that passes imported data through without any modification, conversion, or 26 | /// casting. 27 | public static var passthrough: PrefsStoragePassthroughImportStrategy { 28 | PrefsStoragePassthroughImportStrategy() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportStrategy/PrefsStorageTypedImportStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageTypedImportStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Offers default ``PrefsStorageMappingImportStrategy`` implementation functionality. 10 | public struct PrefsStorageTypedImportStrategy { 11 | public var typeEraseAmbiguousFloatingPoint: Bool 12 | 13 | public init(typeEraseAmbiguousFloatingPoint: Bool = false) { 14 | self.typeEraseAmbiguousFloatingPoint = typeEraseAmbiguousFloatingPoint 15 | } 16 | } 17 | 18 | extension PrefsStorageTypedImportStrategy: PrefsStorageMappingImportStrategy { } 19 | 20 | // MARK: - Static Constructor 21 | 22 | extension PrefsStorageImportStrategy where Self == PrefsStorageTypedImportStrategy { 23 | /// Offers default ``PrefsStorageMappingImportStrategy`` implementation functionality. 24 | public static func typed(typeEraseAmbiguousFloatingPoint: Bool = false) -> PrefsStorageTypedImportStrategy { 25 | PrefsStorageTypedImportStrategy( 26 | typeEraseAmbiguousFloatingPoint: typeEraseAmbiguousFloatingPoint 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageImportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageImportable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Trait for ``PrefsStorage`` that enables loading storage contents. 10 | /// 11 | /// > Note: 12 | /// > 13 | /// > Loading storage contents will not update local cache properties for any `@Pref` keys defined in a `@PrefsSchema` 14 | /// > whose `storageMode` is set to `cachedReadStorageWrite`. The storage mode must be set to `storageOnly` to ensure 15 | /// > data loads correctly. 16 | public protocol PrefsStorageImportable where Self: PrefsStorage { 17 | /// Load key/values into storage. 18 | /// 19 | /// - Returns: Key names for key/value pairs that were imported. 20 | @discardableResult 21 | func load( 22 | from contents: [String: any PrefsStorageValue], 23 | by behavior: PrefsStorageUpdateStrategy 24 | ) throws -> Set 25 | 26 | /// Load key/values into storage. 27 | /// 28 | /// - Returns: Key names for key/value pairs that were imported. 29 | @discardableResult 30 | func load( 31 | unsafe contents: [String: Any], 32 | by behavior: PrefsStorageUpdateStrategy 33 | ) throws -> Set 34 | 35 | /// Import storage contents from a file on disk. 36 | /// 37 | /// - Returns: Key names for key/value pairs that were imported. 38 | @discardableResult 39 | func load( 40 | from file: URL, 41 | format: Format, 42 | by behavior: PrefsStorageUpdateStrategy 43 | ) throws -> Set where Format: PrefsStorageImportFormatFileImportable 44 | 45 | /// Import storage contents from a format's raw data. 46 | /// 47 | /// - Returns: Key names for key/value pairs that were imported. 48 | @discardableResult 49 | func load( 50 | from data: Data, 51 | format: Format, 52 | by behavior: PrefsStorageUpdateStrategy 53 | ) throws -> Set where Format: PrefsStorageImportFormatDataImportable 54 | 55 | /// Import storage contents from a format that supports string encoding/markup. 56 | /// 57 | /// - Returns: Key names for key/value pairs that were imported. 58 | @discardableResult 59 | func load( 60 | from string: String, 61 | format: Format, 62 | by behavior: PrefsStorageUpdateStrategy 63 | ) throws -> Set where Format: PrefsStorageImportFormatStringImportable 64 | } 65 | 66 | // MARK: - Default Implementation 67 | 68 | extension PrefsStorage where Self: PrefsStorageImportable { 69 | @discardableResult 70 | public func load( 71 | from file: URL, 72 | format: Format, 73 | by behavior: PrefsStorageUpdateStrategy 74 | ) throws -> Set where Format: PrefsStorageImportFormatFileImportable { 75 | let loaded = try format.load(from: file) 76 | return try load(unsafe: loaded, by: behavior) 77 | } 78 | 79 | @discardableResult 80 | public func load( 81 | from data: Data, 82 | format: Format, 83 | by behavior: PrefsStorageUpdateStrategy 84 | ) throws -> Set where Format: PrefsStorageImportFormatDataImportable { 85 | let loaded = try format.load(from: data) 86 | return try load(unsafe: loaded, by: behavior) 87 | } 88 | 89 | @discardableResult 90 | public func load( 91 | from string: String, 92 | format: Format, 93 | by behavior: PrefsStorageUpdateStrategy 94 | ) throws -> Set where Format: PrefsStorageImportFormatStringImportable { 95 | let loaded = try format.load(from: string) 96 | return try load(unsafe: loaded, by: behavior) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageImportable/PrefsStorageUpdateStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageUpdateStrategy.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Contents loading behavior for ``PrefsStorage`` load methods. 8 | public enum PrefsStorageUpdateStrategy { 9 | /// Replaces existing storage contents with new contents, removing all existing keys first. 10 | case reinitializing 11 | 12 | /// Merges new content with existing storage contents, overwriting a key's value in the event of a key name collision. 13 | case updating 14 | 15 | /// Merges new content with existing storage contents, using a user-defined predicate in the event of key name collisions. 16 | case updatingWithPredicate(_ predicate: UpdatePredicate) 17 | } 18 | 19 | extension PrefsStorageUpdateStrategy: Sendable { } 20 | 21 | extension PrefsStorageUpdateStrategy { 22 | public typealias UpdatePredicate = @Sendable (_ key: String, _ oldValue: Any, _ newValue: Any) throws -> ValueUpdateResult 23 | } 24 | 25 | extension PrefsStorageUpdateStrategy { 26 | public enum ValueUpdateResult { 27 | case preserveOldValue 28 | case takeNewValue 29 | } 30 | } 31 | 32 | extension PrefsStorageUpdateStrategy.ValueUpdateResult: Sendable { } 33 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage Traits/PrefsStorageInitializable/PrefsStorageInitializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageInitializable.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Trait for ``PrefsStorage`` that enables initializing storage contents. 10 | public protocol PrefsStorageInitializable where Self: PrefsStorageImportable { 11 | /// Initialize with storage contents by importing a file on disk. 12 | init( 13 | from url: URL, 14 | format: Format 15 | ) throws where Format: PrefsStorageImportFormatFileImportable 16 | 17 | /// Initialize with storage contents by importing raw file contents. 18 | init( 19 | from data: Data, 20 | format: Format 21 | ) throws where Format: PrefsStorageImportFormatDataImportable 22 | 23 | /// Initialize with storage contents by importing raw file contents for a format that supports string 24 | /// encoding/markup. 25 | init( 26 | from string: String, 27 | format: Format 28 | ) throws where Format: PrefsStorageImportFormatStringImportable 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage/PrefsStorage+DefaultedPrefsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorage+DefaultedPrefsKey.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // swiftformat:disable wrap 10 | 11 | // MARK: - Get Value 12 | 13 | extension PrefsStorage { 14 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Int { 15 | key.decodeDefaulted(storageValue(forKey: key)) 16 | } 17 | 18 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == String { 19 | key.decodeDefaulted(storageValue(forKey: key)) 20 | } 21 | 22 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Bool { 23 | key.decodeDefaulted(storageValue(forKey: key)) 24 | } 25 | 26 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Double { 27 | key.decodeDefaulted(storageValue(forKey: key)) 28 | } 29 | 30 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Float { 31 | key.decodeDefaulted(storageValue(forKey: key)) 32 | } 33 | 34 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Data { 35 | key.decodeDefaulted(storageValue(forKey: key)) 36 | } 37 | 38 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == Date { 39 | key.decodeDefaulted(storageValue(forKey: key)) 40 | } 41 | 42 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == [Element] { 43 | key.decodeDefaulted(storageValue(forKey: key)) 44 | } 45 | 46 | public func value(forKey key: Key) -> Key.Value where Key.StorageValue == [String: Element] { 47 | key.decodeDefaulted(storageValue(forKey: key)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage/PrefsStorage+PrefsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorage+PrefsKey.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // swiftformat:disable wrap 10 | 11 | // MARK: - Set Value 12 | 13 | extension PrefsStorage { 14 | public func setValue(forKey key: Key, to value: Key.Value?) { 15 | setStorageValue(forKey: key.key, to: key.encode(value)) 16 | } 17 | } 18 | 19 | // MARK: - Get Storage Value 20 | 21 | extension PrefsStorage { 22 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Int { 23 | storageValue(forKey: key.key) 24 | } 25 | 26 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == String { 27 | storageValue(forKey: key.key) 28 | } 29 | 30 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Bool { 31 | storageValue(forKey: key.key) 32 | } 33 | 34 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Double { 35 | storageValue(forKey: key.key) 36 | } 37 | 38 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Float { 39 | storageValue(forKey: key.key) 40 | } 41 | 42 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Data { 43 | storageValue(forKey: key.key) 44 | } 45 | 46 | public func storageValue(forKey key: Key) -> Key.StorageValue? where Key.StorageValue == Date { 47 | storageValue(forKey: key.key) 48 | } 49 | 50 | public func storageValue(forKey key: Key) -> [Element]? where Key.StorageValue == [Element] { 51 | storageValue(forKey: key.key) 52 | } 53 | 54 | public func storageValue(forKey key: Key) -> [String: Element]? where Key.StorageValue == [String: Element] { 55 | storageValue(forKey: key.key) 56 | } 57 | } 58 | 59 | // MARK: - Get Value 60 | 61 | extension PrefsStorage { 62 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Int { 63 | guard let storageValue = storageValue(forKey: key) else { return nil } 64 | return key.decode(storageValue) 65 | } 66 | 67 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == String { 68 | guard let storageValue = storageValue(forKey: key) else { return nil } 69 | return key.decode(storageValue) 70 | } 71 | 72 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Bool { 73 | guard let storageValue = storageValue(forKey: key) else { return nil } 74 | return key.decode(storageValue) 75 | } 76 | 77 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Double { 78 | guard let storageValue = storageValue(forKey: key) else { return nil } 79 | return key.decode(storageValue) 80 | } 81 | 82 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Float { 83 | guard let storageValue = storageValue(forKey: key) else { return nil } 84 | return key.decode(storageValue) 85 | } 86 | 87 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Data { 88 | guard let storageValue = storageValue(forKey: key) else { return nil } 89 | return key.decode(storageValue) 90 | } 91 | 92 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == Date { 93 | guard let storageValue = storageValue(forKey: key) else { return nil } 94 | return key.decode(storageValue) 95 | } 96 | 97 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == [Element] { 98 | guard let storageValue = storageValue(forKey: key) else { return nil } 99 | return key.decode(storageValue) 100 | } 101 | 102 | public func value(forKey key: Key) -> Key.Value? where Key.StorageValue == [String: Element] { 103 | guard let storageValue = storageValue(forKey: key) else { return nil } 104 | return key.decode(storageValue) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage/PrefsStorage+Static.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorage+Static.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension PrefsStorage where Self == DictionaryPrefsStorage { 10 | /// Dictionary prefs storage. 11 | public static var dictionary: DictionaryPrefsStorage { 12 | DictionaryPrefsStorage() 13 | } 14 | 15 | /// Dictionary prefs storage with initial root content. 16 | public static func dictionary(root: [String: any PrefsStorageValue]) -> DictionaryPrefsStorage { 17 | DictionaryPrefsStorage(root: root) 18 | } 19 | 20 | /// Dictionary prefs storage with initial raw untyped root content. 21 | /// You are responsible for ensuring value types are compatible with related methods such as plist conversion. 22 | public static func dictionary(unsafe storage: [String: Any]) -> DictionaryPrefsStorage { 23 | DictionaryPrefsStorage(unsafe: storage) 24 | } 25 | 26 | /// Dictionary prefs storage with initial root content from a JSON file. 27 | public static func dictionary( 28 | from url: URL, 29 | format: Format 30 | ) throws -> DictionaryPrefsStorage where Format: PrefsStorageImportFormatFileImportable { 31 | try DictionaryPrefsStorage(from: url, format: format) 32 | } 33 | 34 | /// Dictionary prefs storage with initial root content from a JSON file. 35 | public static func dictionary( 36 | from data: Data, 37 | format: Format 38 | ) throws -> DictionaryPrefsStorage where Format: PrefsStorageImportFormatDataImportable { 39 | try DictionaryPrefsStorage(from: data, format: format) 40 | } 41 | 42 | /// Dictionary prefs storage with initial root content from a JSON file. 43 | public static func dictionary( 44 | from string: String, 45 | format: Format 46 | ) throws -> DictionaryPrefsStorage where Format: PrefsStorageImportFormatStringImportable { 47 | try DictionaryPrefsStorage(from: string, format: format) 48 | } 49 | } 50 | 51 | extension PrefsStorage where Self == UserDefaultsPrefsStorage { 52 | /// Standard `UserDefaults` suite prefs storage. 53 | public static var userDefaults: UserDefaultsPrefsStorage { 54 | UserDefaultsPrefsStorage() 55 | } 56 | 57 | /// Custom `UserDefaults` suite prefs storage. 58 | public static func userDefaults(suite: UserDefaults) -> UserDefaultsPrefsStorage { 59 | UserDefaultsPrefsStorage(suite: suite) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage/PrefsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorage.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Conform a type to enable it to be used for prefs storage. 10 | /// The type must be a reference type (class). 11 | public protocol PrefsStorage: AnyObject where Self: Sendable { 12 | // MARK: - Set 13 | 14 | func setStorageValue(forKey key: String, to value: StorageValue?) 15 | func setUnsafeStorageValue(forKey key: String, to value: Any?) 16 | 17 | // MARK: - Get 18 | 19 | func storageValue(forKey key: String) -> Int? 20 | func storageValue(forKey key: String) -> String? 21 | func storageValue(forKey key: String) -> Bool? 22 | func storageValue(forKey key: String) -> Double? 23 | func storageValue(forKey key: String) -> Float? 24 | func storageValue(forKey key: String) -> Data? 25 | func storageValue(forKey key: String) -> Date? 26 | func storageValue(forKey key: String) -> [Element]? 27 | func storageValue(forKey key: String) -> [String: Element]? 28 | func unsafeStorageValue(forKey key: String) -> Any? 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage/PrefsStorageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageError.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | public enum PrefsStorageError: LocalizedError { 10 | case contentExportingNotSupported 11 | case contentLoadingNotSupported 12 | case jsonExportError 13 | case jsonFormatNotSupported 14 | case jsonLoadingNotSupported 15 | case jsonWritingNotSupported 16 | case plistExportError 17 | case plistLoadingNotSupported 18 | case plistWritingNotSupported 19 | 20 | public var errorDescription: String? { 21 | switch self { 22 | case .contentExportingNotSupported: 23 | "Exporting content is not supported for this prefs storage implementation." 24 | case .contentLoadingNotSupported: 25 | "Loading content is not supported for this prefs storage implementation." 26 | case .jsonExportError: 27 | "JSON export failed." 28 | case .jsonFormatNotSupported: 29 | "JSON format is not supported or not recognized." 30 | case .jsonLoadingNotSupported: 31 | "Conversion from JSON is not supported for this prefs storage implementation." 32 | case .jsonWritingNotSupported: 33 | "Conversion to JSON format is not supported for this prefs storage implementation." 34 | case .plistExportError: 35 | "PList export failed." 36 | case .plistLoadingNotSupported: 37 | "Conversion from plist format is not supported for this prefs storage implementation." 38 | case .plistWritingNotSupported: 39 | "Conversion to plist format is not supported for this prefs storage implementation." 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorage/PrefsStorageMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageMode.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// Pref schema property access storage mode for ``PrefsSchema`` implementations. 8 | public enum PrefsStorageMode { 9 | /// Storage-backed only with no intermediate cache. 10 | /// 11 | /// Directly read and write from prefs schema `storage` on every access to pref properties without cacheing. 12 | /// This may have performance impacts on frequent accesses or for data types with expensive decoding operations. 13 | case storageOnly 14 | 15 | /// Cache-backed read, storage-backed write. 16 | /// 17 | /// Reads property values from storage on initialization, then utilizes an internal local cache for improved read 18 | /// performance thereafter. All writes are always written to storage immediately. 19 | /// 20 | /// This mode is recommended in most use cases for improved performance. 21 | /// 22 | /// > Note: 23 | /// > This mode is suitable for storage that cannot or will not change externally. Changes made externally 24 | /// > will not be reflected within the schema during its lifespan and will be overwritten with cached values. 25 | case cachedReadStorageWrite 26 | 27 | // TODO: could implement this feature in future if there is a way to enumerate all prefs in a PrefsSchema 28 | // /// Cache only. 29 | // /// Reads property values from storage on initialization, then operates exclusively from cache for reads and 30 | // /// writes. 31 | // /// 32 | // /// Writes are not automatically written to storage. Changes to pref values must be manually committed by calling 33 | // /// `commit()` on the prefs schema which writes all cached pref values to storage. It is recommended to do this 34 | // /// only periodically or upon context switches (such as when the user invokes a Save Settings command, or your 35 | // /// application quits). 36 | // /// 37 | // /// This storage mode is preferable for rare use cases where performance is critical, allowing storage commits 38 | // /// to be invoked electively only during optimal conditions. 39 | // /// 40 | // /// > Note: 41 | // /// > This mode is suitable for storage that cannot or will not change externally. Changes made externally 42 | // /// > will not be reflected within the schema and will be overwritten with cached values. 43 | // case cacheOnly 44 | } 45 | 46 | extension PrefsStorageMode: Equatable { } 47 | 48 | extension PrefsStorageMode: Hashable { } 49 | 50 | extension PrefsStorageMode: Sendable { } 51 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorageValue/PrefsStorageValue Types.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageValue Types.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - Atomic Types 10 | 11 | extension Int: PrefsStorageValue { } 12 | 13 | extension String: PrefsStorageValue { } 14 | 15 | extension Bool: PrefsStorageValue { } 16 | 17 | extension Double: PrefsStorageValue { } 18 | 19 | extension Float: PrefsStorageValue { } 20 | 21 | extension Data: PrefsStorageValue { } 22 | 23 | extension Date: PrefsStorageValue { } 24 | 25 | extension Array: PrefsStorageValue where Element: PrefsStorageValue { } 26 | 27 | extension Dictionary: PrefsStorageValue where Key == String, Value: PrefsStorageValue { } 28 | 29 | // MARK: - Additional Types 30 | 31 | extension NSNumber: PrefsStorageValue { } 32 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/PrefsStorageValue/PrefsStorageValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageValue.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | /// Protocol adopted by format-agnostic atomic value types that are valid for storage in prefs storage. 10 | public protocol PrefsStorageValue where Self: Equatable, Self: Sendable { } 11 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/Utilities/Concurrency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Concurrency.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | @propertyWrapper 10 | public struct SynchronizedLock: @unchecked Sendable { 11 | private var _value: Value 12 | private var lock = NSLock() 13 | 14 | public init(wrappedValue: Value) { 15 | _value = wrappedValue 16 | } 17 | 18 | public var wrappedValue: Value { 19 | get { lock.synchronized { _value } } 20 | set { lock.synchronized { _value = newValue } } 21 | } 22 | 23 | private mutating func synchronized(block: (inout Value) throws -> T) rethrows -> T { 24 | try lock.synchronized { 25 | try block(&_value) 26 | } 27 | } 28 | } 29 | 30 | extension NSLocking { 31 | fileprivate func synchronized(block: () throws -> T) rethrows -> T { 32 | lock() 33 | defer { unlock() } 34 | return try block() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/Utilities/NSNumber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSNumber.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - objCTypeString 10 | 11 | extension NSNumber { 12 | /// Returns `objCType` (Objective-C type encoding) as `String`. 13 | /// 14 | /// Possible strings are a subset of `NSValue`'s Objective-C type encodings. 15 | /// 16 | /// ``` 17 | /// “c”, “C”, “s”, “S”, “i”, “I”, “l”, “L”, “q”, “Q”, “f”, and “d”. 18 | /// ``` 19 | package var objCTypeString: String { 20 | String(cString: objCType) 21 | } 22 | } 23 | 24 | // MARK: - TypeEncoding 25 | 26 | extension NSNumber { 27 | /// Objective-C type encoding for `NSNumber`. 28 | /// 29 | /// See: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html 30 | package enum TypeEncoding: String, Equatable, Hashable { 31 | case char = "c" // A char 32 | case int = "i" // An int 33 | case short = "s" // A short 34 | case long = "l" // A long, treated as a 32-bit quantity on 64-bit programs. 35 | case longLong = "q" // A long long 36 | case unsignedChar = "C" // An unsigned char 37 | case unsignedInt = "I" // An unsigned int 38 | case unsignedShort = "S" // An unsigned short 39 | case unsignedLong = "L" // An unsigned long 40 | case unsignedLongLong = "Q" // An unsigned long long 41 | case float = "f" // A float 42 | case double = "d" // A double 43 | case cBool = "B" // A C++ bool or a C99 _Bool 44 | case void = "v" // A void 45 | case characterString = "*" // A character string (char *) 46 | case object = "@" // An object (whether statically typed or typed id) 47 | case classObject = "#" // A class object (Class) 48 | case methodSelector = ":" // A method selector (SEL) 49 | case unknown 50 | 51 | init(value: NSNumber) { 52 | let objCTypeString = value.objCTypeString 53 | self = Self(rawValue: objCTypeString) ?? .unknown 54 | } 55 | } 56 | 57 | package var typeEncoding: TypeEncoding { 58 | TypeEncoding(value: self) 59 | } 60 | } 61 | 62 | // MARK: - SwiftNumberType 63 | 64 | extension NSNumber { 65 | /// Swift concrete number type detection heuristic for `NSNumber`. 66 | package enum SwiftNumberType: Equatable, Hashable { 67 | case int_uInt_uInt32_uInt64_uInt16 68 | case int8_bool 69 | case uInt8_int16 70 | case double 71 | case float 72 | case unknown 73 | 74 | init(value: NSNumber) { 75 | let objCTypeString = value.objCTypeString 76 | self.init(objCType: objCTypeString) 77 | } 78 | 79 | init(objCType: String) { 80 | let typeEncoding = TypeEncoding(rawValue: objCType) ?? .unknown 81 | self.init(typeEncoding: typeEncoding) 82 | } 83 | 84 | init(typeEncoding: TypeEncoding) { 85 | // Swift Type Encoding When Cast to NSNumber 86 | // ---------- ------------------------------ 87 | // c s i q d f 88 | // - - - - - - 89 | // Int q 90 | // UInt q 91 | // Int8 c 92 | // UInt8 s 93 | // Int16 s 94 | // UInt16 i 95 | // Int32 i 96 | // UInt32 q 97 | // Int64 q 98 | // UInt64 q 99 | // 100 | // Double d 101 | // Float f 102 | // 103 | // Bool c 104 | 105 | self = switch typeEncoding { 106 | case .char: // "c" 107 | .int8_bool 108 | case .short: // "s" 109 | .uInt8_int16 110 | case .longLong: // "q" 111 | .int_uInt_uInt32_uInt64_uInt16 112 | case .double: // "d" 113 | .double 114 | case .float: // "f" 115 | .float 116 | default: 117 | .unknown 118 | } 119 | } 120 | } 121 | 122 | package var potentialNumberType: SwiftNumberType { 123 | SwiftNumberType(value: self) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/Utilities/Outsourced/UserDefaults Outsourced.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults Outsourced.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | /// ---------------------------------------------- 8 | /// ---------------------------------------------- 9 | /// OTCore/Extensions/Foundation/CharacterSet.swift 10 | /// 11 | /// Borrowed from OTCore 1.6.0 under MIT license. 12 | /// https://github.com/orchetect/OTCore 13 | /// Methods herein are unit tested at their source 14 | /// so no unit tests are necessary. 15 | /// ---------------------------------------------- 16 | /// ---------------------------------------------- 17 | /// 18 | 19 | import Foundation 20 | 21 | extension UserDefaults { 22 | // custom optional methods for core data types that don't intrinsically support optionals yet 23 | 24 | /// Convenience method to wrap the built-in `.integer(forKey:)` method in an optional returning nil if the key doesn't exist. 25 | @_disfavoredOverload 26 | func integerOptional(forKey key: String) -> Int? { 27 | guard object(forKey: key) != nil else { return nil } 28 | return integer(forKey: key) 29 | } 30 | 31 | /// Convenience method to wrap the built-in `.double(forKey:)` method in an optional returning nil if the key doesn't exist. 32 | @_disfavoredOverload 33 | func doubleOptional(forKey key: String) -> Double? { 34 | guard object(forKey: key) != nil else { return nil } 35 | return double(forKey: key) 36 | } 37 | 38 | /// Convenience method to wrap the built-in `.float(forKey:)` method in an optional returning nil if the key doesn't exist. 39 | @_disfavoredOverload 40 | func floatOptional(forKey key: String) -> Float? { 41 | guard object(forKey: key) != nil else { return nil } 42 | return float(forKey: key) 43 | } 44 | 45 | /// Convenience method to wrap the built-in `.bool(forKey:)` method in an optional returning nil if the key doesn't exist. 46 | @_disfavoredOverload 47 | func boolOptional(forKey key: String) -> Bool? { 48 | guard object(forKey: key) != nil else { return nil } 49 | return bool(forKey: key) 50 | } 51 | 52 | /// Returns `true` if the key exists. 53 | /// 54 | /// This method is only useful when you don't care about extracting a value from the key and merely want to check for the key's 55 | /// existence. 56 | @_disfavoredOverload 57 | func exists(key: String) -> Bool { 58 | object(forKey: key) != nil 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PrefsKitTypes/Utilities/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension UserDefaults { 10 | func removeAllKeys() { 11 | let keys = dictionaryRepresentation().keys 12 | for key in keys { 13 | removeObject(forKey: key) 14 | } 15 | } 16 | 17 | func merge(_ contents: [String: Any]) { 18 | for element in contents { 19 | set(element.value, forKey: element.key) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/PrefsKitUI/MultiplatformSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiplatformSection.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | /// `Section` SwiftUI view wrapper that incorporates footer content idiomatically for each platform. 10 | /// 11 | /// - On macOS, the footer content is combined into the form content. 12 | /// - On iOS, the footer content is attached below the form content. 13 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | public struct MultiplatformSection: View { 15 | public let header: LocalizedStringKey? 16 | public let content: () -> Content 17 | public let footer: () -> Footer 18 | 19 | public init( 20 | _ header: LocalizedStringKey? = nil, 21 | @ViewBuilder content: @escaping () -> Content, 22 | @ViewBuilder footer: @escaping () -> Footer 23 | ) { 24 | self.header = header 25 | self.content = content 26 | self.footer = footer 27 | } 28 | 29 | public var body: some View { 30 | #if os(macOS) 31 | Section { 32 | VStack { 33 | content() 34 | SectionFooterView { 35 | footer() 36 | } 37 | .foregroundColor(.secondary) 38 | } 39 | } header: { 40 | if let header { 41 | Text(header) 42 | } else { 43 | EmptyView() 44 | } 45 | } 46 | #else 47 | Section { 48 | content() 49 | } footer: { 50 | SectionFooterView { 51 | footer() 52 | } 53 | } 54 | #endif 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PrefsKitUI/PrefsKitUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsKitUI.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | @_exported import PrefsKitCore 8 | -------------------------------------------------------------------------------- /Sources/PrefsKitUI/SectionFooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionFooterView.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | /// Within a SwiftUI `Form` view, use this to wrap `Section` `footer` contents to give it a standardized layout. 10 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 11 | public struct SectionFooterView: View { 12 | public let alignment: Alignment 13 | public let innerAlignment: HorizontalAlignment 14 | public let textAlignment: TextAlignment 15 | public let content: () -> Content 16 | 17 | public init( 18 | alignment: Alignment = .leading, 19 | innerAlignment: HorizontalAlignment = .leading, 20 | textAlignment: TextAlignment = .leading, 21 | @ViewBuilder content: @escaping () -> Content 22 | ) { 23 | self.alignment = alignment 24 | self.innerAlignment = innerAlignment 25 | self.textAlignment = textAlignment 26 | self.content = content 27 | } 28 | 29 | public var body: some View { 30 | ZStack(alignment: alignment) { 31 | Color.clear 32 | VStack(alignment: innerAlignment) { 33 | content() 34 | .multilineTextAlignment(textAlignment) 35 | } 36 | } 37 | .frame(maxWidth: .infinity) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PrefsKitUI/SystemSettings/SystemSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemSettings.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | #if !os(macOS) 10 | import UIKit 11 | #endif 12 | 13 | public enum SystemSettings { 14 | #if os(macOS) 15 | /// Launches System Settings, optionally opening the specified panel. 16 | public static func launch(panel: Panel? = nil) { 17 | let command = launchCommand(panel: panel) 18 | 19 | // launch 20 | let p = Process() 21 | let shellExecutablePath = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh" 22 | p.executableURL = URL(fileURLWithPath: shellExecutablePath) 23 | p.arguments = ["-cl", command] 24 | do { try p.run() } 25 | catch { print(error.localizedDescription) } 26 | } 27 | 28 | static func launchCommand(panel: Panel? = nil) -> String { 29 | // see: https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751 30 | // two methods to launch System Settings: 31 | // - open -b com.apple.systempreferences /System/Library/PreferencePanes/Security.prefPane 32 | // - open "x-apple.systempreferences:com.apple.preference.security" 33 | 34 | // prefer bundle ID over bundle name where possible, since bundle ID allows sub-panel selection 35 | if let panel { 36 | if let bundleID = panel.bundleID { 37 | "open \"x-apple.systempreferences:\(bundleID)\"" 38 | } else { 39 | "open -b com.apple.systempreferences \(panel.bundleName)" 40 | } 41 | } else { 42 | #"open "x-apple.systempreferences""# 43 | } 44 | } 45 | 46 | #elseif !os(watchOS) 47 | 48 | @MainActor 49 | public static func launch() { 50 | guard let url = URL(string: UIApplication.openSettingsURLString) else { return } 51 | UIApplication.shared.open(url) 52 | } 53 | 54 | #endif 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/ActorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct ActorTests { 13 | // Note: 14 | // 15 | // Can't attach @MainActor to the class because there are 16 | // protocol requirements that can't be satisfied in that case 17 | // 18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 19 | @PrefsSchema final class TestSchema: @unchecked Sendable { 20 | @Storage var storage = .dictionary 21 | @StorageMode var storageMode = .cachedReadStorageWrite 22 | 23 | @MainActor @Pref var foo: Int? // <-- can attach to individual properties 24 | @Pref var bar: String? 25 | } 26 | 27 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 28 | @Test 29 | func baseline() { 30 | let prefs = TestSchema() 31 | 32 | Task { @MainActor in prefs.foo = 1 } // <-- needs MainActor context 33 | prefs.bar = "a string" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/ChainingEncodingStrategiesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChainingEncodingStrategiesTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct ChainingEncodingStrategiesTests { 13 | enum MyType: Int { 14 | case foo = 0x12345678 15 | case bar = 0x98765432 16 | } 17 | 18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 19 | @PrefsSchema final class TestSchema: @unchecked Sendable { 20 | @Storage var storage = .dictionary 21 | @StorageMode var storageMode = .cachedReadStorageWrite 22 | 23 | @Pref(coding: .compressedData(algorithm: .lzfse).base64DataString()) public var foo: Data? 24 | 25 | // not logic-tested, just to check if the compiler can infer the protocol extensions 26 | @Pref( 27 | coding: URL.jsonDataPrefsCoding 28 | .compressedData(algorithm: .lzfse) 29 | .compressedData(algorithm: .zlib) 30 | .base64DataString() 31 | ) public var bar: URL? 32 | 33 | @Pref(coding: FloatingPointSign.rawRepresentablePrefsCoding) 34 | public var fpsA: FloatingPointSign? 35 | 36 | @Pref(coding: FloatingPointSign.rawRepresentablePrefsCoding.intAsString) 37 | public var fpsB: FloatingPointSign? 38 | 39 | @Pref(coding: .uInt32AsInt) var binaryInt: UInt32? 40 | } 41 | 42 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 43 | @Test 44 | func chainedEncodingA() async throws { 45 | let schema = TestSchema() 46 | 47 | let testData = Data((1 ... 100).map(\.self)) 48 | 49 | schema.foo = testData 50 | 51 | let encoded: String = try #require(schema.storage.storageValue(forKey: "foo")) 52 | 53 | // check that the storage value is the base-64 string of the compressed data 54 | #expect( 55 | encoded == 56 | "YnZ4LWQAAAABAgMEBQYHCAkKCwwNDg8QERITFBUW" 57 | + "FxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0" 58 | + "NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFS" 59 | + "U1RVVldYWVpbXF1eX2BhYmNkYnZ4JA==" 60 | ) 61 | 62 | let decoded = try #require(schema.foo) 63 | 64 | #expect(decoded == testData) 65 | } 66 | 67 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 68 | @Test 69 | func chainedEncoding_fpsA() async throws { 70 | let schema = TestSchema() 71 | 72 | schema.fpsA = .plus 73 | 74 | #expect(schema.storage.storageValue(forKey: "fpsA") as Int? == FloatingPointSign.plus.rawValue) 75 | #expect(schema.fpsA == .plus) 76 | } 77 | 78 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 79 | @Test 80 | func chainedEncoding_fpsB() async throws { 81 | let schema = TestSchema() 82 | 83 | schema.fpsB = .plus 84 | 85 | #expect(schema.storage.storageValue(forKey: "fpsB") as String? == String(FloatingPointSign.plus.rawValue)) 86 | #expect(schema.fpsB == .plus) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/CustomEncodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomEncodingTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitCore 9 | import Testing 10 | 11 | /// Test `PrefsCodable` implementation that uses custom `getValue(in:)` and `setValue(to:in:)` method overrides to convert 12 | /// to/from raw prefs storage data without using any of the included abstraction protocols such as 13 | /// `RawRepresentablePrefsCodable` or `CodablePrefsCodable`. 14 | @Suite 15 | struct CustomEncodingTests { 16 | struct NonCodableNonRawRepresentable: Equatable { 17 | var value: Int 18 | 19 | init(value: Int) { 20 | self.value = value 21 | } 22 | 23 | init?(encoded: String) { 24 | guard encoded.hasPrefix("VALUE:"), 25 | let val = Int(encoded.dropFirst(6)) 26 | else { return nil } 27 | value = val 28 | } 29 | 30 | func encoded() -> String { 31 | "VALUE:(\(value))" 32 | } 33 | } 34 | 35 | struct CustomPrefCoding: PrefsCodable { 36 | typealias Value = NonCodableNonRawRepresentable 37 | typealias StorageValue = String 38 | 39 | func decode(prefsValue: StorageValue) -> Value? { 40 | NonCodableNonRawRepresentable(encoded: prefsValue) 41 | } 42 | 43 | func encode(prefsValue: Value) -> StorageValue? { 44 | prefsValue.encoded() 45 | } 46 | } 47 | 48 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 49 | @PrefsSchema final class TestSchema: @unchecked Sendable { 50 | @Storage var storage = .dictionary 51 | @StorageMode var storageMode = .cachedReadStorageWrite 52 | 53 | enum Key { 54 | static let custom = "custom" 55 | static let customDefaulted = "customDefaulted" 56 | } 57 | 58 | @Pref(key: Key.custom, coding: CustomPrefCoding()) var custom: NonCodableNonRawRepresentable? 59 | @Pref(key: Key.customDefaulted, coding: CustomPrefCoding()) var customDefaulted: NonCodableNonRawRepresentable = .init(value: 1) 60 | } 61 | 62 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 63 | @Test 64 | func customValueEncoding() { 65 | let schema = TestSchema() 66 | 67 | #expect(schema.custom?.value == nil) 68 | 69 | schema.custom = NonCodableNonRawRepresentable(value: 42) 70 | #expect(schema.custom?.value == 42) 71 | 72 | schema.custom?.value = 5 73 | #expect(schema.custom?.value == 5) 74 | 75 | schema.custom = nil 76 | #expect(schema.custom == nil) 77 | } 78 | 79 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 80 | @Test 81 | func customDefaultedValueEncoding() { 82 | let schema = TestSchema() 83 | 84 | #expect(schema.customDefaulted == NonCodableNonRawRepresentable(value: 1)) 85 | 86 | schema.customDefaulted = NonCodableNonRawRepresentable(value: 42) 87 | #expect(schema.customDefaulted.value == 42) 88 | 89 | schema.customDefaulted = NonCodableNonRawRepresentable(value: 5) 90 | #expect(schema.customDefaulted.value == 5) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/PrefsCodable Strategies/Base64StringDataPrefsCodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64StringDataPrefsCodingTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct Base64StringDataPrefsCodingTests { 13 | struct MyType: Equatable, Codable { 14 | var id: Int 15 | var name: String 16 | } 17 | 18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 19 | @PrefsSchema final class TestSchema: @unchecked Sendable { 20 | @Storage var storage = .dictionary 21 | @StorageMode var storageMode = .storageOnly // important for unit tests in this file! 22 | 23 | // MARK: - Static Constructors 24 | 25 | @Pref(coding: .base64DataString()) var data: Data? 26 | 27 | // MARK: - Chaining Constructor 28 | 29 | @Pref(coding: MyType.jsonDataPrefsCoding.base64DataString()) var myTypeChained: MyType? 30 | } 31 | 32 | // MARK: - Static Constructors 33 | 34 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 35 | @Test 36 | func data() async throws { 37 | let schema = TestSchema() 38 | 39 | let testData = Data([0x01, 0x02, 0x03]) 40 | 41 | schema.data = testData 42 | let getString: String = try #require(schema.storage.storageValue(forKey: "data")) 43 | #expect(getString == "AQID") 44 | #expect(schema.data == testData) 45 | } 46 | 47 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 48 | @Test 49 | func myTypeChained() async throws { 50 | let schema = TestSchema() 51 | 52 | let testType = MyType(id: 123, name: "foo") 53 | 54 | schema.myTypeChained = testType 55 | let getString: String = try #require(schema.storage.storageValue(forKey: "myTypeChained")) 56 | // just check for non-empty content, we won't check actual content since it's not fully deterministic 57 | #expect(getString.count > 10) 58 | #expect(schema.myTypeChained == testType) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/PrefsCodable Strategies/CompressedDataPrefsCodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressedDataPrefsCodingTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct CompressedDataPrefsCodingTests { 13 | struct MyType: Equatable, Codable { 14 | var id: Int 15 | var name: String 16 | } 17 | 18 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 19 | @PrefsSchema final class TestSchema: @unchecked Sendable { 20 | @Storage var storage = .dictionary 21 | @StorageMode var storageMode = .storageOnly // important for unit tests in this file! 22 | 23 | // MARK: - Static Constructors 24 | 25 | @Pref(coding: .compressedData(algorithm: .zlib)) var data: Data? 26 | 27 | // MARK: - Chaining Constructor 28 | 29 | @Pref( 30 | coding: MyType 31 | .jsonDataPrefsCoding 32 | .compressedData(algorithm: .zlib) 33 | ) var myTypeChained: MyType? 34 | } 35 | 36 | // MARK: - Static Constructors 37 | 38 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 39 | @Test 40 | func data() throws { 41 | let schema = TestSchema() 42 | 43 | let testData = Data([0x01, 0x02, 0x03]) 44 | 45 | schema.data = testData 46 | #expect(schema.storage.storageValue(forKey: "data") as Data? == Data([0x63, 0x64, 0x62, 0x06, 0x00])) 47 | #expect(schema.data == testData) 48 | } 49 | 50 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 51 | @Test 52 | func myTypeChained() throws { 53 | let schema = TestSchema() 54 | 55 | let testType = MyType(id: 123, name: "foo") 56 | 57 | schema.myTypeChained = testType 58 | let getData: Data = try #require(schema.storage.storageValue(forKey: "myTypeChained")) 59 | // just check for non-empty content, we won't check actual content since it's not fully deterministic 60 | #expect(getData.count > 10) 61 | #expect(schema.myTypeChained == testType) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/PrefsCodable Strategies/ISO8601StringDatePrefsCodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ISO8601StringDatePrefsCodingTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct ISO8601StringDatePrefsCodingTests { 13 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 14 | @PrefsSchema final class TestSchema: @unchecked Sendable { 15 | @Storage var storage = .dictionary 16 | @StorageMode var storageMode = .storageOnly // important for unit tests in this file! 17 | 18 | // MARK: - Static Constructors 19 | 20 | @Pref(coding: .iso8601DateString) var date: Date? 21 | } 22 | 23 | // MARK: - Static Constructors 24 | 25 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 26 | @Test 27 | func dateISO8601String_SetValue() async throws { 28 | let schema = TestSchema() 29 | 30 | let testDate = Date(timeIntervalSinceReferenceDate: 757_493_476) 31 | 32 | schema.date = testDate 33 | #expect(schema.storage.storageValue(forKey: "date") as String? == "2025-01-02T06:51:16Z") 34 | #expect(schema.date == testDate) 35 | } 36 | 37 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 38 | @Test 39 | func dateISO8601String_SetStorageValue() async throws { 40 | let schema = TestSchema() 41 | 42 | let testDate = Date(timeIntervalSinceReferenceDate: 757_493_476) 43 | 44 | #expect(schema.date == nil) 45 | 46 | schema.storage.setStorageValue(forKey: "date", to: "2025-01-02T06:51:16Z") 47 | #expect(schema.date == testDate) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/PrefsCodable Strategies/URLStringPrefsCodingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLStringPrefsCodingTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct URLStringPrefsCodingTests { 13 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 14 | @PrefsSchema final class TestSchema: @unchecked Sendable { 15 | @Storage var storage = .dictionary 16 | @StorageMode var storageMode = .cachedReadStorageWrite 17 | 18 | // MARK: - Static Constructors 19 | 20 | @Pref(coding: .urlString) var url: URL? 21 | } 22 | 23 | // MARK: - Static Constructors 24 | 25 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 26 | @Test 27 | func urlString() async throws { 28 | let schema = TestSchema() 29 | 30 | let testURL = try #require(URL(string: "https://www.example.com")) 31 | 32 | schema.url = testURL 33 | #expect(schema.storage.storageValue(forKey: "url") as String? == "https://www.example.com") 34 | #expect(schema.url == testURL) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/PrefsSchemaPrefsStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsSchemaPrefsStorageTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitCore 9 | import Testing 10 | 11 | @Suite 12 | struct PrefsSchemaStorageMacroTests { 13 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 14 | @PrefsSchema final class TestSchemaA: @unchecked Sendable { 15 | @Storage var storage = DictionaryPrefsStorage() 16 | @StorageMode var storageMode = .cachedReadStorageWrite 17 | 18 | @Pref var foo: Int? 19 | } 20 | 21 | /// Note: public access level 22 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 23 | @PrefsSchema public final class TestSchemaB: @unchecked Sendable { 24 | @Storage public var storage = .dictionary 25 | @StorageMode public var storageMode = .cachedReadStorageWrite 26 | 27 | @Pref public var foo: Int? 28 | } 29 | 30 | /// No logic testing. Just ensure compiler is happy. 31 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 32 | @Test 33 | func instantiate() { 34 | _ = TestSchemaA() 35 | _ = TestSchemaB() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/RawRepresentableArrayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentableArrayTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitCore 9 | import Testing 10 | 11 | @Suite(.serialized) 12 | struct RawRepresentableArrayTests { 13 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))" 14 | 15 | static var suite: UserDefaults { 16 | UserDefaults(suiteName: domain)! 17 | } 18 | 19 | enum StringEnum: String, RawRepresentable { 20 | case one 21 | case two 22 | } 23 | 24 | enum IntEnum: Int, RawRepresentable { 25 | case one = 1 26 | case two = 2 27 | } 28 | 29 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 30 | @PrefsSchema final class TestSchema: @unchecked Sendable { 31 | @Storage var storage: AnyPrefsStorage 32 | @StorageMode var storageMode: PrefsStorageMode 33 | 34 | init(storage: PrefsStorage, storageMode: PrefsStorageMode) { 35 | self.storage = AnyPrefsStorage(storage) 36 | self.storageMode = storageMode 37 | } 38 | 39 | enum Key: String { 40 | case arrayA 41 | case arrayADefaulted 42 | case arrayB 43 | case arrayBDefaulted 44 | } 45 | 46 | @Pref( 47 | key: Key.arrayA.rawValue, 48 | coding: [StringEnum].rawRepresentableArrayPrefsCoding 49 | ) var arrayA: [StringEnum]? 50 | 51 | @Pref( 52 | key: Key.arrayADefaulted.rawValue, 53 | coding: [StringEnum].rawRepresentableArrayPrefsCoding 54 | ) var arrayADefaulted: [StringEnum] = [.one, .two] 55 | 56 | @Pref( 57 | key: Key.arrayB.rawValue, 58 | coding: [IntEnum].rawRepresentableArrayPrefsCoding 59 | ) var arrayB: [IntEnum]? 60 | 61 | @Pref( 62 | key: Key.arrayBDefaulted.rawValue, 63 | coding: [IntEnum].rawRepresentableArrayPrefsCoding 64 | ) var arrayBDefaulted: [IntEnum] = [.one, .two] 65 | } 66 | 67 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 68 | static var schemas: [TestSchema] { 69 | [ 70 | TestSchema(storage: .dictionary, storageMode: .cachedReadStorageWrite), 71 | TestSchema(storage: .dictionary, storageMode: .storageOnly), 72 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .cachedReadStorageWrite), 73 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .storageOnly) 74 | ] 75 | } 76 | 77 | // MARK: - Init 78 | 79 | init() async throws { 80 | UserDefaults.standard.removePersistentDomain(forName: Self.domain) 81 | } 82 | 83 | // MARK: - Tests 84 | 85 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 86 | @Test(arguments: schemas) 87 | func stringRepresentable(schema: TestSchema) throws { 88 | schema.arrayA = [.two, .two, .one] 89 | #expect(schema.arrayA == [.two, .two, .one]) 90 | 91 | #expect(schema.arrayADefaulted == [.one, .two]) 92 | 93 | schema.arrayADefaulted = [.two, .two, .one] 94 | #expect(schema.arrayADefaulted == [.two, .two, .one]) 95 | 96 | // check underlying storage is as expected 97 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayA.rawValue) as? [String]) 98 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayADefaulted.rawValue) as? [String]) 99 | } 100 | 101 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 102 | @Test(arguments: schemas) 103 | func intRepresentable(schema: TestSchema) throws { 104 | schema.arrayB = [.two, .two, .one] 105 | #expect(schema.arrayB == [.two, .two, .one]) 106 | 107 | #expect(schema.arrayBDefaulted == [.one, .two]) 108 | 109 | schema.arrayBDefaulted = [.two, .two, .one] 110 | #expect(schema.arrayBDefaulted == [.two, .two, .one]) 111 | 112 | // check underlying storage is as expected 113 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayB.rawValue) as? [Int]) 114 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.arrayBDefaulted.rawValue) as? [Int]) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/PrefsKitCoreTests/RawRepresentableDictionaryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentableDictionaryTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitCore 9 | import Testing 10 | 11 | @Suite(.serialized) 12 | struct RawRepresentableDictionaryTests { 13 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))" 14 | 15 | static var suite: UserDefaults { 16 | UserDefaults(suiteName: domain)! 17 | } 18 | 19 | enum StringEnum: String, RawRepresentable { 20 | case one 21 | case two 22 | } 23 | 24 | enum IntEnum: Int, RawRepresentable { 25 | case one = 1 26 | case two = 2 27 | } 28 | 29 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 30 | @PrefsSchema final class TestSchema: @unchecked Sendable { 31 | @Storage var storage: AnyPrefsStorage 32 | @StorageMode var storageMode: PrefsStorageMode 33 | 34 | init(storage: PrefsStorage, storageMode: PrefsStorageMode) { 35 | self.storage = AnyPrefsStorage(storage) 36 | self.storageMode = storageMode 37 | } 38 | 39 | enum Key: String { 40 | case dictA 41 | case dictADefaulted 42 | case dictB 43 | case dictBDefaulted 44 | } 45 | 46 | @Pref( 47 | key: Key.dictA.rawValue, 48 | coding: [String: StringEnum].rawRepresentableDictionaryPrefsCoding 49 | ) var dictA: [String: StringEnum]? 50 | 51 | @Pref( 52 | key: Key.dictADefaulted.rawValue, 53 | coding: [String: StringEnum].rawRepresentableDictionaryPrefsCoding 54 | ) var dictADefaulted: [String: StringEnum] = ["a": .one, "b": .two] 55 | 56 | @Pref( 57 | key: Key.dictB.rawValue, 58 | coding: [String: IntEnum].rawRepresentableDictionaryPrefsCoding 59 | ) var dictB: [String: IntEnum]? 60 | 61 | @Pref( 62 | key: Key.dictBDefaulted.rawValue, 63 | coding: [String: IntEnum].rawRepresentableDictionaryPrefsCoding 64 | ) var dictBDefaulted: [String: IntEnum] = ["a": .one, "b": .two] 65 | } 66 | 67 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 68 | static var schemas: [TestSchema] { 69 | [ 70 | TestSchema(storage: .dictionary, storageMode: .cachedReadStorageWrite), 71 | TestSchema(storage: .dictionary, storageMode: .storageOnly), 72 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .cachedReadStorageWrite), 73 | TestSchema(storage: .userDefaults(suite: suite), storageMode: .storageOnly) 74 | ] 75 | } 76 | 77 | // MARK: - Init 78 | 79 | init() async throws { 80 | UserDefaults.standard.removePersistentDomain(forName: Self.domain) 81 | } 82 | 83 | // MARK: - Tests 84 | 85 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 86 | @Test(arguments: schemas) 87 | func stringRepresentable(schema: TestSchema) throws { 88 | schema.dictA = ["a": .two, "b": .two, "c": .one] 89 | #expect(schema.dictA == ["a": .two, "b": .two, "c": .one]) 90 | 91 | #expect(schema.dictADefaulted == ["a": .one, "b": .two]) 92 | 93 | schema.dictADefaulted = ["a": .two, "b": .two, "c": .one] 94 | #expect(schema.dictADefaulted == ["a": .two, "b": .two, "c": .one]) 95 | 96 | // check underlying storage is as expected 97 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictA.rawValue) as? [String: String]) 98 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictADefaulted.rawValue) as? [String: String]) 99 | } 100 | 101 | @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) 102 | @Test(arguments: schemas) 103 | func intRepresentable(schema: TestSchema) throws { 104 | schema.dictB = ["a": .two, "b": .two, "c": .one] 105 | #expect(schema.dictB == ["a": .two, "b": .two, "c": .one]) 106 | 107 | #expect(schema.dictBDefaulted == ["a": .one, "b": .two]) 108 | 109 | schema.dictBDefaulted = ["a": .two, "b": .two, "c": .one] 110 | #expect(schema.dictBDefaulted == ["a": .two, "b": .two, "c": .one]) 111 | 112 | // check underlying storage is as expected 113 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictB.rawValue) as? [String: Int]) 114 | let _ = try #require(schema.storage.unsafeStorageValue(forKey: TestSchema.Key.dictBDefaulted.rawValue) as? [String: Int]) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/PrefsKitTypesTests/PrefsStorage Traits/PrefsStorageInitializableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageInitializableTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitTypes 9 | import Testing 10 | 11 | /// Test prefs storage inits for PList and JSON. 12 | @Suite(.serialized) 13 | struct PrefsStorageInitializableTests { 14 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))" 15 | 16 | typealias StorageBackend = PrefsStorage & PrefsStorageInitializable 17 | static var storageBackends: [any StorageBackend.Type] { 18 | [ 19 | DictionaryPrefsStorage.self 20 | // (`UserDefaultsPrefsStorage` does not conform to the protocol) 21 | ] 22 | } 23 | 24 | typealias Key1 = TestContent.Basic.Root.Key1 25 | typealias Key2 = TestContent.Basic.Root.Key2 26 | typealias Key3 = TestContent.Basic.Root.Key3 27 | typealias Key4 = TestContent.Basic.Root.Key4 28 | typealias Key5 = TestContent.Basic.Root.Key5 29 | typealias Key6 = TestContent.Basic.Root.Key6 30 | typealias Key7 = TestContent.Basic.Root.Key7 31 | typealias Key8 = TestContent.Basic.Root.Key8 32 | typealias Key9 = TestContent.Basic.Root.Key9 33 | typealias Key10 = TestContent.Basic.Root.Key10 34 | typealias Key11 = TestContent.Basic.Root.Key11 35 | 36 | // MARK: - JSON Tests 37 | 38 | @Test(arguments: Self.storageBackends) 39 | func initJSONData(storageType: any PrefsStorageInitializable.Type) async throws { 40 | let data = try #require(TestContent.Basic.jsonString.data(using: .utf8)) 41 | let storage = try storageType.init( 42 | from: data, 43 | format: .json(strategy: TestContent.Basic.JSONPrefsStorageImportStrategy()) 44 | ) 45 | 46 | try await TestContent.Basic.checkContent(in: storage) 47 | } 48 | 49 | @Test(arguments: Self.storageBackends) 50 | func initJSONString(storageType: any PrefsStorageInitializable.Type) async throws { 51 | let storage = try storageType.init( 52 | from: TestContent.Basic.jsonString, 53 | format: .json(strategy: TestContent.Basic.JSONPrefsStorageImportStrategy()) 54 | ) 55 | 56 | try await TestContent.Basic.checkContent(in: storage) 57 | } 58 | 59 | // MARK: - PList Tests 60 | 61 | @Test(arguments: Self.storageBackends) 62 | func initPListData(storageType: any PrefsStorageInitializable.Type) async throws { 63 | let data = try #require(TestContent.Basic.plistString.data(using: .utf8)) 64 | let storage = try storageType.init(from: data, format: .plist()) 65 | 66 | try await TestContent.Basic.checkContent(in: storage) 67 | } 68 | 69 | @Test(arguments: Self.storageBackends) 70 | func initPListString(storageType: any PrefsStorageInitializable.Type) async throws { 71 | let storage = try storageType.init(from: TestContent.Basic.plistString, format: .plist()) 72 | 73 | try await TestContent.Basic.checkContent(in: storage) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/PrefsKitTypesTests/PrefsStorage/PrefsStorage+Static Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorage+Static Tests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import PrefsKitTypes 9 | import Testing 10 | 11 | @Suite struct PrefsStorageStaticTests { 12 | /// No logic testing, just ensure compiler is happy with our syntax sugar. 13 | @Test 14 | func varSyntax() { 15 | let _: PrefsStorage = AnyPrefsStorage(.dictionary) 16 | let _: PrefsStorage = AnyPrefsStorage(.dictionary(root: [:])) 17 | let _: PrefsStorage = AnyPrefsStorage(.userDefaults) 18 | let _: PrefsStorage = AnyPrefsStorage(.userDefaults(suite: .standard)) 19 | 20 | let _: PrefsStorage = DictionaryPrefsStorage() 21 | let _: PrefsStorage = UserDefaultsPrefsStorage() 22 | 23 | let _: PrefsStorage = .dictionary 24 | let _: PrefsStorage = .dictionary(root: [:]) 25 | let _: PrefsStorage = .userDefaults 26 | let _: PrefsStorage = .userDefaults(suite: .standard) 27 | } 28 | 29 | /// No logic testing, just ensure compiler is happy with our syntax sugar. 30 | @Test 31 | func anySyntax() { 32 | func foo(_ storage: any PrefsStorage) { } 33 | 34 | foo(AnyPrefsStorage(.dictionary)) 35 | foo(AnyPrefsStorage(.dictionary(root: [:]))) 36 | foo(AnyPrefsStorage(.userDefaults)) 37 | foo(AnyPrefsStorage(.userDefaults(suite: .standard))) 38 | 39 | foo(DictionaryPrefsStorage()) 40 | foo(UserDefaultsPrefsStorage()) 41 | 42 | foo(.dictionary) 43 | foo(.dictionary(root: [:])) 44 | foo(.userDefaults) 45 | foo(.userDefaults(suite: .standard)) 46 | } 47 | 48 | /// No logic testing, just ensure compiler is happy with our syntax sugar. 49 | @Test 50 | func someSyntax() { 51 | func foo(_ storage: some PrefsStorage) { } 52 | 53 | foo(AnyPrefsStorage(.dictionary)) 54 | foo(AnyPrefsStorage(.dictionary(root: [:]))) 55 | foo(AnyPrefsStorage(.userDefaults)) 56 | foo(AnyPrefsStorage(.userDefaults(suite: .standard))) 57 | 58 | foo(DictionaryPrefsStorage()) 59 | foo(UserDefaultsPrefsStorage()) 60 | 61 | // doesn't work. huh? 62 | foo(.dictionary) 63 | foo(.dictionary(root: [:])) 64 | foo(.userDefaults) 65 | foo(.userDefaults(suite: .standard)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/PrefsKitTypesTests/PrefsStorage/PrefsStorageArrayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefsStorageArrayTests.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | @testable import PrefsKitTypes 9 | import Testing 10 | 11 | @Suite(.serialized) 12 | struct PrefsStorageArrayTests { 13 | static let domain = "com.orchetect.PrefsKit.\(type(of: Self.self))" 14 | 15 | static var storageBackends: [AnyPrefsStorage] { 16 | [ 17 | AnyPrefsStorage(.dictionary), 18 | AnyPrefsStorage(.userDefaults(suite: UserDefaults(suiteName: domain)!)) 19 | ] 20 | } 21 | 22 | // MARK: - Init 23 | 24 | init() async throws { 25 | UserDefaults.standard.removePersistentDomain(forName: Self.domain) 26 | } 27 | 28 | // MARK: - Atomic 29 | 30 | @Test(arguments: Self.storageBackends) 31 | func intArray(storage: AnyPrefsStorage) async throws { 32 | storage.setStorageValue(forKey: "foo", to: [1, 2, 3]) 33 | 34 | let key = AnyAtomicPrefsKey<[Int]>(key: "foo") 35 | 36 | let value: [Int] = try #require(storage.value(forKey: key)) 37 | 38 | #expect(value == [1, 2, 3]) 39 | } 40 | 41 | @Test(arguments: Self.storageBackends) 42 | func stringArray(storage: AnyPrefsStorage) async throws { 43 | storage.setStorageValue(forKey: "foo", to: ["a", "b", "c"]) 44 | 45 | let key = AnyAtomicPrefsKey<[String]>(key: "foo") 46 | 47 | let value = try #require(storage.value(forKey: key)) 48 | 49 | #expect(value == ["a", "b", "c"]) 50 | } 51 | 52 | @Test(arguments: Self.storageBackends) 53 | func boolArray(storage: AnyPrefsStorage) async throws { 54 | storage.setStorageValue(forKey: "foo", to: [true, false, true]) 55 | 56 | let key = AnyAtomicPrefsKey<[Bool]>(key: "foo") 57 | 58 | let value = try #require(storage.value(forKey: key)) 59 | 60 | #expect(value == [true, false, true]) 61 | } 62 | 63 | @Test(arguments: Self.storageBackends) 64 | func doubleArray(storage: AnyPrefsStorage) async throws { 65 | storage.setStorageValue(forKey: "foo", to: [1.5, 2.5, 3.5] as [Double]) 66 | 67 | let key = AnyAtomicPrefsKey<[Double]>(key: "foo") 68 | 69 | let value = try #require(storage.value(forKey: key)) 70 | 71 | #expect(value == [1.5, 2.5, 3.5]) 72 | } 73 | 74 | @Test(arguments: Self.storageBackends) 75 | func floatArray(storage: AnyPrefsStorage) async throws { 76 | storage.setStorageValue(forKey: "foo", to: [1.5, 2.5, 3.5] as [Float]) 77 | 78 | let key = AnyAtomicPrefsKey<[Float]>(key: "foo") 79 | 80 | let value = try #require(storage.value(forKey: key)) 81 | 82 | #expect(value == [1.5, 2.5, 3.5]) 83 | } 84 | 85 | @Test(arguments: Self.storageBackends) 86 | func dataArray(storage: AnyPrefsStorage) async throws { 87 | storage.setStorageValue(forKey: "foo", to: [Data([0x01]), Data([0x02])]) 88 | 89 | let key = AnyAtomicPrefsKey<[Data]>(key: "foo") 90 | 91 | let value = try #require(storage.value(forKey: key)) 92 | 93 | #expect(value == [Data([0x01]), Data([0x02])]) 94 | } 95 | 96 | @Test(arguments: Self.storageBackends) 97 | func anyPrefsArrayArray(storage: AnyPrefsStorage) async throws { 98 | let array: [Any] = [1, 2, "string", true] 99 | storage.setUnsafeStorageValue(forKey: "foo", to: array) 100 | 101 | let value: [Any] = try #require(storage.unsafeStorageValue(forKey: "foo") as? [Any]) 102 | try #require(value.count == 4) 103 | 104 | #expect(value[0] as? Int == 1) 105 | #expect(value[1] as? Int == 2) 106 | #expect(value[2] as? String == "string") 107 | #expect(value[3] as? Bool == true) 108 | } 109 | 110 | // MARK: - Nested 111 | 112 | @Test(arguments: Self.storageBackends) 113 | func nestedStringArray(storage: AnyPrefsStorage) async throws { 114 | storage.setStorageValue(forKey: "foo", to: [["a", "b"], ["c"]]) 115 | 116 | let key = AnyAtomicPrefsKey<[[String]]>(key: "foo") 117 | 118 | let value = try #require(storage.value(forKey: key)) 119 | 120 | #expect(value == [["a", "b"], ["c"]]) 121 | } 122 | 123 | @Test(arguments: Self.storageBackends) 124 | func nestedAnyArray(storage: AnyPrefsStorage) async throws { 125 | let array: [Any] = [["a", 2], [true]] 126 | storage.setUnsafeStorageValue(forKey: "foo", to: array) 127 | 128 | let value: [Any] = try #require(storage.unsafeStorageValue(forKey: "foo") as? [Any]) 129 | try #require(value.count == 2) 130 | 131 | let subArray1 = try #require(value[0] as? [Any]) 132 | try #require(subArray1.count == 2) 133 | #expect(subArray1[0] as? String == "a") 134 | #expect(subArray1[1] as? Int == 2) 135 | 136 | let subArray2 = try #require(value[1] as? [Any]) 137 | try #require(subArray2.count == 1) 138 | #expect(subArray2[0] as? Bool == true) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/PrefsKitTypesTests/TestContent/TestContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestContent.swift 3 | // PrefsKit • https://github.com/orchetect/PrefsKit 4 | // © 2025 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | enum TestContent { } 8 | --------------------------------------------------------------------------------