Created
March 19, 2021 14:19
-
-
Save AvdLee/61e082d60b459bcb0a080ce14a7080b1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentMapper.swift | |
// | |
// | |
// Created by Antoine van der Lee on 09/03/2021. | |
// | |
import Foundation | |
import ContentKit | |
/// A generic `Mapper` to map from a source type to a destination type. | |
struct Mapper<Source: Mappable, Destination>: ConcreteMapping { | |
enum Error: Swift.Error { | |
/// Trying to map to a destination Type that doesn't match the expected destination. | |
case invalidDestinationType | |
} | |
let source: Source | |
let destination: Destination | |
let isReversed: Bool | |
/// Maps from a non-optional source to a non-optional destination value. | |
/// - Parameters: | |
/// - sourceKeyPath: The source key path for mapping. | |
/// - destinationKeyPath: The destination key path for mapping. | |
func map<Value>(_ sourceKeyPath: ReferenceWritableKeyPath<Source, Value>, _ destinationKeyPath: ReferenceWritableKeyPath<Destination, Value>) { | |
if isReversed { | |
source[keyPath: sourceKeyPath] = destination[keyPath: destinationKeyPath] | |
} else { | |
destination[keyPath: destinationKeyPath] = source[keyPath: sourceKeyPath] | |
} | |
} | |
/// Maps from a non-optional source to a optional destination value. | |
/// - Parameters: | |
/// - sourceKeyPath: The source key path for mapping. | |
/// - destinationKeyPath: The destination key path for mapping. | |
/// - defaultValue: An optional default value to use if the destination value turns out to be `nil`. | |
func map<Value>(_ sourceKeyPath: ReferenceWritableKeyPath<Source, Value>, _ destinationKeyPath: ReferenceWritableKeyPath<Destination, Value?>, defaultValue: Value? = nil) { | |
if isReversed { | |
guard let destinationValue = destination[keyPath: destinationKeyPath] ?? defaultValue else { return } | |
source[keyPath: sourceKeyPath] = destinationValue | |
} else { | |
destination[keyPath: destinationKeyPath] = source[keyPath: sourceKeyPath] | |
} | |
} | |
/// Maps from a non-optional source to a non-optional destination value using the given transforms. | |
/// - Parameters: | |
/// - sourceKeyPath: The source key path for mapping. | |
/// - destinationKeyPath: The destination key path for mapping. | |
/// - transform: The transform to use for forward mapping. | |
/// - reversedTransform: The transform to use for reversed mapping. | |
func map<Value, TransformedValue>(_ sourceKeyPath: ReferenceWritableKeyPath<Source, Value>, _ destinationKeyPath: ReferenceWritableKeyPath<Destination, TransformedValue?>, transform: (Value) -> TransformedValue?, reversedTransform: ((TransformedValue) -> Value?)?) { | |
if isReversed { | |
guard let destinationValue = destination[keyPath: destinationKeyPath], let transformedValue = reversedTransform?(destinationValue) else { return } | |
source[keyPath: sourceKeyPath] = transformedValue | |
} else { | |
destination[keyPath: destinationKeyPath] = transform(source[keyPath: sourceKeyPath]) | |
} | |
} | |
/// Maps forward-only from a non-optional source to a non-optional destination value using the given transform. | |
/// - Parameters: | |
/// - sourceKeyPath: The source key path for mapping. | |
/// - destinationKeyPath: The destination key path for mapping. | |
/// - transform: The transform to use for forward mapping. | |
func map<Value, TransformedValue>(_ sourceKeyPath: KeyPath<Source, Value>, _ destinationKeyPath: ReferenceWritableKeyPath<Destination, TransformedValue>, transform: (Value) -> TransformedValue) { | |
guard !isReversed else { return } | |
destination[keyPath: destinationKeyPath] = transform(source[keyPath: sourceKeyPath]) | |
} | |
/// Maps using a custom mapping. | |
/// - Parameter mapping: The mapping to perform for the given source and destination. | |
/// - Throws: An error the occurred during the custom mapping. | |
func customMapping(_ mapping: (Source, Destination, Bool) throws -> Void) rethrows { | |
try mapping(source, destination, isReversed) | |
} | |
} | |
// MARK: - Protocol definitions | |
/// Defines a mappable instance. | |
protocol Mappable { | |
/// Maps the values using the given mapper. The mapper contains the destination instance. | |
/// - Parameter mapper: The mapper to use for mapping. | |
func mapValues(using mapper: Mapping) throws | |
} | |
extension Mappable { | |
/// Creates a mapper and calls the `mapValues(mapper)` on the `Mappable` instance. | |
/// - Parameters: | |
/// - destination: The destination value to map the values to. | |
/// - isReversed: Whether the mapping is forward or reversed. | |
/// - Throws: An error if mapping failed. | |
func mapValues<Destination>(to destination: Destination, isReversed: Bool = false) throws { | |
let mapper = Mapper(source: self, destination: destination, isReversed: isReversed) | |
try mapValues(using: mapper) | |
} | |
} | |
// MARK: - Mappable Protocol conformance | |
/// Defines an instance which is responsible for mapping values. | |
protocol Mapping { | |
/// Creates a mapper for the given source and destination input. | |
/// - Parameters: | |
/// - source: The source to use for the mapping. | |
/// - destination: The destination to use for the mapping. | |
func mapper<Source: Mappable, Destination>(for source: Source, destination: Destination.Type) throws -> Mapper<Source, Destination> | |
} | |
extension Mapping where Self: ConcreteMapping { | |
func mapper<Source: Mappable, Destination>(for source: Source, destination: Destination.Type) throws -> Mapper<Source, Destination> { | |
guard let destination = self.destination as? Destination else { | |
throw Mapper<Source, Destination>.Error.invalidDestinationType | |
} | |
return Mapper(source: source, destination: destination, isReversed: isReversed) | |
} | |
} | |
/// A more explicit type defining a concrete mapper with a given source and destination type. | |
protocol ConcreteMapping: Mapping { | |
associatedtype Source: Mappable | |
associatedtype Destination | |
/// The source to use for mapping. | |
var source: Source { get } | |
/// The destination to use for mapping. | |
var destination: Destination { get } | |
/// Whether the mapping is forward or reversed. | |
var isReversed: Bool { get } | |
/// Creates a new mapper using the given source and destination type. | |
/// - Parameters: | |
/// - source: The source to use for mapping. | |
/// - destination: The destination to use for mapping. | |
/// - isReversed: Whether the mapping is forward or reversed. | |
init(source: Source, destination: Destination, isReversed: Bool) | |
/// Creates the destination type using the mapped values. | |
func createMappedDestination() throws -> Destination | |
} | |
extension ConcreteMapping { | |
func createMappedDestination() throws -> Destination { | |
try source.mapValues(using: self) | |
return destination | |
} | |
} | |
// MARK: - ReversedMappable Protocol Definition | |
/// Defines a type that's reversed mappable using the mappings from a `Mappable` type. | |
protocol ReversedMappable { | |
func mapValues<Destination: Mappable>(to destination: Destination) throws | |
} | |
// MARK: - ReversedMappable Protocol conformance | |
extension ReversedMappable { | |
func mapValues<Destination: Mappable>(to destination: Destination) throws { | |
try destination.mapValues(to: self, isReversed: true) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment