From aaf351644400fdc72a677880c37b6efa45ecce0a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 15 Dec 2023 14:03:06 -0600 Subject: [PATCH 1/5] Update docs for @DependencyClient about default values. --- Sources/DependenciesMacros/Macros.swift | 34 +++++++++++++++++++ .../DependencyClientMacro.swift | 1 + .../DependenciesMacrosPlugin/Support.swift | 9 +++++ 3 files changed, 44 insertions(+) diff --git a/Sources/DependenciesMacros/Macros.swift b/Sources/DependenciesMacros/Macros.swift index 860c2c3c..51b21d37 100644 --- a/Sources/DependenciesMacros/Macros.swift +++ b/Sources/DependenciesMacros/Macros.swift @@ -80,6 +80,40 @@ /// 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#> } +/// } +/// ``` +/// +/// 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 `[]`.` +/// +/// The reason we require a default for these endpoints is because one of the primary uses of +/// ``DependencyClient()`` is to generate a default client that you can immediately access as +/// `ApiClient()`. This is a client such that when any endpoint is invoked in the simulator a +/// purple runtime warning is triggered, and when it is invoked in tests a test failure is +/// triggered. This is a great starting point for your dependency, and then you can override it +/// as needed. +/// /// [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 49fda196..9e757734 100644 --- a/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift +++ b/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift @@ -32,6 +32,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..6966dbe2 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( From ac8e4f82795d23c17be8a887c50f6e6b65b39f3a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 15 Dec 2023 14:04:24 -0600 Subject: [PATCH 2/5] wip --- .../DependencyClientMacroTests.swift | 6 +++++- .../DependencyEndpointMacroTests.swift | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift b/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift index b880d73b..3a630bac 100644 --- a/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift +++ b/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift @@ -530,10 +530,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 6d81df46..201f5963 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 From 4c5be700f1552973d28b373d02b364211af2b3de Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 15 Dec 2023 14:08:24 -0600 Subject: [PATCH 3/5] wip --- Sources/DependenciesMacros/Macros.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/DependenciesMacros/Macros.swift b/Sources/DependenciesMacros/Macros.swift index 51b21d37..18bddb3e 100644 --- a/Sources/DependenciesMacros/Macros.swift +++ b/Sources/DependenciesMacros/Macros.swift @@ -111,8 +111,8 @@ /// ``DependencyClient()`` is to generate a default client that you can immediately access as /// `ApiClient()`. This is a client such that when any endpoint is invoked in the simulator a /// purple runtime warning is triggered, and when it is invoked in tests a test failure is -/// triggered. This is a great starting point for your dependency, and then you can override it -/// as needed. +/// triggered. This is a great starting point for your dependency in tests and SwiftUI previews, +/// and then you can override it as needed. /// /// [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 From dae33f0536fa8ccabf5455dcd23934c761325fbf Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 15 Dec 2023 14:12:56 -0600 Subject: [PATCH 4/5] wip --- Sources/DependenciesMacros/Macros.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/DependenciesMacros/Macros.swift b/Sources/DependenciesMacros/Macros.swift index 18bddb3e..156f798a 100644 --- a/Sources/DependenciesMacros/Macros.swift +++ b/Sources/DependenciesMacros/Macros.swift @@ -103,17 +103,14 @@ /// } /// ``` /// +/// 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 `[]`.` /// -/// The reason we require a default for these endpoints is because one of the primary uses of -/// ``DependencyClient()`` is to generate a default client that you can immediately access as -/// `ApiClient()`. This is a client such that when any endpoint is invoked in the simulator a -/// purple runtime warning is triggered, and when it is invoked in tests a test failure is -/// triggered. This is a great starting point for your dependency in tests and SwiftUI previews, -/// and then you can override it as needed. -/// /// [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)) From b5715969e9f9cf83b62262629e923e1e24c35a1a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 15 Dec 2023 12:15:26 -0800 Subject: [PATCH 5/5] Apply suggestions from code review --- Sources/DependenciesMacrosPlugin/Support.swift | 2 +- .../DependencyClientMacroTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/DependenciesMacrosPlugin/Support.swift b/Sources/DependenciesMacrosPlugin/Support.swift index 6966dbe2..48dc35f3 100644 --- a/Sources/DependenciesMacrosPlugin/Support.swift +++ b/Sources/DependenciesMacrosPlugin/Support.swift @@ -191,7 +191,7 @@ extension MacroExpansionContext { 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 \ + 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 '[]'. diff --git a/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift b/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift index 3a630bac..38af9b7e 100644 --- a/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift +++ b/Tests/DependenciesMacrosPluginTests/DependencyClientMacroTests.swift @@ -531,7 +531,7 @@ final class DependencyClientMacroTests: BaseTestCase { ┬──────────────────────────── ╰─ 🛑 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 '[]'. + 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#> }'