Skip to content

Commit 39b7142

Browse files
Encode/Decode SemanticVersion as a single value string
Since SemanticVersion is LosslessStringConvertible, it seems to make more sense for its encoding/decoding strategy to use that to allow it to be serialized more succinctly as a single string value, rather than using the synthesized structured encoding provided by simply declaring Codable conformance. This is also more likely to conform to how such values will be served up by APIs. This change implements custom init(from:) and encode(to:) methods to provide this behavior, as well as unit tests to verify the behavior. Note this *is* a breaking change to the encoded format of the structure.
1 parent 3a9f1b8 commit 39b7142

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

Sources/SemanticVersion/SemanticVersion.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Foundation
2222
/// 2. MINOR version when you add functionality in a backwards compatible manner, and
2323
/// PATCH version when you make backwards compatible bug fixes.
2424
/// Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
25-
public struct SemanticVersion: Codable, Equatable, Hashable {
25+
public struct SemanticVersion: Equatable, Hashable {
2626
public var major: Int
2727
public var minor: Int
2828
public var patch: Int
@@ -50,6 +50,25 @@ public struct SemanticVersion: Codable, Equatable, Hashable {
5050
}
5151
}
5252

53+
extension SemanticVersion: Codable {
54+
public init(from decoder: Decoder) throws {
55+
let container = try decoder.singleValueContainer()
56+
guard let version = SemanticVersion(try container.decode(String.self)) else {
57+
throw DecodingError.dataCorrupted(
58+
DecodingError.Context(
59+
codingPath: container.codingPath,
60+
debugDescription: "Expected valid semver 2.0 string"
61+
)
62+
)
63+
}
64+
self = version
65+
}
66+
67+
public func encode(to encoder: Encoder) throws {
68+
var container = encoder.singleValueContainer()
69+
try container.encode(description)
70+
}
71+
}
5372

5473
extension SemanticVersion: LosslessStringConvertible {
5574

Tests/SemanticVersionTests/SemanticVersionTests.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,58 @@ final class SemanticVersionTests: XCTestCase {
179179
XCTAssertFalse(SemanticVersion(0, 0, 0).isPatchRelease)
180180
}
181181

182+
func test_encodable() throws {
183+
let encoder = JSONEncoder()
184+
var actual: String
185+
186+
actual = String(data: try encoder.encode(SemanticVersion(1, 2, 3)), encoding: .utf8)!
187+
XCTAssertEqual(actual, #""1.2.3""#)
188+
189+
actual = String(data: try encoder.encode(SemanticVersion(3, 2, 1, "alpha.4")), encoding: .utf8)!
190+
XCTAssertEqual(actual, #""3.2.1-alpha.4""#)
191+
192+
actual = String(data: try encoder.encode(SemanticVersion(3, 2, 1, "", "build.42")), encoding: .utf8)!
193+
XCTAssertEqual(actual, #""3.2.1+build.42""#)
194+
195+
actual = String(data: try encoder.encode(SemanticVersion(7, 7, 7, "beta.423", "build.17")), encoding: .utf8)!
196+
XCTAssertEqual(actual, #""7.7.7-beta.423+build.17""#)
197+
}
198+
199+
func test_decodable() throws {
200+
let decoder = JSONDecoder()
201+
var json: Data
202+
203+
json = #""1.2.3-a.4+42.7""#.data(using: .utf8)!
204+
XCTAssertEqual(
205+
try decoder.decode(SemanticVersion.self, from: json),
206+
SemanticVersion(1, 2, 3, "a.4", "42.7")
207+
)
208+
209+
json = #"["1.2.3-a.4+42.7", "7.7.7"]"#.data(using: .utf8)!
210+
XCTAssertEqual(
211+
try decoder.decode([SemanticVersion].self, from: json),
212+
[SemanticVersion(1, 2, 3, "a.4", "42.7"), SemanticVersion(7, 7, 7)]
213+
)
214+
215+
struct Foo: Decodable, Equatable {
216+
let v: SemanticVersion
217+
}
218+
219+
json = #"{"v": "1.2.3-a.4+42.7"}"#.data(using: .utf8)!
220+
XCTAssertEqual(
221+
try decoder.decode(Foo.self, from: json),
222+
Foo(v: SemanticVersion(1, 2, 3, "a.4", "42.7"))
223+
)
224+
225+
json = #"{"v": "I AM NOT A SEMVER"}"#.data(using: .utf8)!
226+
XCTAssertThrowsError(_ = try decoder.decode(Foo.self, from: json)) { error in
227+
switch error as? DecodingError {
228+
case .dataCorrupted(let context):
229+
XCTAssertEqual(context.codingPath.map(\.stringValue), ["v"])
230+
XCTAssertEqual(context.debugDescription, "Expected valid semver 2.0 string")
231+
default:
232+
XCTFail("Expected DecodingError.dataCorrupted, got \(error)")
233+
}
234+
}
235+
}
182236
}

0 commit comments

Comments
 (0)