Skip to content

Commit c574960

Browse files
committed
Use a simpler metadata decoding approach
1 parent 060141d commit c574960

File tree

7 files changed

+226
-116
lines changed

7 files changed

+226
-116
lines changed

Modules/Sources/Networking/Mapper/MetaDataMapper.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,6 @@ struct MetaDataMapper: Mapper {
2121
return metadata.filter { !$0.key.hasPrefix("_") }
2222
}
2323

24-
/// Decodes MetaData from a KeyedDecodingContainer with flexible support for both array and dictionary formats
25-
/// - Parameters:
26-
/// - container: The KeyedDecodingContainer to decode from
27-
/// - key: The key for the metadata field
28-
/// - filterInternalKeys: Whether to filter out keys that start with "_" (default: true)
29-
/// - Returns: Array of MetaData objects
30-
static func decodeMetaData<K>(from container: KeyedDecodingContainer<K>,
31-
forKey key: KeyedDecodingContainer<K>.Key,
32-
filterInternalKeys: Bool = true) -> [MetaData] {
33-
let metadata: [MetaData] = {
34-
// Try to decode as array first (standard format)
35-
if let metaDataArray = try? container.decode([MetaData].self, forKey: key) {
36-
return metaDataArray
37-
}
38-
39-
// Try to decode as object keyed by index strings – this may happen when plugins break the response format
40-
if let metaDataDict = try? container.decode([String: MetaData].self, forKey: key) {
41-
return Array(metaDataDict.values)
42-
}
43-
44-
return []
45-
}()
46-
47-
return filterInternalKeys ? metadata.filter { !$0.key.hasPrefix("_") } : metadata
48-
}
4924

5025
}
5126

@@ -65,9 +40,4 @@ private struct MetaDataEnvelope: Decodable {
6540
private enum CodingKeys: String, CodingKey {
6641
case metadata = "meta_data"
6742
}
68-
69-
init(from decoder: Decoder) throws {
70-
let container = try decoder.container(keyedBy: CodingKeys.self)
71-
self.metadata = MetaDataMapper.decodeMetaData(from: container, forKey: .metadata, filterInternalKeys: false)
72-
}
7343
}

Modules/Sources/Networking/Model/Product/Product.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import NetworkingCore
23
import CocoaLumberjackSwift
34
import Codegen
45

@@ -535,8 +536,9 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
535536
let menuOrder = try container.decode(Int.self, forKey: .menuOrder)
536537

537538
// Filter out metadata if the key is prefixed with an underscore (internal meta keys)
538-
// Support both array format and object keyed by index strings via MetaDataMapper
539-
let customFields = MetaDataMapper.decodeMetaData(from: container, forKey: .metadata)
539+
// Support both array format and object keyed by index strings
540+
let flexibleMetaData = try container.decodeIfPresent(FlexibleMetaDataArray.self, forKey: .metadata)
541+
let customFields = flexibleMetaData?.metadata.filter { !$0.key.hasPrefix("_") } ?? []
540542

541543
// In some isolated cases, it appears to be some malformed meta-data that causes this line to throw hence the whole product decoding to throw.
542544
// Since add-ons are optional, `try?` will be used to prevent the whole decoding to stop.

Modules/Sources/Networking/Model/Product/ProductMetadataExtractor.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import WordPressShared
3+
import NetworkingCore
34

