├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── DataKit │ ├── Builder │ ├── DataBuilder.swift │ ├── FormatBuilder+Read.swift │ ├── FormatBuilder+ReadWrite.swift │ ├── FormatBuilder+Write.swift │ └── FormatBuilder.swift │ ├── Conversions │ ├── Conversion+Cast.swift │ ├── Conversion+Clamped.swift │ ├── Conversion+Encoding.swift │ ├── Conversion+Exactly.swift │ ├── Conversion+KeyPath.swift │ ├── Conversion+Map.swift │ ├── Conversion+Measurement.swift │ ├── Conversion+PrefixCount.swift │ ├── Conversion+Reversible.swift │ ├── Conversion+VariableCount.swift │ └── Conversion.swift │ ├── Environment │ ├── Environment+Endianness.swift │ ├── Environment+Suffix.swift │ ├── EnvironmentValues.swift │ └── SkipChecksumVerification.swift │ ├── Error.swift │ ├── Exports.swift │ ├── Property │ ├── Checksum.swift │ ├── Convert.swift │ ├── Custom.swift │ ├── Environment.swift │ ├── EnvironmentProperty.swift │ ├── KeyPath.swift │ ├── OnRead.swift │ ├── OnWrite.swift │ ├── Property+Conversion.swift │ ├── Property+Custom.swift │ ├── Property.swift │ ├── Scope.swift │ └── Using.swift │ ├── ReadWritable │ ├── Format.swift │ ├── ReadWritable.swift │ └── ReadWritableProperty.swift │ ├── Readable │ ├── ReadContainer.swift │ ├── ReadContext.swift │ ├── Readable.swift │ └── ReadableProperty.swift │ ├── Values │ ├── ReadWritable+FloatingPoint.swift │ ├── ReadWritable+Integer.swift │ ├── ReadWritable+Optional.swift │ └── ReadWritable+Raw.swift │ └── Writable │ ├── Writable.swift │ ├── WritableProperty.swift │ └── WriteContainer.swift └── Tests └── DataKitTests ├── Conversion └── StringConversionTests.swift ├── DataBuilderTests.swift ├── DataKitTests.swift ├── Extensions.swift ├── Models.swift └── Readme ├── Readme.swift ├── ReadmeExtensions.swift └── ReadmeTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 QuickBird 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.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CRC", 6 | "repositoryURL": "https://github.com/QuickBirdEng/crc-swift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "0616be13dec7b6dc4b55e266cba39c1772902b08", 10 | "version": "0.1.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DataKit", 7 | products: [ 8 | .library( 9 | name: "DataKit", 10 | targets: ["DataKit"] 11 | ), 12 | ], 13 | dependencies: [ 14 | .package( 15 | url: "https://github.com/QuickBirdEng/crc-swift.git", 16 | from: "0.1.1" 17 | ), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "DataKit", 22 | dependencies: [ 23 | .product(name: "CRC", package: "crc-swift"), 24 | ] 25 | ), 26 | .testTarget( 27 | name: "DataKitTests", 28 | dependencies: ["DataKit"] 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![‎DataKit](https://github.com/QuickBirdEng/DataKit/assets/15239005/2b8fc619-2c29-4900-984b-9187ae7a5b57) 3 | 4 | DataKit offers a modern, intuitive and declarative interface for reading and writing binary formatted data in Swift. 5 | 6 | ## 🏃‍♂️Getting started 7 | 8 | As an introduction into how this library can be used to make working with binary formatted data easier, let me first introduce you to the type, we are going to read/write. Let's assume we are building a weather station and we are using the following type(s) to give updates about the currently measured values: 9 | 10 | ```swift 11 | struct WeatherStationFeatures: OptionSet, ReadWritable { 12 | var rawValue: UInt8 13 | 14 | static var hasTemperature = Self(rawValue: 1 << 0) 15 | static var hasHumidity = Self(rawValue: 1 << 1) 16 | static var usesMetricUnits = Self(rawValue: 1 << 2) 17 | } 18 | 19 | struct WeatherStationUpdate { 20 | 21 | var features: WeatherStationFeatures 22 | var temperature: Measurement 23 | var humidity: Double 24 | 25 | } 26 | ``` 27 | 28 | The encoded format should be: 29 | - Each message starts with a byte with the value 0x02. 30 | - The following byte contains multiple feature flags: 31 | - bit 0 is set: Using °C instead of °F for the temperature 32 | - bit 1 is set: The message contains temperature information 33 | - bit 2 is set: The message contains humidity information 34 | - Temperature as a big-endian 32-bit floating-point number 35 | - Relative Humidity as UInt8 in the range of [0, 100] 36 | - CRC-32 with the default polynomial for the whole message (incl. 0x02 prefix). 37 | 38 | ### Writing data 39 | 40 | You have two options for converting the above type `WeatherStationUpdate` into data: A `DataBuilder` or the `Writable` protocol. If you intend to both read and write the data - make sure to read the `Reading & Writing data` section 41 | 42 | #### DataBuilder 43 | 44 | A `DataBuilder` provides you with a very simple and limited interface. Using the power of result builders, you can simply state the values to be written in a given order and `DataBuilder` will take over all the work to encode the values and append the individual bytes to form a `Data` object. `DataBuilder` is always expected to return a `Data` object without throwing errors, which is why conversion are not supported here - You might want to have a look at the `Writable` protocol then! 45 | 46 | ```swift 47 | extension WeatherStationUpdate { 48 | 49 | @DataBuilder var data: Data { 50 | UInt8(0x02) 51 | features 52 | if features.contains(.hasTemperature) { 53 | Float(temperature.converted(to: features.contains(.usesMetricUnits) ? .celsius : .fahrenheit).value) 54 | } 55 | if features.contains(.hasHumidity) { 56 | UInt8(humidity * 100) 57 | } 58 | CRC32.default 59 | } 60 | 61 | } 62 | ``` 63 | 64 | With this addition, you can easily get the data of this object using its `data` property. 65 | 66 | #### Writable 67 | 68 | With the power of keyPaths and result builders, you can also write your objects into `Data` using the `Writable` protocol and its `writeFormat` property. Simply state out individual fixed values (e.g. byte prefixes), keyPaths with `Writable` values or other constructs that are further explained in the `Extras` section of this document. 69 | 70 | ```swift 71 | extension WeatherStationUpdate: Writable { 72 | 73 | static var writeFormat: WriteFormat { 74 | Scope { 75 | UInt8(0x02) 76 | 77 | \.features 78 | 79 | Using(\.features) { features in 80 | if features.contains(.hasTemperature) { 81 | let unit: UnitTemperature = 82 | features.contains(.usesMetricUnits) ? .celsius : .fahrenheit 83 | Convert(\.temperature) { 84 | $0.converted(to: unit).cast(Float.self) 85 | } 86 | } 87 | if features.contains(.hasHumidity) { 88 | Convert(\.humidity) { 89 | Double($0) / 100 90 | } writing: { 91 | UInt8($0 * 100) 92 | } 93 | } 94 | } 95 | 96 | CRC32.default 97 | } 98 | .endianness(.big) 99 | } 100 | 101 | } 102 | ``` 103 | 104 | By conforming to the `Writable` protocol, you are now able to simply call its `write` function to write its data out: 105 | 106 | ```swift 107 | let message: WeatherStationUpdate = ... 108 | let messageData = try message.write() // You can also inject a custom environment here, if needed 109 | ``` 110 | 111 | ### Reading data 112 | 113 | Supporting reading of data into objects is slightly more complicated. Conforming to the `Readable` protocol will require you to implement both an initializer to create an object from a given `ReadContext` and a static `readFormat` property describing how data is aligned. 114 | 115 | A `ReadContext` provides you with the values that have been read using the `readFormat`. Make sure to use the same keyPaths in the initializer and `readFormat` to ensure smooth reading of values. 116 | 117 | ```swift 118 | extension WeatherStationUpdate: Readable { 119 | 120 | init(from context: ReadContext) throws { 121 | features = try context.read(for: \.features) 122 | temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin) 123 | humidity = try context.readIfPresent(for: \.humidity) ?? .nan 124 | } 125 | 126 | static var readFormat: ReadFormat { 127 | Scope { 128 | UInt8(0x02) 129 | 130 | \.features 131 | 132 | Using(\.features) { features in 133 | if features.contains(.hasTemperature) { 134 | let unit: UnitTemperature = 135 | features.contains(.usesMetricUnits) ? .celsius : .fahrenheit 136 | Convert(\.temperature) { 137 | $0.converted(to: unit).cast(Float.self) 138 | } 139 | } 140 | if features.contains(.hasHumidity) { 141 | Convert(\.humidity) { 142 | Double($0) / 100 143 | } writing: { 144 | UInt8($0 * 100) 145 | } 146 | } 147 | } 148 | 149 | CRC32.default 150 | } 151 | .endianness(.big) 152 | } 153 | 154 | } 155 | ``` 156 | 157 | By implementing all these requirements of the `Readable` protocol, you now gain another initializer `init(_: Data) throws` to read objects from `Data` objects: 158 | 159 | ```swift 160 | let data: Data = ... 161 | let message = try WeatherStationUpdate(data) // You can also inject a custom environment here, if needed 162 | ``` 163 | 164 | ### Reading & Writing data 165 | 166 | To make a type both `Readable` and `Writable`, you can conform your type to the `ReadWritable` protocol. Instead of providing a separate format for reading and writing, you can define a `Format` property that is used for both reading and writing. For our example type, we can simply merge the two formats into one and provide the initializer for creating an object from a given `ReadContext`. 167 | 168 | ```swift 169 | extension WeatherStationUpdate: ReadWritable { 170 | 171 | init(from context: ReadContext) throws { 172 | features = try context.read(for: \.features) 173 | temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin) 174 | humidity = try context.readIfPresent(for: \.humidity) ?? .nan 175 | } 176 | 177 | static var format: Format { 178 | Scope { 179 | UInt8(0x02) 180 | 181 | \.features 182 | 183 | Using(\.features) { features in 184 | if features.contains(.hasTemperature) { 185 | let unit: UnitTemperature = 186 | features.contains(.usesMetricUnits) ? .celsius : .fahrenheit 187 | Convert(\.temperature) { 188 | $0.converted(to: unit).cast(Float.self) 189 | } 190 | } 191 | if features.contains(.hasHumidity) { 192 | Convert(\.humidity) { 193 | Double($0) / 100 194 | } writing: { 195 | UInt8($0 * 100) 196 | } 197 | } 198 | } 199 | 200 | CRC32.default 201 | } 202 | .endianness(.big) 203 | } 204 | 205 | } 206 | ``` 207 | 208 | Hooray, you can now read and write your objects! 🎉 209 | 210 | ## 🤸‍♂️ Extras 211 | 212 | Reading/Writing data is often quite complicated and different format pose different challenges to minimize payloads, reduce bandwidth, improve performance, etc. To make it easy to handle different common scenarios, `DataKit` provides a couple of extra features to handle the most common challenges. 213 | 214 | ### ✏️ Convert / Custom / Property 215 | 216 | In some special cases, you might need more control over how data is read/written. For these cases, the wrappers `Custom`, `Convert` and `Property` might be of interest. 217 | 218 | - `Property` makes it easy to wrap a keyPath, if the Root type may not be recognized by the compiler. You can further use functions on it to map a `Property` to either a `Custom` or `Convert` wrapper. 219 | - `Convert` allows you to convert a keyPath's value before reading/writing it. Oftentimes, this is very usefuly for sequence values with variable size. You can either provide custom conversion methods directly or use a pre-existing `Conversion`/`ReversibleConversion` value. 220 | - `Custom` allows you to access the raw reading/writing functionality with direct access to the `ReadContainer`/`WriteContainer` and respective context values. If you need the read/write behavior more than once in your codebase, you might want to have a look at conversions though. 221 | 222 | ### 💱 Conversion / ReversibleConversion 223 | 224 | For some types, there is not a single "correct" format (e.g. thinking about Pascal vs C strings), which is why `DataKit` uses so called `Conversion` values to allow for conversion to be defined once and then used multiple times. Especially helpful is the `ReversibleConversion` type that allows for conversion to be provided in both directions at the same time. 225 | 226 | Assuming our type has a `\.string` keyPath with a `String` value, you could either use a suffix 0-byte to encode the string using UTF8 (similar to C strings): 227 | ```swift 228 | Convert(\.string) { // UTF8 string with a suffix 0-byte 229 | $0.encoded(.utf8).dynamicCount 230 | } 231 | .suffix(0 as UInt8) 232 | ``` 233 | 234 | Or you encode the string with a prefix byte containing the byte count (similar to Pascal ShortString): 235 | ```swift 236 | Convert(\.string) { // Ascii string with a prefix count byte 237 | $0.encoded(.ascii).prefixCount(UInt8.self) 238 | } 239 | ``` 240 | 241 | There are many more conversion available, e.g. for converting between integer/floating-point types, making it easy to convert directly to your preferred types without the need of converting yourself. 242 | 243 | ### 🤓 Using 244 | 245 | With a `Using` construct, you can access values from the `ReadContext` or the value to be written. `Using` can be very helpful, if values in the data itself depend on each other or how individual values need to be read or written. 246 | 247 | ### Environment 248 | 249 | Similar to SwiftUI's environment, you can also modify the behavior of individual components in `DataKit` using the `Environment`. 250 | You can modify the environment when starting the reading/writing process or using modifiers inside the readFormat/writeFormat/format-properties. 251 | To access environment values, you may want to have a look at the `Environment` type to be used in one of the format builders - or for more direct access both `ReadContainer` and `WriteContainer` have a `environment` property. 252 | 253 | Here are some modifiers, you might want to use: 254 | 255 | - `endianness`: By default, `DataKit` reads & writes values in the endianness of the current machine (except for CRCs, where big-endian is used). If your protocol requires a different endianness, make sure to specify a concrete one. 256 | - `skipChecksumVerification`: If you want to have a CRC to be created when writing out data, but ignore an incorrect checksum value when reading, you can set this property to `true` - by default `false` is used. Read the `Checksums` section for more information. 257 | - `suffix`: For values with dynamic count (e.g. a sequence of values with a 0-suffix-byte), you can specify a `.dynamicCount` conversion on a given property. The specified value will stop the reading process of a given value and the value will be written out after the given sequence's end is encountered. 258 | 259 | Feel free to add your own environment values using the `EnvironnentKey` protocol and an extension to the `EnvironmentValues` struct (very similar to SwiftUI). 260 | 261 | ### 🚧 Scope 262 | 263 | Some constructs (e.g. CRC checksums) make assumptions about the data as a whole and not only the part where the specific value is read/written. By using `Scope`, you can limit that data context to where it is individually placed. 264 | 265 | As an example, let's assume our `WeatherStationUpdate` is supposed to ignore the prefix `0x02` byte for the checksum calculation. We could simply exclude it from the scope: 266 | 267 | ```swift 268 | static var format: Format { 269 | UInt8(0x02) 270 | 271 | Scope { 272 | \.features 273 | 274 | Using(\.features) { features in 275 | if features.contains(.hasTemperature) { 276 | let unit: UnitTemperature = 277 | features.contains(.usesMetricUnits) ? .celsius : .fahrenheit 278 | Convert(\.temperature) { 279 | $0.converted(to: unit).cast(Float.self) 280 | } 281 | } 282 | if features.contains(.hasHumidity) { 283 | Convert(\.humidity) { 284 | Double($0) / 100 285 | } writing: { 286 | UInt8($0 * 100) 287 | } 288 | } 289 | } 290 | 291 | CRC32.default 292 | } 293 | .endianness(.big) 294 | } 295 | ``` 296 | 297 | With this change in place, the CRC will only be verified on the Scope itself and the prefix byte is ignored! 298 | 299 | ### ✅ Checksums 300 | 301 | `DataKit`'s dependency [`crc-swift`](https://github.com/QuickBirdEng/crc-swift.org) provides CRC checksums and and easy-to-conform protocol for your own custom checksum values. 302 | 303 | You may simply specify the checksum value itself inside one of the format builders. Alternatively, use a `ChecksumProperty` with a keyPath, so that you can store checksums in properties and write custom checksums from properties. In combination to the `skipChecksumVerification` environment value, you can also verify the checksum at a later stage for example. 304 | 305 | ## 🛠 Installation 306 | 307 | `DataKit` currently only supports Swift package manager. 308 | 309 | #### Swift Package Manager 310 | 311 | See [this WWDC presentation](https://developer.apple.com/videos/play/wwdc2019/408/) about more information how to adopt Swift packages in your app. 312 | 313 | Specify `https://github.com/QuickBirdEng/DataKit.git` as the package link. 314 | 315 | #### Manually 316 | 317 | If you prefer not to use a dependency manager, you can integrate DataKit into your project manually by downloading the source code and placing the files in your project directory. 318 | 319 | ## 👤 Author 320 | 321 | DataKit is created with ❤️ by [QuickBird](https://quickbirdstudios.com). 322 | 323 | ## ❤️ Contributing 324 | 325 | Feel free to open issues for help, found bugs or to discuss new feature requests. Happy to help! 326 | Open a pull request, if you want to propose changes to DataKit. 327 | 328 | ## 📃 License 329 | 330 | DataKit is released under an MIT license. See [License.md](https://github.com/QuickBirdEng/DataKit/blob/master/LICENSE) for more information. 331 | -------------------------------------------------------------------------------- /Sources/DataKit/Builder/DataBuilder.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | @resultBuilder 5 | public enum DataBuilder { 6 | 7 | public struct Component { 8 | 9 | // MARK: Stored Properties 10 | 11 | public let append: (_ to: inout Data) -> Void 12 | 13 | // MARK: Initialization 14 | 15 | public init(append: @escaping (_ to: inout Data) -> Void) { 16 | self.append = append 17 | } 18 | 19 | } 20 | 21 | public static func buildBlock(_ components: Component...) -> Component { 22 | Component { data in 23 | for component in components { 24 | component.append(&data) 25 | } 26 | } 27 | } 28 | 29 | public static func buildExpression(_ expression: I) -> Component { 30 | Component { data in 31 | withUnsafeBytes(of: expression.bigEndian) { 32 | data.append(contentsOf: $0) 33 | } 34 | } 35 | } 36 | 37 | public static func buildExpression(_ expression: F) -> Component { 38 | buildExpression(expression.bitPattern) 39 | } 40 | 41 | public static func buildExpression( 42 | _ expression: R 43 | ) -> Component where R.RawValue: FixedWidthInteger { 44 | buildExpression(expression.rawValue) 45 | } 46 | 47 | public static func buildOptional(_ component: Component?) -> Component { 48 | Component { 49 | component?.append(&$0) 50 | } 51 | } 52 | 53 | public static func buildEither(first component: Component) -> Component { 54 | component 55 | } 56 | 57 | public static func buildEither(second component: Component) -> Component { 58 | component 59 | } 60 | 61 | public static func buildLimitedAvailability(_ component: Component) -> Component { 62 | component 63 | } 64 | 65 | public static func buildFinalResult(_ component: Component) -> Data { 66 | var data = Data() 67 | component.append(&data) 68 | return data 69 | } 70 | 71 | public static func buildExpression(_ expression: C) -> Component { 72 | Component { data in 73 | let value = expression.calculate(for: data) 74 | buildExpression(value).append(&data) 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/DataKit/Builder/FormatBuilder+Read.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias ReadFormatBuilder = FormatBuilder> 11 | 12 | extension FormatBuilder where Root: Readable, Format == ReadFormat { 13 | 14 | public static func buildExpression(_ expression: V) -> Format { 15 | .init { container, _ in 16 | let value = try V(from: &container) 17 | if value != expression { 18 | throw UnexpectedValueError(expectedValue: expression, actualValue: value) 19 | } 20 | } 21 | } 22 | 23 | public static func buildExpression(_ expression: C) -> Format where C.Value: Readable { 24 | buildExpression( 25 | ReadFormat { container, _ in 26 | let verificationData = container.consumedData 27 | try expression.verify(C.Value(from: &container), for: verificationData) 28 | } 29 | .endianness(.big) 30 | ) 31 | } 32 | 33 | public static func buildExpression(_ expression: V) -> Format where V.Root == Root { 34 | .init(read: expression.read) 35 | } 36 | 37 | public static func buildExpression(_ expression: KeyPath) -> Format { 38 | buildExpression(Property(expression)) 39 | } 40 | 41 | public static func buildExpression(_ expression: S) -> Format where S.Element: Readable & Equatable { 42 | .init(expression.map(buildExpression)) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DataKit/Builder/FormatBuilder+ReadWrite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias ReadWriteFormatBuilder = FormatBuilder> 11 | 12 | extension FormatBuilder where Root: ReadWritable, Format == ReadWriteFormat { 13 | 14 | public static func buildExpression(_ expression: V) -> Format where V.Root == Format.Root { 15 | .init( 16 | read: ReadFormatBuilder.buildExpression(expression), 17 | write: WriteFormatBuilder.buildExpression(expression) 18 | ) 19 | } 20 | 21 | public static func buildExpression(_ expression: V) -> Format { 22 | .init( 23 | read: ReadFormatBuilder.buildExpression(expression), 24 | write: WriteFormatBuilder.buildExpression(expression) 25 | ) 26 | } 27 | 28 | public static func buildExpression(_ expression: KeyPath) -> Format { 29 | .init( 30 | read: ReadFormatBuilder.buildExpression(expression), 31 | write: WriteFormatBuilder.buildExpression(expression) 32 | ) 33 | } 34 | 35 | public static func buildExpression(_ expression: C) -> Format where C.Value: ReadWritable { 36 | .init( 37 | read: ReadFormatBuilder.buildExpression(expression), 38 | write: WriteFormatBuilder.buildExpression(expression) 39 | ) 40 | } 41 | 42 | public static func buildExpression(_ expression: S) -> Format where S.Element: ReadWritable & Equatable { 43 | .init(expression.map(buildExpression)) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DataKit/Builder/FormatBuilder+Write.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias WriteFormatBuilder = FormatBuilder> 11 | 12 | extension FormatBuilder where Root: Writable, Format == WriteFormat { 13 | 14 | public static func buildExpression(_ expression: V) -> Format where V.Root == Format.Root { 15 | .init(write: expression.write) 16 | } 17 | 18 | public static func buildExpression(_ expression: V) -> Format { 19 | .init { container, _ in 20 | try expression.write(to: &container) 21 | } 22 | } 23 | 24 | public static func buildExpression(_ expression: KeyPath) -> Format { 25 | buildExpression(Property(expression)) 26 | } 27 | 28 | public static func buildExpression(_ expression: C) -> Format where C.Value: Writable { 29 | buildExpression( 30 | WriteFormat { container, _ in 31 | try expression.calculate(for: container.data) 32 | .write(to: &container) 33 | } 34 | .endianness(.big) 35 | ) 36 | } 37 | 38 | public static func buildExpression(_ expression: S) -> Format where S.Element: Writable { 39 | .init(expression.map(buildExpression)) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/DataKit/Builder/FormatBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public enum FormatBuilder where Format.Root == Root { 12 | 13 | // MARK: Expressions 14 | 15 | public static func buildExpression(_ expression: ()) -> Format { 16 | .init([]) 17 | } 18 | 19 | // MARK: Components 20 | 21 | public static func buildArray(_ components: [Format]) -> Format { 22 | .init(components) 23 | } 24 | 25 | public static func buildBlock(_ component: Format) -> Format { 26 | component 27 | } 28 | 29 | public static func buildBlock(_ components: Format...) -> Format { 30 | .init(components) 31 | } 32 | 33 | public static func buildEither(first component: Format) -> Format { 34 | component 35 | } 36 | 37 | public static func buildEither(second component: Format) -> Format { 38 | component 39 | } 40 | 41 | public static func buildLimitedAvailability(_ component: Format) -> Format { 42 | component 43 | } 44 | 45 | public static func buildOptional(_ component: Format?) -> Format { 46 | .init(component.map { [$0] } ?? []) 47 | } 48 | 49 | public static func buildPartialBlock(first: Format) -> Format { 50 | first 51 | } 52 | 53 | public static func buildPartialBlock(accumulated: Format, next: Format) -> Format { 54 | .init([accumulated, next]) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+Cast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion where Target: BinaryFloatingPoint { 11 | 12 | public func cast( 13 | _ target: NewTarget.Type = NewTarget.self, 14 | from source: Target.Type = Target.self 15 | ) -> Appended { 16 | appending { NewTarget($0) } 17 | } 18 | 19 | public func cast( 20 | _ target: NewTarget.Type = NewTarget.self, 21 | from source: Target.Type = Target.self 22 | ) -> Appended { 23 | appending { NewTarget($0) } 24 | } 25 | 26 | } 27 | 28 | extension Conversion where Target: BinaryInteger { 29 | 30 | public func cast( 31 | _ target: NewTarget.Type = NewTarget.self, 32 | from source: Target.Type = Target.self 33 | ) -> Appended { 34 | appending { NewTarget($0) } 35 | } 36 | 37 | public func cast( 38 | _ target: NewTarget.Type = NewTarget.self, 39 | from source: Target.Type = Target.self 40 | ) -> Appended { 41 | appending { NewTarget($0) } 42 | } 43 | 44 | } 45 | 46 | extension ReversibleConversion where Target: BinaryFloatingPoint { 47 | 48 | public func cast( 49 | _ target: NewTarget.Type = NewTarget.self, 50 | from source: Target.Type = Target.self 51 | ) -> Appended { 52 | appending { $0.cast() } revert: { $0.cast() } 53 | } 54 | 55 | public func cast( 56 | _ target: NewTarget.Type = NewTarget.self, 57 | from source: Target.Type = Target.self 58 | ) -> Appended { 59 | appending { $0.cast() } revert: { $0.cast() } 60 | } 61 | 62 | } 63 | 64 | extension ReversibleConversion where Target: BinaryInteger { 65 | 66 | public func cast( 67 | _ target: NewTarget.Type = NewTarget.self, 68 | from source: Target.Type = Target.self 69 | ) -> Appended { 70 | appending { $0.cast() } revert: { $0.cast() } 71 | } 72 | 73 | public func cast( 74 | _ target: NewTarget.Type = NewTarget.self, 75 | from source: Target.Type = Target.self 76 | ) -> Appended { 77 | appending { $0.cast() } revert: { $0.cast() } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+Clamped.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion where Target: BinaryInteger { 11 | 12 | public func clamped( 13 | to target: NewTarget.Type = NewTarget.self, 14 | from source: Target.Type = Target.self 15 | ) -> Appended { 16 | appending { NewTarget(clamping: $0) } 17 | } 18 | 19 | } 20 | 21 | extension ReversibleConversion where Target: BinaryInteger { 22 | 23 | public func clamped( 24 | to target: NewTarget.Type = NewTarget.self, 25 | from source: Target.Type = Target.self 26 | ) -> Appended { 27 | appending { 28 | $0.clamped() 29 | } revert: { 30 | $0.clamped() 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+Encoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion where Target: StringProtocol { 11 | 12 | public func encoded(_ encoding: String.Encoding, allowLossyConversion: Bool = false) -> Appended { 13 | appending { string in 14 | guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else { 15 | throw ConversionError(source: string, targetType: Data.self) 16 | } 17 | return data 18 | } 19 | } 20 | 21 | } 22 | 23 | extension Conversion where Target: Sequence { 24 | 25 | public func encoded(_ encoding: String.Encoding) -> Appended { 26 | appending { bytes in 27 | guard let string = String(bytes: bytes, encoding: encoding) else { 28 | throw ConversionError(source: bytes, targetType: String.self) 29 | } 30 | return string 31 | } 32 | } 33 | 34 | } 35 | 36 | extension ReversibleConversion where Target == String { 37 | 38 | public func encoded(_ encoding: String.Encoding, allowLossyConversion: Bool = false) -> Appended { 39 | appending { 40 | $0.encoded(encoding, allowLossyConversion: allowLossyConversion) 41 | } revert: { 42 | $0.encoded(encoding) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+Exactly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion where Target: BinaryFloatingPoint { 11 | 12 | public func exactly( 13 | _ target: NewTarget.Type = NewTarget.self, 14 | from source: Target.Type = Target.self 15 | ) -> Appended { 16 | appending { value in 17 | guard let result = NewTarget(exactly: value) else { 18 | throw ConversionError(source: value, targetType: NewTarget.self) 19 | } 20 | return result 21 | 22 | } 23 | } 24 | 25 | public func exactly( 26 | _ target: NewTarget.Type = NewTarget.self, 27 | from source: Target.Type = Target.self 28 | ) -> Appended { 29 | appending { value in 30 | guard let result = NewTarget(exactly: value) else { 31 | throw ConversionError(source: value, targetType: NewTarget.self) 32 | } 33 | return result 34 | 35 | } 36 | } 37 | 38 | } 39 | 40 | extension Conversion where Target: BinaryInteger { 41 | 42 | public func exactly( 43 | _ target: NewTarget.Type = NewTarget.self, 44 | from source: Target.Type = Target.self 45 | ) -> Appended { 46 | appending { value in 47 | guard let result = NewTarget(exactly: value) else { 48 | throw ConversionError(source: value, targetType: NewTarget.self) 49 | } 50 | return result 51 | 52 | } 53 | } 54 | 55 | public func exactly( 56 | _ target: NewTarget.Type = NewTarget.self, 57 | from source: Target.Type = Target.self 58 | ) -> Appended { 59 | appending { value in 60 | guard let result = NewTarget(exactly: value) else { 61 | throw ConversionError(source: value, targetType: NewTarget.self) 62 | } 63 | return result 64 | 65 | } 66 | } 67 | 68 | } 69 | 70 | extension ReversibleConversion where Target: BinaryFloatingPoint { 71 | 72 | public func exactly( 73 | _ target: NewTarget.Type = NewTarget.self, 74 | from source: Target.Type = Target.self 75 | ) -> Appended { 76 | appending { 77 | $0.exactly() 78 | } revert: { 79 | $0.exactly() 80 | } 81 | } 82 | 83 | public func exactly( 84 | _ target: NewTarget.Type = NewTarget.self, 85 | from source: Target.Type = Target.self 86 | ) -> Appended { 87 | appending { 88 | $0.exactly() 89 | } revert: { 90 | $0.exactly() 91 | } 92 | } 93 | 94 | } 95 | 96 | extension ReversibleConversion where Target: BinaryInteger { 97 | 98 | public func exactly( 99 | _ target: NewTarget.Type = NewTarget.self, 100 | from source: Target.Type = Target.self 101 | ) -> Appended { 102 | appending { 103 | $0.exactly() 104 | } revert: { 105 | $0.exactly() 106 | } 107 | } 108 | 109 | public func exactly( 110 | _ target: NewTarget.Type = NewTarget.self, 111 | from source: Target.Type = Target.self 112 | ) -> Appended { 113 | appending { 114 | $0.exactly() 115 | } revert: { 116 | $0.exactly() 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+KeyPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion { 11 | 12 | public func at( 13 | _ keyPath: KeyPath 14 | ) -> Appended { 15 | appending { $0[keyPath: keyPath] } 16 | } 17 | 18 | } 19 | 20 | extension ReversibleConversion { 21 | 22 | public func at( 23 | _ forward: KeyPath, 24 | _ backward: KeyPath 25 | ) -> Appended { 26 | appending { 27 | $0.at(forward) 28 | } revert: { 29 | $0.at(backward) 30 | } 31 | } 32 | 33 | public func at( 34 | symmetric keyPath: KeyPath, 35 | forward: Bool = true, 36 | backward: Bool = true 37 | ) -> Appended { 38 | appending { 39 | forward ? $0.at(keyPath) : $0 40 | } revert: { 41 | backward ? $0.at(keyPath) : $0 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+Map.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | extension Conversion where Target: Sequence { 9 | 10 | public func map( 11 | to target: NewTarget.Type = NewTarget.self, 12 | _ make: Conversion.Make 13 | ) -> Appended { 14 | let conversion = Conversion.make(make) 15 | return appending { value in 16 | try .init( 17 | value.map { 18 | try conversion.convert($0) 19 | } 20 | ) 21 | } 22 | } 23 | 24 | } 25 | 26 | extension ReversibleConversion where Target: RangeReplaceableCollection { 27 | 28 | public func map( 29 | to target: NewTarget.Type = NewTarget.self, 30 | _ make: ReversibleConversion.Make 31 | ) -> Appended { 32 | let conversion = ReversibleConversion.make(make) 33 | return appending { 34 | $0.map { _ in conversion.conversion } 35 | } revert: { 36 | $0.map { _ in conversion.reversion } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+Measurement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.07.23. 6 | // 7 | 8 | extension Conversion { 9 | 10 | public func converted( 11 | to unit: UnitType 12 | ) -> Appended where Target == Measurement { 13 | appending { $0.converted(to: unit).value } 14 | } 15 | 16 | public func converted( 17 | to unit: UnitType 18 | ) -> Appended> where Target == Double { 19 | appending { .init(value: $0, unit: unit) } 20 | } 21 | 22 | } 23 | 24 | extension ReversibleConversion { 25 | 26 | public func converted( 27 | to unit: UnitType 28 | ) -> Appended where Target == Measurement { 29 | appending { 30 | $0.converted(to: unit) 31 | } revert: { 32 | $0.converted(to: unit) 33 | } 34 | } 35 | 36 | public func converted( 37 | to unit: UnitType 38 | ) -> Appended> where Target == Double { 39 | appending { 40 | $0.converted(to: unit) 41 | } revert: { 42 | $0.converted(to: unit) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+PrefixCount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion where Target: Sequence { 11 | 12 | public func prefixCount( 13 | _ type: Count.Type 14 | ) -> Appended> { 15 | appending { .init(values: .init($0)) } 16 | } 17 | 18 | } 19 | 20 | extension Conversion { 21 | 22 | public func prefixCount( 23 | _ type: Count.Type 24 | ) -> Appended where Target == PrefixCountArray { 25 | appending { .init($0.values) } 26 | } 27 | 28 | } 29 | 30 | extension ReversibleConversion { 31 | 32 | public func prefixCount( 33 | _ type: Count.Type 34 | ) -> Appended> where Target: RangeReplaceableCollection { 35 | appending { 36 | $0.prefixCount(Count.self) 37 | } revert: { 38 | $0.prefixCount(Count.self) 39 | } 40 | } 41 | 42 | } 43 | 44 | public struct PrefixCountArray { 45 | 46 | // MARK: Stored Properties 47 | 48 | public let values: [Element] 49 | 50 | // MARK: Initialization 51 | 52 | public init(values: [Element]) { 53 | self.values = values 54 | } 55 | 56 | } 57 | 58 | extension PrefixCountArray: Readable where Count: Readable, Element: Readable { 59 | 60 | public init(from context: ReadContext) throws { 61 | let count = try context.read(for: \.values.count) 62 | self.values = try (0.. { 66 | Convert(\.values.count) { 67 | $0.exactly(from: Count.self) 68 | } 69 | 70 | Using(\.values.count) { count in 71 | for index in 0.. { 82 | Property(\.values.count) 83 | .conversion { $0.exactly(Count.self) } 84 | 85 | Using(\.values.count) { count in 86 | for index in 0.. { 11 | 12 | // MARK: Stored Properties 13 | 14 | private let _convert: (Source) throws -> Target 15 | private let _revert: (Target) throws -> Source 16 | 17 | // MARK: Initialization 18 | 19 | private init( 20 | convert: @escaping (Source) throws -> Target, 21 | revert: @escaping (Target) throws -> Source 22 | ) { 23 | self._convert = convert 24 | self._revert = revert 25 | } 26 | 27 | // MARK: Methods 28 | 29 | public func convert(_ source: Source) throws -> Target { 30 | try _convert(source) 31 | } 32 | 33 | public func convert(_ target: Target) throws -> Source { 34 | try _revert(target) 35 | } 36 | 37 | } 38 | 39 | extension ReversibleConversion { 40 | 41 | // MARK: Nested Types 42 | 43 | public typealias Make = (ReversibleConversion) -> ReversibleConversion 44 | 45 | // MARK: Static Functions 46 | 47 | public static func make(_ make: Make) -> Self { 48 | make(.init { $0 } revert: { $0 }) 49 | } 50 | 51 | } 52 | 53 | extension ReversibleConversion { 54 | 55 | // MARK: Nested Types 56 | 57 | public typealias Appended = ReversibleConversion 58 | 59 | // MARK: Methods 60 | 61 | public func appending( 62 | convert: @escaping (Target) throws -> NewTarget, 63 | revert: @escaping (NewTarget) throws -> Target 64 | ) -> Appended { 65 | .init { 66 | try convert(_convert($0)) 67 | } revert: { 68 | try _revert(revert($0)) 69 | } 70 | } 71 | 72 | public func appending( 73 | convert: Conversion.Make, 74 | revert: Conversion.Make 75 | ) -> Appended { 76 | appending( 77 | convert: Conversion.make(convert)._convert, 78 | revert: Conversion.make(revert)._convert 79 | ) 80 | } 81 | 82 | } 83 | 84 | extension ReversibleConversion { 85 | 86 | // MARK: Computed Properties 87 | 88 | public var conversion: Conversion { 89 | .init(_convert) 90 | } 91 | 92 | public var reversion: Conversion { 93 | .init(_revert) 94 | } 95 | 96 | public var inverted: ReversibleConversion { 97 | .init(convert: _revert, revert: _convert) 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion+VariableCount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Conversion where Target: Sequence { 11 | 12 | public var dynamicCount: Appended> { 13 | appending { .init($0) } 14 | } 15 | 16 | } 17 | 18 | extension Conversion { 19 | 20 | public func dynamicCount( 21 | _ target: NewTarget.Type = NewTarget.self 22 | ) -> Appended where Target == DynamicCountArray { 23 | appending { NewTarget($0.values) } 24 | } 25 | 26 | } 27 | 28 | extension ReversibleConversion where Target: RangeReplaceableCollection { 29 | 30 | public var dynamicCount: Appended> { 31 | appending { 32 | $0.dynamicCount 33 | } revert: { 34 | $0.dynamicCount() 35 | } 36 | } 37 | 38 | } 39 | 40 | public struct DynamicCountArray { 41 | 42 | // MARK: Stored Properties 43 | 44 | public let values: [Element] 45 | 46 | // MARK: Initialization 47 | 48 | public init>(_ values: S) { 49 | self.values = Array(values) 50 | } 51 | 52 | } 53 | 54 | extension DynamicCountArray: Readable where Element: Readable { 55 | 56 | public init(from context: ReadContext) throws { 57 | self.values = try context.read(for: \.values) 58 | } 59 | 60 | public static var readFormat: ReadFormat { 61 | ReadFormat { container, context in 62 | var values = [Element]() 63 | 64 | if let suffix = container.environment.suffix { 65 | while !(suffix.isRequired ? false : container.remainingData.isEmpty) && !container.remainingData.starts(with: suffix.data) { 66 | try values.append(Element(from: &container)) 67 | } 68 | if !container.remainingData.isEmpty { 69 | _ = try container.consume(suffix.data.count) 70 | } 71 | } else { 72 | while !container.remainingData.isEmpty { 73 | try values.append(Element(from: &container)) 74 | } 75 | } 76 | try context.write(values, for: \.values) 77 | } 78 | } 79 | 80 | } 81 | 82 | extension DynamicCountArray: Writable where Element: Writable { 83 | 84 | public static var writeFormat: WriteFormat { 85 | WriteFormat { container, root in 86 | for value in root.values { 87 | try value.write(to: &container) 88 | } 89 | 90 | if let suffix = container.environment.suffix { 91 | container.append(suffix.data) 92 | } 93 | } 94 | } 95 | 96 | } 97 | 98 | extension DynamicCountArray: ReadWritable where Element: ReadWritable { 99 | 100 | public static var format: Format { 101 | Format(read: readFormat, write: writeFormat) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Sources/DataKit/Conversions/Conversion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Conversion { 11 | 12 | // MARK: Stored Properties 13 | 14 | internal let _convert: (Source) throws -> Target 15 | 16 | // MARK: Initialization 17 | 18 | internal init(_ convert: @escaping (Source) throws -> Target) { 19 | self._convert = convert 20 | } 21 | 22 | // MARK: Methods 23 | 24 | public func convert(_ source: Source) throws -> Target { 25 | try _convert(source) 26 | } 27 | 28 | } 29 | 30 | extension Conversion { 31 | 32 | public typealias Make = (Conversion) -> Conversion 33 | 34 | public static func make(_ make: Make) -> Self { 35 | make(.init { $0 }) 36 | } 37 | 38 | } 39 | 40 | extension Conversion { 41 | 42 | public typealias Appended = Conversion 43 | 44 | public func appending( 45 | _ transform: @escaping (Target) throws -> NewTarget 46 | ) -> Appended { 47 | .init { try transform(convert($0)) } 48 | } 49 | 50 | public func appending( 51 | _ conversion: Conversion 52 | ) -> Appended { 53 | appending(conversion.convert) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/DataKit/Environment/Environment+Endianness.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 24.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Endianness: Equatable { 11 | case little 12 | case big 13 | } 14 | 15 | private enum EndiannessKey: EnvironmentKey { 16 | static var defaultValue: Endianness? { nil } 17 | } 18 | 19 | extension EnvironmentValues { 20 | public var endianness: Endianness? { 21 | get { self[EndiannessKey.self] } 22 | set { self[EndiannessKey.self] = newValue } 23 | } 24 | } 25 | 26 | extension FormatProperty { 27 | public func endianness(_ value: Endianness?) -> EnvironmentProperty { 28 | environment(\.endianness, value) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DataKit/Environment/Environment+Suffix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Suffix { 11 | public let data: Data 12 | public let isRequired: Bool 13 | 14 | } 15 | 16 | private struct SuffixKey: EnvironmentKey { 17 | static var defaultValue: Suffix? { nil } 18 | } 19 | 20 | extension EnvironmentValues { 21 | public var suffix: Suffix? { 22 | get { self[SuffixKey.self] } 23 | set { self[SuffixKey.self] = newValue } 24 | } 25 | } 26 | 27 | extension FormatProperty { 28 | 29 | public func suffix(_ data: Data?, isRequired: Bool = true) -> EnvironmentProperty { 30 | environment(\.suffix, data.map { .init(data: $0, isRequired: isRequired) }) 31 | } 32 | 33 | public func suffix(_ value: V, isRequired: Bool = true) -> EnvironmentProperty { 34 | transformEnvironment { environment in 35 | let data = try value.write(with: environment) 36 | environment.suffix = .init(data: data, isRequired: isRequired) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DataKit/Environment/EnvironmentValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 24.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol EnvironmentKey { 11 | associatedtype Value 12 | 13 | static var defaultValue: Value { get } 14 | } 15 | 16 | public struct EnvironmentValues { 17 | 18 | // MARK: Stored Properties 19 | 20 | private var values = [ObjectIdentifier: Any]() 21 | 22 | // MARK: Initialization 23 | 24 | public init() {} 25 | 26 | // MARK: Methods 27 | 28 | public subscript(_ key: Key.Type) -> Key.Value { 29 | get { values[ObjectIdentifier(Key.self), default: Key.defaultValue] as! Key.Value } 30 | set { values[ObjectIdentifier(Key.self)] = newValue } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/DataKit/Environment/SkipChecksumVerification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 15.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | private enum SkipChecksumVerificationKey: EnvironmentKey { 11 | static var defaultValue: Bool { false } 12 | } 13 | 14 | extension EnvironmentValues { 15 | public var skipChecksumVerification: Bool { 16 | get { self[SkipChecksumVerificationKey.self] } 17 | set { self[SkipChecksumVerificationKey.self] = newValue } 18 | } 19 | } 20 | 21 | extension FormatProperty { 22 | public func skipChecksumVerification(_ value: Bool = true) -> EnvironmentProperty { 23 | environment(\.skipChecksumVerification, value) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DataKit/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 15.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ConversionError: Error { 11 | public let source: Any 12 | public let targetType: Any.Type 13 | } 14 | 15 | public struct CannotWriteNilError: Error {} 16 | 17 | public struct UnexpectedValueError: Error { 18 | public let expectedValue: Any 19 | public let actualValue: Any 20 | } 21 | -------------------------------------------------------------------------------- /Sources/DataKit/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.07.23. 6 | // 7 | 8 | @_exported import Foundation 9 | @_exported import CRC 10 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Checksum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 31.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ChecksumProperty: FormatProperty { 11 | 12 | // MARK: Nested Types 13 | 14 | public typealias Root = Format.Root 15 | public typealias Value = ChecksumType.Value 16 | 17 | // MARK: Stored Properties 18 | 19 | internal let format: Format 20 | 21 | // MARK: Initialization 22 | 23 | public init( 24 | _ checksum: ChecksumType, 25 | at keyPath: KeyPath? = nil 26 | ) where Format == ReadFormat, ChecksumType.Value: Readable { 27 | self.format = ReadFormatBuilder.buildExpression( 28 | ReadFormat { container, context in 29 | let verificationData = container.consumedData 30 | let value = try Value(from: &container) 31 | try checksum.verify(value, for: verificationData) 32 | if let keyPath { 33 | try context.write(value, for: keyPath) 34 | } 35 | } 36 | .endianness(.big) 37 | ) 38 | } 39 | 40 | public init( 41 | _ checksum: ChecksumType, 42 | at keyPath: KeyPath? = nil 43 | ) where Format == ReadFormat, ChecksumType.Value: Readable { 44 | self.format = ReadFormatBuilder.buildExpression( 45 | ReadFormat { container, context in 46 | let verificationData = container.consumedData 47 | let value = try Value(from: &container) 48 | try checksum.verify(value, for: verificationData) 49 | if let keyPath { 50 | try context.write(value, for: keyPath) 51 | } 52 | } 53 | .endianness(.big) 54 | ) 55 | } 56 | 57 | public init( 58 | _ checksum: ChecksumType, 59 | at keyPath: KeyPath? = nil 60 | ) where Format == WriteFormat, ChecksumType.Value: Writable { 61 | self.format = WriteFormatBuilder.buildExpression( 62 | WriteFormat { container, root in 63 | let value = keyPath.map { root[keyPath: $0] } 64 | ?? checksum.calculate(for: container.data) 65 | try value.write(to: &container) 66 | } 67 | .endianness(.big) 68 | ) 69 | } 70 | 71 | public init( 72 | _ checksum: ChecksumType, 73 | at keyPath: KeyPath? = nil 74 | ) where Format == WriteFormat, ChecksumType.Value: Writable { 75 | self.format = WriteFormatBuilder.buildExpression( 76 | WriteFormat { container, root in 77 | let value = keyPath.flatMap { root[keyPath: $0] } 78 | ?? checksum.calculate(for: container.data) 79 | try value.write(to: &container) 80 | } 81 | .endianness(.big) 82 | ) 83 | } 84 | 85 | public init( 86 | _ checksum: ChecksumType, 87 | at keyPath: KeyPath? = nil 88 | ) where Format == ReadWriteFormat, ChecksumType.Value: ReadWritable { 89 | self.format = ReadWriteFormatBuilder.buildExpression( 90 | ReadWriteFormat( 91 | read: .init { container, context in 92 | let verificationData = container.consumedData 93 | let value = try Value(from: &container) 94 | try checksum.verify(value, for: verificationData) 95 | if let keyPath { 96 | try context.write(value, for: keyPath) 97 | } 98 | }, 99 | write: .init { container, root in 100 | let value = keyPath.map { root[keyPath: $0] } 101 | ?? checksum.calculate(for: container.data) 102 | try value.write(to: &container) 103 | } 104 | ) 105 | .endianness(.big) 106 | ) 107 | } 108 | 109 | public init( 110 | _ checksum: ChecksumType, 111 | at keyPath: KeyPath? = nil 112 | ) where Format == ReadWriteFormat, ChecksumType.Value: ReadWritable { 113 | self.format = ReadWriteFormatBuilder.buildExpression( 114 | ReadWriteFormat( 115 | read: .init { container, context in 116 | let verificationData = container.consumedData 117 | let value = try Value(from: &container) 118 | try checksum.verify(value, for: verificationData) 119 | if let keyPath { 120 | try context.write(value, for: keyPath) 121 | } 122 | }, 123 | write: .init { container, root in 124 | let value = keyPath.flatMap { root[keyPath: $0] } 125 | ?? checksum.calculate(for: container.data) 126 | try value.write(to: &container) 127 | } 128 | ) 129 | .endianness(.big) 130 | ) 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Convert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 27.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Convert: FormatProperty { 11 | 12 | // MARK: Nested Types 13 | 14 | public typealias Root = Format.Root 15 | 16 | // MARK: Stored Properties 17 | 18 | internal let format: Format 19 | 20 | // MARK: Initialization 21 | 22 | public init( 23 | _ keyPath: KeyPath, 24 | conversion makeConversion: Conversion.Make 25 | ) where Root: Readable, Format == ReadFormat { 26 | self.init( 27 | keyPath, 28 | convert: Conversion.make(makeConversion).convert 29 | ) 30 | } 31 | 32 | public init( 33 | _ keyPath: KeyPath, 34 | convert: @escaping (ConvertedValue) throws -> Value 35 | ) where Root: Readable, Format == ReadFormat { 36 | self.format = ReadFormat { container, context in 37 | let value = try convert(ConvertedValue(from: &container)) 38 | try context.write(value, for: keyPath) 39 | } 40 | } 41 | 42 | public init( 43 | _ keyPath: KeyPath, 44 | conversion makeConversion: Conversion.Make 45 | ) where Root: Writable, Format == WriteFormat { 46 | self.init( 47 | keyPath, 48 | convert: Conversion.make(makeConversion).convert 49 | ) 50 | } 51 | 52 | public init( 53 | _ keyPath: KeyPath, 54 | convert: @escaping (Value) throws -> ConvertedValue 55 | ) where Root: Writable, Format == WriteFormat { 56 | self.format = WriteFormat { container, root in 57 | try convert(root[keyPath: keyPath]).write(to: &container) 58 | } 59 | } 60 | 61 | public init( 62 | _ keyPath: KeyPath, 63 | conversion makeConversion: ReversibleConversion.Make 64 | ) where Root: ReadWritable, Format == ReadWriteFormat { 65 | let conversion = ReversibleConversion.make(makeConversion) 66 | self.init(keyPath, reading: conversion.convert, writing: conversion.convert) 67 | } 68 | 69 | public init( 70 | _ keyPath: KeyPath, 71 | reading: @escaping (ConvertedValue) throws -> Value, 72 | writing: @escaping (Value) throws -> ConvertedValue 73 | ) where Root: ReadWritable, Format == ReadWriteFormat { 74 | self.format = ReadWriteFormat( 75 | read: .init { container, context in 76 | let value = try reading(ConvertedValue(from: &container)) 77 | try context.write(value, for: keyPath) 78 | }, 79 | write: .init { container, root in 80 | try writing(root[keyPath: keyPath]).write(to: &container) 81 | } 82 | ) 83 | } 84 | 85 | } 86 | 87 | extension Convert: ReadableProperty where Format: ReadableProperty { 88 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 89 | try format.read(from: &container, context: &context) 90 | } 91 | } 92 | 93 | extension Convert: WritableProperty where Format: WritableProperty { 94 | public func write(to container: inout WriteContainer, using root: Root) throws { 95 | try format.write(to: &container, using: root) 96 | } 97 | } 98 | 99 | 100 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Custom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 27.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Custom: FormatProperty { 11 | 12 | // MARK: Nested Types 13 | 14 | public typealias Root = Format.Root 15 | 16 | // MARK: Stored Properties 17 | 18 | internal let format: Format 19 | 20 | // MARK: Initialization 21 | 22 | public init( 23 | _ keyPath: KeyPath, 24 | read: @escaping (inout ReadContainer) -> Value 25 | ) where Root: Readable, Format == ReadFormat { 26 | self.format = ReadFormat { container, context in 27 | try context.write(read(&container), for: keyPath) 28 | } 29 | } 30 | 31 | public init( 32 | _ keyPath: KeyPath, 33 | write: @escaping (inout WriteContainer, Value) throws -> Void 34 | ) where Root: Writable, Format == WriteFormat { 35 | self.format = WriteFormat { container, root in 36 | try write(&container, root[keyPath: keyPath]) 37 | } 38 | } 39 | 40 | public init( 41 | _ keyPath: KeyPath, 42 | read: @escaping (inout ReadContainer) -> Value, 43 | write: @escaping (inout WriteContainer, Value) throws -> Void 44 | ) where Root: ReadWritable, Format == ReadWriteFormat { 45 | self.format = ReadWriteFormat( 46 | read: Custom(keyPath, read: read).format, 47 | write: Custom(keyPath, write: write).format 48 | ) 49 | } 50 | 51 | } 52 | 53 | extension Custom: ReadableProperty where Format: ReadableProperty { 54 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 55 | try format.read(from: &container, context: &context) 56 | } 57 | } 58 | 59 | extension Custom: WritableProperty where Format: WritableProperty { 60 | public func write(to container: inout WriteContainer, using root: Root) throws { 61 | try format.write(to: &container, using: root) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 15.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Environment: FormatProperty { 11 | 12 | // MARK: Nested Types 13 | 14 | public typealias Root = Format.Root 15 | 16 | // MARK: Stored Properties 17 | 18 | private let keyPath: KeyPath 19 | private let format: (Value) throws -> Format 20 | 21 | // MARK: Initialization 22 | 23 | public init ( 24 | _ keyPath: KeyPath, 25 | @FormatBuilder format: @escaping (Value) throws -> Format 26 | ) where Format == ReadFormat { 27 | self.keyPath = keyPath 28 | self.format = format 29 | } 30 | 31 | public init ( 32 | _ keyPath: KeyPath, 33 | @FormatBuilder format: @escaping (Value) throws -> Format 34 | ) where Format == WriteFormat { 35 | self.keyPath = keyPath 36 | self.format = format 37 | } 38 | 39 | public init ( 40 | _ keyPath: KeyPath, 41 | @FormatBuilder format: @escaping (Value) throws -> Format 42 | ) where Format == ReadWriteFormat { 43 | self.keyPath = keyPath 44 | self.format = format 45 | } 46 | 47 | } 48 | 49 | extension Environment: ReadableProperty where Format: ReadableProperty { 50 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 51 | let value = container.environment[keyPath: keyPath] 52 | try format(value).read(from: &container, context: &context) 53 | } 54 | } 55 | 56 | extension Environment: WritableProperty where Format: WritableProperty { 57 | public func write(to container: inout WriteContainer, using root: Root) throws { 58 | let value = container.environment[keyPath: keyPath] 59 | try format(value).write(to: &container, using: root) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/EnvironmentProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FormatProperty { 11 | 12 | public func environment( 13 | _ keyPath: WritableKeyPath, 14 | _ value: Value 15 | ) -> EnvironmentProperty { 16 | EnvironmentProperty(self) { $0[keyPath: keyPath] = value } 17 | } 18 | 19 | public func transformEnvironment( 20 | _ keyPath: WritableKeyPath, 21 | transform: @escaping (inout Value) throws -> Void 22 | ) -> EnvironmentProperty { 23 | EnvironmentProperty(self) { try transform(&$0[keyPath: keyPath]) } 24 | } 25 | 26 | public func transformEnvironment( 27 | transform: @escaping (inout EnvironmentValues) throws -> Void 28 | ) -> EnvironmentProperty { 29 | EnvironmentProperty(self) { try transform(&$0) } 30 | } 31 | 32 | } 33 | 34 | public struct EnvironmentProperty: FormatProperty { 35 | 36 | // MARK: Nested Types 37 | 38 | public typealias Root = Format.Root 39 | 40 | // MARK: Stored Properties 41 | 42 | private let format: Format 43 | private let transform: (inout EnvironmentValues) throws -> Void 44 | 45 | // MARK: Initialization 46 | 47 | public init( 48 | _ format: Format, 49 | transform: @escaping (inout EnvironmentValues) throws -> Void 50 | ) { 51 | self.format = format 52 | self.transform = transform 53 | } 54 | 55 | } 56 | 57 | extension EnvironmentProperty: ReadableProperty where Format: ReadableProperty { 58 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 59 | let previousEnvironment = container.environment 60 | try transform(&container.environment) 61 | try format.read(from: &container, context: &context) 62 | container.environment = previousEnvironment 63 | } 64 | } 65 | 66 | extension EnvironmentProperty: WritableProperty where Format: WritableProperty { 67 | public func write(to container: inout WriteContainer, using root: Root) throws { 68 | let previousEnvironment = container.environment 69 | try transform(&container.environment) 70 | try format.write(to: &container, using: root) 71 | container.environment = previousEnvironment 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/KeyPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 28.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension KeyPath: FormatProperty {} 11 | 12 | extension KeyPath: ReadableProperty where Root: Readable, Value: Readable { 13 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 14 | try context.write(Value(from: &container), for: self) 15 | } 16 | } 17 | 18 | extension KeyPath: WritableProperty where Root: Writable, Value: Writable { 19 | public func write(to container: inout WriteContainer, using root: Root) throws { 20 | try root[keyPath: self].write(to: &container) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/OnRead.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 14.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct OnRead: ReadableProperty, WritableProperty { 11 | 12 | // MARK: Stored Properties 13 | 14 | private let format: ReadFormat 15 | 16 | // MARK: Initialization 17 | 18 | public init(@ReadFormatBuilder format: () -> ReadFormat) { 19 | self.format = format() 20 | } 21 | 22 | // MARK: Methods 23 | 24 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 25 | try format.read(from: &container, context: &context) 26 | } 27 | 28 | public func write(to container: inout WriteContainer, using root: Root) throws {} 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/OnWrite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 14.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct OnWrite: ReadableProperty, WritableProperty { 11 | 12 | // MARK: Stored Properties 13 | 14 | private let format: WriteFormat 15 | 16 | // MARK: Initialization 17 | 18 | public init(@WriteFormatBuilder format: () -> WriteFormat) { 19 | self.format = format() 20 | } 21 | 22 | // MARK: Methods 23 | 24 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws {} 25 | 26 | public func write(to container: inout WriteContainer, using root: Root) throws { 27 | try format.write(to: &container, using: root) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Property+Conversion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Property where Root: Readable { 11 | 12 | public func conversion( 13 | _ makeConversion: Conversion.Make 14 | ) -> Convert> { 15 | Convert(keyPath, conversion: makeConversion) 16 | } 17 | 18 | public func converted( 19 | _ convert: @escaping (ConvertedValue) throws -> Value 20 | ) -> Convert> { 21 | Convert(keyPath, convert: convert) 22 | } 23 | 24 | } 25 | 26 | extension Property where Root: Writable { 27 | 28 | public func conversion( 29 | _ makeConversion: Conversion.Make 30 | ) -> Convert> { 31 | Convert(keyPath, conversion: makeConversion) 32 | } 33 | 34 | public func converted( 35 | _ convert: @escaping (Value) throws -> ConvertedValue 36 | ) -> Convert> { 37 | Convert(keyPath, convert: convert) 38 | } 39 | 40 | } 41 | 42 | extension Property where Root: ReadWritable { 43 | 44 | public func conversion( 45 | _ makeConversion: ReversibleConversion.Make 46 | ) -> Convert> { 47 | Convert(keyPath, conversion: makeConversion) 48 | } 49 | 50 | public func converted( 51 | reading: @escaping (ConvertedValue) throws -> Value, 52 | writing: @escaping (Value) throws -> ConvertedValue 53 | ) -> Convert> { 54 | Convert(keyPath, reading: reading, writing: writing) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Property+Custom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Property where Root: Readable { 11 | 12 | public func read( 13 | _ read: @escaping (inout ReadContainer) -> Value 14 | ) -> Custom> { 15 | Custom(keyPath, read: read) 16 | } 17 | 18 | } 19 | 20 | extension Property where Root: Writable { 21 | 22 | public func write( 23 | _ write: @escaping (inout WriteContainer, Value) throws -> Void 24 | ) -> Custom> { 25 | Custom(keyPath, write: write) 26 | } 27 | 28 | } 29 | 30 | extension Property where Root: ReadWritable { 31 | 32 | public func read( 33 | _ read: @escaping (inout ReadContainer) -> Value, 34 | write: @escaping (inout WriteContainer, Value) throws -> Void 35 | ) -> Custom> { 36 | Custom(keyPath, read: read, write: write) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Property.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 14.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Property: FormatProperty { 11 | 12 | // MARK: Stored Properties 13 | 14 | internal let keyPath: KeyPath 15 | 16 | // MARK: Initialization 17 | 18 | public init(_ keyPath: KeyPath) { 19 | self.keyPath = keyPath 20 | } 21 | 22 | } 23 | 24 | extension Property: ReadableProperty where Root: Readable, Value: Readable { 25 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 26 | let value = try Value(from: &container) 27 | try context.write(value, for: keyPath) 28 | } 29 | } 30 | 31 | extension Property: WritableProperty where Root: Writable, Value: Writable { 32 | public func write(to container: inout WriteContainer, using root: Root) throws { 33 | try root[keyPath: keyPath].write(to: &container) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Scope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 14.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Scope: FormatProperty { 11 | 12 | // MARK: Nested Types 13 | 14 | public typealias Root = Format.Root 15 | 16 | // MARK: Stored Properties 17 | 18 | private let endInset: Int 19 | private let format: Format 20 | 21 | // MARK: Initialization 22 | 23 | public init( 24 | endInset: Int = 0, 25 | @FormatBuilder format: () -> Format 26 | ) where Format == ReadFormat { 27 | self.endInset = endInset 28 | self.format = format() 29 | } 30 | 31 | public init( 32 | @FormatBuilder format: () -> Format 33 | ) where Format == WriteFormat { 34 | self.endInset = 0 35 | self.format = format() 36 | } 37 | 38 | public init( 39 | endInset: Int = 0, 40 | @FormatBuilder format: () -> Format 41 | ) where Format == ReadWriteFormat { 42 | self.endInset = endInset 43 | self.format = format() 44 | } 45 | 46 | } 47 | 48 | extension Scope: ReadableProperty where Format: ReadableProperty { 49 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 50 | guard let endIndex = container.data.index(container.data.endIndex, offsetBy: -endInset, limitedBy: container.index) else { 51 | throw ReadContainer.LengthExceededError() 52 | } 53 | var nestedContainer = ReadContainer(data: container.remainingData.prefix(upTo: endIndex), environment: container.environment) 54 | try format.read(from: &nestedContainer, context: &context) 55 | let distance = nestedContainer.data.distance(from: nestedContainer.data.startIndex, to: nestedContainer.index) 56 | container.index = container.data.index(container.index, offsetBy: distance) 57 | } 58 | } 59 | 60 | extension Scope: WritableProperty where Format: WritableProperty { 61 | public func write(to container: inout WriteContainer, using root: Root) throws { 62 | var nestedContainer = WriteContainer(environment: container.environment) 63 | try format.write(to: &nestedContainer, using: root) 64 | container.append(nestedContainer.data) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/DataKit/Property/Using.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 23.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Using: FormatProperty { 11 | 12 | // MARK: Nested Types 13 | 14 | public typealias Root = Format.Root 15 | 16 | // MARK: Stored Properties 17 | 18 | private let keyPath: KeyPath 19 | private let format: (Value) throws -> Format 20 | 21 | // MARK: Initialization 22 | 23 | public init( 24 | _ keyPath: KeyPath, 25 | @FormatBuilder with format: @escaping (Value) throws -> Format 26 | ) where Format == ReadFormat { 27 | self.keyPath = keyPath 28 | self.format = format 29 | } 30 | 31 | public init( 32 | _ keyPath: KeyPath, 33 | @FormatBuilder with format: @escaping (Value) throws -> Format 34 | ) where Format == WriteFormat { 35 | self.keyPath = keyPath 36 | self.format = format 37 | } 38 | 39 | public init( 40 | _ keyPath: KeyPath, 41 | @FormatBuilder with format: @escaping (Value) throws -> Format 42 | ) where Format == ReadWriteFormat { 43 | self.keyPath = keyPath 44 | self.format = format 45 | } 46 | 47 | } 48 | 49 | extension Using: ReadableProperty where Format: ReadableProperty { 50 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 51 | let value = try context.read(for: keyPath) 52 | try format(value).read(from: &container, context: &context) 53 | } 54 | } 55 | 56 | extension Using: WritableProperty where Format: WritableProperty { 57 | public func write(to container: inout WriteContainer, using root: Root) throws { 58 | let value = root[keyPath: keyPath] 59 | try format(value).write(to: &container, using: root) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DataKit/ReadWritable/Format.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 15.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol FormatProperty { 11 | associatedtype Root 12 | } 13 | 14 | public protocol FormatType: FormatProperty { 15 | init(_ multiple: [Self]) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DataKit/ReadWritable/ReadWritable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 22.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ReadWritable: Readable, Writable { 11 | 12 | @FormatBuilder 13 | static var format: Format { get throws } 14 | 15 | } 16 | 17 | extension ReadWritable { 18 | 19 | public typealias FormatBuilder = ReadWriteFormatBuilder 20 | public typealias Format = ReadWriteFormat 21 | 22 | public static var readFormat: ReadFormat { 23 | get throws { 24 | try ReadFormat(read: format.read) 25 | } 26 | } 27 | 28 | public static var writeFormat: WriteFormat { 29 | get throws { 30 | try WriteFormat(write: format.write) 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/DataKit/ReadWritable/ReadWritableProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 22.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ReadWriteFormat: FormatType, ReadableProperty, WritableProperty { 11 | 12 | // MARK: Stored Properties 13 | 14 | private let readFormat: ReadFormat 15 | private let writeFormat: WriteFormat 16 | 17 | // MARK: Initialization 18 | 19 | public init(read: ReadFormat, write: WriteFormat) { 20 | self.readFormat = read 21 | self.writeFormat = write 22 | } 23 | 24 | public init(_ multiple: [ReadWriteFormat]) { 25 | self.init( 26 | read: .init(multiple.map(\.readFormat)), 27 | write: .init(multiple.map(\.writeFormat)) 28 | ) 29 | } 30 | 31 | // MARK: Methods 32 | 33 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 34 | try readFormat.read(from: &container, context: &context) 35 | } 36 | 37 | public func write(to container: inout WriteContainer, using root: Root) throws { 38 | try writeFormat.write(to: &container, using: root) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/DataKit/Readable/ReadContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 15.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ReadContainer { 11 | 12 | // MARK: Nested Types 13 | 14 | public struct LengthExceededError: Error {} 15 | 16 | // MARK: Stored Properties 17 | 18 | public let data: Data 19 | public var index: Data.Index 20 | public var environment: EnvironmentValues 21 | 22 | // MARK: Computed Properties 23 | 24 | public var consumedData: Data { 25 | data.prefix(upTo: index) 26 | } 27 | 28 | public var remainingData: Data { 29 | data.suffix(from: index) 30 | } 31 | 32 | // MARK: Initialization 33 | 34 | public init( 35 | data: Data, 36 | index: Data.Index? = nil, 37 | environment: EnvironmentValues 38 | ) { 39 | self.data = data 40 | self.index = index ?? data.startIndex 41 | self.environment = environment 42 | } 43 | 44 | // MARK: Methods 45 | 46 | public mutating func consume(_ count: Int) throws -> Data { 47 | guard count <= data.distance(from: index, to: data.endIndex) else { 48 | throw LengthExceededError() 49 | } 50 | let newIndex = data.index(index, offsetBy: count) 51 | defer { index = newIndex } 52 | return data[index.. { 11 | 12 | // MARK: Nested Types 13 | 14 | public struct ValueDoesNotExistError: Error { 15 | public let keyPath: PartialKeyPath 16 | } 17 | 18 | public struct ValueTypeMismatchError: Error { 19 | public let value: Any 20 | public let expectedType: Any.Type 21 | } 22 | 23 | // MARK: Stored Properties 24 | 25 | private var values = [PartialKeyPath: Any]() 26 | 27 | // MARK: Initialization 28 | 29 | public init() {} 30 | 31 | // MARK: Methods 32 | 33 | public mutating func write(_ value: Value, for keyPath: KeyPath) throws { 34 | values[keyPath] = value 35 | } 36 | 37 | public func read(for keyPath: KeyPath) throws -> Value { 38 | guard let value = values[keyPath] else { 39 | throw ValueDoesNotExistError(keyPath: keyPath) 40 | } 41 | guard let result = value as? Value else { 42 | throw ValueTypeMismatchError(value: value, expectedType: Value.self) 43 | } 44 | return result 45 | } 46 | 47 | public func readIfPresent(for keyPath: KeyPath) throws -> Value? { 48 | guard let value = values[keyPath] else { 49 | return nil 50 | } 51 | guard let result = value as? Value else { 52 | throw ValueTypeMismatchError(value: value, expectedType: Value.self) 53 | } 54 | return result 55 | } 56 | 57 | public func readIfPresent(for keyPath: KeyPath) throws -> Value? { 58 | guard let value = values[keyPath] else { 59 | return nil 60 | } 61 | guard let result = value as? Value else { 62 | throw ValueTypeMismatchError(value: value, expectedType: Value.self) 63 | } 64 | return result 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/DataKit/Readable/Readable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 21.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Readable { 11 | 12 | init(from context: ReadContext) throws 13 | 14 | @ReadBuilder 15 | static var readFormat: ReadFormat { get throws } 16 | 17 | } 18 | 19 | extension Readable { 20 | 21 | public typealias ReadBuilder = ReadFormatBuilder 22 | 23 | public init(from container: inout ReadContainer) throws { 24 | var context = ReadContext() 25 | try Self.readFormat.read(from: &container, context: &context) 26 | try self.init(from: context) 27 | } 28 | 29 | public init(_ data: Data, environment: EnvironmentValues = EnvironmentValues()) throws { 30 | var container = ReadContainer(data: data, environment: environment) 31 | try self.init(from: &container) 32 | } 33 | 34 | public init(_ data: Data, transform: (inout EnvironmentValues) throws -> Void) throws { 35 | var environment = EnvironmentValues() 36 | try transform(&environment) 37 | try self.init(data, environment: environment) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DataKit/Readable/ReadableProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 21.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ReadableProperty: FormatProperty where Root: Readable { 11 | func read(from container: inout ReadContainer, context: inout ReadContext) throws 12 | } 13 | 14 | public struct ReadFormat: ReadableProperty { 15 | 16 | // MARK: Stored Properties 17 | 18 | private let _read: (inout ReadContainer, inout ReadContext) throws -> Void 19 | 20 | // MARK: Initialization 21 | 22 | public init(read: @escaping (inout ReadContainer, inout ReadContext) throws -> Void) { 23 | self._read = read 24 | } 25 | 26 | // MARK: Methods 27 | 28 | public func read(from container: inout ReadContainer, context: inout ReadContext) throws { 29 | try _read(&container, &context) 30 | } 31 | 32 | } 33 | 34 | extension ReadFormat: FormatType { 35 | public init(_ multiple: [ReadFormat]) { 36 | self.init { container, context in 37 | for format in multiple { 38 | try format.read(from: &container, context: &context) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/DataKit/Values/ReadWritable+FloatingPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol FixedWidthFloatingPoint: BinaryFloatingPoint { 11 | associatedtype BitPattern: FixedWidthInteger 12 | 13 | init(bitPattern: BitPattern) 14 | var bitPattern: BitPattern { get } 15 | } 16 | 17 | #if arch(arm64) 18 | @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) 19 | extension Float16: FixedWidthFloatingPoint, ReadWritable {} 20 | #endif 21 | 22 | extension Float32: FixedWidthFloatingPoint, ReadWritable {} 23 | extension Float64: FixedWidthFloatingPoint, ReadWritable {} 24 | 25 | extension FixedWidthFloatingPoint where Self: ReadWritable, BitPattern: ReadWritable { 26 | 27 | public init(from context: ReadContext) throws { 28 | try self.init(bitPattern: context.read(for: \.bitPattern)) 29 | } 30 | 31 | @FormatBuilder 32 | public static var format: Format { 33 | \.bitPattern 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/DataKit/Values/ReadWritable+Integer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Int: ReadWritable {} 11 | extension Int8: ReadWritable {} 12 | extension Int16: ReadWritable {} 13 | extension Int32: ReadWritable {} 14 | extension Int64: ReadWritable {} 15 | 16 | extension UInt: ReadWritable {} 17 | extension UInt8: ReadWritable {} 18 | extension UInt16: ReadWritable {} 19 | extension UInt32: ReadWritable {} 20 | extension UInt64: ReadWritable {} 21 | 22 | extension FixedWidthInteger where Self: ReadWritable { 23 | 24 | public init(from context: ReadContext) throws { 25 | self = try context.read(for: \.self) 26 | } 27 | 28 | public static var format: Format { 29 | Format( 30 | read: .init { container, context in 31 | var value: Self = 0 32 | 33 | try withUnsafeMutableBytes(of: &value) { bytes in 34 | try bytes.copyBytes( 35 | from: container.consume(MemoryLayout.size) 36 | ) 37 | } 38 | 39 | let readValue: Self = { 40 | switch container.environment.endianness { 41 | case .little: 42 | return .init(littleEndian: value) 43 | case .big: 44 | return .init(bigEndian: value) 45 | case nil: 46 | return value 47 | } 48 | }() 49 | 50 | try context.write(readValue, for: \.self) 51 | }, 52 | write: .init { container, value in 53 | let writtenValue: Self = { 54 | switch container.environment.endianness { 55 | case .little: 56 | return value.littleEndian 57 | case .big: 58 | return value.bigEndian 59 | case nil: 60 | return value 61 | } 62 | }() 63 | 64 | withUnsafeBytes(of: writtenValue) { 65 | container.append(contentsOf: $0) 66 | } 67 | } 68 | ) 69 | } 70 | 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /Sources/DataKit/Values/ReadWritable+Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Optional: Readable where Wrapped: Readable { 11 | 12 | public init(from context: ReadContext) throws { 13 | self = try context.read(for: \.self) 14 | } 15 | 16 | public static var readFormat: ReadFormat { 17 | ReadFormat { container, context in 18 | try context.write(.some(Wrapped(from: &container)), for: \.self) 19 | } 20 | } 21 | 22 | } 23 | 24 | extension Optional: Writable where Wrapped: Writable { 25 | 26 | public static var writeFormat: WriteFormat { 27 | WriteFormat { container, value in 28 | try value?.write(to: &container) 29 | } 30 | } 31 | 32 | } 33 | 34 | extension Optional: ReadWritable where Wrapped: ReadWritable { 35 | 36 | public static var format: Format { 37 | Format(read: readFormat, write: writeFormat) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DataKit/Values/ReadWritable+Raw.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 16.07.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension RawRepresentable where Self: Readable, RawValue: Readable { 11 | 12 | public init(from context: ReadContext) throws { 13 | let rawValue = try context.read(for: \.rawValue) 14 | guard let value = Self(rawValue: rawValue) else { 15 | throw ConversionError(source: rawValue, targetType: Self.self) 16 | } 17 | self = value 18 | } 19 | 20 | @ReadBuilder 21 | public static var readFormat: ReadFormat { 22 | \.rawValue 23 | } 24 | 25 | } 26 | 27 | extension RawRepresentable where Self: Writable, RawValue: Writable { 28 | 29 | @WriteBuilder 30 | public static var writeFormat: WriteFormat { 31 | \.rawValue 32 | } 33 | 34 | } 35 | 36 | extension RawRepresentable where Self: ReadWritable, RawValue: ReadWritable { 37 | 38 | @FormatBuilder 39 | public static var format: Format { 40 | \.rawValue 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/DataKit/Writable/Writable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 21.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Writable { 11 | 12 | @WriteBuilder 13 | static var writeFormat: WriteFormat { get throws } 14 | 15 | } 16 | 17 | extension Writable { 18 | 19 | public typealias WriteBuilder = WriteFormatBuilder 20 | 21 | public func write(to container: inout WriteContainer) throws { 22 | try Self.writeFormat.write(to: &container, using: self) 23 | } 24 | 25 | public func write(with environment: EnvironmentValues = EnvironmentValues()) throws -> Data { 26 | var container = WriteContainer(environment: environment) 27 | try write(to: &container) 28 | return container.data 29 | } 30 | 31 | public func write(transform: (inout EnvironmentValues) throws -> Void) throws -> Data { 32 | var environment = EnvironmentValues() 33 | try transform(&environment) 34 | return try write(with: environment) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/DataKit/Writable/WritableProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 21.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol WritableProperty: FormatProperty where Root: Writable { 11 | func write(to container: inout WriteContainer, using root: Root) throws 12 | } 13 | 14 | public struct WriteFormat: WritableProperty { 15 | 16 | // MARK: Stored Properties 17 | 18 | private let _write: (inout WriteContainer, Root) throws -> Void 19 | 20 | // MARK: Initialization 21 | 22 | public init(write: @escaping (inout WriteContainer, Root) throws -> Void) { 23 | self._write = write 24 | } 25 | 26 | // MARK: Methods 27 | 28 | public func write(to container: inout WriteContainer, using root: Root) throws { 29 | try _write(&container, root) 30 | } 31 | 32 | } 33 | 34 | extension WriteFormat: FormatType { 35 | 36 | public init(_ multiple: [WriteFormat]) { 37 | self.init { container, root in 38 | for format in multiple { 39 | try format.write(to: &container, using: root) 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/DataKit/Writable/WriteContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.06.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct WriteContainer { 11 | 12 | // MARK: Stored Properties 13 | 14 | public private(set) var data: Data 15 | public var environment: EnvironmentValues 16 | 17 | // MARK: Initialization 18 | 19 | public init( 20 | data: Data = Data(), 21 | environment: EnvironmentValues 22 | ) { 23 | self.data = data 24 | self.environment = environment 25 | } 26 | 27 | // MARK: Methods 28 | 29 | public mutating func transform(_ transform: (inout Data) throws -> V) rethrows -> V { 30 | try transform(&data) 31 | } 32 | 33 | public mutating func append(_ newData: Data) { 34 | data.append(newData) 35 | } 36 | 37 | public mutating func append(_ buffer: UnsafeBufferPointer) { 38 | data.append(buffer) 39 | } 40 | 41 | public mutating func append(contentsOf bytes: [UInt8]) { 42 | data.append(contentsOf: bytes) 43 | } 44 | 45 | public mutating func append>(contentsOf elements: S) { 46 | data.append(contentsOf: elements) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Tests/DataKitTests/Conversion/StringConversionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import DataKit 9 | import XCTest 10 | 11 | final class StringConversionTests: XCTestCase { 12 | 13 | func testVariableCountNoSuffix() { 14 | struct Wrapper: ReadWritable { 15 | let value: String 16 | 17 | init(value: String) { 18 | self.value = value 19 | } 20 | 21 | init(from context: ReadContext) throws { 22 | self.value = try context.read(for: \.value) 23 | } 24 | 25 | static var format: Format { 26 | Scope(endInset: MemoryLayout.size) { 27 | Convert(\.value) { 28 | $0.encoded(.utf8).dynamicCount 29 | } 30 | } 31 | 32 | CRC32.default 33 | } 34 | } 35 | 36 | for value in ["", "abc", "😂☺️", "1234"] { 37 | let wrapper = Wrapper(value: value) 38 | XCTAssertNoThrow { 39 | let data = try wrapper.write() 40 | let decodedWrapper = try Wrapper(data) 41 | XCTAssertEqual(decodedWrapper.value, value) 42 | XCTAssertEqual(try decodedWrapper.write(), data) 43 | } 44 | } 45 | } 46 | 47 | func testVariableCountSuffix() { 48 | struct Wrapper: ReadWritable { 49 | let value: String 50 | 51 | init(value: String) { 52 | self.value = value 53 | } 54 | 55 | init(from context: ReadContext) throws { 56 | self.value = try context.read(for: \.value) 57 | } 58 | 59 | static var format: Format { 60 | Convert(\.value) { 61 | $0.encoded(.utf8).dynamicCount 62 | } 63 | .suffix(0 as UInt8) 64 | 65 | CRC32.default 66 | } 67 | } 68 | 69 | for value in ["", "abc", "😂☺️", "1234"] { 70 | let wrapper = Wrapper(value: value) 71 | XCTAssertNoThrow { 72 | let data = try wrapper.write() 73 | let decodedWrapper = try Wrapper(data) 74 | XCTAssertEqual(decodedWrapper.value, value) 75 | XCTAssertEqual(try decodedWrapper.write(), data) 76 | } 77 | } 78 | } 79 | 80 | func testPrefixCount() { 81 | struct Wrapper: ReadWritable { 82 | let value: String 83 | 84 | init(value: String) { 85 | self.value = value 86 | } 87 | 88 | init(from context: ReadContext) throws { 89 | self.value = try context.read(for: \.value) 90 | } 91 | 92 | static var format: Format { 93 | Convert(\.value) { 94 | $0.encoded(.utf8).prefixCount(UInt8.self) 95 | } 96 | 97 | CRC32.default 98 | } 99 | } 100 | 101 | for value in ["", "abc", "😂☺️", "1234"] { 102 | let wrapper = Wrapper(value: value) 103 | XCTAssertNoThrow { 104 | let data = try wrapper.write() 105 | let decodedWrapper = try Wrapper(data) 106 | XCTAssertEqual(decodedWrapper.value, value) 107 | XCTAssertEqual(try decodedWrapper.write(), data) 108 | } 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Tests/DataKitTests/DataBuilderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import DataKit 3 | 4 | final class DataBuilderTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | // This is an example of a functional test case. 8 | // Use XCTAssert and related functions to verify your tests produce the correct 9 | // results. 10 | 11 | let original = MyItem(id: 5, nestedItems: []) 12 | let data = try original.write() 13 | let read = try MyItem(data) 14 | print(data.map { String(format: "%02hhx", $0) }.joined()) 15 | print(original, read) 16 | } 17 | 18 | func testExample2() throws { 19 | let original = MyItem(id: 10, nestedItems: [.init(id: 2, value: -15.3)]) 20 | let data = try original.write() 21 | let read = try MyItem(data) 22 | print(data.map { String(format: "%02hhx", $0) }.joined()) 23 | print(original) 24 | print(read) 25 | XCTAssertEqual(original, read) 26 | } 27 | 28 | func testExample2Directly() throws { 29 | var environment = EnvironmentValues() 30 | environment.endianness = .little 31 | var writeContainer = WriteContainer(environment: environment) 32 | 33 | let value: Double = 124 34 | try value.write(to: &writeContainer) 35 | var readContainer = ReadContainer(data: writeContainer.data, environment: environment) 36 | XCTAssertEqual(value, try .init(from: &readContainer)) 37 | } 38 | 39 | func testExample3() throws { 40 | let values = (0..<100).map { _ in Double.random(in: -500...500) } 41 | 42 | for value in values { 43 | XCTAssertEqual(value, Double(bitPattern: value.bitPattern)) 44 | XCTAssertEqual(value, Double(bitPattern: UInt64(littleEndian: value.bitPattern.littleEndian))) 45 | XCTAssertEqual(value, Double(bitPattern: UInt64(bigEndian: value.bitPattern.bigEndian))) 46 | } 47 | } 48 | 49 | func testDataBuilder() throws { 50 | let value = WeatherStationUpdate(features: [.hasTemperature, .usesMetricUnits], temperature: .init(value: 15, unit: .celsius), humidity: 0.6) 51 | print(value.data.map { String(format: "%02hhx", $0) }.joined()) 52 | 53 | print("hallo".utf8.map { String(format: "%02hhx", $0) }.joined()) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Tests/DataKitTests/DataKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Tests/DataKitTests/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import XCTest 9 | 10 | extension Data { 11 | 12 | func copy(transform: (inout Data) -> Void) -> Data { 13 | var copy = self 14 | transform(©) 15 | return copy 16 | } 17 | 18 | var byteArrayDescription: String { 19 | "[" + map { String(format: "0x%02hX", $0) }.joined(separator: ", ") + "]" 20 | } 21 | 22 | } 23 | 24 | func XCTAssertNoThrow(_ run: () throws -> Void) { 25 | XCTAssertNoThrow(try run()) 26 | } 27 | -------------------------------------------------------------------------------- /Tests/DataKitTests/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 14.07.23. 6 | // 7 | 8 | import Foundation 9 | import DataKit 10 | 11 | public struct MyItem: Equatable { 12 | 13 | var id: Int64 14 | var nestedItems: [MyNestedItem] 15 | 16 | } 17 | 18 | extension MyItem: ReadWritable { 19 | 20 | public static var format: Format { 21 | Scope { 22 | UInt8(0x02) 23 | 24 | Scope { 25 | MyNestedItem(id: 2, value: -4) 26 | } 27 | .endianness(.big) 28 | 29 | Convert(\.id) { $0.exactly(UInt16.self) } 30 | .endianness(.big) 31 | 32 | Convert(\.nestedItems) { $0.prefixCount(UInt8.self) } 33 | 34 | CRC32.default 35 | } 36 | } 37 | 38 | public init(from context: ReadContext) throws { 39 | self.id = try context.read(for: \.id) 40 | self.nestedItems = try context.read(for: \.nestedItems) 41 | } 42 | 43 | } 44 | 45 | public struct MyNestedItem: Equatable { 46 | 47 | var id: Int64 48 | var value: Double 49 | 50 | } 51 | 52 | extension MyNestedItem: ReadWritable { 53 | 54 | public init(from context: ReadContext) throws { 55 | self.id = try context.read(for: \.id) 56 | self.value = try context.read(for: \.value) 57 | } 58 | 59 | public static var format: Format { 60 | Scope { 61 | Convert(\.id) { $0.exactly(UInt16.self) } 62 | .endianness(.big) 63 | 64 | Property(\.value) 65 | .endianness(.little) 66 | 67 | CRC32.default 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Tests/DataKitTests/Readme/Readme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 25.07.23. 6 | // 7 | 8 | import DataKit 9 | 10 | struct WeatherStationFeatures: OptionSet, ReadWritable { 11 | var rawValue: UInt8 12 | 13 | static var hasTemperature = Self(rawValue: 1 << 0) 14 | static var hasHumidity = Self(rawValue: 1 << 1) 15 | static var usesMetricUnits = Self(rawValue: 1 << 2) 16 | } 17 | 18 | struct WeatherStationUpdate { 19 | 20 | var features: WeatherStationFeatures 21 | var temperature: Measurement 22 | var humidity: Double 23 | 24 | } 25 | 26 | extension WeatherStationUpdate { 27 | 28 | @DataBuilder var data: Data { 29 | UInt8(0x02).bigEndian 30 | features 31 | if features.contains(.hasTemperature) { 32 | Float(temperature.converted(to: features.contains(.usesMetricUnits) ? .celsius : .fahrenheit).value) 33 | } 34 | if features.contains(.hasHumidity) { 35 | UInt8(humidity * 100) 36 | } 37 | CRC32.default 38 | } 39 | 40 | } 41 | 42 | extension WeatherStationUpdate: ReadWritable { 43 | 44 | init(from context: ReadContext) throws { 45 | features = try context.read(for: \.features) 46 | temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin) 47 | humidity = try context.readIfPresent(for: \.humidity) ?? .nan 48 | } 49 | 50 | static var format: Format { 51 | Scope { 52 | UInt8(0x02) 53 | 54 | \.features 55 | 56 | Using(\.features) { features in 57 | if features.contains(.hasTemperature) { 58 | let unit: UnitTemperature = 59 | features.contains(.usesMetricUnits) ? .celsius : .fahrenheit 60 | Convert(\.temperature) { 61 | $0.converted(to: unit).cast(Float.self) 62 | } 63 | } 64 | if features.contains(.hasHumidity) { 65 | Convert(\.humidity) { 66 | Double($0) / 100 67 | } writing: { 68 | UInt8($0 * 100) 69 | } 70 | } 71 | } 72 | 73 | CRC32.default 74 | } 75 | .endianness(.big) 76 | } 77 | 78 | } 79 | 80 | -------------------------------------------------------------------------------- /Tests/DataKitTests/Readme/ReadmeExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import DataKit 9 | 10 | extension WeatherStationUpdate { 11 | 12 | init(temperature: Double? = nil, unit: UnitTemperature = .fahrenheit, humidity: Double? = nil) { 13 | self.init( 14 | features: [ 15 | temperature != nil ? .hasTemperature : [], 16 | humidity != nil ? .hasHumidity : [], 17 | unit != .fahrenheit ? .usesMetricUnits : [], 18 | ], 19 | temperature: .init(value: temperature ?? .nan, unit: unit), 20 | humidity: humidity ?? .nan 21 | ) 22 | } 23 | 24 | } 25 | 26 | extension WeatherStationUpdate: Equatable { 27 | 28 | static func == (lhs: WeatherStationUpdate, rhs: WeatherStationUpdate) -> Bool { 29 | guard lhs.features.rawValue == rhs.features.rawValue else { 30 | return false 31 | } 32 | if lhs.features.contains(.hasTemperature) { 33 | guard lhs.temperature.unit == rhs.temperature.unit && lhs.temperature.value == rhs.temperature.value else { 34 | return false 35 | } 36 | } else { 37 | guard lhs.temperature.value.isNaN && rhs.temperature.value.isNaN else { 38 | return false 39 | } 40 | } 41 | if lhs.features.contains(.hasHumidity) { 42 | guard lhs.humidity == rhs.humidity else { 43 | return false 44 | } 45 | } else { 46 | guard lhs.humidity.isNaN && rhs.humidity.isNaN else { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Tests/DataKitTests/Readme/ReadmeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 26.07.23. 6 | // 7 | 8 | import DataKit 9 | import Foundation 10 | import XCTest 11 | 12 | final class ReadmeTests: XCTestCase { 13 | 14 | // MARK: Stored Properties 15 | 16 | private let allFeaturesValues = (0...0b111).map(WeatherStationFeatures.init(rawValue:)) 17 | 18 | // MARK: Methods 19 | 20 | func testReadWriting() throws { 21 | let expectedCodings: [(WeatherStationUpdate, Data)] = [ 22 | (.init(), Data([0x02, 0x00, 0x73, 0xEF, 0x70, 0x7D])), 23 | (.init(unit: .celsius), Data([0x02, 0x04, 0x74, 0x82, 0xB4, 0x64])), 24 | (.init(temperature: 32), Data([0x02, 0x01, 0x42, 0x00, 0x00, 0x00, 0xF0, 0x77, 0xB9, 0xAE])), 25 | (.init(humidity: 0.5), Data([0x02, 0x02, 0x32, 0x06, 0x24, 0x3E, 0x7E])), 26 | (.init(temperature: 0, unit: .celsius), Data([0x02, 0x05, 0x00, 0x00, 0x00, 0x00, 0x34, 0xEA, 0x8F, 0xD8])), 27 | (.init(temperature: 32, humidity: 0.5), Data([0x02, 0x03, 0x42, 0x00, 0x00, 0x00, 0x32, 0x66, 0x83, 0xE6, 0x50])), 28 | (.init(temperature: 0, unit: .celsius, humidity: 0.5), Data([0x02, 0x07, 0x00, 0x00, 0x00, 0x00, 0x32, 0xDF, 0x21, 0xAF, 0x6F])), 29 | ] 30 | 31 | for (expectedValue, expectedData) in expectedCodings { 32 | let writtenValue = try expectedValue.write() 33 | XCTAssertEqual(writtenValue, expectedData, "\(writtenValue.byteArrayDescription) != \(expectedData.byteArrayDescription)") 34 | let actualData = expectedValue.data 35 | XCTAssertEqual(actualData, expectedData, "\(actualData.byteArrayDescription) != \(expectedData.byteArrayDescription)") 36 | 37 | XCTAssertNoThrow { 38 | let readValue = try WeatherStationUpdate(expectedData) 39 | XCTAssertEqual(readValue, expectedValue) 40 | } 41 | 42 | let wrongPrefixData = expectedData.copy { $0[0] = 0x03 } 43 | XCTAssertNotEqual(wrongPrefixData, expectedData) 44 | XCTAssertThrowsError(try WeatherStationUpdate(wrongPrefixData)) { error in 45 | XCTAssert(error is UnexpectedValueError) 46 | } 47 | 48 | let wrongChecksumData = expectedData.copy { $0[$0.endIndex - 2] &+= 1 } 49 | XCTAssertNotEqual(wrongChecksumData, expectedData) 50 | XCTAssertThrowsError(try WeatherStationUpdate(wrongChecksumData)) { error in 51 | XCTAssert(error is VerificationError) 52 | } 53 | } 54 | } 55 | 56 | func testCompareWriteData() throws { 57 | for features in allFeaturesValues { 58 | let message = WeatherStationUpdate( 59 | features: features, 60 | temperature: .init(value: 15, unit: .celsius), 61 | humidity: 0.6 62 | ) 63 | 64 | XCTAssertEqual(message.data, try message.write()) 65 | } 66 | } 67 | 68 | } 69 | --------------------------------------------------------------------------------