Skip to content

Commit dcf9990

Browse files
authored
Add printer (#11)
This PR allows macros to generate `Printable` conformance for `struct` and `enum`. Any `Printable` can be printed by a `Printer`. For instance ```swift @ParseStruct struct Header { ... } let header = Header(parsing: [...]) let bytes: [UInt8] = header.printParsed(printer: .byteArray) let hexString: String = header.printParsed(printer: .hexString()) let data: Data = header.printParsed(printer: .data) ```
1 parent e7b80b0 commit dcf9990

29 files changed

+2137
-59
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A declarative Swift package for parsing binary data using macros, built on top o
1515
- **Custom byte counts**: Parse types with specific byte lengths
1616
- **Remaining data parsing**: Parse all remaining bytes in a buffer
1717
- **Enum parsing**: Parse enums with pattern matching and associated values
18+
- **Parser is printer**: Serialize parsed structures back into binary data
1819

1920
## Requirements
2021

@@ -303,12 +304,14 @@ If you encounter any issues, have feature requests, or want to suggest improveme
303304

304305
### Future Directions
305306

306-
Some areas we're considering for future development:
307+
Roadmap:
307308

308-
- **More convenient APIs** - Shorter syntax and better autocomplete support, such as merging `ParseStruct` and `ParseEnum`
309-
- **Advanced validation** - Runtime validation of parsing constraints, such as require minimal byte size checking in front instead of at each parsing
310-
- **Performance optimizations** - Further optimization of generated parsing code, such as linear time enum matching for constant bytes provided (`O( max(n, m) )` instead of `O(n * m)`, where `n` is the number of cases and `m` is the max number of bytes provided for each case)
311-
- **Porting to prio iOS/macOS 26** - because `Span` is introduced only in iOS/macOS 26, port of using `withUnsafePointer` can be provided to prior versions of OSes for better compatibility
309+
- [x] Parsers as printer
310+
- [ ] Porting to prior iOS 18/macOS 15: Because `Span` is introduced only in iOS 18/macOS 15, port of using `withUnsafePointer` can be provided to prior versions of OSes for better compatibility
311+
- [ ] Bitmask support
312+
- [ ] Advanced validation: Runtime validation of parsing constraints, such as require minimal byte size checking in front instead of at each parsing
313+
- [ ] Performance optimizations: Further optimization of generated parsing code, such as linear time enum matching for constant bytes provided (`O( max(n, m) )` instead of `O(n * m)`, where `n` is the number of cases and `m` is the max number of bytes provided for each case)
314+
- [ ] API improvements: Shorter syntax and better autocomplete support, such as merging `ParseStruct` and `ParseEnum`
312315

313316
Your feedback on these directions and other ideas is highly appreciated!
314317

Sources/BinaryParseKit/BinaryParseKit.swift

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public import BinaryParsing
1515
/// - byteCount: The number of bytes to skip
1616
/// - because: A descriptive reason for skipping these bytes (used for documentation)
1717
///
18-
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
18+
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
1919
///
2020
/// Example:
2121
/// ```swift
@@ -37,12 +37,12 @@ public macro skip(byteCount: ByteCount, because: String) = #externalMacro(
3737

3838
// MARK: - Field Parsing
3939

40-
/// Parses a field using the type's default `Parsable` implementation.
40+
/// Parses a field that conforms to ``Parsable``.
4141
///
4242
/// This macro marks a field for parsing using the type's built-in parsing behavior.
43-
/// The field type must conform to `Parsable` protocol.
43+
/// The field type must conform to ``Parsable`` protocol.
4444
///
45-
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
45+
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
4646
///
4747
/// Example:
4848
/// ```swift
@@ -58,11 +58,11 @@ public macro parse() = #externalMacro(module: "BinaryParseKitMacros", type: "Emp
5858
/// Parses a field with a specific endianness.
5959
///
6060
/// Use this macro when you need to parse a field with a specific byte order
61-
/// (big-endian or little-endian). The field type must conform to `EndianParsable`.
61+
/// (big-endian or little-endian). The field type must conform to ``EndianParsable``.
6262
///
63-
/// - Parameter endianness: The byte order to use for parsing (`.big` or `.little`)
63+
/// - Parameter endianness: The byte order to use for parsing (``Endianness/big`` or ``Endianness/little``)
6464
///
65-
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
65+
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
6666
///
6767
/// Example:
6868
/// ```swift
@@ -78,14 +78,14 @@ public macro parse() = #externalMacro(module: "BinaryParseKitMacros", type: "Emp
7878
@attached(peer)
7979
public macro parse(endianness: Endianness) = #externalMacro(module: "BinaryParseKitMacros", type: "EmptyPeerMacro")
8080

81-
/// Parses a field with a specific byte count.
81+
/// Parses a field that conforms to ``SizedParsable`` with a specific byte count.
8282
///
8383
/// Use this macro when you need to parse a field using a specific number of bytes.
84-
/// The field type must conform to `SizedParsable`.
84+
/// The field type must conform to ``SizedParsable``.
8585
///
8686
/// - Parameter byteCount: The number of bytes to read for this field
8787
///
88-
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
88+
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
8989
///
9090
/// Example:
9191
/// ```swift
@@ -104,15 +104,16 @@ public macro parse(byteCount: ByteCount) = #externalMacro(
104104
type: "EmptyPeerMacro",
105105
)
106106

107-
/// Parses a field with a byte count determined by another field's value.
107+
/// Parses a field that conforms to ``SizedParsable`` with a byte count determined by another field's value
108108
///
109109
/// Use this macro to create variable-length fields where the length is specified
110-
/// by a previously parsed field. The field type must conform to `SizedParsable`.
110+
/// by a previously parsed field. The field type must conform to ``SizedParsable``.
111111
///
112112
/// - Parameter byteCountOf: A KeyPath to another field whose value determines the byte count
113113
///
114-
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
115-
/// The referenced field must be parsed before this field.
114+
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
115+
///
116+
/// - Important: The referenced field in `byteCountOf` must be parsed before this field.
116117
///
117118
/// Example:
118119
/// ```swift
@@ -131,10 +132,10 @@ public macro parse<R, V: BinaryInteger>(byteCountOf: KeyPath<R, V>) = #externalM
131132
type: "EmptyPeerMacro",
132133
)
133134

134-
/// Parses a field with both specific byte count and endianness.
135+
/// Parses a field that conforms to ``EndianSizedParsable`` with both specific byte count and endianness.
135136
///
136137
/// Use this macro when you need precise control over both the number of bytes
137-
/// and the byte order for parsing. The field type must conform to `EndianSizedParsable`.
138+
/// and the byte order for parsing. The field type must conform to ``EndianSizedParsable``.
138139
///
139140
/// - Parameters:
140141
/// - byteCount: The number of bytes to read for this field
@@ -156,17 +157,18 @@ public macro parse(byteCount: ByteCount, endianness: Endianness) = #externalMacr
156157
type: "EmptyPeerMacro",
157158
)
158159

