Skip to content

Commit 246d886

Browse files
committed
Use a higher-level protocol for encoding/decoding.
Motivation: Right now we disambiguate the multiple top-level SH types by using three separate entry points on the encoder/decoder. This isn't great: it forces that information to be known statically at compile time, making it much harder to write generic APIs that use structured header fields. Modifications: - Provide a new protocol, `StructuredHeaderField`, that must be used for the top-level `Codable` object. - Provide `StructuredHeaderFieldType` to communicate the types of structured header fields. - Remove the multiple entry points. - Update the tests. Result: Types will encode what kind of structured header field they are.
1 parent 101dec0 commit 246d886

File tree

4 files changed

+234
-144
lines changed

4 files changed

+234
-144
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,25 @@ In most cases users should prefer to use `CodableStructuredHeaders` unless they
4141
```swift
4242
let field = Array("Sec-CH-Example, Sec-CH-Example-2".utf8)
4343

44+
struct AcceptCH: StructuredHeaderField {
45+
static let structuredFiedType: StructuredHeaderFieldType = .list
46+
47+
var items: [String]
48+
}
49+
4450
// Decoding
4551
let decoder = StructuredFieldDecoder()
46-
let parsed = try decoder.decodeListField([String].self, from: field)
52+
let parsed = try decoder.decode(AcceptCH.self, from: field)
4753

4854
// Encoding
4955
let encoder = StructuredFieldEncoder()
50-
let serialized = try encoder.encodeListField(["Sec-CH-Example", "Sec-CH-Example-2"])
56+
let serialized = try encoder.encode(AcceptCH(items: ["Sec-CH-Example", "Sec-CH-Example-2"]))
5157
```
5258

5359
However, structured header fields can be substantially more complex. Structured header fields can make use of 4 containers and 6 base item types. The containers are:
5460

55-
1. Dictionaries. These are top-level elements and associate token keys with values. The values may be items, or may be inner lists, and each value may also have parameters associated with them. `CodableStructuredHeaders` can model dictionaries as either Swift objects (where the property names are dictionary keys), or as Swift dictionaries.
56-
2. Lists. These are top-level elements, providing a sequence of items or inner lists. Each item or inner list may have parameters associated with them. `CodableStructuredHeaders` models these as Swift `Array`s where the entries are items of some kind.
61+
1. Dictionaries. These are top-level elements and associate token keys with values. The values may be items, or may be inner lists, and each value may also have parameters associated with them. `CodableStructuredHeaders` can model dictionaries as either Swift objects (where the property names are dictionary keys).
62+
2. Lists. These are top-level elements, providing a sequence of items or inner lists. Each item or inner list may have parameters associated with them. `CodableStructuredHeaders` models these as Swift objects with one key, `items`, that must be a collection of entries.
5763
3. Inner Lists. These are lists that may be sub-entries of a dictionary or a list. The list entries are items, which may have parameters associated with them: additionally, an inner list may have parameters associated with itself as well. `CodableStructuredHeaders` models these as either Swift `Array`s _or_, if it's important to extract parameters, as a two-field Swift `struct` where one field is called `items` and contains an `Array`, and other field is called `parameters` and contains a dictionary.
5864
4. Parameters. Parameters associated token keys with items without parameters. These are used to store metadata about objects within a field. `CodableStructuredHeaders` models these as either Swift objects (where the property names are the parameter keys) or as Swift dictionaries.
5965

@@ -68,6 +74,8 @@ The base types are:
6874

6975
For any Structured Header Field Item, the item may either be represented directly by the appropriate type, or by a Swift struct with two properties: `item` and `parameters`. This latter mode is how parameters on a given item may be captured.
7076

77+
The top-level structured header field must identify what kind of header field it corresponds to: `.item`, `.list`, or `.dictionary`. This is inherent in the type of the field and will be specified in the relevant field specification.
78+
7179
## Lower Levels
7280

7381
In some cases the Codable interface will not be either performant enough or powerful enough for the intended use-case. In cases like this, users can use the types in the `StructuredHeaders` module instead.

Sources/CodableStructuredHeaders/Encoder/StructuredFieldEncoder.swift

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,30 @@ extension StructuredFieldEncoder {
3838
}
3939

4040
extension StructuredFieldEncoder {
41+
/// Attempt to encode an object into a structured header field.
42+
///
43+
/// - parameters:
44+
/// - data: The object to encode.
45+
/// - throws: If the header field could not be encoded, or could not be serialized.
46+
/// - returns: The bytes representing the HTTP structured header field.
47+
public func encode<StructuredField: StructuredHeaderField>(_ data: StructuredField) throws -> [UInt8] {
48+
switch StructuredField.structuredFieldType {
49+
case .item:
50+
return try self.encodeItemField(data)
51+
case .list:
52+
return try self.encodeListField(data)
53+
case .dictionary:
54+
return try self.encodeDictionaryField(data)
55+
}
56+
}
57+
4158
/// Attempt to encode an object into a structured header dictionary field.
4259
///
4360
/// - parameters:
4461
/// - data: The object to encode.
4562
/// - throws: If the header field could not be encoded, or could not be serialized.
4663
/// - returns: The bytes representing the HTTP structured header field.
47-
public func encodeDictionaryField<StructuredField: Encodable>(_ data: StructuredField) throws -> [UInt8] {
64+
private func encodeDictionaryField<StructuredField: Encodable>(_ data: StructuredField) throws -> [UInt8] {
4865
let serializer = StructuredFieldSerializer()
4966
let encoder = _StructuredFieldEncoder(serializer, keyEncodingStrategy: self.keyEncodingStrategy)
5067
return try encoder.encodeDictionaryField(data)
@@ -56,7 +73,7 @@ extension StructuredFieldEncoder {
5673
/// - data: The object to encode.
5774
/// - throws: If the header field could not be encoded, or could not be serialized.
5875
/// - returns: The bytes representing the HTTP structured header field.
59-
public func encodeListField<StructuredField: Encodable>(_ data: StructuredField) throws -> [UInt8] {
76+
private func encodeListField<StructuredField: Encodable>(_ data: StructuredField) throws -> [UInt8] {
6077
let serializer = StructuredFieldSerializer()
6178
let encoder = _StructuredFieldEncoder(serializer, keyEncodingStrategy: self.keyEncodingStrategy)
6279
return try encoder.encodeListField(data)
@@ -68,7 +85,7 @@ extension StructuredFieldEncoder {
6885
/// - data: The object to encode.
6986
/// - throws: If the header field could not be encoded, or could not be serialized.
7087
/// - returns: The bytes representing the HTTP structured header field.
71-
public func encodeItemField<StructuredField: Encodable>(_ data: StructuredField) throws -> [UInt8] {
88+
private func encodeItemField<StructuredField: Encodable>(_ data: StructuredField) throws -> [UInt8] {
7289
let serializer = StructuredFieldSerializer()
7390
let encoder = _StructuredFieldEncoder(serializer, keyEncodingStrategy: self.keyEncodingStrategy)
7491
return try encoder.encodeItemField(data)
@@ -635,7 +652,7 @@ extension _StructuredFieldEncoder {
635652
try self.encode(value, forKey: key)
636653
default:
637654
// Ok, we don't know what this is. This can only happen for a dictionary, or
638-
// for anything with parameters, or for inner lists.
655+
// for anything with parameters, or for lists, or for inner lists.
639656
switch self.currentStackEntry.storage {
640657
case .dictionaryHeader:
641658
// Ah, this is a dictionary, good to know. Initialize the storage, keep going.
@@ -686,7 +703,18 @@ extension _StructuredFieldEncoder {
686703
throw StructuredHeaderError.invalidTypeForItem
687704
}
688705

689-
case .listHeader, .list, .itemHeader, .bareInnerList,
706+
case .listHeader:
707+
switch key {
708+
case "items":
709+
// Ok, we're a list. Good to know.
710+
self.push(key: .init(stringValue: key), newStorage: .list([]))
711+
try value.encode(to: self)
712+
try self.pop()
713+
default:
714+
throw StructuredHeaderError.invalidTypeForItem
715+
}
716+
717+
case .list, .itemHeader, .bareInnerList,
690718
.parameters:
691719
throw StructuredHeaderError.invalidTypeForItem
692720
}
@@ -794,6 +822,9 @@ extension _StructuredFieldEncoder {
794822
map[key.stringValue] = .item(Item(item))
795823
self = .dictionary(map)
796824

825+
case (.listHeader, .list(let list)) where key.stringValue == "items":
826+
self = .list(list)
827+
797828
case (.listHeader, .innerList(let innerList)) where key.intValue != nil:
798829
self = .list([.innerList(innerList)])
799830

Tests/StructuredHeadersTests/StructuredFieldDecoderTests.swift

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct ListyDictionaryParameterisedList: Codable, Equatable {
3131
var parameters: ListyDictionaryFieldParameters
3232
}
3333

34-
fileprivate struct Item<Base: Codable & Equatable>: StructuredHeaderField, Equatable {
34+
struct ItemField<Base: Codable & Equatable>: StructuredHeaderField, Equatable {
3535
static var structuredFieldType: StructuredHeaderFieldType {
3636
return .item
3737
}
@@ -43,7 +43,7 @@ fileprivate struct Item<Base: Codable & Equatable>: StructuredHeaderField, Equat
4343
}
4444
}
4545

46-
fileprivate struct List<Base: Codable & Equatable>: StructuredHeaderField, Equatable {
46+
struct List<Base: Codable & Equatable>: StructuredHeaderField, Equatable {
4747
static var structuredFieldType: StructuredHeaderFieldType {
4848
return .list
4949
}
@@ -55,6 +55,26 @@ fileprivate struct List<Base: Codable & Equatable>: StructuredHeaderField, Equat
5555
}
5656
}
5757

58+
struct DictionaryField<Key: Codable & Hashable, Value: Codable & Equatable>: StructuredHeaderField, Equatable {
59+
static var structuredFieldType: StructuredHeaderFieldType {
60+
return .dictionary
61+
}
62+
63+
var items: [Key: Value]
64+
65+
init(_ items: [Key: Value]) {
66+
self.items = items
67+
}
68+
69+
func encode(to encoder: Encoder) throws {
70+
try self.items.encode(to: encoder)
71+
}
72+
73+
init(from decoder: Decoder) throws {
74+
self.items = try .init(from: decoder)
75+
}
76+
}
77+
5878
/// An example ListyDictionary structured header field.
5979
///
6080
/// An example of this field is: 'primary=bar;q=1.0, secondary=baz;q=0.5;fallback=last, acceptablejurisdictions=(AU;q=1.0 GB;q=0.9 FR);fallback=primary'
@@ -95,16 +115,16 @@ final class StructuredFieldDecoderTests: XCTestCase {
95115
func testCanDecodeIntegersInVariousWays() throws {
96116
let headerField = "5;bar=baz"
97117

98-
XCTAssertEqual(Item(UInt8(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
99-
XCTAssertEqual(Item(Int8(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
100-
XCTAssertEqual(Item(UInt16(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
101-
XCTAssertEqual(Item(Int16(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
102-
XCTAssertEqual(Item(UInt32(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
103-
XCTAssertEqual(Item(Int32(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
104-
XCTAssertEqual(Item(UInt64(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
105-
XCTAssertEqual(Item(Int64(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
106-
XCTAssertEqual(Item(UInt(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
107-
XCTAssertEqual(Item(Int(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
118+
XCTAssertEqual(ItemField(UInt8(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
119+
XCTAssertEqual(ItemField(Int8(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
120+
XCTAssertEqual(ItemField(UInt16(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
121+
XCTAssertEqual(ItemField(Int16(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
122+
XCTAssertEqual(ItemField(UInt32(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
123+
XCTAssertEqual(ItemField(Int32(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
124+
XCTAssertEqual(ItemField(UInt64(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
125+
XCTAssertEqual(ItemField(Int64(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
126+
XCTAssertEqual(ItemField(UInt(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
127+
XCTAssertEqual(ItemField(Int(5)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
108128
}
109129

110130
func testOutOfRangeNumbersAreReported() throws {
@@ -113,41 +133,41 @@ final class StructuredFieldDecoderTests: XCTestCase {
113133
// rather than crashing for all non-Int64 types. (Ignoring Int/UInt due to their platform
114134
// dependence)
115135
let headerField = "-999999999999999;bar=baz"
116-
let expected = Item(Int64(-999_999_999_999_999))
117-
118-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int8>.self, from: Array(headerField.utf8)))
119-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt8>.self, from: Array(headerField.utf8)))
120-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int16>.self, from: Array(headerField.utf8)))
121-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt16>.self, from: Array(headerField.utf8)))
122-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int32>.self, from: Array(headerField.utf8)))
123-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt32>.self, from: Array(headerField.utf8)))
124-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt64>.self, from: Array(headerField.utf8)))
136+
let expected = ItemField(Int64(-999_999_999_999_999))
137+
138+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int8>.self, from: Array(headerField.utf8)))
139+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt8>.self, from: Array(headerField.utf8)))
140+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int16>.self, from: Array(headerField.utf8)))
141+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt16>.self, from: Array(headerField.utf8)))
142+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int32>.self, from: Array(headerField.utf8)))
143+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt32>.self, from: Array(headerField.utf8)))
144+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt64>.self, from: Array(headerField.utf8)))
125145
XCTAssertEqual(expected, try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
126146
}
127147

128148
func testDoubleAndFloatInterchangeable() throws {
129149
let headerField = "5.0;bar=baz"
130150

131-
XCTAssertEqual(Item(Float(5.0)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
132-
XCTAssertEqual(Item(Double(5.0)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
151+
XCTAssertEqual(ItemField(Float(5.0)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
152+
XCTAssertEqual(ItemField(Double(5.0)), try StructuredFieldDecoder().decode(from: Array(headerField.utf8)))
133153
}
134154

135155
func testAskingForTheWrongType() throws {
136156
let headerField = "gzip"
137157
let intField = "5"
138158

139-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int8>.self, from: Array(headerField.utf8)))
140-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt8>.self, from: Array(headerField.utf8)))
141-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int16>.self, from: Array(headerField.utf8)))
142-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt16>.self, from: Array(headerField.utf8)))
143-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int32>.self, from: Array(headerField.utf8)))
144-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt32>.self, from: Array(headerField.utf8)))
145-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Int64>.self, from: Array(headerField.utf8)))
146-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<UInt64>.self, from: Array(headerField.utf8)))
147-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Double>.self, from: Array(headerField.utf8)))
148-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Float>.self, from: Array(headerField.utf8)))
149-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<Bool>.self, from: Array(headerField.utf8)))
150-
XCTAssertThrowsError(try StructuredFieldDecoder().decode(Item<String>.self, from: Array(intField.utf8)))
159+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int8>.self, from: Array(headerField.utf8)))
160+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt8>.self, from: Array(headerField.utf8)))
161+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int16>.self, from: Array(headerField.utf8)))
162+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt16>.self, from: Array(headerField.utf8)))
163+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int32>.self, from: Array(headerField.utf8)))
164+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt32>.self, from: Array(headerField.utf8)))
165+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Int64>.self, from: Array(headerField.utf8)))
166+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<UInt64>.self, from: Array(headerField.utf8)))
167+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Double>.self, from: Array(headerField.utf8)))
168+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Float>.self, from: Array(headerField.utf8)))
169+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<Bool>.self, from: Array(headerField.utf8)))
170+
XCTAssertThrowsError(try StructuredFieldDecoder().decode(ItemField<String>.self, from: Array(intField.utf8)))
151171
}
152172

153173
func testDecodingTopLevelItemWithParameters() throws {
@@ -227,7 +247,7 @@ final class StructuredFieldDecoderTests: XCTestCase {
227247
func testDecodingBinaryAsTopLevelData() throws {
228248
let headerField = ":AQIDBA==:"
229249
XCTAssertEqual(
230-
Item(Data([1, 2, 3, 4])),
250+
ItemField(Data([1, 2, 3, 4])),
231251
try StructuredFieldDecoder().decode(from: Array(headerField.utf8))
232252
)
233253
}
@@ -320,7 +340,7 @@ final class StructuredFieldDecoderTests: XCTestCase {
320340
func testDecodingDecimalAsTopLevelData() throws {
321341
let headerField = "987654321.123"
322342
XCTAssertEqual(
323-
Item(Decimal(string: "987654321.123")!),
343+
ItemField(Decimal(string: "987654321.123")!),
324344
try StructuredFieldDecoder().decode(from: Array(headerField.utf8))
325345
)
326346
}

0 commit comments

Comments
 (0)