Stitcher 宏

License Release

一个 Stitcher 的支持包,它使用 Swift 宏定义元编程实用程序,从而能够自动为函数和初始化器进行参数注入,以及为自动依赖项注册提供实用程序。

目录

✔️ 最低要求

StitcherMacros 至少需要 iOS 13、macOS 10.15、tvOS 13watchOS 6 以及 Swift 版本 5.9

🧰 功能特性

📦 安装

安装后,StitcherMacros 将自动安装并导出 Stitcher

Swift 包

您可以使用 Xcode 11.0 或更高版本将 Stitcher 添加为 Swift Package 依赖项,方法是选择 File > Swift Packages > Add Package Dependency... 或 Xcode 13.0 及更高版本中的 File > Add packages...,然后添加以下 URL

https://github.com/athankefalas/StitcherMacros.git

手动安装

您也可以通过下载 StitcherMacros 项目并将其包含在您的项目中来手动安装此库。

📋 库概览

本节中讨论的一些项目基于您熟悉 Stitcher 库中定义的基本概念。

参数注入

参数注入是指在类型的初始化器中或在调用函数时,将类型的依赖项作为参数注入的做法。 为了自动生成初始化器或函数,并使它们的参数由 Stitcher 自动注入,目标 initfunc 声明必须使用 @InjectedParameters 宏进行标记。

class UploadService {
    
    private let networkingSession: NetworkingSession
    
    @InjectedParameters
    init(networkingSession: NetworkingSession) {
        self.networkingSession = networkingSession
    }
}

它将被扩展为

class UploadService {
    
    private let networkingSession: NetworkingSession
    
    @InjectedParameters
    init(networkingSession: NetworkingSession) {
        self.networkingSession = networkingSession
    }

    /// Automatically generated convenience initializer
    convenience init() {
        self.init(
            networkingSession: try! DependencyGraph.inject(
                byType: NetworkingSession.self
            )
        )
    }
}

类似地,相同的模式可以用于函数,其中某些或所有参数被注入

class UploadService {
    
    @InjectedParameters(ignoring: "file")
    func upload(file: URL, networkingSession: NetworkingSession) {}
}

这将扩展为以下内容

class UploadService {
    
    @InjectedParameters(ignoring: "file")
    func upload(file: URL, networkingSession: NetworkingSession) {}

    /// Automatically generated function when macro is expanded:
    func upload(file: URL) {
        upload(
            file: file,
            networkingSession: try! DependencyGraph.inject(
                byType: NetworkingSession.self
            )
        )
    }
}

配置

InjectedParameters 宏可以使用以下参数进行配置

父类型

当与类型初始化器一起使用时,父类型参数用于控制宏输出。 对于 Swift 版本 <= 5.10,编译器插件引擎不提供附加父类型的 peer 宏(例如 InjectedParameters)的信息。 因此,当在 Swift 6 之前的 Swift 版本中将此宏添加到 structenum 初始化器时,还必须提供父类型种类,以避免在生成的代码中出现编译时错误。

可接受的父类型值的定义

/// The kind of parent an attached peer macro has.
public enum AttachedParentKind {
    case actorParent
    case classParent
    case enumParent
    case structParent
}

此参数的默认值为 .classParent,它与 classactor 类型兼容,因为它们都是引用类型,并且辅助初始化器可以以相同的方式定义为 convenience 初始化器。

在结构体初始化器上使用 InjectedParameters

struct Service {
    
    let repository: EntityRepository
    
    @InjectedParameters(parent: .structParent)
    init(repository: EntityRepository) {
        self.repository = repository
    }
}
策略

strategy 参数控制依赖项的注入方式,按类型或按名称。 默认情况下,生成代码中的所有依赖项都将使用 .stitcherByType 策略注入。

class Service {
    
    @InjectedParameters(strategy: .stitcherByType)
    func findAll(in repository: EntityRepository) -> [Entity] { [] }

    /// Automatically generated function when macro is expanded:
    func findAll() -> [Entity]  {
        findAll(
            in: try! DependencyGraph.inject(
                byType: EntityRepository.self
            )
        )
    }
    
    @InjectedParameters(strategy: .stitcherByType)
    func clear(_ repository: EntityRepository) {}

    /// Automatically generated function when macro is expanded:
    func clear() {
        clear(
            try! DependencyGraph.inject(
                byType: EntityRepository.self
            )
        )
    }
}

为了使用按名称注入,可以改为使用 .stitcherByName 策略。 注入期间使用的名称是原始声明中使用的参数名称。

class Service {
    
    @InjectedParameters(strategy: .stitcherByName)
    func findAll(in repository: EntityRepository) -> [Entity] { [] }
    
    /// Automatically generated function when macro is expanded:
    func findAll() -> [Entity]  {
        findAll(
            in: try! DependencyGraph.inject(
                byName: "repository"
            )
        )
    }

    @InjectedParameters(strategy: .stitcherByName)
    func clear(_ repository: EntityRepository) {}

