The library implies working with two Model Layers:
- Application Model Layer - The layer of the application level, which is used throughout the application.
- Raw Model Layer (DTO) - The low-level layer to which (or from which) data is mapped for (or from) the server.
But it is also allowed to use only one model layer or not to use models at all.
Two protocols are responsible for defining the model from this layer:
There is also an alias RawMappable
For entities that conform to the Codable protocols, there is a default mapping implementation.
Example:
enum Type: Int, Codable {
case owner
case member
}
struct PhotoEntry: Codable {
let id: String
let ref: String
}
extension PhotoEntry: RawDecodable {
public typealias Raw = Json
}
struct UserEntry: Codable {
let name: String
let age: Int
let type: Type
let photos: [PhotoEntry]
}
extension UserEntry: RawDecodable {
public typealias Raw = Json
}This code will be sufficient to map the server response to the UserEntry and PhotoEntry entities.
It is considered good practice to add the "Entry" postfix to DTO entities.
Two protocols are responsible for defining the model from this layer:
There is also an alias DTOConvertible
Example:
struct Photo {
let id: String
let image: String
}
extension Photo: DTODecodable {
public typealias DTO = PhotoEntry
static func from(dto: PhotoEntry) throws -> Photo {
return .init(id: dto.id, image: dto.ref)
}
}
struct User {
let name: String
let age: Int
let type: Type
let photos: [Photo]
}
extension User: DTODecodable {
public typealias DTO = UserEntry
static func from(dto: UserEntry) throws -> Photo {
return try .init(name: dto.name,
age: dto.age,
type: dto.type,
photos: .from(dto: dto.photos))
}
}Thus, we obtain a pair of two models, where:
UserEntry: RawDecodable- DTO-LayerUser: DTODecodable- App-Layer
Arrays with elements of type DTOConvertible and RawMappable also satisfy these protocols and have default implementations for their methods.
Let say we have a certain product.
struct Product: DTODecodable {
let id: String
let name: String
let alias: String?
static func from(dto: ProductEntry) -> Product {
return .init(id: dto.id, name: dto.name, alias: dto.alias)
}
}And the requirements are as follows:
- Always output
aliasas the product name. - In case
alias == niloralias.isEmpty, outputname.
These requirements are due to the fact that alias is set by the user, while name is the default product name.
Of course, it's clear that writing something like this everywhere is a bad idea:
if let alias = model.alias, !alias.isEmpty {
self.productNameLabel.text = alias
} else {
self.productNameLabel.text = model.name
}If we have a DTO layer, we can solve this problem during data mapping:
static func from(dto: ProductEntry) -> Product {
let alias = {
guard let alias = dto.alias, !alias.IsEmpty else {
return dto.name
}
return alias
}()
return .init(id: dto.id, name: dto.name, alias: alias)
}In this scenario, we solve the problem of mismatching business models to transport models at the mapping level, without carrying these mismatches up the hierarchy.
Sometimes it's convenient to represent an entity coming from the server as several different entities. For example, the server sends one large model, but for a specific request, we only need a certain subset of fields.
In such cases, having two layers of models helps solve the problem perfectly.
struct PaymentEntry: Codable, RawMappable {
typealias Raw = Json
let subitems: [PaymentEntry]
let mask: String?
let regexp: String?
let action: String?
}
struct PayemntAction: DTOEncodable {
let action: String
static func from(dto: PaymentEntry) -> PayemntAction {
guard let action = dto.action else { throw .badType }
return .init(action: action)
}
}
struct PaymentField: DTOEncodable {
let inputMask: String
let regExp: String
static func from(dto: PaymentEntry) -> PaymentField {
guard let mask = dto.mask, let regExp = dto.regExp else {
throw .badType
}
return .init(inputMask: mask, regExp: regExp)
}
}
struct PaymentList: DTOEncodable {
let subitems: [PaymentEntry]
static func from(dto: PaymentEntry) -> PaymentList {
guard let subitems = .from(dto: dto.subitems) else { throw .badType }
return .init(subitems: subitems)
}
}