Skip to content

Instantly share code, notes, and snippets.

@AvdLee
Created March 19, 2021 14:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AvdLee/61e082d60b459bcb0a080ce14a7080b1 to your computer and use it in GitHub Desktop.
Save AvdLee/61e082d60b459bcb0a080ce14a7080b1 to your computer and use it in GitHub Desktop.
//
// 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