From 39b71428dc9bad26ac4147f6e3cee863fa6a669c Mon Sep 17 00:00:00 2001 From: Chris Eplett Date: Mon, 25 Sep 2023 10:07:56 -0400 Subject: [PATCH 1/6] 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. --- Sources/SemanticVersion/SemanticVersion.swift | 21 +++++++- .../SemanticVersionTests.swift | 54 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Sources/SemanticVersion/SemanticVersion.swift b/Sources/SemanticVersion/SemanticVersion.swift index f772b81..67dc252 100644 --- a/Sources/SemanticVersion/SemanticVersion.swift +++ b/Sources/SemanticVersion/SemanticVersion.swift @@ -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 @@ -50,6 +50,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 { diff --git a/Tests/SemanticVersionTests/SemanticVersionTests.swift b/Tests/SemanticVersionTests/SemanticVersionTests.swift index a3ad6c6..17656bb 100644 --- a/Tests/SemanticVersionTests/SemanticVersionTests.swift +++ b/Tests/SemanticVersionTests/SemanticVersionTests.swift @@ -179,4 +179,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)") + } + } + } } From 708b84cf21915933e02d7bff954b85ac4d35acfe Mon Sep 17 00:00:00 2001 From: Chris Eplett Date: Fri, 3 Nov 2023 08:58:56 -0400 Subject: [PATCH 2/6] Add coding strategy support This allows SemanticVersion to optionally be encoded/decoded as a semver string, in addition to supporting the pre-existing memberwise coding. --- .../SemanticVersion+Codable.swift | 104 ++++++++++ Sources/SemanticVersion/SemanticVersion.swift | 20 -- .../SemanticVersionCodingTests.swift | 193 ++++++++++++++++++ .../SemanticVersionTests.swift | 55 ----- 4 files changed, 297 insertions(+), 75 deletions(-) create mode 100644 Sources/SemanticVersion/SemanticVersion+Codable.swift create mode 100644 Tests/SemanticVersionTests/SemanticVersionCodingTests.swift diff --git a/Sources/SemanticVersion/SemanticVersion+Codable.swift b/Sources/SemanticVersion/SemanticVersion+Codable.swift new file mode 100644 index 0000000..f3c3168 --- /dev/null +++ b/Sources/SemanticVersion/SemanticVersion+Codable.swift @@ -0,0 +1,104 @@ +// Copyright Dave Verwer, Sven A. Schmidt, and other contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +public enum SemanticVersionStrategy { + /// Encode/decode the `SemanticVersion` as a structure to/from a JSON object + case memberwise + /// Encode/decode the `SemanticVersion` to/fromfrom a string that conforms to the + /// semantic version 2.0 specification at https://semver.org. + case semverString + + internal static let `default`: Self = .semverString +} + +extension JSONEncoder { + /// The strategy to use in decoding semantic versions. Defaults to `.semverString`. + public var semanticVersionEncodingStrategy: SemanticVersionStrategy { + get { userInfo.semanticDecodingStrategy } + set { userInfo.semanticDecodingStrategy = newValue } + } +} + +extension JSONDecoder { + /// The strategy to use in decoding semantic versions. Defaults to `.succint`. + public var semanticVersionDecodingStrategy: SemanticVersionStrategy { + get { userInfo.semanticDecodingStrategy } + set { userInfo.semanticDecodingStrategy = newValue } + } +} + +private extension [CodingUserInfoKey: Any] { + var semanticDecodingStrategy: SemanticVersionStrategy { + get { + (self[.semanticVersionStrategy] as? SemanticVersionStrategy) ?? .default + } + set { + self[.semanticVersionStrategy] = newValue + } + } +} + +private extension CodingUserInfoKey { + static let semanticVersionStrategy = Self(rawValue: "SemanticVersionEncodingStrategy")! +} + +extension SemanticVersion: Codable { + enum CodingKeys: CodingKey { + case major + case minor + case patch + case preRelease + case build + } + + public init(from decoder: Decoder) throws { + switch decoder.userInfo.semanticDecodingStrategy { + case .memberwise: + let container = try decoder.container(keyedBy: CodingKeys.self) + self.major = try container.decode(Int.self, forKey: .major) + self.minor = try container.decode(Int.self, forKey: .minor) + self.patch = try container.decode(Int.self, forKey: .patch) + self.preRelease = try container.decode(String.self, forKey: .preRelease) + self.build = try container.decode(String.self, forKey: .build) + case .semverString: + 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 { + switch encoder.userInfo.semanticDecodingStrategy { + case .memberwise: + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(major, forKey: .major) + try container.encode(minor, forKey: .minor) + try container.encode(patch, forKey: .patch) + try container.encode(preRelease, forKey: .preRelease) + try container.encode(build, forKey: .build) + case .semverString: + var container = encoder.singleValueContainer() + try container.encode(description) + } + } +} diff --git a/Sources/SemanticVersion/SemanticVersion.swift b/Sources/SemanticVersion/SemanticVersion.swift index 67dc252..d5bbf0f 100644 --- a/Sources/SemanticVersion/SemanticVersion.swift +++ b/Sources/SemanticVersion/SemanticVersion.swift @@ -50,26 +50,6 @@ public struct SemanticVersion: 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 { /// Initialize a version from a string. Returns `nil` if the string is not a semantic version. diff --git a/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift b/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift new file mode 100644 index 0000000..b0b0f36 --- /dev/null +++ b/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift @@ -0,0 +1,193 @@ +// +// SemanticVersionCodingTests.swift +// +// +// Created by Chris Eplett on 11/3/23. +// + +import XCTest + +import SemanticVersion + +final class SemanticVersionCodingTests: XCTestCase { + func test_semverString_is_default() throws { + XCTAssertEqual(.semverString, JSONEncoder().semanticVersionEncodingStrategy) + XCTAssertEqual(.semverString, JSONDecoder().semanticVersionDecodingStrategy) + } + + func test_encodable_semverString() throws { + let encoder = JSONEncoder() + var actual: String + + encoder.semanticVersionEncodingStrategy = .semverString + + 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_encodable_memberwise() throws { + let encoder = JSONEncoder() + var actual: String + + encoder.semanticVersionEncodingStrategy = .memberwise + + actual = String(data: try encoder.encode(SemanticVersion(1, 2, 3)), encoding: .utf8)! + XCTAssertTrue(actual.contains(#""major":1"#)) + XCTAssertTrue(actual.contains(#""minor":2"#)) + XCTAssertTrue(actual.contains(#""patch":3"#)) + XCTAssertTrue(actual.contains(#""preRelease":"""#)) + XCTAssertTrue(actual.contains(#""build":"""#)) + + actual = String(data: try encoder.encode(SemanticVersion(3, 2, 1, "alpha.4")), encoding: .utf8)! + XCTAssertTrue(actual.contains(#""major":3"#)) + XCTAssertTrue(actual.contains(#""minor":2"#)) + XCTAssertTrue(actual.contains(#""patch":1"#)) + XCTAssertTrue(actual.contains(#""preRelease":"alpha.4""#)) + XCTAssertTrue(actual.contains(#""build":"""#)) + + actual = String(data: try encoder.encode(SemanticVersion(3, 2, 1, "", "build.42")), encoding: .utf8)! + XCTAssertTrue(actual.contains(#""major":3"#)) + XCTAssertTrue(actual.contains(#""minor":2"#)) + XCTAssertTrue(actual.contains(#""patch":1"#)) + XCTAssertTrue(actual.contains(#""preRelease":"""#)) + XCTAssertTrue(actual.contains(#""build":"build.42""#)) + + actual = String(data: try encoder.encode(SemanticVersion(7, 7, 7, "beta.423", "build.17")), encoding: .utf8)! + XCTAssertTrue(actual.contains(#""major":7"#)) + XCTAssertTrue(actual.contains(#""minor":7"#)) + XCTAssertTrue(actual.contains(#""patch":7"#)) + XCTAssertTrue(actual.contains(#""preRelease":"beta.423""#)) + XCTAssertTrue(actual.contains(#""build":"build.17""#)) + } + + func test_decodable_semverString() throws { + let decoder = JSONDecoder() + var json: Data + + decoder.semanticVersionDecodingStrategy = .semverString + + 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)") + } + } + } + + func test_decodable_memberwise() throws { + let decoder = JSONDecoder() + var json: Data + + decoder.semanticVersionDecodingStrategy = .memberwise + + json = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "preRelease": "a.4", + "build": "42.7" + } + """.data(using: .utf8)! + XCTAssertEqual( + try decoder.decode(SemanticVersion.self, from: json), + SemanticVersion(1, 2, 3, "a.4", "42.7") + ) + + json = """ + [ + { + "major": 1, + "minor": 2, + "patch": 3, + "preRelease": "a.4", + "build": "42.7" + },{ + "major": 7, + "minor": 7, + "patch": 7, + "preRelease": "", + "build": "" + } + ] + """.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": { + "major": 1, + "minor": 2, + "patch": 3, + "preRelease": "a.4", + "build": "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": { + "major": 1, + "preRelease": "a.4", + "build": "42.7" + } + } + """.data(using: .utf8)! + XCTAssertThrowsError(_ = try decoder.decode(Foo.self, from: json)) { error in + switch error as? DecodingError { + case .keyNotFound(let key, let context): + XCTAssertEqual("minor", key.stringValue) + XCTAssertEqual(["v"], context.codingPath.map(\.stringValue)) + default: + XCTFail("Expected DecodingError.keyNotFound, got \(error)") + } + } + } +} diff --git a/Tests/SemanticVersionTests/SemanticVersionTests.swift b/Tests/SemanticVersionTests/SemanticVersionTests.swift index 17656bb..a9d32c0 100644 --- a/Tests/SemanticVersionTests/SemanticVersionTests.swift +++ b/Tests/SemanticVersionTests/SemanticVersionTests.swift @@ -178,59 +178,4 @@ final class SemanticVersionTests: XCTestCase { XCTAssertTrue(SemanticVersion(0, 1, 1).isPatchRelease) 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)") - } - } - } } From 2c3283e91d3ab2ece6462f261df63badb580dfb8 Mon Sep 17 00:00:00 2001 From: Chris Eplett Date: Fri, 3 Nov 2023 09:04:56 -0400 Subject: [PATCH 3/6] A little bit of cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove the default constant since it’s only used in one place now * Fix a comment typo --- Sources/SemanticVersion/SemanticVersion+Codable.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/SemanticVersion/SemanticVersion+Codable.swift b/Sources/SemanticVersion/SemanticVersion+Codable.swift index f3c3168..4223156 100644 --- a/Sources/SemanticVersion/SemanticVersion+Codable.swift +++ b/Sources/SemanticVersion/SemanticVersion+Codable.swift @@ -20,8 +20,6 @@ public enum SemanticVersionStrategy { /// Encode/decode the `SemanticVersion` to/fromfrom a string that conforms to the /// semantic version 2.0 specification at https://semver.org. case semverString - - internal static let `default`: Self = .semverString } extension JSONEncoder { @@ -33,7 +31,7 @@ extension JSONEncoder { } extension JSONDecoder { - /// The strategy to use in decoding semantic versions. Defaults to `.succint`. + /// The strategy to use in decoding semantic versions. Defaults to `.semverString`. public var semanticVersionDecodingStrategy: SemanticVersionStrategy { get { userInfo.semanticDecodingStrategy } set { userInfo.semanticDecodingStrategy = newValue } @@ -43,7 +41,7 @@ extension JSONDecoder { private extension [CodingUserInfoKey: Any] { var semanticDecodingStrategy: SemanticVersionStrategy { get { - (self[.semanticVersionStrategy] as? SemanticVersionStrategy) ?? .default + (self[.semanticVersionStrategy] as? SemanticVersionStrategy) ?? .semverString } set { self[.semanticVersionStrategy] = newValue From 802f67667a50db0be2cd42a4448506b2825e236e Mon Sep 17 00:00:00 2001 From: Chris Eplett Date: Fri, 3 Nov 2023 09:22:58 -0400 Subject: [PATCH 4/6] Udpate the README Add more detail around new Codable support --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ee5fdf..a934a50 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,20 @@ let dict = [ // [{major 3, minor 0, patch 0,... ] // SemanticVersion is Codable -let data = try JSONEncoder().encode(v123) // 58 bytes -let decoded = try JSONDecoder().decode(SemanticVersion.self, from: data) // 1.2.3 -decoded == v123 // true +// Note: the strategy defaults to `.semverString` +let stringEncoder = JSONEncoder() +stringEncoder.semanticVersionEncodingStrategy = .semverString +let stringDecoder = JSONDecoder() +stringDecoder.semanticVersionDecodingStrategy = .semverString +let stringData = try stringEncoder.encode(v123) // 7 bytes -> "1.2.3", including quotes +let stringDecoded = try stringDecoder.decode(SemanticVersion.self, from: stringData) // 1.2.3 +stringDecoded == v123 // true + +let memberwiseEncoder = JSONEncoder() +memberwiseEncoder.semanticVersionEncodingStrategy = .memberwise +let memberwiseDecoder = JSONDecoder() +memberwiseDecoder.semanticVersionDecodingStrategy = .memberwise +let memberwiseData = try memberwiseEncoder.encode(v123) // 58 bytes +let memberwiseDecoded = try memberwiseDecoder.decode(SemanticVersion.self, from: memberwiseData) // 1.2.3 +memberwiseDecoded == v123 // true ``` From 9048ff1f716ae7bcf9600efb53cf65fd924b32c9 Mon Sep 17 00:00:00 2001 From: Chris Eplett Date: Mon, 13 Nov 2023 09:41:21 -0500 Subject: [PATCH 5/6] Rename memberwise * Change it to `defaultCodable` * Make it the default instead of `semverString` --- README.md | 18 +++++++++--------- .../SemanticVersion+Codable.swift | 10 +++++----- .../SemanticVersionCodingTests.swift | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a934a50..d6f2896 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,15 @@ let dict = [ // [{major 3, minor 0, patch 0,... ] // SemanticVersion is Codable -// Note: the strategy defaults to `.semverString` +// Note: the strategy defaults to `.defaultCodable` +let defaultEncoder = JSONEncoder() +defaultEncoder.semanticVersionEncodingStrategy = .defaultCodable +let defaultDecoder = JSONDecoder() +defaultDecoder.semanticVersionDecodingStrategy = .defaultCodable +let defaultData = try defaultEncoder.encode(v123) // 58 bytes +let defaultDecoded = try defaultDecoder.decode(SemanticVersion.self, from: defaultData) // 1.2.3 +defaultDecoded == v123 // true + let stringEncoder = JSONEncoder() stringEncoder.semanticVersionEncodingStrategy = .semverString let stringDecoder = JSONDecoder() @@ -64,12 +72,4 @@ stringDecoder.semanticVersionDecodingStrategy = .semverString let stringData = try stringEncoder.encode(v123) // 7 bytes -> "1.2.3", including quotes let stringDecoded = try stringDecoder.decode(SemanticVersion.self, from: stringData) // 1.2.3 stringDecoded == v123 // true - -let memberwiseEncoder = JSONEncoder() -memberwiseEncoder.semanticVersionEncodingStrategy = .memberwise -let memberwiseDecoder = JSONDecoder() -memberwiseDecoder.semanticVersionDecodingStrategy = .memberwise -let memberwiseData = try memberwiseEncoder.encode(v123) // 58 bytes -let memberwiseDecoded = try memberwiseDecoder.decode(SemanticVersion.self, from: memberwiseData) // 1.2.3 -memberwiseDecoded == v123 // true ``` diff --git a/Sources/SemanticVersion/SemanticVersion+Codable.swift b/Sources/SemanticVersion/SemanticVersion+Codable.swift index 4223156..6d7d23c 100644 --- a/Sources/SemanticVersion/SemanticVersion+Codable.swift +++ b/Sources/SemanticVersion/SemanticVersion+Codable.swift @@ -16,14 +16,14 @@ import Foundation public enum SemanticVersionStrategy { /// Encode/decode the `SemanticVersion` as a structure to/from a JSON object - case memberwise + case defaultCodable /// Encode/decode the `SemanticVersion` to/fromfrom a string that conforms to the /// semantic version 2.0 specification at https://semver.org. case semverString } extension JSONEncoder { - /// The strategy to use in decoding semantic versions. Defaults to `.semverString`. + /// The strategy to use in decoding semantic versions. Defaults to `.defaultCodable`. public var semanticVersionEncodingStrategy: SemanticVersionStrategy { get { userInfo.semanticDecodingStrategy } set { userInfo.semanticDecodingStrategy = newValue } @@ -41,7 +41,7 @@ extension JSONDecoder { private extension [CodingUserInfoKey: Any] { var semanticDecodingStrategy: SemanticVersionStrategy { get { - (self[.semanticVersionStrategy] as? SemanticVersionStrategy) ?? .semverString + (self[.semanticVersionStrategy] as? SemanticVersionStrategy) ?? .defaultCodable } set { self[.semanticVersionStrategy] = newValue @@ -64,7 +64,7 @@ extension SemanticVersion: Codable { public init(from decoder: Decoder) throws { switch decoder.userInfo.semanticDecodingStrategy { - case .memberwise: + case .defaultCodable: let container = try decoder.container(keyedBy: CodingKeys.self) self.major = try container.decode(Int.self, forKey: .major) self.minor = try container.decode(Int.self, forKey: .minor) @@ -87,7 +87,7 @@ extension SemanticVersion: Codable { public func encode(to encoder: Encoder) throws { switch encoder.userInfo.semanticDecodingStrategy { - case .memberwise: + case .defaultCodable: var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(major, forKey: .major) try container.encode(minor, forKey: .minor) diff --git a/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift b/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift index b0b0f36..b60d423 100644 --- a/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift +++ b/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift @@ -10,9 +10,9 @@ import XCTest import SemanticVersion final class SemanticVersionCodingTests: XCTestCase { - func test_semverString_is_default() throws { - XCTAssertEqual(.semverString, JSONEncoder().semanticVersionEncodingStrategy) - XCTAssertEqual(.semverString, JSONDecoder().semanticVersionDecodingStrategy) + func test_defaultCodable_is_default() throws { + XCTAssertEqual(.defaultCodable, JSONEncoder().semanticVersionEncodingStrategy) + XCTAssertEqual(.defaultCodable, JSONDecoder().semanticVersionDecodingStrategy) } func test_encodable_semverString() throws { @@ -34,11 +34,11 @@ final class SemanticVersionCodingTests: XCTestCase { XCTAssertEqual(actual, #""7.7.7-beta.423+build.17""#) } - func test_encodable_memberwise() throws { + func test_encodable_defaultCodable() throws { let encoder = JSONEncoder() var actual: String - encoder.semanticVersionEncodingStrategy = .memberwise + encoder.semanticVersionEncodingStrategy = .defaultCodable actual = String(data: try encoder.encode(SemanticVersion(1, 2, 3)), encoding: .utf8)! XCTAssertTrue(actual.contains(#""major":1"#)) @@ -109,11 +109,11 @@ final class SemanticVersionCodingTests: XCTestCase { } } - func test_decodable_memberwise() throws { + func test_decodable_defaultCodable() throws { let decoder = JSONDecoder() var json: Data - decoder.semanticVersionDecodingStrategy = .memberwise + decoder.semanticVersionDecodingStrategy = .defaultCodable json = """ { From 727b6d88a0d2d1130d4f23f48d70bb1f535f85b6 Mon Sep 17 00:00:00 2001 From: Chris Eplett Date: Mon, 13 Nov 2023 09:56:46 -0500 Subject: [PATCH 6/6] Fix Swift 5.6 compilation issue --- Sources/SemanticVersion/SemanticVersion+Codable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SemanticVersion/SemanticVersion+Codable.swift b/Sources/SemanticVersion/SemanticVersion+Codable.swift index 6d7d23c..8b106ae 100644 --- a/Sources/SemanticVersion/SemanticVersion+Codable.swift +++ b/Sources/SemanticVersion/SemanticVersion+Codable.swift @@ -38,7 +38,7 @@ extension JSONDecoder { } } -private extension [CodingUserInfoKey: Any] { +private extension Dictionary where Key == CodingUserInfoKey, Value == Any { var semanticDecodingStrategy: SemanticVersionStrategy { get { (self[.semanticVersionStrategy] as? SemanticVersionStrategy) ?? .defaultCodable