From f9b32078017275d1be1551c3bb64151fe6679095 Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 29 Apr 2024 10:04:48 -0400 Subject: [PATCH] Adding Codable to MappedValueRepresentable (#14) --- .swiftlint.yml | 1 - Scripts/lint.sh | 2 +- Sources/Options/CodingOptions.swift | 44 +++++++++ .../Documentation.docc/Documentation.md | 44 +++++++-- Sources/Options/EnumSet.swift | 7 +- .../MappedValueRepresentable+Codable.swift | 98 +++++++++++++++++++ .../Options/MappedValueRepresentable.swift | 14 ++- Tests/OptionsTests/EnumSetTests.swift | 6 +- ...appedValueCollectionRepresentedTests.swift | 14 +++ .../Mocks/MockCollectionEnum.swift | 2 +- 10 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 Sources/Options/CodingOptions.swift create mode 100644 Sources/Options/MappedValueRepresentable+Codable.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index aebc975..6be46e8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -76,7 +76,6 @@ opt_in_rules: - sorted_first_last - sorted_imports - static_operator - - strict_fileprivate - strong_iboutlet - toggle_bool - trailing_closure diff --git a/Scripts/lint.sh b/Scripts/lint.sh index fd0a0e3..31c3fa9 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -35,7 +35,7 @@ pushd $PACKAGE_DIR if [ -z "$CI" ]; then $MINT_RUN swiftformat . - $MINT_RUN swiftlint autocorrect + $MINT_RUN swiftlint --fix fi $MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS . diff --git a/Sources/Options/CodingOptions.swift b/Sources/Options/CodingOptions.swift new file mode 100644 index 0000000..368411c --- /dev/null +++ b/Sources/Options/CodingOptions.swift @@ -0,0 +1,44 @@ +// +// CodingOptions.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +public struct CodingOptions: OptionSet, Sendable { + public static let allowMappedValueDecoding: CodingOptions = .init(rawValue: 1) + public static let encodeAsMappedValue: CodingOptions = .init(rawValue: 2) + + public static let `default`: CodingOptions = + [.allowMappedValueDecoding, encodeAsMappedValue] + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} diff --git a/Sources/Options/Documentation.docc/Documentation.md b/Sources/Options/Documentation.docc/Documentation.md index cf4e2c5..9d2723e 100644 --- a/Sources/Options/Documentation.docc/Documentation.md +++ b/Sources/Options/Documentation.docc/Documentation.md @@ -6,9 +6,9 @@ Swift Package for more powerful `Enum` types. **Options** provides a powerful set of features for `Enum` and `OptionSet` types: -* Providing additional representations for `Enum` types besides the `RawType rawValue` -* Being able to interchange between `Enum` and `OptionSet` types -* Using an additional value type for a `Codable` `OptionSet` +- Providing additional representations for `Enum` types besides the `RawType rawValue` +- Being able to interchange between `Enum` and `OptionSet` types +- Using an additional value type for a `Codable` `OptionSet` ### Requirements @@ -38,7 +38,7 @@ Use version up to `1.0`. Let's say we are using an `Enum` for a list of popular social media networks: ```swift -enum SocialNetwork { +enum SocialNetwork : Int { case digg case aim case bebo @@ -69,7 +69,7 @@ struct SocialHandle { However we also want to provide a way to have a unique set of social networks available: ```swift -struct SocialNetworks : OptionSet { +struct SocialNetworkSet : Int, OptionSet { ... } @@ -77,7 +77,39 @@ let user : User let networks : SocialNetworks = user.availableNetworks() ``` -Insert more text here. +We can then simply use ``Options()`` macro to generate both these types: + +```swift +@Options +enum SocialNetwork : Int { + case digg + case aim + case bebo + case delicious + case eworld + case googleplus + case itunesping + case jaiku + case miiverse + case musically + case orkut + case posterous + case stumbleupon + case windowslive + case yahoo +} +``` + +Now we can use the newly create `SocialNetworkSet` type to store a set of values: + +```swift +let networks : SocialNetworks +networks = [.aim, .delicious, .googleplus, .windowslive] +``` + +### Multiple Value Types + +With the ``Options()`` macro, we add the ability to encode and decode values not only from their raw Integer value but also from a String. This is useful for when you want to store the values in ## Topics diff --git a/Sources/Options/EnumSet.swift b/Sources/Options/EnumSet.swift index 69a4b3b..4e5f768 100644 --- a/Sources/Options/EnumSet.swift +++ b/Sources/Options/EnumSet.swift @@ -1,5 +1,6 @@ /// Generic struct for using Enums with RawValue type of Int as an Optionset -public struct EnumSet: OptionSet, Sendable +public struct EnumSet: + OptionSet, Sendable, ExpressibleByArrayLiteral where EnumType.RawValue == Int { public typealias RawValue = EnumType.RawValue @@ -12,6 +13,10 @@ public struct EnumSet: OptionSet, Sendable self.rawValue = rawValue } + public init(arrayLiteral elements: EnumType...) { + self.init(values: elements) + } + /// Creates the EnumSet based on the values in the array. /// - Parameter values: Array of enum values. public init(values: [EnumType]) { diff --git a/Sources/Options/MappedValueRepresentable+Codable.swift b/Sources/Options/MappedValueRepresentable+Codable.swift new file mode 100644 index 0000000..979c620 --- /dev/null +++ b/Sources/Options/MappedValueRepresentable+Codable.swift @@ -0,0 +1,98 @@ +// +// MappedValueRepresentable+Codable.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +extension DecodingError { + internal static func invalidRawValue(_ rawValue: some Any) -> DecodingError { + .dataCorrupted( + .init(codingPath: [], debugDescription: "Raw Value \(rawValue) is invalid.") + ) + } +} + +extension SingleValueDecodingContainer { + fileprivate func decodeAsRawValue() throws -> T + where T.RawValue: Decodable { + let rawValue = try decode(T.RawValue.self) + guard let value = T(rawValue: rawValue) else { + throw DecodingError.invalidRawValue(rawValue) + } + return value + } + + fileprivate func decodeAsMappedType() throws -> T + where T.RawValue: Decodable, T.MappedType: Decodable { + let mappedValues: T.MappedType + do { + mappedValues = try decode(T.MappedType.self) + } catch { + return try decodeAsRawValue() + } + + let rawValue = try T.rawValue(basedOn: mappedValues) + + guard let value = T(rawValue: rawValue) else { + throw DecodingError.dataCorrupted( + .init(codingPath: [], debugDescription: "Invalid Raw Value.") + ) + } + + return value + } +} + +extension MappedValueRepresentable + where Self: Decodable, MappedType: Decodable, RawValue: Decodable { + /// Decodes the type. + /// - Parameter decoder: Decoder. + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + + if Self.codingOptions.contains(.allowMappedValueDecoding) { + self = try container.decodeAsMappedType() + } else { + self = try container.decodeAsRawValue() + } + } +} + +extension MappedValueRepresentable + where Self: Encodable, MappedType: Encodable, RawValue: Encodable { + /// Encoding the type. + /// - Parameter decoder: Encodes. + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + if Self.codingOptions.contains(.encodeAsMappedValue) { + try container.encode(mappedValue()) + } else { + try container.encode(rawValue) + } + } +} diff --git a/Sources/Options/MappedValueRepresentable.swift b/Sources/Options/MappedValueRepresentable.swift index 415ca82..ec1fd19 100644 --- a/Sources/Options/MappedValueRepresentable.swift +++ b/Sources/Options/MappedValueRepresentable.swift @@ -29,6 +29,11 @@ public protocol MappedValueRepresentable: RawRepresentable, CaseIterable, Sendable { associatedtype MappedType = String + + static var codingOptions: CodingOptions { + get + } + /// Gets the raw value based on the MappedType. /// - Parameter value: MappedType value. /// - Returns: The raw value of the enumeration based on the `MappedType `value. @@ -41,6 +46,11 @@ public protocol MappedValueRepresentable: RawRepresentable, CaseIterable, Sendab } extension MappedValueRepresentable { + /// Options regarding how the type can be decoded or encoded. + public static var codingOptions: CodingOptions { + .default + } + /// Gets the mapped value of the enumeration. /// - Parameter rawValue: The raw value of the enumeration /// which pretains to its index in the `mappedValues` Array. @@ -48,10 +58,6 @@ extension MappedValueRepresentable { /// if the raw value (i.e. index) is outside the range of the `mappedValues` array. /// - Returns: /// The Mapped Type value based on the value in the array at the raw value index. - - /// Gets the mapped value of the enumeration. - - /// - Returns: The `MappedType` value public func mappedValue() throws -> MappedType { try Self.mappedValue(basedOn: rawValue) } diff --git a/Tests/OptionsTests/EnumSetTests.swift b/Tests/OptionsTests/EnumSetTests.swift index c741779..5e55439 100644 --- a/Tests/OptionsTests/EnumSetTests.swift +++ b/Tests/OptionsTests/EnumSetTests.swift @@ -75,8 +75,10 @@ internal final class EnumSetTests: XCTestCase { internal func testInitValues() { let values: [MockCollectionEnum] = [.a, .b, .c] - let set = EnumSet(values: values) - XCTAssertEqual(set.rawValue, 7) + let setA = EnumSet(values: values) + XCTAssertEqual(setA.rawValue, 7) + let setB: MockCollectionEnumSet = [.a, .b, .c] + XCTAssertEqual(setB.rawValue, 7) } internal func testArray() { diff --git a/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift b/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift index 8719c74..7045019 100644 --- a/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift +++ b/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift @@ -74,4 +74,18 @@ internal final class MappedValueCollectionRepresentedTests: XCTestCase { XCTAssertEqual(caughtError, .valueNotFound) } + + internal func testCodable() throws { + let encoder: JSONEncoder = .init() + let decoder: JSONDecoder = .init() + let enumValue = MockCollectionEnum.a + let stringValue = try String(data: encoder.encode(enumValue), encoding: .utf8) + let actualStringValue = try MockCollectionEnum.mappedValue(basedOn: enumValue.rawValue) + XCTAssertEqual(stringValue, "\"\(actualStringValue)\"") + XCTAssertEqual(stringValue, "\"a\"") + let expectedStringValue = "a" + let data = "\"\(expectedStringValue)\"".data(using: .utf8) ?? .init() + let actualValue = try decoder.decode(MockCollectionEnum.self, from: data) + XCTAssertEqual(actualValue, .a) + } } diff --git a/Tests/OptionsTests/Mocks/MockCollectionEnum.swift b/Tests/OptionsTests/Mocks/MockCollectionEnum.swift index c588f4f..25082a0 100644 --- a/Tests/OptionsTests/Mocks/MockCollectionEnum.swift +++ b/Tests/OptionsTests/Mocks/MockCollectionEnum.swift @@ -32,7 +32,7 @@ import Options #if swift(>=5.10) // swiftlint:disable identifier_name @Options - internal enum MockCollectionEnum: Int, Sendable { + internal enum MockCollectionEnum: Int, Sendable, Codable { case a case b case c