45
/// Helper to extract specific data from inside `Product` metadata.
56
/// Sample Json:
@@ -32,12 +33,8 @@ internal struct ProductMetadataExtractor: Decodable {
3233
/// Decode main metadata supporting both array and object formats.
3334
///
3435
init(from decoder: Decoder) throws {
35-
let container = try decoder.container(keyedBy: MetaDataKeys.self)
36-
self.metadata = MetaDataMapper.decodeMetaData(from: container, forKey: .metadata, filterInternalKeys: false)
37-
}
38-
39-
private enum MetaDataKeys: String, CodingKey {
40-
case metadata = "meta_data"
36+
let flexibleMetaData = try FlexibleMetaDataArray(from: decoder)
37+
self.metadata = flexibleMetaData.metadata
4138
}
4239

4340
/// Searches product metadata for subscription data and converts it to a `ProductSubscription` if possible.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
/// Wrapper type for flexible MetaData array decoding that supports both array and dictionary formats
4+
public struct FlexibleMetaDataArray: Decodable {
5+
public let metadata: [MetaData]
6+
7+
public init(from decoder: Decoder) throws {
8+
let container = try decoder.singleValueContainer()
9+
10+
// Try to decode as array first (standard format)
11+
if let metaDataArray = try? container.decode([MetaData].self) {
12+
self.metadata = metaDataArray
13+
return
14+
}
15+
16+
// Try to decode as object keyed by index strings – this may happen when plugins break the response format
17+
if let metaDataDict = try? container.decode([String: MetaData].self) {
18+
self.metadata = Array(metaDataDict.values)
19+
return
20+
}
21+
22+
// Fallback to empty array
23+
self.metadata = []
24+
}
25+
}

Modules/Sources/NetworkingCore/Model/Order.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ public struct Order: Decodable, Sendable, GeneratedCopiable, GeneratedFakeable {
201201
// "payment_url" is only available on stores with version >= 6.4
202202
let paymentURL = try container.decodeIfPresent(URL.self, forKey: .paymentURL)
203203

204-
let allOrderMetaData = try? container.decode([MetaData].self, forKey: .metadata)
204+
let flexibleOrderMetaData = try container.decodeIfPresent(FlexibleMetaDataArray.self, forKey: .metadata)
205+
let allOrderMetaData = flexibleOrderMetaData?.metadata
205206
var chargeID: String? = nil
206207
chargeID = allOrderMetaData?.first(where: { $0.key == "_charge_id" })?.value.stringValue
207208

Modules/Tests/NetworkingTests/Mapper/MetaDataMapperTests.swift

Lines changed: 50 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -50,112 +50,85 @@ final class MetaDataMapperTests: XCTestCase {
5050
XCTAssertEqual(metadata[6], MetaData(metadataID: 7, key: "empty_field", value: ""))
5151
}
5252

53-
/// Tests that MetaDataMapper.decodeMetaData can decode from array format using KeyedDecodingContainer
53+
/// Tests that FlexibleMetaDataArray can decode from array format
5454
///
55-
func test_decodeMetaData_from_KeyedContainer_array_format() throws {
55+
func test_FlexibleMetaDataArray_decodes_from_array_format() throws {
5656
// Given - JSON with metadata as array
5757
let jsonString = """
58-
{
59-
"meta_data": [
60-
{
61-
"id": 1001,
62-
"key": "custom_field_1",
63-
"value": "value1"
64-
},
65-
{
66-
"id": 1002,
67-
"key": "_internal_field",
68-
"value": "internal_value"
69-
},
70-
{
71-
"id": 1003,
72-
"key": "custom_field_2",
73-
"value": "value2"
74-
}
75-
]
76-
}
77-
"""
78-
79-
struct TestObject: Decodable {
80-
let metadata: [MetaData]
81-
82-
private enum CodingKeys: String, CodingKey {
83-
case metadata = "meta_data"
84-
}
85-
86-
init(from decoder: Decoder) throws {
87-
let container = try decoder.container(keyedBy: CodingKeys.self)
88-
self.metadata = MetaDataMapper.decodeMetaData(from: container, forKey: .metadata)
58+
[
59+
{
60+
"id": 1001,
61+
"key": "custom_field_1",
62+
"value": "value1"
63+
},
64+
{
65+
"id": 1002,
66+
"key": "_internal_field",
67+
"value": "internal_value"
68+
},
69+
{
70+
"id": 1003,
71+
"key": "custom_field_2",
72+
"value": "value2"
8973
}
90-
}
74+
]
75+
"""
9176

9277
let data = jsonString.data(using: .utf8)!
9378
let decoder = JSONDecoder()
9479

9580
// When
96-
let testObject = try decoder.decode(TestObject.self, from: data)
81+
let flexibleMetaData = try decoder.decode(FlexibleMetaDataArray.self, from: data)
9782

9883
// Then
99-
XCTAssertEqual(testObject.metadata.count, 2) // Internal field should be filtered out
100-
XCTAssertEqual(testObject.metadata[0].metadataID, 1001)
101-
XCTAssertEqual(testObject.metadata[0].key, "custom_field_1")
102-
XCTAssertEqual(testObject.metadata[0].value.stringValue, "value1")
103-
XCTAssertEqual(testObject.metadata[1].metadataID, 1003)
104-
XCTAssertEqual(testObject.metadata[1].key, "custom_field_2")
105-
XCTAssertEqual(testObject.metadata[1].value.stringValue, "value2")
84+
XCTAssertEqual(flexibleMetaData.metadata.count, 3) // All fields should be present (no internal filtering in wrapper)
85+
XCTAssertEqual(flexibleMetaData.metadata[0].metadataID, 1001)
86+
XCTAssertEqual(flexibleMetaData.metadata[0].key, "custom_field_1")
87+
XCTAssertEqual(flexibleMetaData.metadata[0].value.stringValue, "value1")
88+
XCTAssertEqual(flexibleMetaData.metadata[1].metadataID, 1002)
89+
XCTAssertEqual(flexibleMetaData.metadata[1].key, "_internal_field")
90+
XCTAssertEqual(flexibleMetaData.metadata[1].value.stringValue, "internal_value")
91+
XCTAssertEqual(flexibleMetaData.metadata[2].metadataID, 1003)
92+
XCTAssertEqual(flexibleMetaData.metadata[2].key, "custom_field_2")
93+
XCTAssertEqual(flexibleMetaData.metadata[2].value.stringValue, "value2")
10694
}
10795

108-
/// Tests that MetaDataMapper.decodeMetaData can decode from dictionary format using KeyedDecodingContainer
96+
/// Tests that FlexibleMetaDataArray can decode from dictionary format
10997
///
110-
func test_decodeMetaData_from_KeyedContainer_dictionary_format() throws {
98+
func test_FlexibleMetaDataArray_decodes_from_dictionary_format() throws {
11199
// Given - JSON with metadata as object keyed by index strings
112100
let jsonString = """
113101
{
114-
"meta_data": {
115-
"0": {
116-
"id": 2001,
117-
"key": "dict_field_1",
118-
"value": "dict_value1"
119-
},
120-
"1": {
121-
"id": 2002,
122-
"key": "_internal_dict_field",
123-
"value": "internal_dict_value"
124-
},
125-
"2": {
126-
"id": 2003,
127-
"key": "dict_field_2",
128-
"value": "dict_value2"
129-
}
102+
"0": {
103+
"id": 2001,
104+
"key": "dict_field_1",
105+
"value": "dict_value1"
106+
},
107+
"1": {
108+
"id": 2002,
109+
"key": "_internal_dict_field",
110+
"value": "internal_dict_value"
111+
},
112+
"2": {
113+
"id": 2003,
114+
"key": "dict_field_2",
115+
"value": "dict_value2"
130116
}
131117
}
132118
"""
133119

134-
struct TestObject: Decodable {
135-
let metadata: [MetaData]
136-
137-
private enum CodingKeys: String, CodingKey {
138-
case metadata = "meta_data"
139-
}
140-
141-
init(from decoder: Decoder) throws {
142-
let container = try decoder.container(keyedBy: CodingKeys.self)
143-
self.metadata = MetaDataMapper.decodeMetaData(from: container, forKey: .metadata)
144-
}
145-
}
146-
147120
let data = jsonString.data(using: .utf8)!
148121
let decoder = JSONDecoder()
149122

150123
// When
151-
let testObject = try decoder.decode(TestObject.self, from: data)
124+
let flexibleMetaData = try decoder.decode(FlexibleMetaDataArray.self, from: data)
152125

153126
// Then
154-
XCTAssertEqual(testObject.metadata.count, 2) // Internal field should be filtered out
155-
let fieldNames = Set(testObject.metadata.map { $0.key })
127+
XCTAssertEqual(flexibleMetaData.metadata.count, 3) // All fields should be present
128+
let fieldNames = Set(flexibleMetaData.metadata.map { $0.key })
156129
XCTAssertTrue(fieldNames.contains("dict_field_1"))
157130
XCTAssertTrue(fieldNames.contains("dict_field_2"))
158-
XCTAssertFalse(fieldNames.contains("_internal_dict_field"))
131+
XCTAssertTrue(fieldNames.contains("_internal_dict_field"))
159132
}
160133

161134
}

0 commit comments

Comments
 (0)