DTOMacro

一个生成 DTO 的 Swift 宏。

要点速览

先决条件

安装

Swift 包管理器

将以下内容添加到您的 Package.swift 文件中

dependencies: [
    .package(url: "https://github.com/OctoPoulpeStudio/DTOMacro.git", from: "1.0.0")
]

用法

  1. 导入 DTOMacro:
import DTOMacro
  1. 将属性 @DecodableFromDTO 添加到您的业务数据的结构体名称中
@DecodableFromDTO
struct MyData {
	let name: String
	let birthday:Date?
}
  1. 将转换属性添加到要转换的属性中。

3.1. @ConvertDTOType(from: SourceType, to: BusinessDataType, convert: ConversionClosure) - SourceType: 您收到的数据的类型 - BusinessType: 您要转换的属性的类型 - ConversionClosure: 用于将 SourceType 转换为 BusinessType 的显式闭包。 - > [!NOTE]: 闭包的主体必须位于属性中,因为它会被 SwiftSyntax 复制为字符串。

@DecodableFromDTO
struct MyData {
	let name: String
	@ConvertDTOType(from: String, to: Date, convert: {ISO8601DateFormatter().date(from:$0)})
	let birthday:Date?
}

3.2. 使用 @ConvertFromDTO 自动转换从 DTO 创建的对象

@DecodableFromDTO
struct MyData {
	let name: String
	@ConvertFromDTO
	let myProperty:ATypeUsingTheDecodableFromDTOProtocol
}

示例

@DecodableFromDTO
struct MyData {
	let name: String
	@ConvertDTOType(from: String, to: Date, convert: {ISO8601DateFormatter().date(from:$0)})
	let birthday:Date?
}

将生成

extension MyData: DecodableFromDTOProtocol {
    public struct DTO: Decodable {
        public let name: String
        public let birthdate: String
    }
    private struct DTOConversionProcessor {
        fileprivate static var birthdate: (String) -> Date? = {
            ISO8601DateFormatter().date(from: $0)
        }
    }
    public init(from dto: DTO) {
        self.name = dto.name
        self.birthdate = DTOConversionProcessor.birthdate(dto.birthdate)
    }
}
  1. 您还可以使用 @DTOProperty(name: "a_name") 属性来更改 DTO 对象中属性的名称,以反映您接收到的名称,类似于使用 CodingKeys

像这样

@DecodableFromDTO
struct MyData {
    @DTOProperty(name: "a_name")
    let name: String
    @DTOProperty(name: "date_of_birth")
    @ConvertDTOType(from: String, to: Date?, convert: { ISO8601DateFormatter().date(from:$0)})
    let birthdate: Date?
}

它将展开为

extension MyData: DecodableFromDTOProtocol {
    public struct DTO: Decodable {
        public let a_name: String
        public let date_of_birth: String
    }
    private struct DTOConversionProcessor {
        fileprivate static var date_of_birth: (String) -> Date? = {
            ISO8601DateFormatter().date(from: $0)
        }
    }
    public init(from dto: DTO) {
        self.name = dto.a_name
        self.birthdate = DTOConversionProcessor.date_of_birth(dto.date_of_birth)
    }
}

高级功能

您可以将 access 参数传递给 @DecodableFromDTO 属性,以定义 DTO 属性的可访问性,如下所示

@DecodableFromDTO(access: .internal)
struct MyData {...

注意

private 访问器不可用,否则无法将 DTO 中的数据传输到主类型。

默认情况下,访问器是 public

详细解释

什么是 DTO

DTO 代表数据传输对象 (Data Transfer Object)。这是一种设计模式,允许业务数据与应用程序的需求保持一致。

如何使用它?

通过在您的语言(本例中为 Swift)中创建一个中间数据类型,该数据类型表示应用程序接收到的数据结构(通常是通过 JSON 从 API 接收)。

例如,您可能会收到来自服务器的名为“birthdate”的数据,它代表一个日期,但实际上是一个字符串。

如何无缝地将字符串转换为日期?您可以使用一个特殊的 init(from:Decoder) 初始化器,其中包含您忘记如何编写的特定代码,或者您可以使用 DTO 通过这个中间对象轻松转换数据。

这将看起来像这样

接收到的 JSON
{
	birthdate:"2023-07-07T17:06:40.0433333+02:00"
}
业务数据
struct MyData {
	let birthday:Date?
}

在这里,您无法直接将 String 转换为 Date?

因此,您可以使用 DTO 设计模式

DTO 对象
struct MyDataDTO {
	let birthdate: String
}

并在您的业务数据类型中创建一个特殊的 init

转换后的业务数据
struct MyData {
	let birthday:Date?
	init(from dto: MyDataDTO) {
		self.birthdate = ISO8601DateFormatter().date(from:dto.birthday)
	}
}

现在,使用 DTO 的更好方法是将 DTO 类型嵌套到您的业务数据中,从而以统一的方式编写它们,我们可以将它们嵌入到协议 DecodableFromDTOProtocol 中。同时,我们可以将它们分组在一个扩展中,以保持我们的业务数据干净

DecodableFromDTOProtocol
public protocol DecodableFromDTOProtocol {
    associatedtype DTO: Decodable
    init(from dto: DTO) throws
}
带有扩展的业务数据
struct MyData {
	let birthday:Date?
}

extension MyData: DecodableFromDTOProtocol {
	public struct DTO {
		let birthday: String
	}
	init(from dto: DTO) {
		self.birthday = ISO8601DateFormatter().date(from:dto.birthday)
	}
}

我们拥有的代码易于阅读,但不太容易编写。正如您所看到的,它添加了大量的样板代码。现在我们有一个名为 DTO 的嵌套结构体,它具有与我们主结构体相同的属性,并且我们有一个新的初始化器来执行转换。

这意味着如果发生任何更改,我们需要注意 3 个地方。

使用 SwiftMacro 减少样板代码

SwiftMacro 是 Swift 5.9 的一项功能,它允许我们开发者创建在编译时生成代码的代码。

那么如何使用 SwiftMacro 来拯救我们呢?

我们的想法是创建一个执行 DecodableFromDTO 所有工作的 attached macro。 并使用其他不生成任何代码的 attached macro 作为一种通过滥用宏系统来标记属性的方式来生成我们自己的属性。

因此,DecodableFromDTO 是获取任何代码生成的强制性步骤。 其他属性 @ConvertDTOType(from: APIType, to: BusinessType, convert: ConversionClosure)@ConvertFromDTO 只是为 DecodableFromDTO 提供上下文,但不生成任何内容。

[!NOTE]

宏创建

DecodableFromDTO宏的工作方式如下

其他说明

此包嵌入了 DecodableFromDTOProtocol 协议。 它还嵌入了 JsonDecoder 的一个扩展,以直接和无缝地从 JSON 获取业务数据

extension JSONDecoder {
    func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: DecodableFromDTOProtocol {
        try T(from: decode(T.DTO.self, from: data))
    }
}

[!NOTE]:该协议和扩展是由 Luis Recuenco 创建的,并取自他在 Better Programming 上的文章。