├── .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 | 
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