Skip to content

Instantly share code, notes, and snippets.

@liamnichols
Created December 17, 2021 16:04
Show Gist options
  • Save liamnichols/03856d2cdab2e55725fdda7a517fb873 to your computer and use it in GitHub Desktop.
Save liamnichols/03856d2cdab2e55725fdda7a517fb873 to your computer and use it in GitHub Desktop.
import Cocoa
import Foundation
// MARK: - DataModule
// Types here have a 1:1 match against the API spec and are generated from an OpenAPI specificaition
// They are public types of the DataModule intended only for use within the DomainModule
public struct CollectionDTO: Equatable {
public let id: Int
public let title: String
public let secretSauce: String
public let author: UserDTO
}
public struct UserDTO: Equatable {
public let id: Int
public let name: String
}
// MARK: - DomainModule
// @_implementationOnly import DataModule
// Public types here can represent data from various sources (API, Disk) that can
// then be exposed to the AppModule.
// To keep concerns seperated, avoid exposing DTO interfaces publicly. To do this,
// there are two approaches that I can think of:
//
// 1) Map to an entirely differnet type
// 2) @dynamicMemberLookup
//
// The below example achieves this by introducing a public 'Base' protocol and
// a wrapper type that supports @dynamicMemberLookup. This helps keep a concrete
// type (that is Equatable) with a clean public interface without manually keeping
// DTOs, Models and their mappers in sync.
public protocol CollectionBase {
var id: Int { get }
var title: String { get }
}
extension CollectionDTO: CollectionBase { }
@dynamicMemberLookup
public struct Collection: Equatable {
internal let dto: CollectionDTO
/// Indicates if the collection was 'pinned' by the user locally on their device
public let isPinned: Bool
/// The author of the collection
public var author: User {
User(dto: dto.author)
}
/// Accessors for base properties inherited from the remote representation
public subscript<Value>(dynamicMember keyPath: KeyPath<CollectionBase, Value>) -> Value {
dto[keyPath: keyPath]
}
}
public protocol UserBase {
var id: Int { get }
var name: String { get }
}
extension UserDTO: UserBase { }
@dynamicMemberLookup
public struct User: Equatable {
internal let dto: UserDTO
/// Accessors for base properties inherited from the remote representation
public subscript<Value>(dynamicMember keyPath: KeyPath<UserBase, Value>) -> Value {
dto[keyPath: keyPath]
}
}
// MARK: - Usage
let userDto = UserDTO(id: 2, name: "John")
let collectionDto = CollectionDTO(id: 1, title: "My Collection", secretSauce: "perinaise", author: userDto)
let collection = Collection(dto: collectionDto, isPinned: true)
collection.id // 1
collection.title // "My Collection"
collection.isPinned // true
collection.author.id // 2
collection.author.name // "John"
// collection.secretSauce
// ^~~~~~~~~~~ error: Value of type 'Collection' has no dynamic member
// 'secretSauce' using key path from root type 'CollectionBase'
// Pros:
// -----
//
// - No need to redefine the entire type (i.e initialisers etc)
// - No need for a mapper
// - Less code and less duplication (less chance of a mistake)
//
// Cons:
// -----
//
// - Autocomplete could be sketchy? (It actually seems to work well in Xcode 13 though)
// - Hard for discoverability since there is no static interface
// - Faking/Stubbing requires an instance of the DTO
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment