Skip to content

Commit 27b6e74

Browse files
author
Gal Shelef
committed
CR fix: make AttributeValue and Status use synthesized Codable implementation for swift >= 5.5
* choose a Codable synthesized vs. explicit extension for AttributeValue and Status based on swift version * add tests for swift >= 5.5 for forward compatibility of the explicit Codable implementation
1 parent ffbdc90 commit 27b6e74

File tree

4 files changed

+212
-29
lines changed

4 files changed

+212
-29
lines changed

Sources/OpenTelemetryApi/Common/AttributeValue.swift

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ public enum AttributeValue: Equatable, CustomStringConvertible, Hashable {
6161
}
6262
}
6363

64-
// this explicit Codable implementation for AttributeValue will probably be redundant with Swift 5.5
65-
extension AttributeValue: Codable {
64+
internal struct AttributeValueExplicitCodable : Codable {
65+
let attributeValue: AttributeValue
66+
6667
enum CodingKeys: String, CodingKey {
6768
case string
6869
case bool
@@ -73,7 +74,15 @@ extension AttributeValue: Codable {
7374
case intArray
7475
case doubleArray
7576
}
76-
77+
78+
enum AssociatedValueCodingKeys: String, CodingKey {
79+
case associatedValue = "_0"
80+
}
81+
82+
internal init(attributeValue: AttributeValue) {
83+
self.attributeValue = attributeValue
84+
}
85+
7786
public init(from decoder: Decoder) throws {
7887
let container = try decoder.container(keyedBy: CodingKeys.self)
7988

@@ -86,45 +95,83 @@ extension AttributeValue: Codable {
8695

8796
switch container.allKeys.first.unsafelyUnwrapped {
8897
case .string:
89-
self = .string(try container.decode(String.self, forKey: .string))
98+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .string)
99+
self.attributeValue = .string(try nestedContainer.decode(String.self, forKey: .associatedValue))
90100
case .bool:
91-
self = .bool(try container.decode(Bool.self, forKey: .bool))
101+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .bool)
102+
self.attributeValue = .bool(try nestedContainer.decode(Bool.self, forKey: .associatedValue))
92103
case .int:
93-
self = .int(try container.decode(Int.self, forKey: .int))
104+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .int)
105+
self.attributeValue = .int(try nestedContainer.decode(Int.self, forKey: .associatedValue))
94106
case .double:
95-
self = .double(try container.decode(Double.self, forKey: .double))
107+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .double)
108+
self.attributeValue = .double(try nestedContainer.decode(Double.self, forKey: .associatedValue))
96109
case .stringArray:
97-
self = .stringArray(try container.decode([String].self, forKey: .stringArray))
110+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .stringArray)
111+
self.attributeValue = .stringArray(try nestedContainer.decode([String].self, forKey: .associatedValue))
98112
case .boolArray:
99-
self = .boolArray(try container.decode([Bool].self, forKey: .boolArray))
113+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .boolArray)
114+
self.attributeValue = .boolArray(try nestedContainer.decode([Bool].self, forKey: .associatedValue))
100115
case .intArray:
101-
self = .intArray(try container.decode([Int].self, forKey: .intArray))
116+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .intArray)
117+
self.attributeValue = .intArray(try nestedContainer.decode([Int].self, forKey: .associatedValue))
102118
case .doubleArray:
103-
self = .doubleArray(try container.decode([Double].self, forKey: .doubleArray))
119+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .doubleArray)
120+
self.attributeValue = .doubleArray(try nestedContainer.decode([Double].self, forKey: .associatedValue))
104121
}
105122
}
106123

