From c31b1445c4fae49e6fdb75496b895a3653f6aefc Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:46:13 -0600 Subject: [PATCH] Update docs for @DependencyClient about default values. (#164) * Update docs for @DependencyClient about default values. * wip * wip * wip * Apply suggestions from code review --------- Co-authored-by: Stephen Celis --- Sources/DependenciesMacros/Macros.swift | 31 +++++++++++++++++++ .../DependencyClientMacro.swift | 1 + .../DependenciesMacrosPlugin/Support.swift | 9 ++++++ .../DependencyClientMacroTests.swift | 6 +++- .../DependencyEndpointMacroTests.swift | 12 +++++-- 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Sources/DependenciesMacros/Macros.swift b/Sources/DependenciesMacros/Macros.swift index 860c2c3c..156f798a 100644 --- a/Sources/DependenciesMacros/Macros.swift +++ b/Sources/DependenciesMacros/Macros.swift @@ -80,6 +80,37 @@ /// instances of the client. Creating that initializer manually is quite laborious, and you have to /// update it each time a new endpoint is added to the client. /// +/// ## Restrictions +/// +/// Usage of the ``DependencyClient()`` macro does have a restriction to be aware of. If your +/// client has a closure that is non-throwing and non-void returning like below, then you +/// will get a compile-time error letting you know a default must be provided: +/// +/// ```swift +/// @DependencyClient +/// struct APIClient { +/// // 🛑 Default value required for non-throwing closure 'isFavorite' +/// var isFavorite: () -> Bool +/// } +/// ``` +/// +/// The error also comes with a helpful fix-it to let you know what needs to be done: +/// +/// ```swift +/// @DependencyClient +/// struct APIClient { +/// var isFavorite: () -> Bool = { <#Bool#> } +/// } +/// ``` +/// +/// The reason we require a default for these endpoints is so that you immediately get access to +/// a default client via `APIClient()`, which is handy to use in tests and SwiftUI previews. The +/// only way to do this, without crashing at runtime, is if you provide defaults for your endpoints. +/// +/// To fix you must supply a closure that returns a default value. The default value can be anything +/// and does not need to signify a real value. For example, if the endpoint returns a boolean, you +/// can return `false`, or if it returns an array, you can return `[]`.` +/// /// [designing-dependencies]: https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependencies/designingdependencies /// [separating-interface-implementation]: https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependencies/livepreviewtest#Separating-interface-and-implementation @attached(member, names: named(init)) diff --git a/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift b/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift index a19fc1ad..19a1dd05 100644 --- a/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift +++ b/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift @@ -33,6 +33,7 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro { var unimplementedDefault = functionType.unimplementedDefault unimplementedDefault.append(placeholder: functionType.returnClause.type.trimmed.description) context.diagnose( + clientName: declaration.as(StructDeclSyntax.self)?.name.text, node: binding, identifier: identifier, unimplementedDefault: unimplementedDefault diff --git a/Sources/DependenciesMacrosPlugin/Support.swift b/Sources/DependenciesMacrosPlugin/Support.swift index 1fa256d8..48dc35f3 100644 --- a/Sources/DependenciesMacrosPlugin/Support.swift +++ b/Sources/DependenciesMacrosPlugin/Support.swift @@ -178,6 +178,7 @@ extension VariableDeclSyntax { extension MacroExpansionContext { func diagnose( + clientName: String? = nil, node: PatternBindingSyntax, identifier: TokenSyntax, unimplementedDefault: ClosureExprSyntax @@ -188,6 +189,14 @@ extension MacroExpansionContext { message: MacroExpansionErrorMessage( """ Default value required for non-throwing closure '\(identifier)' + + Defaults are required so that the macro can generate a default, "unimplemented" version \ + of the dependency\(clientName.map { " via '\($0)()'"} ?? ""). The default value can be \ + anything and does not need to signify a real value. For example, if the endpoint returns \ + a boolean, you can return 'false', or if it returns an array, you can return '[]'. + + See the documentation for @DependencyClient for more information: \ + https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependenciesmacros/dependencyclient()#Restrictions """ ), fixIt: FixIt( diff --git a/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift b/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift index 518804a9..2c1a37ba 100644 --- a/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift +++ b/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift @@ -638,10 +638,14 @@ final class DependencyClientMacroTests: BaseTestCase { var endpoint: @Sendable () -> Int ┬──────────────────────────── ╰─ 🛑 Default value required for non-throwing closure 'endpoint' + + Defaults are required so that the macro can generate a default, "unimplemented" version of the dependency via 'Client()'. The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'. + + See the documentation for @DependencyClient for more information: https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependenciesmacros/dependencyclient()#Restrictions ✏️ Insert '= { <#Int#> }' } """ - } fixes: { + }fixes: { """ @DependencyClient struct Client: Sendable { diff --git a/Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift b/Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift index 4a0a0ffc..db7150ab 100644 --- a/Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift +++ b/Tests/DependenciesMacrosPluginTests/DependencyEndpointMacroTests.swift @@ -92,10 +92,14 @@ final class DependencyEndpointMacroTests: BaseTestCase { var endpoint: () -> Bool ┬─────────────────── ╰─ 🛑 Default value required for non-throwing closure 'endpoint' + + Defaults are required so that the macro can generate a default, "unimplemented" version of the dependency. The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'. + + See the documentation for @DependencyClient for more information: https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependenciesmacros/dependencyclient()#Restrictions ✏️ Insert '= { <#Bool#> }' } """ - } fixes: { + }fixes: { """ struct Client { @DependencyEndpoint @@ -142,10 +146,14 @@ final class DependencyEndpointMacroTests: BaseTestCase { var endpoint: (Int, Bool, String) -> Bool ┬──────────────────────────────────── ╰─ 🛑 Default value required for non-throwing closure 'endpoint' + + Defaults are required so that the macro can generate a default, "unimplemented" version of the dependency. The default value can be anything and does not need to signify a real value. For example, if the endpoint returns a boolean, you can return 'false', or if it returns an array, you can return '[]'. + + See the documentation for @DependencyClient for more information: https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependenciesmacros/dependencyclient()#Restrictions ✏️ Insert '= { _, _, _ in <#Bool#> }' } """ - } fixes: { + }fixes: { """ struct Client { @DependencyEndpoint