159-
/// Parses a field with byte count from another field and specific endianness.
160+
/// Parses a field that conforms to ``EndianSizedParsable`` with byte count from another field and specific endianness.
160161
///
161162
/// Combines variable-length parsing with endianness control. The field type
162-
/// must conform to `EndianSizedParsable`.
163+
/// must conform to ``EndianSizedParsable``.
163164
///
164165
/// - Parameters:
165166
/// - byteCountOf: A KeyPath to another field whose value determines the byte count
166-
/// - endianness: The byte order to use for parsing (`.big` or `.little`)
167+
/// - endianness: The byte order to use for parsing (``Endianness/big`` or ``Endianness/little``)
167168
///
168-
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
169-
/// The referenced field must be parsed before this field.
169+
/// - Note: This macro has no effect on its own unless used alongside `@ParseStruct` on struct fields.
170+
///
171+
/// - Important: The referenced field in `byteCountOf` must be parsed before this field.
170172
///
171173
/// Example:
172174
/// ```swift
@@ -189,10 +191,11 @@ public macro parse<R, V: BinaryInteger>(byteCountOf: KeyPath<R, V>, endianness:
189191
///
190192
/// Use this macro to parse all remaining bytes from the current position to the end
191193
/// of the data. This is typically used for the last field in a structure.
192-
/// The field type must conform to `SizedParsable`.
194+
/// The field type must conform to ``SizedParsable``.
193195
///
194196
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
195-
/// Only one `@parseRest` field is allowed per struct, and it must be the last field.
197+
///
198+
/// - Note: Only one `@parseRest` field is allowed per struct, and it must be the last field.
196199
///
197200
/// Example:
198201
/// ```swift
@@ -214,12 +217,13 @@ public macro parseRest() = #externalMacro(
214217
/// Parses all remaining bytes with a specific endianness.
215218
///
216219
/// Like `parseRest()`, but applies endianness conversion to the remaining data.
217-
/// The field type must conform to `EndianSizedParsable`.
220+
/// The field type must conform to ``EndianSizedParsable``.
218221
///
219-
/// - Parameter endianness: The byte order to use for parsing (`.big` or `.little`)
222+
/// - Parameter endianness: The byte order to use for parsing (``Endianness/big`` or ``Endianness/little``)
220223
///
221224
/// - Note: This macro must be used alongside `@ParseStruct` on struct fields.
222-
/// Only one `@parseRest` field is allowed per struct, and it must be the last field.
225+
///
226+
/// - Note: Only one `@parseRest` field is allowed per struct, and it must be the last field.
223227
///
224228
/// Example:
225229
/// ```swift
@@ -240,16 +244,16 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
240244

241245
// MARK: - Struct Parsing
242246

243-
/// Generates a `Parsable` implementation for a struct with annotated fields.
247+
/// Generates a ``Parsable`` implementation for a struct with annotated fields.
244248
///
245249
/// This macro analyzes the struct's fields marked with `@parse`, `@skip`, and `@parseRest`
246250
/// macros and generates the necessary parsing code to read binary data into the struct.
247251
///
248252
/// The generated code includes:
249-
/// - A `Parsable` conformance
250-
/// - An initializer that reads from a `ParserSpan`
253+
/// - A ``Parsable`` conformance
251254
/// - Type validation functions to ensure fields conform to required protocols
252255
/// - Proper error handling for parsing failures
256+
/// - A ``Printable`` conformance
253257
///
254258
/// - Note: All fields except those with accessors (`get` and `set`) must be parsed must be marked with `@parse`
255259
/// variants.
@@ -273,28 +277,29 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
273277
/// let data = Data([0x89, 0x50, 0x4E, 0x47, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00])
274278
/// let header = try FileHeader(parsing: data)
275279
/// ```
276-
@attached(extension, conformances: BinaryParseKit.Parsable, names: arbitrary)
280+
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
277281
public macro ParseStruct() = #externalMacro(
278282
module: "BinaryParseKitMacros",
279283
type: "ConstructStructParseMacro",
280284
)
281285

282286
// MARK: - Parse Enum
283287

284-
/// Generates a `Parsable` implementation for an enum with annotated cases.
288+
/// Generates a ``Parsable`` implementation for an enum with annotated cases.
285289
///
286290
/// This macro analyzes the enum's cases marked with `@match`, `@matchAndTake`, and `@matchDefault` with optional
287291
/// associated values,
288292
/// whose parsing can be specified using ``parse(byteCount:)`` and ``skip(byteCount:because:)`` macros.
289293
///
290294
/// The generated code includes:
291-
/// - A `Parsable` conformance
295+
/// - A ``Parsable`` conformance
296+
/// - A ``Printable`` conformance
292297
///
293298
/// - Note: All enum cases must be marked with `@match` variants, which is intentional by design, which I don't think is
294299
/// necessary and is possible to be lifted int the future.
295300
/// - Note: Only one `@matchDefault` case is allowed per enum, and has to be declared at the end of all other cases.
296301
/// - Note: any `match` macro has to proceed `parse` and `skip` macros.
297-
@attached(extension, conformances: BinaryParseKit.Parsable, names: arbitrary)
302+
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
298303
public macro ParseEnum() = #externalMacro(
299304
module: "BinaryParseKitMacros",
300305
type: "ConstructEnumParseMacro",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Custom Printer
2+
3+
This articles describes how to implement a custom printer by conforming to the `Printer` protocol.
4+
5+
## Printer Protocol
6+
7+
The ``Printer`` protocols requires implementing a single method ``Printer/print(_:)-(PrinterIntel)`` that takes in a ``PrinterIntel`` and outputs ``Printer/PrinterOutput``.
8+
9+
``PrinterIntel`` provides context information about the parsed object and is in a recursive structure. There are the following cases:
10+
11+
- ``PrinterIntel/builtIn(_:)`` with ``PrinterIntel/BuiltInPrinterIntel``: for built-in types like `UInt8`, `Int32`, or user defined types that are not marked with `@ParseStruct` or `@ParseEnum`.
12+
- ``PrinterIntel/skip(_:)`` with ``PrinterIntel/SkipPrinterIntel``: for skipped bytes.
13+
- ``PrinterIntel/struct(_:)`` with ``PrinterIntel/StructPrinterIntel``: for types marked with `@ParseStruct`.
14+
- ``PrinterIntel/enum(_:)`` with ``PrinterIntel/EnumCasePrinterIntel``: for types marked with `@ParseEnum`.

0 commit comments

Comments
 (0)