107124
public func encode(to encoder: Encoder) throws {
108125

109126
var container = encoder.container(keyedBy: CodingKeys.self)
110127

111-
switch self {
128+
switch self.attributeValue {
112129
case .string(let value):
113-
try container.encode(value, forKey: .string)
130+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .string)
131+
try nestedContainer.encode(value, forKey: .associatedValue)
114132
case .bool(let value):
115-
try container.encode(value, forKey: .bool)
133+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .bool)
134+
try nestedContainer.encode(value, forKey: .associatedValue)
116135
case .int(let value):
117-
try container.encode(value, forKey: .int)
136+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .int)
137+
try nestedContainer.encode(value, forKey: .associatedValue)
118138
case .double(let value):
119-
try container.encode(value, forKey: .double)
139+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .double)
140+
try nestedContainer.encode(value, forKey: .associatedValue)
120141
case .stringArray(let value):
121-
try container.encode(value, forKey: .stringArray)
142+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .stringArray)
143+
try nestedContainer.encode(value, forKey: .associatedValue)
122144
case .boolArray(let value):
123-
try container.encode(value, forKey: .boolArray)
145+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .boolArray)
146+
try nestedContainer.encode(value, forKey: .associatedValue)
124147
case .intArray(let value):
125-
try container.encode(value, forKey: .intArray)
148+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .intArray)
149+
try nestedContainer.encode(value, forKey: .associatedValue)
126150
case .doubleArray(let value):
127-
try container.encode(value, forKey: .doubleArray)
151+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .doubleArray)
152+
try nestedContainer.encode(value, forKey: .associatedValue)
128153
}
129154
}
130155
}
156+
157+
#if swift(>=5.5)
158+
// swift 5.5 supports synthesizing Codable for enums with associated values
159+
// see https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md
160+
extension AttributeValue: Codable { }
161+
#else
162+
// for older swift versions use a forward compatible explicit Codable implementation
163+
extension AttributeValue: Codable {
164+
165+
public init(from decoder: Decoder) throws {
166+
let explicitDecoded = AttributeValueExplicitCodable(from: decoder)
167+
168+
self = explicitDecoded.attributeValue
169+
}
170+
171+
public func encode(to encoder: Encoder) throws {
172+
let explicitEncoded = AttributeValueExplicitCodable(attributeValue: self)
173+
174+
explicitEncoded.encode(to: encoder)
175+
}
176+
}
177+
#endif

Sources/OpenTelemetryApi/Trace/Status.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ extension Status: CustomStringConvertible {
6161
}
6262
}
6363

64-
// this explicit Codable implementation for Status will probably be redundant with Swift 5.5
65-
extension Status: Codable {
64+
internal struct StatusExplicitCodable : Codable {
65+
let status: Status
66+
6667
enum CodingKeys: String, CodingKey {
6768
case ok
6869
case unset
@@ -77,6 +78,10 @@ extension Status: Codable {
7778
case description
7879
}
7980

81+
internal init(status: Status) {
82+
self.status = status
83+
}
84+
8085
public init(from decoder: Decoder) throws {
8186
let container = try decoder.container(keyedBy: CodingKeys.self)
8287

@@ -90,21 +95,21 @@ extension Status: Codable {
9095
switch container.allKeys.first.unsafelyUnwrapped {
9196
case .ok:
9297
_ = try container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .ok)
93-
self = .ok
98+
self.status = .ok
9499
case .unset:
95100
_ = try container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .unset)
96-
self = .unset
101+
self.status = .unset
97102
case .error:
98103
let nestedContainer = try container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .error)
99-
self = .error(description: try nestedContainer.decode(String.self, forKey: .description))
104+
self.status = .error(description: try nestedContainer.decode(String.self, forKey: .description))
100105
}
101106
}
102107

103108
public func encode(to encoder: Encoder) throws {
104109

105110
var container = encoder.container(keyedBy: CodingKeys.self)
106111

107-
switch self {
112+
switch self.status {
108113
case .ok:
109114
_ = container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .ok)
110115
case .unset:
@@ -115,3 +120,25 @@ extension Status: Codable {
115120
}
116121
}
117122
}
123+
124+
#if swift(>=5.5)
125+
// swift 5.5 supports synthesizing Codable for enums with associated values
126+
// see https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md
127+
extension Status: Codable { }
128+
#else
129+
// for older swift versions use a forward compatible explicit Codable implementation
130+
extension Status: Codable {
131+
132+
public init(from decoder: Decoder) throws {
133+
let explicitDecoded = StatusExplicitCodable(from: decoder)
134+
135+
self = explicitDecoded.status
136+
}
137+
138+
public func encode(to encoder: Encoder) throws {
139+
let explicitEncoded = StatusExplicitCodable(status: self)
140+
141+
explicitEncoded.encode(to: encoder)
142+
}
143+
}
144+
#endif

Tests/OpenTelemetryApiTests/Common/AttributeValueTests.swift

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import OpenTelemetryApi
6+
@testable import OpenTelemetryApi
77
import XCTest
88

99
class AttributeValueTest: XCTestCase {
@@ -149,6 +149,87 @@ class AttributeValueTest: XCTestCase {
149149

150150
XCTAssertThrowsError(try decoder.decode(AttributeValue.self, from: "".data(using: .utf8)!))
151151
XCTAssertThrowsError(try decoder.decode(AttributeValue.self,
152-
from: #"{"string":"MyStringAttributeValue", "int":1234}"#.data(using: .utf8)!))
152+
from: #"{"string":{"_0":"MyStringAttributeValue"}, "int":{"_0":1234}}"#.data(using: .utf8)!))
153153
}
154+
155+
#if swift(>=5.5)
156+
// this test covers forward compatibility of the pre swift 5.5 encoding with post swift 5.5 decoding
157+
func testAttributeValue_ExplicitCodableForwardCompatibility() throws {
158+
159+
let encoder = JSONEncoder()
160+
let decoder = JSONDecoder()
161+
162+
var attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.string(""))
163+
var decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
164+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
165+
166+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.string("MyStringAttributeValue"))
167+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
168+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
169+
170+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.bool(true))
171+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
172+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
173+
174+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.int(123456))
175+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
176+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
177+
178+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.int(0))
179+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
180+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
181+
182+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.int(-123456))
183+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
184+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
185+
186+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.double(1.23456))
187+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
188+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
189+
190+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.double(0.0))
191+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
192+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
193+
194+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.double(-1.23456))
195+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
196+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
197+
198+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.stringArray([]))
199+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
200+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
201+
202+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.stringArray(["MyStringAttributeValue1", "MyStringAttributeValue2"]))
203+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
204+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
205+
206+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.boolArray([]))
207+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
208+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
209+
210+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.boolArray([true, false]))
211+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
212+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
213+
214+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.intArray([]))
215+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
216+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
217+
218+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.intArray([1, 3, 2]))
219+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
220+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
221+
222+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.doubleArray([]))
223+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
224+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
225+
226+
attribute = AttributeValueExplicitCodable(attributeValue: AttributeValue.doubleArray([1.11, 0.01, -2.22]))
227+
decodedAttribute = try decoder.decode(AttributeValue.self, from: try encoder.encode(attribute))
228+
XCTAssertEqual(attribute.attributeValue, decodedAttribute)
229+
230+
XCTAssertThrowsError(try decoder.decode(AttributeValueExplicitCodable.self, from: "".data(using: .utf8)!))
231+
XCTAssertThrowsError(try decoder.decode(AttributeValueExplicitCodable.self,
232+
from: #"{"string":{"_0":"MyStringAttributeValue"}, "int":{"_0":1234}}"#.data(using: .utf8)!))
233+
}
234+
#endif
154235
}

