Skip to content

Commit

Permalink
Encode/Decode SemanticVersion as a single value string
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chriseplettsonos committed Sep 25, 2023
1 parent 45e2ec8 commit bac34e8
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 1 deletion.
21 changes: 20 additions & 1 deletion Sources/SemanticVersion/SemanticVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Foundation
/// 2. MINOR version when you add functionality in a backwards compatible manner, and
/// PATCH version when you make backwards compatible bug fixes.
/// Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
public struct SemanticVersion: Codable, Equatable, Hashable {
public struct SemanticVersion: Equatable, Hashable {
public var major: Int
public var minor: Int
public var patch: Int
Expand All @@ -45,6 +45,25 @@ public struct SemanticVersion: Codable, Equatable, Hashable {
}
}

extension SemanticVersion: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let version = SemanticVersion(try container.decode(String.self)) else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Expected valid semver 2.0 string"
)
)
}
self = version
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(description)
}
}

extension SemanticVersion: LosslessStringConvertible {

Expand Down
54 changes: 54 additions & 0 deletions Tests/SemanticVersionTests/SemanticVersionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,58 @@ final class SemanticVersionTests: XCTestCase {
XCTAssertFalse(SemanticVersion(0, 0, 0).isPatchRelease)
}

func test_encodable() throws {
let encoder = JSONEncoder()
var actual: String

actual = String(data: try encoder.encode(SemanticVersion(1, 2, 3)), encoding: .utf8)!
XCTAssertEqual(actual, #""1.2.3""#)

actual = String(data: try encoder.encode(SemanticVersion(3, 2, 1, "alpha.4")), encoding: .utf8)!
XCTAssertEqual(actual, #""3.2.1-alpha.4""#)

actual = String(data: try encoder.encode(SemanticVersion(3, 2, 1, "", "build.42")), encoding: .utf8)!
XCTAssertEqual(actual, #""3.2.1+build.42""#)

actual = String(data: try encoder.encode(SemanticVersion(7, 7, 7, "beta.423", "build.17")), encoding: .utf8)!
XCTAssertEqual(actual, #""7.7.7-beta.423+build.17""#)
}

func test_decodable() throws {
let decoder = JSONDecoder()
var json: Data

json = #""1.2.3-a.4+42.7""#.data(using: .utf8)!
XCTAssertEqual(
try decoder.decode(SemanticVersion.self, from: json),
SemanticVersion(1, 2, 3, "a.4", "42.7")
)

json = #"["1.2.3-a.4+42.7", "7.7.7"]"#.data(using: .utf8)!
XCTAssertEqual(
try decoder.decode([SemanticVersion].self, from: json),
[SemanticVersion(1, 2, 3, "a.4", "42.7"), SemanticVersion(7, 7, 7)]
)

struct Foo: Decodable, Equatable {
let v: SemanticVersion
}

json = #"{"v": "1.2.3-a.4+42.7"}"#.data(using: .utf8)!
XCTAssertEqual(
try decoder.decode(Foo.self, from: json),
Foo(v: SemanticVersion(1, 2, 3, "a.4", "42.7"))
)

json = #"{"v": "I AM NOT A SEMVER"}"#.data(using: .utf8)!
XCTAssertThrowsError(_ = try decoder.decode(Foo.self, from: json)) { error in
switch error as? DecodingError {
case .dataCorrupted(let context):
XCTAssertEqual(context.codingPath.map(\.stringValue), ["v"])
XCTAssertEqual(context.debugDescription, "Expected valid semver 2.0 string")
default:
XCTFail("Expected DecodingError.dataCorrupted, got \(error)")
}
}
}
}

0 comments on commit bac34e8

Please sign in to comment.