From a2fd9e31d5fdf1a0e9d61fe76ab5a4461d10b08a Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 29 Apr 2024 16:43:39 -0400 Subject: [PATCH] Updated Documentation and API (#13) --- .gitignore | 5 +- .swiftlint.yml | 1 - Documentation/Reference/Options/README.md | 29 -- .../enums/MappedValueRepresentableError.md | 17 - .../Reference/Options/extensions/EnumSet.md | 46 --- .../MappedValueCollectionRepresented.md | 48 --- .../MappedValueDictionaryRepresented.md | 48 --- .../extensions/MappedValueRepresentable.md | 29 -- .../MappedValueCollectionRepresented.md | 10 - .../MappedValueDictionaryRepresented.md | 10 - .../protocols/MappedValueRepresentable.md | 40 --- .../Reference/Options/structs/EnumSet.md | 50 --- .../Reference/Options/structs/MappedEnum.md | 65 ---- .../Options/typealiases/EnumSet.RawValue.md | 7 - README.md | 290 ++++++------------ Scripts/gh-md-toc | 76 +++-- Scripts/lint.sh | 2 +- Sources/Options/Array.swift | 18 +- Sources/Options/CodingOptions.swift | 49 +++ Sources/Options/Dictionary.swift | 8 +- .../Documentation.docc/Documentation.md | 162 ++++++++++ Sources/Options/EnumSet.swift | 56 +++- Sources/Options/Macro.swift | 4 +- Sources/Options/MappedEnum.swift | 12 +- .../MappedValueRepresentable+Codable.swift | 97 ++++++ .../Options/MappedValueRepresentable.swift | 18 +- .../MappedValueRepresentableError.swift | 1 + ...ted.swift => MappedValueRepresented.swift} | 11 +- Sources/Options/MappedValues.swift | 10 +- Tests/OptionsTests/EnumSetTests.swift | 6 +- ...appedValueCollectionRepresentedTests.swift | 80 +++++ .../Mocks/MockCollectionEnum.swift | 9 +- .../Mocks/MockDictionaryEnum.swift | 4 +- 33 files changed, 647 insertions(+), 671 deletions(-) delete mode 100644 Documentation/Reference/Options/README.md delete mode 100644 Documentation/Reference/Options/enums/MappedValueRepresentableError.md delete mode 100644 Documentation/Reference/Options/extensions/EnumSet.md delete mode 100644 Documentation/Reference/Options/extensions/MappedValueCollectionRepresented.md delete mode 100644 Documentation/Reference/Options/extensions/MappedValueDictionaryRepresented.md delete mode 100644 Documentation/Reference/Options/extensions/MappedValueRepresentable.md delete mode 100644 Documentation/Reference/Options/protocols/MappedValueCollectionRepresented.md delete mode 100644 Documentation/Reference/Options/protocols/MappedValueDictionaryRepresented.md delete mode 100644 Documentation/Reference/Options/protocols/MappedValueRepresentable.md delete mode 100644 Documentation/Reference/Options/structs/EnumSet.md delete mode 100644 Documentation/Reference/Options/structs/MappedEnum.md delete mode 100644 Documentation/Reference/Options/typealiases/EnumSet.RawValue.md create mode 100644 Sources/Options/CodingOptions.swift create mode 100644 Sources/Options/Documentation.docc/Documentation.md create mode 100644 Sources/Options/MappedValueRepresentable+Codable.swift rename Sources/Options/{MappedValueGenericRepresented.swift => MappedValueRepresented.swift} (89%) diff --git a/.gitignore b/.gitignore index bd44408..008465e 100644 --- a/.gitignore +++ b/.gitignore @@ -127,4 +127,7 @@ fastlane/test_output iOSInjectionProject/ .mint -Output \ No newline at end of file +Output + +# Due to support for 5.10 and below +Package.resolved \ No newline at end of file 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/Documentation/Reference/Options/README.md b/Documentation/Reference/Options/README.md deleted file mode 100644 index f7afffc..0000000 --- a/Documentation/Reference/Options/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Reference Documentation - -## Protocols - -- [MappedValueCollectionRepresented](protocols/MappedValueCollectionRepresented.md) -- [MappedValueDictionaryRepresented](protocols/MappedValueDictionaryRepresented.md) -- [MappedValueRepresentable](protocols/MappedValueRepresentable.md) - -## Structs - -- [EnumSet](structs/EnumSet.md) -- [MappedEnum](structs/MappedEnum.md) - -## Enums - -- [MappedValueRepresentableError](enums/MappedValueRepresentableError.md) - -## Extensions - -- [EnumSet](extensions/EnumSet.md) -- [MappedValueCollectionRepresented](extensions/MappedValueCollectionRepresented.md) -- [MappedValueDictionaryRepresented](extensions/MappedValueDictionaryRepresented.md) -- [MappedValueRepresentable](extensions/MappedValueRepresentable.md) - -## Typealiases - -- [EnumSet.RawValue](typealiases/EnumSet.RawValue.md) - -This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) \ No newline at end of file diff --git a/Documentation/Reference/Options/enums/MappedValueRepresentableError.md b/Documentation/Reference/Options/enums/MappedValueRepresentableError.md deleted file mode 100644 index 6c4a2a9..0000000 --- a/Documentation/Reference/Options/enums/MappedValueRepresentableError.md +++ /dev/null @@ -1,17 +0,0 @@ -**ENUM** - -# `MappedValueRepresentableError` - -```swift -public enum MappedValueRepresentableError: Error -``` - -An Error thrown when the `MappedType` value or `RawType` value -are invalid for an `Enum`. - -## Cases -### `valueNotFound` - -```swift -case valueNotFound -``` diff --git a/Documentation/Reference/Options/extensions/EnumSet.md b/Documentation/Reference/Options/extensions/EnumSet.md deleted file mode 100644 index e8af1f1..0000000 --- a/Documentation/Reference/Options/extensions/EnumSet.md +++ /dev/null @@ -1,46 +0,0 @@ -**EXTENSION** - -# `EnumSet` -```swift -extension EnumSet where EnumType: CaseIterable -``` - -## Methods -### `array()` - -```swift -public func array() -> [EnumType] -``` - -Returns an array of the enum values based on the OptionSet -- Returns: Array for each value represented by the enum. - -### `init(from:)` - -```swift -public init(from decoder: Decoder) throws -``` - -Decodes the EnumSet based on an Array of MappedTypes. -- Parameter decoder: Decoder which contains info as an array of MappedTypes. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| decoder | Decoder which contains info as an array of MappedTypes. | - -### `encode(to:)` - -```swift -public func encode(to encoder: Encoder) throws -``` - -Encodes the EnumSet based on an Array of MappedTypes. -- Parameter encoder: Encoder which will contain info as an array of MappedTypes. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| encoder | Encoder which will contain info as an array of MappedTypes. | \ No newline at end of file diff --git a/Documentation/Reference/Options/extensions/MappedValueCollectionRepresented.md b/Documentation/Reference/Options/extensions/MappedValueCollectionRepresented.md deleted file mode 100644 index 7c43682..0000000 --- a/Documentation/Reference/Options/extensions/MappedValueCollectionRepresented.md +++ /dev/null @@ -1,48 +0,0 @@ -**EXTENSION** - -# `MappedValueCollectionRepresented` -```swift -public extension MappedValueCollectionRepresented -``` - -## Methods -### `rawValue(basedOn:)` - -```swift -static func rawValue(basedOn value: MappedType) throws -> RawValue -``` - -Gets the raw value based on the MappedType by finding the index of the mapped value. -- Parameter value: MappedType value. -- Throws: `MappedValueCollectionRepresentedError.valueNotFound` - If the value was not found in the array -- Returns: - The raw value of the enumeration - based on the index the MappedType value was found at. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| value | MappedType value. | - -### `mappedValue(basedOn:)` - -```swift -static func mappedValue(basedOn rawValue: RawValue) throws -> MappedType -``` - -Gets the mapped value based on the rawValue -by access the array at the raw value subscript. -- 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. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| rawValue | The raw value of the enumeration which pretains to its index in the `mappedValues` Array. | \ No newline at end of file diff --git a/Documentation/Reference/Options/extensions/MappedValueDictionaryRepresented.md b/Documentation/Reference/Options/extensions/MappedValueDictionaryRepresented.md deleted file mode 100644 index 2dbb3d5..0000000 --- a/Documentation/Reference/Options/extensions/MappedValueDictionaryRepresented.md +++ /dev/null @@ -1,48 +0,0 @@ -**EXTENSION** - -# `MappedValueDictionaryRepresented` -```swift -public extension MappedValueDictionaryRepresented -``` - -## Methods -### `rawValue(basedOn:)` - -```swift -static func rawValue(basedOn value: MappedType) throws -> RawValue -``` - -Gets the raw value based on the MappedType by finding the key of the mapped value. -- Parameter value: MappedType value. -- Throws: `MappedValueCollectionRepresentedError.valueNotFound` - If the value was not found in the dictionary -- Returns: - The raw value of the enumeration - based on the key the MappedType value. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| value | MappedType value. | - -### `mappedValue(basedOn:)` - -```swift -static func mappedValue(basedOn rawValue: RawValue) throws -> MappedType -``` - -Gets the mapped value based on the rawValue -by access the dictionary at the raw value key. -- 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. key) is not in the dictionary. -- Returns: - The Mapped Type value based on the raw value key. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| rawValue | The raw value of the enumeration which pretains to its index in the `mappedValues` Array. | \ No newline at end of file diff --git a/Documentation/Reference/Options/extensions/MappedValueRepresentable.md b/Documentation/Reference/Options/extensions/MappedValueRepresentable.md deleted file mode 100644 index d60f114..0000000 --- a/Documentation/Reference/Options/extensions/MappedValueRepresentable.md +++ /dev/null @@ -1,29 +0,0 @@ -**EXTENSION** - -# `MappedValueRepresentable` -```swift -public extension MappedValueRepresentable -``` - -## Methods -### `mappedValue()` - -```swift -func mappedValue() throws -> MappedType -``` - -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 - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| rawValue | The raw value of the enumeration which pretains to its index in the `mappedValues` Array. | \ No newline at end of file diff --git a/Documentation/Reference/Options/protocols/MappedValueCollectionRepresented.md b/Documentation/Reference/Options/protocols/MappedValueCollectionRepresented.md deleted file mode 100644 index eccfc19..0000000 --- a/Documentation/Reference/Options/protocols/MappedValueCollectionRepresented.md +++ /dev/null @@ -1,10 +0,0 @@ -**PROTOCOL** - -# `MappedValueCollectionRepresented` - -```swift -public protocol MappedValueCollectionRepresented: MappedValueRepresentable - where RawValue == Int, MappedType: Equatable -``` - -Protocol which simplifies mapped value by using an ordered Array of values. diff --git a/Documentation/Reference/Options/protocols/MappedValueDictionaryRepresented.md b/Documentation/Reference/Options/protocols/MappedValueDictionaryRepresented.md deleted file mode 100644 index d43d620..0000000 --- a/Documentation/Reference/Options/protocols/MappedValueDictionaryRepresented.md +++ /dev/null @@ -1,10 +0,0 @@ -**PROTOCOL** - -# `MappedValueDictionaryRepresented` - -```swift -public protocol MappedValueDictionaryRepresented: MappedValueRepresentable - where RawValue == Int, MappedType: Equatable -``` - -Protocol which simplifies mapped value by using a dictionary. diff --git a/Documentation/Reference/Options/protocols/MappedValueRepresentable.md b/Documentation/Reference/Options/protocols/MappedValueRepresentable.md deleted file mode 100644 index d2d54cf..0000000 --- a/Documentation/Reference/Options/protocols/MappedValueRepresentable.md +++ /dev/null @@ -1,40 +0,0 @@ -**PROTOCOL** - -# `MappedValueRepresentable` - -```swift -public protocol MappedValueRepresentable: RawRepresentable, CaseIterable -``` - -## Methods -### `rawValue(basedOn:)` - -```swift -static func rawValue(basedOn string: MappedType) throws -> RawValue -``` - -Gets the raw value based on the MappedType. -- Parameter value: MappedType value. -- Returns: The raw value of the enumeration based on the `MappedType `value. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| value | MappedType value. | - -### `mappedValue(basedOn:)` - -```swift -static func mappedValue(basedOn rawValue: RawValue) throws -> MappedType -``` - -Gets the `MappedType` value based on the `rawValue`. -- Parameter rawValue: The raw value of the enumeration. -- Returns: The Mapped Type value based on the `rawValue`. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| rawValue | The raw value of the enumeration. | \ No newline at end of file diff --git a/Documentation/Reference/Options/structs/EnumSet.md b/Documentation/Reference/Options/structs/EnumSet.md deleted file mode 100644 index 36f9ca9..0000000 --- a/Documentation/Reference/Options/structs/EnumSet.md +++ /dev/null @@ -1,50 +0,0 @@ -**STRUCT** - -# `EnumSet` - -```swift -public struct EnumSet: OptionSet - where EnumType.RawValue == Int -``` - -Generic struct for using Enums with RawValue type of Int as an Optionset - -## Properties -### `rawValue` - -```swift -public let rawValue: Int -``` - -Raw Value of the OptionSet - -## Methods -### `init(rawValue:)` - -```swift -public init(rawValue: Int) -``` - -Creates the EnumSet based on the `rawValue` -- Parameter rawValue: Integer raw value of the OptionSet - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| rawValue | Integer raw value of the OptionSet | - -### `init(values:)` - -```swift -public init(values: [EnumType]) -``` - -Creates the EnumSet based on the values in the array. -- Parameter values: Array of enum values. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| values | Array of enum values. | \ No newline at end of file diff --git a/Documentation/Reference/Options/structs/MappedEnum.md b/Documentation/Reference/Options/structs/MappedEnum.md deleted file mode 100644 index 18695d7..0000000 --- a/Documentation/Reference/Options/structs/MappedEnum.md +++ /dev/null @@ -1,65 +0,0 @@ -**STRUCT** - -# `MappedEnum` - -```swift -public struct MappedEnum: Codable - where EnumType.MappedType: Codable -``` - -A generic struct for enumerations which allow for additional values attached. - -## Properties -### `value` - -```swift -public let value: EnumType -``` - -Base Enumeraion value. - -## Methods -### `init(value:)` - -```swift -public init(value: EnumType) -``` - -Creates an instance based on the base enumeration value. -- Parameter value: Base Enumeration value. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| value | Base Enumeration value. | - -### `init(from:)` - -```swift -public init(from decoder: Decoder) throws -``` - -Decodes the value based on the mapped value. -- Parameter decoder: Decoder. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| decoder | Decoder. | - -### `encode(to:)` - -```swift -public func encode(to encoder: Encoder) throws -``` - -Encodes the value based on the mapped value. -- Parameter encoder: Encoder. - -#### Parameters - -| Name | Description | -| ---- | ----------- | -| encoder | Encoder. | \ No newline at end of file diff --git a/Documentation/Reference/Options/typealiases/EnumSet.RawValue.md b/Documentation/Reference/Options/typealiases/EnumSet.RawValue.md deleted file mode 100644 index b818eba..0000000 --- a/Documentation/Reference/Options/typealiases/EnumSet.RawValue.md +++ /dev/null @@ -1,7 +0,0 @@ -**TYPEALIAS** - -# `EnumSet.RawValue` - -```swift -public typealias RawValue = EnumType.RawValue -``` diff --git a/README.md b/README.md index 074e3f1..40ed2ed 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,17 @@

Options

-Swift Package for more powerful `Enum` types. +More powerful options for `Enum` and `OptionSet` types. [![SwiftPM](https://img.shields.io/badge/SPM-Linux%20%7C%20iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-success?logo=swift)](https://swift.org) [![Twitter](https://img.shields.io/badge/twitter-@brightdigit-blue.svg?style=flat)](http://twitter.com/brightdigit) ![GitHub](https://img.shields.io/github/license/brightdigit/Options) ![GitHub issues](https://img.shields.io/github/issues/brightdigit/Options) - -[![macOS](https://github.com/brightdigit/Options/workflows/macOS/badge.svg)](https://github.com/brightdigit/Options/actions?query=workflow%3AmacOS) -[![ubuntu](https://github.com/brightdigit/Options/workflows/ubuntu/badge.svg)](https://github.com/brightdigit/Options/actions?query=workflow%3Aubuntu) -[![Travis (.com)](https://img.shields.io/travis/com/brightdigit/Options?logo=travis&?label=travis-ci)](https://travis-ci.com/brightdigit/Options) -[![Bitrise](https://img.shields.io/bitrise/1c35b4466fb0a529?logo=bitrise&?label=bitrise&token=ryjPpLP4dkC5v-RV1fzKaw)](https://app.bitrise.io/app/1c35b4466fb0a529) -[![CircleCI](https://img.shields.io/circleci/build/github/brightdigit/Options?logo=circleci&?label=circle-ci&token=a7ad8ba0bfc08f6c9c0ce786ac9c1ddfac871993)](https://app.circleci.com/pipelines/github/brightdigit/Options) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/brightdigit/Options/Options.yml?label=actions&logo=github&?branch=main) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FOptions%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/brightdigit/Options) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FOptions%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/brightdigit/Options) - [![Codecov](https://img.shields.io/codecov/c/github/brightdigit/Options)](https://codecov.io/gh/brightdigit/Options) [![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/brightdigit/Options)](https://www.codefactor.io/repository/github/brightdigit/Options) [![codebeat badge](https://codebeat.co/badges/c47b7e58-867c-410b-80c5-57e10140ba0f)](https://codebeat.co/projects/github-com-brightdigit-mistkit-main) @@ -32,254 +26,156 @@ Swift Package for more powerful `Enum` types. # Table of Contents - * [**Introduction**](#introduction) - * [**Features**](#features) - * [**Installation**](#installation) - * [**Usage**](#usage) - * [Setting up a **`MappedValueRepresentable`** Enum](#setting-up-a-mappedvaluerepresentable-enum) - * [Using **`MappedValueCollectionRepresented`**](#using-mappedvaluecollectionrepresented) - * [Codable Enums using a **`MappedEnum`** Type](#codable-enums-using-a-mappedenum-type) - * [Using Enums in OptionSets with **`EnumSet`**](#using-enums-in-optionsets-with-enumset) - * [Converting **`EnumSet`** to Enum Array](#converting-enumset-to-enum-array) - * [Codable **`EnumSet`** using a **`MappedValueRepresentable`** Enum](#codable-enumset-using-a-mappedvaluerepresentable-enum) - * [Further Code Documentation](#further-code-documentation) - * [**License**](#license) + * [Introduction](#introduction) + * [Requirements](#requirements) + * [Installation](#installation) + * [Usage](#usage) + * [Versatile Options with Enums and OptionSets](#versatile-options-with-enums-and-optionsets) + * [Multiple Value Types](#multiple-value-types) + * [Creating an OptionSet](#creating-an-optionset) + * [Further Code Documentation](#further-code-documentation) + * [License](#license) # Introduction -**Options** provides a features to `Enum` and `OptionSet` types such as: - -* Providing additional value types besides the `RawType rawValue` -* Being able to interchange between `Enum` and `OptionSet` types -* Using an additional value type for a `Codable` `OptionSet` +**Options** provides a powerful set of features for `Enum` and `OptionSet` types: -## Example +- 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` -Let's say you have `Enum` type: +# Requirements -```swift -enum ContinuousIntegrationSystem { - case github - case travisci - case circleci - case bitrise -} -``` +**Apple Platforms** -We want two things: - -* Use it as an `OptionSet` so we can store multiple CI sytems -* Store and parse it via a `String` - -If `OptionSet` requires an `Int` `RawType`, how can we parse and store as `String`? - -With **Options** we can enable `ContinuousIntegrationSystem` to do both: - -```swift -enum ContinuousIntegrationSystem: Int, MappedValueCollectionRepresented { - case github - case travisci - case circleci - case bitrise - - typealias MappedType = String - - static let mappedValues = [ - "github", - "travisci", - "circleci", - "bitrise" - ] -} +- Xcode 14.1 or later +- Swift 5.7.1 or later +- iOS 16 / watchOS 9 / tvOS 16 / macOS 12 or later deployment targets -typealias ContinuousIntegrationSystemSet = EnumSet +**Linux** -let systems = ContinuousIntegrationSystemSet([.travisci, .github]) -``` +- Ubuntu 20.04 or later +- Swift 5.7.1 or later # Installation -Swift Package Manager is Apple's decentralized dependency manager to integrate libraries to your Swift projects. It is now fully integrated with Xcode 11. - -To integrate **Options** into your project using SPM, specify it in your Package.swift file: - -```swift -let package = Package( - ... - dependencies: [ - .package(url: "https://github.com/brightdigit/Options", from: "0.1.0") - ], - targets: [ - .target( - name: "YourTarget", - dependencies: ["Options", ...]), - ... - ] -) +Use the Swift Package Manager to install this library via the repository url: + +``` +https://github.com/brightdigit/Options.git ``` +Use version up to `1.0`. + # Usage -## Setting up a [MappedValueRepresentable](/Documentation/Reference/Options/protocols/MappedValueRepresentable.md) Enum +## Versatile Options -So let's say we our `enum`: +Let's say we are using an `Enum` for a list of popular social media networks: ```swift -enum ContinuousIntegrationSystem: Int { - case github - case travisci - case circleci - case bitrise +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 } ``` -We want to be able to make it available as an `OptionSet` so it needs an `RawType` of `Int`. -However we want to decode and encode it via `Codable` as a `String`. - -**Options** has a protocol `MappedValueRepresentable` which allows to do that by implementing it. +We'll be using this as a way to define a particular social handle: ```swift -enum ContinuousIntegrationSystem: Int, MappedValueRepresentable { - case github - case travisci - case circleci - case bitrise - - static func rawValue(basedOn string: String) throws -> Int { - if (string == "github") { - return 0 - } else { - ... - } else { - throw ... - } - } - - static func mappedValue(basedOn rawValue: Int) throws -> String { - if (rawValue == 0) { - return "github" - } else { - ... - } else { - throw ... - } - } +struct SocialHandle { + let name : String + let network : SocialNetwork } ``` -This can be simplified further by using [`MappedValueCollectionRepresented`](/Documentation/Reference/Options/protocols/MappedValueCollectionRepresented.md). - -## Using [MappedValueCollectionRepresented](/Documentation/Reference/Options/protocols/MappedValueCollectionRepresented.md) - -By using `MappedValueCollectionRepresented`, you can simplify implementing [`MappedValueRepresentable`](/Documentation/Reference/Options/protocols/MappedValueRepresentable.md): +However we also want to provide a way to have a unique set of social networks available: ```swift -enum ContinuousIntegrationSystem: Int, MappedValueCollectionRepresented { - case github - case travisci - case circleci - case bitrise - - static let mappedValues = [ - "github", - "travisci", - "circleci", - "bitrise" - ] +struct SocialNetworkSet : Int, OptionSet { +... } -``` -Now we we've made it simplifies implementing [`MappedValueRepresentable`](/Documentation/Reference/Options/protocols/MappedValueRepresentable.md) so let's look how to use it with `Codable`. - -## Codable Enums using a [MappedEnum](/Documentation/Reference/Options/structs/MappedEnum.md) Type - -So you've setup a `MappedValueRepresentable` `enum`, the next part is having the `MappedType` which in this case is `String` the part that's used in `Codable`. +let user : User +let networks : SocialNetworkSet = user.availableNetworks() +``` -This is where [`MappedEnum`](/Documentation/Reference/Options/structs/MappedEnum.md) is used: +We can then simply use ``Options()`` macro to generate both these types: ```swift -struct BuildSetup : Codable { - let ci: MappedEnum +@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 if the `String` can be used in encoding and decoding the value rather than the `RawType` `Int`: +Now we can use the newly create `SocialNetworkSet` type to store a set of values: -```json -{ - "ci" : "github" -} +```swift +let networks : SocialNetworkSet +networks = [.aim, .delicious, .googleplus, .windowslive] ``` -Next, let's take a look how we could use `ContinuousIntegrationSystem` in an `OptionSet`. +## Multiple Value Types -## Using Enums in OptionSets with [EnumSet](/Documentation/Reference/Options/structs/EnumSet.md) +With the ``Options()`` macro, we add the ability to encode and decode values not only from their raw value but also from a another type such as a string. This is useful for when you want to store the values in JSON format. -[`EnumSet`](/Documentation/Reference/Options/structs/EnumSet.md) allows you to interchangeably use `Enum` with an `OptionSet`. [`EnumSet`](/Documentation/Reference/Options/structs/EnumSet.md) is a Generic `struct` while takes any `Enum` type with a `RawType`. So we can create an `OptionSet` instance which uses out `ContinuousIntegrationSystem`: +For instance, with a type like `SocialNetwork` we need need to store the value as an Integer: -```swift -let systems = EnumSet([.travisci, .github]) +```json +5 ``` -## Converting EnumSet to Enum Array - -If your `Enum` implements `CaseIterable`, then you can extract the individual `ContinuousIntegrationSystem` enum values with `.array()`: - -```swift -enum ContinuousIntegrationSystem: Int, CaseIterable { - case github - case travisci - case circleci - case bitrise -} - -let systems = EnumSet([.travisci, .github]) +However by adding the ``Options()`` macro we can also decode from a String: -print(systems.array()) +``` +"googleplus" ``` -Lastly, let's put all this together. - -## Codable EnumSet using a MappedValueRepresentable Enum +## Creating an OptionSet -If your `enum` implements `MappedValueRepresentable` and you use it in an [`EnumSet`](/Documentation/Reference/Options/structs/EnumSet.md), then you can allow for your `OptionSet` to be `Codable` as an `Array` of values rather than the cumulative `rawValue`: +We can also have a new `OptionSet` type created. ``Options()`` create a new `OptionSet` type with the suffix `-Set`. This new `OptionSet` will automatically work with your enum to create a distinct set of values. Additionally it will decode and encode your values as an Array of String. This means the value: ```swift -enum ContinuousIntegrationSystem: Int, MappedValueCollectionRepresented, CaseIterable { -case github -case travisci -case circleci -case bitrise - -static let mappedValues = [ - "github", - "travisci", - "circleci", - "bitrise" -] -} - - -struct BuildSetup : Codable { - let systems: EnumSet -} - -let systems = BuildSetup(systems: EnumSet(values: [.travisci, .github])) +[.aim, .delicious, .googleplus, .windowslive] ``` -For our `systems` variable, our `Codable` data would be: +is encoded as: ```json -{ - "systems" : ["travisci", "github"] -} +["aim", "delicious", "googleplus", "windowslive"] ``` -This will make it easier for making our data human-readable instead of using the `rawValue` of `3`. - # Further Code Documentation -[Documentation Here](/Documentation/Reference/Options/README.md) +[Documentation Here](https://swiftpackageindex.com/brightdigit/Options/main/documentation/options) # License diff --git a/Scripts/gh-md-toc b/Scripts/gh-md-toc index 8d35839..03b5ddd 100755 --- a/Scripts/gh-md-toc +++ b/Scripts/gh-md-toc @@ -23,7 +23,7 @@ # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) # -gh_toc_version="0.8.0" +gh_toc_version="0.10.0" gh_user_agent="gh-md-toc v$gh_toc_version" @@ -55,24 +55,24 @@ gh_toc_md2html() { URL=https://api.github.com/markdown/raw - if [ ! -z "$GH_TOC_TOKEN" ]; then + if [ -n "$GH_TOC_TOKEN" ]; then TOKEN=$GH_TOC_TOKEN else TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" if [ -f "$TOKEN_FILE" ]; then - TOKEN="$(cat $TOKEN_FILE)" + TOKEN="$(cat "$TOKEN_FILE")" fi fi - if [ ! -z "${TOKEN}" ]; then + if [ -n "${TOKEN}" ]; then AUTHORIZATION="Authorization: token ${TOKEN}" fi local gh_tmp_file_md=$gh_file_md if [ "$skip_header" = "yes" ]; then - if grep -Fxq "" $gh_src; then + if grep -Fxq "" "$gh_src"; then # cut everything before the toc gh_tmp_file_md=$gh_file_md~~ - sed '1,//d' $gh_file_md > $gh_tmp_file_md + sed '1,//d' "$gh_file_md" > "$gh_tmp_file_md" fi fi @@ -84,7 +84,7 @@ gh_toc_md2html() { -H "$AUTHORIZATION" \ "$URL") - rm -f $gh_file_md~~ + rm -f "${gh_file_md}~~" if [ "$?" != "0" ]; then echo "XXNetworkErrorXX" @@ -152,7 +152,8 @@ gh_toc(){ echo fi else - local rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header") + local rawhtml + rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header") if [ "$rawhtml" == "XXNetworkErrorXX" ]; then echo "Parsing local markdown file requires access to github API" echo "Please make sure curl is installed and check your network connectivity" @@ -165,10 +166,11 @@ gh_toc(){ echo "or place GitHub auth token here: ${TOKEN_FILE}" exit 1 fi - local toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"` + local toc + toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"` echo "$toc" if [ "$need_replace" = "yes" ]; then - if grep -Fxq "" $gh_src && grep -Fxq "" $gh_src; then + if grep -Fxq "" "$gh_src" && grep -Fxq "" "$gh_src"; then echo "Found markers" else echo "You don't have or in your file...exiting" @@ -176,14 +178,16 @@ gh_toc(){ fi local ts="<\!--ts-->" local te="<\!--te-->" - local dt=`date +'%F_%H%M%S'` + local dt + dt=$(date +'%F_%H%M%S') local ext=".orig.${dt}" local toc_path="${gh_src}.toc.${dt}" local toc_createdby="" - local toc_footer="" + local toc_footer + toc_footer="" # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html # clear old TOC - sed -i${ext} "/${ts}/,/${te}/{//!d;}" "$gh_src" + sed -i"${ext}" "/${ts}/,/${te}/{//!d;}" "$gh_src" # create toc file echo "${toc}" > "${toc_path}" if [ "${no_footer}" != "yes" ]; then @@ -198,7 +202,7 @@ gh_toc(){ fi echo if [ "${no_backup}" = "yes" ]; then - rm ${toc_path} ${gh_src}${ext} + rm "$toc_path" "$gh_src$ext" fi echo "!! TOC was added into: '$gh_src'" if [ -z "${no_backup}" ]; then @@ -218,6 +222,8 @@ gh_toc(){ # $2 - number of spaces used to indent. # gh_toc_grab() { + + href_regex="/href=\"[^\"]+?\"/" common_awk_script=' modified_href = "" split(href, chars, "") @@ -237,26 +243,25 @@ gh_toc_grab() { } print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")" ' - if [ `uname -s` == "OS/390" ]; then + if [ "`uname -s`" == "OS/390" ]; then grepcmd="pcregrep -o" echoargs="" awkscript='{ - level = substr($0, length($0), 1) - text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5) - href = substr($0, match($0, "href=\"([^\"]+)?\"")+6, RLENGTH-7) + level = substr($0, 3, 1) + text = substr($0, match($0, /<\/span><\/a>[^<]*<\/h/)+11, RLENGTH-14) + href = substr($0, match($0, '$href_regex')+6, RLENGTH-7) '"$common_awk_script"' }' else grepcmd="grep -Eo" echoargs="-e" awkscript='{ - level = substr($0, length($0), 1) - text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5) - href = substr($0, match($0, "href=\"[^\"]+?\"")+6, RLENGTH-7) + level = substr($0, 3, 1) + text = substr($0, match($0, /">.*<\/h/)+2, RLENGTH-5) + href = substr($0, match($0, '$href_regex')+6, RLENGTH-7) '"$common_awk_script"' }' fi - href_regex='href=\"[^\"]+?\"' # if closed is on the new line, then move it on the prev line # for example: @@ -265,8 +270,11 @@ gh_toc_grab() { # became: The command foo1 sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | + # Sometimes a line can start with . Fix that. + sed -e ':a' -e 'N' -e '$!ba' -e 's/\n//g' | sed 's/<\/code>//g' | @@ -275,7 +283,7 @@ gh_toc_grab() { sed 's/]*[^<]*<\/g-emoji> //g' | # now all rows are like: - # ... title.. # format result line # * $0 - whole string # * last element of each row: "/dev/null`; then - echo `$tool --version | head -n 1` + if type $tool &>/dev/null; then + $tool --version | head -n 1 else echo "not installed" fi @@ -310,7 +318,8 @@ show_version() { } show_help() { - local app_name=$(basename "$0") + local app_name + app_name=$(basename "$0") echo "GitHub TOC generator ($app_name): $gh_toc_version" echo "" echo "Usage:" @@ -355,17 +364,18 @@ gh_toc_app() { if [ "$1" = "-" ]; then if [ -z "$TMPDIR" ]; then TMPDIR="/tmp" - elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then + elif [ -n "$TMPDIR" ] && [ ! -d "$TMPDIR" ]; then mkdir -p "$TMPDIR" fi local gh_tmp_md - if [ `uname -s` == "OS/390" ]; then - local timestamp=$(date +%m%d%Y%H%M%S) + if [ "`uname -s`" == "OS/390" ]; then + local timestamp + timestamp=$(date +%m%d%Y%H%M%S) gh_tmp_md="$TMPDIR/tmp.$timestamp" else - gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX) + gh_tmp_md=$(mktemp "$TMPDIR/tmp.XXXXXX") fi - while read input; do + while read -r input; do echo "$input" >> "$gh_tmp_md" done gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent" @@ -408,4 +418,4 @@ gh_toc_app() { # # Entry point # -gh_toc_app "$@" +gh_toc_app "$@" \ No newline at end of file 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/Array.swift b/Sources/Options/Array.swift index e192aa1..0c4db74 100644 --- a/Sources/Options/Array.swift +++ b/Sources/Options/Array.swift @@ -29,11 +29,16 @@ // swiftlint:disable:next line_length @available(*, deprecated, renamed: "MappedValueGenericRepresented", message: "Use MappedValueGenericRepresented instead.") -public protocol MappedValueCollectionRepresented: MappedValueGenericRepresented +public protocol MappedValueCollectionRepresented: MappedValueRepresented where MappedValueType: Sequence {} -extension Array: MappedValues where Element: Equatable { - public func key(value: Element) throws -> Int { +extension Array: MappedValues where Element: Equatable {} + +extension Collection where Element: Equatable, Self: MappedValues { + /// Get the index based on the value passed. + /// - Parameter value: Value to search. + /// - Returns: Index found. + public func key(value: Element) throws -> Self.Index { guard let index = firstIndex(of: value) else { throw MappedValueRepresentableError.valueNotFound } @@ -41,8 +46,11 @@ extension Array: MappedValues where Element: Equatable { return index } - public func value(key: Int) throws -> Element { - guard key < count, key >= 0 else { + /// Gets the value based on the index. + /// - Parameter key: The index. + /// - Returns: The value at index. + public func value(key: Self.Index) throws -> Element { + guard key < endIndex, key >= startIndex else { throw MappedValueRepresentableError.valueNotFound } return self[key] diff --git a/Sources/Options/CodingOptions.swift b/Sources/Options/CodingOptions.swift new file mode 100644 index 0000000..e26ad68 --- /dev/null +++ b/Sources/Options/CodingOptions.swift @@ -0,0 +1,49 @@ +// +// 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 + +/// Options for how a ``MappedValueRepresentable`` type is encoding and decoded. +public struct CodingOptions: OptionSet, Sendable { + /// Allow decoding from String + public static let allowMappedValueDecoding: CodingOptions = .init(rawValue: 1) + + /// Encode the value as a String. + public static let encodeAsMappedValue: CodingOptions = .init(rawValue: 2) + + /// Default options. + public static let `default`: CodingOptions = + [.allowMappedValueDecoding, encodeAsMappedValue] + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} diff --git a/Sources/Options/Dictionary.swift b/Sources/Options/Dictionary.swift index 79e2b8e..a7a3b31 100644 --- a/Sources/Options/Dictionary.swift +++ b/Sources/Options/Dictionary.swift @@ -29,11 +29,11 @@ // swiftlint:disable:next line_length @available(*, deprecated, renamed: "MappedValueGenericRepresented", message: "Use MappedValueGenericRepresented instead.") -public protocol MappedValueDictionaryRepresented: MappedValueGenericRepresented +public protocol MappedValueDictionaryRepresented: MappedValueRepresented where MappedValueType == [Int: MappedType] {} -extension Dictionary: MappedValues where Key == Int, Value: Equatable { - public func key(value: Value) throws -> Int { +extension Dictionary: MappedValues where Value: Equatable { + public func key(value: Value) throws -> Key { let pair = first { $0.value == value } guard let key = pair?.key else { throw MappedValueRepresentableError.valueNotFound @@ -42,7 +42,7 @@ extension Dictionary: MappedValues where Key == Int, Value: Equatable { return key } - public func value(key: Int) throws -> Value { + public func value(key: Key) throws -> Value { guard let value = self[key] else { throw MappedValueRepresentableError.valueNotFound } diff --git a/Sources/Options/Documentation.docc/Documentation.md b/Sources/Options/Documentation.docc/Documentation.md new file mode 100644 index 0000000..994798d --- /dev/null +++ b/Sources/Options/Documentation.docc/Documentation.md @@ -0,0 +1,162 @@ +# ``Options`` + +More powerful options for `Enum` and `OptionSet` types. + +## Overview + +**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` + +### Requirements + +**Apple Platforms** + +- Xcode 14.1 or later +- Swift 5.7.1 or later +- iOS 16 / watchOS 9 / tvOS 16 / macOS 12 or later deployment targets + +**Linux** + +- Ubuntu 20.04 or later +- Swift 5.7.1 or later + +### Installation + +Use the Swift Package Manager to install this library via the repository url: + +``` +https://github.com/brightdigit/Options.git +``` + +Use version up to `1.0`. + +### Versatile Options + +Let's say we are using an `Enum` for a list of popular social media networks: + +```swift +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 +} +``` + +We'll be using this as a way to define a particular social handle: + +```swift +struct SocialHandle { + let name : String + let network : SocialNetwork +} +``` + +However we also want to provide a way to have a unique set of social networks available: + +```swift +struct SocialNetworkSet : Int, OptionSet { +... +} + +let user : User +let networks : SocialNetworkSet = user.availableNetworks() +``` + +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 : SocialNetworkSet +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 value but also from a another type such as a string. This is useful for when you want to store the values in JSON format. + +For instance, with a type like `SocialNetwork` we need need to store the value as an Integer: + +```json +5 +``` + +However by adding the ``Options()`` macro we can also decode from a String: + +``` +"googleplus" +``` + +### Creating an OptionSet + +We can also have a new `OptionSet` type created. ``Options()`` create a new `OptionSet` type with the suffix `-Set`. This new `OptionSet` will automatically work with your enum to create a distinct set of values. Additionally it will decode and encode your values as an Array of String. This means the value: + +```swift +[.aim, .delicious, .googleplus, .windowslive] +``` + +is encoded as: + +```json +["aim", "delicious", "googleplus", "windowslive"] +``` + +## Topics + +### Options conformance + +- ``Options()`` +- ``MappedValueRepresentable`` +- ``MappedValueRepresented`` +- ``EnumSet`` + +### Advanced customization + +- ``CodingOptions`` +- ``MappedValues`` + +### Errors + +- ``MappedValueRepresentableError-2k4ki`` + +### Deprecated + +- ``MappedValueCollectionRepresented`` +- ``MappedValueDictionaryRepresented`` +- ``MappedEnum`` diff --git a/Sources/Options/EnumSet.swift b/Sources/Options/EnumSet.swift index 69a4b3b..c2e447e 100644 --- a/Sources/Options/EnumSet.swift +++ b/Sources/Options/EnumSet.swift @@ -1,17 +1,49 @@ -/// Generic struct for using Enums with RawValue type of Int as an Optionset -public struct EnumSet: OptionSet, Sendable - where EnumType.RawValue == Int { +/// Generic struct for using Enums with `RawValue`. +/// +/// If you have an `enum` such as: +/// ```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 +/// } +/// ``` +/// An ``EnumSet`` could be used to store multiple values as an `OptionSet`: +/// ```swift +/// let socialNetworks : EnumSet = +/// [.digg, .aim, .yahoo, .miiverse] +/// ``` +public struct EnumSet: + OptionSet, Sendable, ExpressibleByArrayLiteral + where EnumType.RawValue: FixedWidthInteger & Sendable { public typealias RawValue = EnumType.RawValue /// Raw Value of the OptionSet - public let rawValue: Int + public let rawValue: RawValue /// Creates the EnumSet based on the `rawValue` /// - Parameter rawValue: Integer raw value of the OptionSet - public init(rawValue: Int) { + public init(rawValue: RawValue) { 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]) { @@ -20,13 +52,19 @@ public struct EnumSet: OptionSet, Sendable } internal static func cumulativeValue( - basedOnRawValues rawValues: Set) -> Int { + basedOnRawValues rawValues: Set) -> RawValue { rawValues.map { 1 << $0 }.reduce(0, |) } } +extension FixedWidthInteger { + fileprivate static var one: Self { + 1 + } +} + extension EnumSet where EnumType: CaseIterable { - internal static func enums(basedOnRawValue rawValue: Int) -> [EnumType] { + internal static func enums(basedOnRawValue rawValue: RawValue) -> [EnumType] { let cases = EnumType.allCases.sorted { $0.rawValue < $1.rawValue } var values = [EnumType]() var current = rawValue @@ -34,8 +72,8 @@ extension EnumSet where EnumType: CaseIterable { guard current > 0 else { break } - let rawValue = 1 << item.rawValue - if current & rawValue != 0 { + let rawValue = RawValue.one << item.rawValue + if current & rawValue != .zero { values.append(item) current -= rawValue } diff --git a/Sources/Options/Macro.swift b/Sources/Options/Macro.swift index e120423..ab7ff7b 100644 --- a/Sources/Options/Macro.swift +++ b/Sources/Options/Macro.swift @@ -30,9 +30,11 @@ import Foundation #if swift(>=5.10) + /// Sets an enumeration up to implement + /// ``MappedValueRepresentable`` and ``MappedValueRepresented``. @attached( extension, - conformances: MappedValueRepresentable, MappedValueGenericRepresented, + conformances: MappedValueRepresentable, MappedValueRepresented, names: named(MappedType), named(mappedValues) ) @attached(peer, names: suffixed(Set)) diff --git a/Sources/Options/MappedEnum.swift b/Sources/Options/MappedEnum.swift index 1e651d5..9298ada 100644 --- a/Sources/Options/MappedEnum.swift +++ b/Sources/Options/MappedEnum.swift @@ -1,4 +1,10 @@ /// A generic struct for enumerations which allow for additional values attached. +@available( + *, + deprecated, + renamed: "MappedValueRepresentable", + message: "Use `MappedValueRepresentable` with `CodingOptions`." +) public struct MappedEnum: Codable, Sendable where EnumType.MappedType: Codable { /// Base Enumeraion value. @@ -20,7 +26,8 @@ public struct MappedEnum: Codable, Sendable let label = try container.decode(EnumType.MappedType.self) let rawValue = try EnumType.rawValue(basedOn: label) guard let value = EnumType(rawValue: rawValue) else { - preconditionFailure("Invalid Raw Value.") + assertionFailure("Every mappedValue should always return a valid rawValue.") + throw DecodingError.invalidRawValue(rawValue) } self.value = value } @@ -42,7 +49,8 @@ public struct MappedEnum: Codable, Sendable let label = try container.decode(EnumType.MappedType.self) let rawValue = try EnumType.rawValue(basedOn: label) guard let value = EnumType(rawValue: rawValue) else { - preconditionFailure("Invalid Raw Value.") + assertionFailure("Every mappedValue should always return a valid rawValue.") + throw DecodingError.invalidRawValue(rawValue) } self.value = value } diff --git a/Sources/Options/MappedValueRepresentable+Codable.swift b/Sources/Options/MappedValueRepresentable+Codable.swift new file mode 100644 index 0000000..550f7c6 --- /dev/null +++ b/Sources/Options/MappedValueRepresentable+Codable.swift @@ -0,0 +1,97 @@ +// +// 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 { + assertionFailure("Every mappedValue should always return a valid rawValue.") + throw DecodingError.invalidRawValue(rawValue) + } + + 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..896f4a9 100644 --- a/Sources/Options/MappedValueRepresentable.swift +++ b/Sources/Options/MappedValueRepresentable.swift @@ -27,8 +27,17 @@ // OTHER DEALINGS IN THE SOFTWARE. // +/// An enum which has an additional value attached. +/// - Note: ``Options()`` macro will automatically set this up for you. public protocol MappedValueRepresentable: RawRepresentable, CaseIterable, Sendable { + /// The additional value type. associatedtype MappedType = String + + /// Options for how the enum should be decoded or encoded. + 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 +50,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 +62,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/Sources/Options/MappedValueRepresentableError.swift b/Sources/Options/MappedValueRepresentableError.swift index 3d1389f..d303174 100644 --- a/Sources/Options/MappedValueRepresentableError.swift +++ b/Sources/Options/MappedValueRepresentableError.swift @@ -34,6 +34,7 @@ import Foundation /// An Error thrown when the `MappedType` value or `RawType` value /// are invalid for an `Enum`. public enum MappedValueRepresentableError: Error, Sendable { + /// Whenever a value or key cannot be found. case valueNotFound } #else diff --git a/Sources/Options/MappedValueGenericRepresented.swift b/Sources/Options/MappedValueRepresented.swift similarity index 89% rename from Sources/Options/MappedValueGenericRepresented.swift rename to Sources/Options/MappedValueRepresented.swift index 271510b..26ef4e5 100644 --- a/Sources/Options/MappedValueGenericRepresented.swift +++ b/Sources/Options/MappedValueRepresented.swift @@ -1,5 +1,5 @@ // -// MappedValueGenericRepresented.swift +// MappedValueRepresented.swift // SimulatorServices // // Created by Leo Dion. @@ -28,14 +28,15 @@ // /// Protocol which simplifies ``MappedValueRepresentable``by using a ``MappedValues``. -public protocol MappedValueGenericRepresented: MappedValueRepresentable - where RawValue == Int, MappedType: Equatable { - associatedtype MappedValueType: MappedValues +public protocol MappedValueRepresented: MappedValueRepresentable + where MappedType: Equatable { + /// A object to lookup values and keys for mapped values. + associatedtype MappedValueType: MappedValues /// An array of the mapped values which lines up with each case. static var mappedValues: MappedValueType { get } } -extension MappedValueGenericRepresented { +extension MappedValueRepresented { /// Gets the raw value based on the MappedType by finding the index of the mapped value. /// - Parameter value: MappedType value. /// - Throws: `MappedValueCollectionRepresentedError.valueNotFound` diff --git a/Sources/Options/MappedValues.swift b/Sources/Options/MappedValues.swift index 8d067eb..67d0a45 100644 --- a/Sources/Options/MappedValues.swift +++ b/Sources/Options/MappedValues.swift @@ -29,12 +29,14 @@ import Foundation -/// Protocol which provides a method for ``MappedValueGenericRepresented`` to pull values. -public protocol MappedValues { +/// Protocol which provides a method for ``MappedValueRepresented`` to pull values. +public protocol MappedValues { /// Raw Value Type associatedtype Value: Equatable + /// Key Value Type + associatedtype Key: Equatable /// get the key vased on the value. - func key(value: Value) throws -> Int + func key(value: Value) throws -> Key /// get the value based on the key/index. - func value(key: Int) throws -> Value + func value(key: Key) throws -> Value } 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..98565ea 100644 --- a/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift +++ b/Tests/OptionsTests/MappedValueCollectionRepresentedTests.swift @@ -74,4 +74,84 @@ internal final class MappedValueCollectionRepresentedTests: XCTestCase { XCTAssertEqual(caughtError, .valueNotFound) } + + internal func testCodingOptions() { + XCTAssertEqual(MockDictionaryEnum.codingOptions, .default) + } + + internal func testInvalidRaw() throws { + let rawValue = Int.random(in: 5 ... 1_000) + + let rawValueJSON = "\(rawValue)" + + let rawValueJSONData = rawValueJSON.data(using: .utf8)! + + let decodingError: DecodingError + do { + let value = try Self.decoder.decode(MockCollectionEnum.self, from: rawValueJSONData) + XCTAssertNil(value) + return + } catch let error as DecodingError { + decodingError = error + } + + XCTAssertNotNil(decodingError) + } + + internal func testCodable() throws { + let argumentSets = MockCollectionEnum.allCases.flatMap { + [($0, true), ($0, false)] + }.flatMap { + [($0.0, $0.1, true), ($0.0, $0.1, false)] + } + + for arguments in argumentSets { + try codableTest(value: arguments.0, allowMappedValue: arguments.1, encodeAsMappedValue: arguments.2) + } + } + + static let encoder = JSONEncoder() + static let decoder = JSONDecoder() + + private func codableTest(value: MockCollectionEnum, allowMappedValue: Bool, encodeAsMappedValue: Bool) throws { + let mappedValue = try value.mappedValue() + let rawValue = value.rawValue + + let mappedValueJSON = "\"\(mappedValue)\"" + let rawValueJSON = "\(rawValue)" + + let mappedValueJSONData = mappedValueJSON.data(using: .utf8)! + let rawValueJSONData = rawValueJSON.data(using: .utf8)! + + let oldOptions = MockCollectionEnum.codingOptions + MockCollectionEnum.codingOptions = .init([ + allowMappedValue ? CodingOptions.allowMappedValueDecoding : nil, + encodeAsMappedValue ? CodingOptions.encodeAsMappedValue : nil + ].compactMap { $0 }) + + defer { + MockCollectionEnum.codingOptions = oldOptions + } + + let mappedDecodeResult = Result { + try Self.decoder.decode(MockCollectionEnum.self, from: mappedValueJSONData) + } + + let actualRawValueDecoded = try Self.decoder.decode(MockCollectionEnum.self, from: rawValueJSONData) + + let actualEncodedJSON = try Self.encoder.encode(value) + + switch (allowMappedValue, mappedDecodeResult) { + case (true, let .success(actualMappedDecodedValue)): + XCTAssertEqual(actualMappedDecodedValue, value) + case (false, let .failure(error)): + XCTAssert(error is DecodingError) + default: + XCTFail("Unmatched situation \(allowMappedValue): \(mappedDecodeResult)") + } + + XCTAssertEqual(actualRawValueDecoded, value) + + XCTAssertEqual(actualEncodedJSON, encodeAsMappedValue ? mappedValueJSONData : rawValueJSONData) + } } diff --git a/Tests/OptionsTests/Mocks/MockCollectionEnum.swift b/Tests/OptionsTests/Mocks/MockCollectionEnum.swift index c588f4f..45a530a 100644 --- a/Tests/OptionsTests/Mocks/MockCollectionEnum.swift +++ b/Tests/OptionsTests/Mocks/MockCollectionEnum.swift @@ -32,15 +32,17 @@ 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 case d + + static var codingOptions: CodingOptions = .default } #else // swiftlint:disable identifier_name - internal enum MockCollectionEnum: Int, MappedValueCollectionRepresented { + internal enum MockCollectionEnum: Int, MappedValueCollectionRepresented, Codable { case a case b case c @@ -52,5 +54,8 @@ import Options "c", "d" ] + static var codingOptions: CodingOptions = .default } + + typealias MockCollectionEnumSet = EnumSet #endif diff --git a/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift b/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift index 77be088..0cb80da 100644 --- a/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift +++ b/Tests/OptionsTests/Mocks/MockDictionaryEnum.swift @@ -40,7 +40,7 @@ import Options } #else // swiftlint:disable identifier_name - internal enum MockDictionaryEnum: Int, MappedValueDictionaryRepresented { + internal enum MockDictionaryEnum: Int, MappedValueDictionaryRepresented, Codable { case a = 2 case b = 5 case c = 6 @@ -53,4 +53,6 @@ import Options 12: "d" ] } + + typealias MockDictionaryEnumSet = EnumSet #endif