Tests/OpenTelemetryApiTests/Trace/StatusTests.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import OpenTelemetryApi
6+
@testable import OpenTelemetryApi
77
import XCTest
88

99
final class StatusTests: XCTestCase {
@@ -49,4 +49,32 @@ final class StatusTests: XCTestCase {
4949
XCTAssertThrowsError(try decoder.decode(Status.self,
5050
from: #"{"error":{"description":"Error"}, "ok":{}}"#.data(using: .utf8)!))
5151
}
52+
53+
#if swift(>=5.5)
54+
// this test covers forward compatibility of the pre swift 5.5 encoding with post swift 5.5 decoding
55+
func testStatusExplicitCodableForwardCompatibility() throws {
56+
let encoder = JSONEncoder()
57+
let decoder = JSONDecoder()
58+
59+
var status = StatusExplicitCodable(status: Status.ok)
60+
var decodedStatus = try decoder.decode(Status.self, from: try encoder.encode(status))
61+
XCTAssertEqual(status.status, decodedStatus)
62+
63+
status = StatusExplicitCodable(status: Status.unset)
64+
decodedStatus = try decoder.decode(Status.self, from: try encoder.encode(status))
65+
XCTAssertEqual(status.status, decodedStatus)
66+
67+
status = StatusExplicitCodable(status: Status.error(description: "Error"))
68+
decodedStatus = try decoder.decode(Status.self, from: try encoder.encode(status))
69+
XCTAssertEqual(status.status, decodedStatus)
70+
71+
status = StatusExplicitCodable(status: Status.error(description: ""))
72+
decodedStatus = try decoder.decode(Status.self, from: try encoder.encode(status))
73+
XCTAssertEqual(status.status, decodedStatus)
74+
75+
XCTAssertThrowsError(try decoder.decode(StatusExplicitCodable.self, from: "".data(using: .utf8)!))
76+
XCTAssertThrowsError(try decoder.decode(StatusExplicitCodable.self,
77+
from: #"{"error":{"description":"Error"}, "ok":{}}"#.data(using: .utf8)!))
78+
}
79+
#endif
5280
}

0 commit comments

Comments
 (0)