Skip to content

Commit

Permalink
Adding Codable to MappedValueRepresentable (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
leogdion authored Apr 29, 2024
1 parent f6cd85c commit f9b3207
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 16 deletions.
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ opt_in_rules:
- sorted_first_last
- sorted_imports
- static_operator
- strict_fileprivate
- strong_iboutlet
- toggle_bool
- trailing_closure
Expand Down
2 changes: 1 addition & 1 deletion Scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
44 changes: 44 additions & 0 deletions Sources/Options/CodingOptions.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
44 changes: 38 additions & 6 deletions Sources/Options/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -69,15 +69,47 @@ 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 {
...
}

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

Expand Down
7 changes: 6 additions & 1 deletion Sources/Options/EnumSet.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// Generic struct for using Enums with RawValue type of Int as an Optionset
public struct EnumSet<EnumType: RawRepresentable>: OptionSet, Sendable
public struct EnumSet<EnumType: RawRepresentable>:
OptionSet, Sendable, ExpressibleByArrayLiteral
where EnumType.RawValue == Int {
public typealias RawValue = EnumType.RawValue

Expand All @@ -12,6 +13,10 @@ public struct EnumSet<EnumType: RawRepresentable>: 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]) {
Expand Down
98 changes: 98 additions & 0 deletions Sources/Options/MappedValueRepresentable+Codable.swift
Original file line number Diff line number Diff line change
@@ -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<T: MappedValueRepresentable>() 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<T: MappedValueRepresentable>() 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)
}
}
}
14 changes: 10 additions & 4 deletions Sources/Options/MappedValueRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -41,17 +46,18 @@ 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.
/// - Throws: `MappedValueCollectionRepresentedError.valueNotFound`
/// 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)
}
Expand Down
6 changes: 4 additions & 2 deletions Tests/OptionsTests/EnumSetTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
14 changes: 14 additions & 0 deletions Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/OptionsTests/Mocks/MockCollectionEnum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f9b3207

Please sign in to comment.