    /// Automatically generated function when macro is expanded:
    func clear() {
        clear(
            try! DependencyGraph.inject(byName: "repository")
        )
    }
}
忽略的参数

忽略的参数是一个参数名称序列,用于控制哪些初始化器 - 函数参数将被宏忽略,并在调用时手动注入。

class Service {
    
    @InjectedParameters(
        ignoring: "imageData", "attributes"
    )
    func upload(
        imageData: Data,
        convertedWith attributes: ImageAttributes = [],
        processingService: ImageProcessingService
    ) async throws {}
    
    /// Automatically generated function when macro is expanded:
    func upload(
        imageData: Data,
        convertedWith attributes: ImageAttributes = []
    ) async throws  {
        try await upload(
            imageData: imageData,
            convertedWith: attributes,
            processingService: try! DependencyGraph.inject(
                    byType: ImageProcessingService.self
                )
            )
    }
}

支持的初始化器 / 函数

@InjectedParameters 宏支持所有具有非泛型参数的初始化器 - 函数。 如果初始化器 - 函数具有泛型参数,并且需要对其余参数进行注入,则必须使用 ignored 参数显式忽略该泛型参数。

请注意,所有生成的初始化器 - 函数都标有 @_disfavoredOverload 属性,并且比显式声明的变体具有更低的调用优先级。 此外,作为类型首选初始化器也支持的生成的初始化器会自动使用 @PreferredInitializer 宏进行标记。

首选初始化器

@PreferredInitializer 宏是一个标记属性,用于声明应在 @Registerable 宏自动生成依赖项注册期间使用哪个初始化器。

支持的初始化器

首选初始化器支持任何没有泛型参数、不会通过返回 nil 或抛出错误而失败,并且是同步的初始化器。

依赖项注册

可以使用 @Registerable 宏自动生成依赖项注册,并通过遵循 RegisterableDependency 协议将其作为静态成员附加到指定的类型。

@Registerable
class Service {
    
    init() {}
}

// Expanded macro:
extension Service: RegisterableDependency, AutoregisterableDependency {

    static let dependencyRegistration = GeneratedDependencyRegistration<Service>(
        locator: .type(Service.self),
        scope: .automatic(for: Service.self),
        eagerness: .lazy
    ) {
        Service()
    }
}

此外,如果依赖项通过名称或关联值进行定位,则定位器原始值也会自动添加到一致性中

@Registerable(by: .name("service"))
class Service {
    
    init() {}    
}

// Expanded macro:
extension Service: RegisterableDependency, AutoregisterableDependency {

    static let dependencyName: String = "service"

    static let dependencyRegistration = GeneratedDependencyRegistration<Service>(
        locator: .name("service"),
        scope: .automatic(for: Service.self),
        eagerness: .lazy
    ) {
        Service()
    }
}

在运行时,可以将注册添加到依赖项容器,如下所示

@main
struct SomeApp: App {
    
    @Dependencies
    var container = DependencyContainer {
        // Dependency registration
        Service.dependencyRegistration
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

配置

可以使用与使用 Dependency 结构的基本依赖项注册相同的选项来配置 @Registerable 宏。

定位器 (Locator)

控制依赖项的定位方式。 默认情况下,依赖项将按其类型进行定位。

作用域 (Scope)

控制依赖项的作用域。 默认情况下,将使用的依赖项作用域是 .automatic()

急切性 (Eagerness)

控制依赖项的急切性。 默认情况下,将使用的依赖项急切性是 .lazy

首选初始化器

可以使用 @PreferredInitializer 宏来消除歧义,以确定将选择哪个初始化器来实例化依赖项。 如果发现多个首选初始化器,将选择参数数量最少的初始化器。

自动注册的依赖项组

Stitcher Macros 包含一个模板,可以选择与 Sourcery 一起使用,以便在构建时从所有使用 @Registerable 宏标记的依赖项动态生成一个依赖项组。

此外,还可以使用完全自定义的模板,无论是通过定位 @Registerable 宏属性还是 AutoregisterableDependency 协议。

添加 Sourcery

  1. 按照 Sourcery 存储库中的步骤安装命令行工具或包插件。
  2. 创建一个 Sourcery 配置文件。 可以在 Sourcery/sourcery.yml 中找到一个简单的示例配置文件。
  3. 将 Sourcery 添加为构建阶段脚本提交阶段触发脚本
  4. 将自动生成的依赖项组添加到 DependencyContainer 中,以在运行时注册依赖项。
@main
struct SomeApp: App {
    
    @Dependencies
    var container = DependencyContainer {
        // Autogenerated Dependency registrations
        DependencyGroup.autoregisteredDependencies
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Sourcery 集成是可选的,提供的模板和配置旨在作为简单的启动点,因此建议对其进行修改,以更好地适应特定项目的构建阶段/管道。

🐞 问题和功能请求

如果您在使用该库时遇到问题,或者有功能请求,请务必提出问题。