Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@DependencyClient macro #132

Merged
merged 49 commits into from
Nov 15, 2023
Merged

@DependencyClient macro #132

merged 49 commits into from
Nov 15, 2023

Conversation

stephencelis
Copy link
Member

@stephencelis stephencelis commented Nov 13, 2023

This PR adds a new DependenciesMacros1 target to the library, which houses a couple macros that aid in designing dependencies for your application and for use with this library.

The main macro is @DependencyClient, which can be applied to the "struct of closures" style of dependency clients:

@DependencyClient
struct AudioPlayerClient {
  var loop: (_ url: URL) async throws -> Void
  var play: (_ url: URL) async throws -> Void
  var setVolume: (_ volume: Float) async -> Void
  var stop: () async -> Void
}

This does a few things for you. First, it automatically provides a default for each endpoint that simply throws an error and triggers an XCTest failure. This means you get an "unimplemented" client for free with no additional work. This allows you to simplify the testValue of your TestDependencyKey conformance like so:

 extension AudioPlayerClient: TestDependencyKey {
-  static let testValue = Self(
-    loop: unimplemented("AudioPlayerClient.loop"),
-    play: unimplemented("AudioPlayerClient.play"),
-    setVolume: unimplemented("AudioPlayerClient.setVolume"),
-    stop: unimplemented("AudioPlayerClient.stop")
-  )
+  static let testValue = Self()
 }

This behaves the exact same as before, but now all of the code is generated for you.

Further, when you provide argument labels the client's closure endpoints, the macro turns that information into methods with argument labels. This means you can invoke the play endpoint like so:

try await player.play(url: URL(filePath: "..."))

And finally, the macro also generates a public initializer for you with all of the client's endpoints. One typically needs to maintain this initializer when separate the interface of the dependency from the implementation (see
Separating interface and implementation for more information). But now there is no need to maintain that code as it is automatically provided for you by the macro.

Footnotes

  1. DependenciesMacros is defined as a new module so folks do not automatically incur the cost of building SwiftSyntax, which macros depend on and is quite heavyweight.

with:
branch: swift-5.8-release
tag: 5.8-RELEASE
# windows:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @brianmichel, we've been experiencing flakiness with windows CI and so have been slowly disabling them. Not sure if this is something you want to look into eventually.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mbrandonw sorry for the delay, we're in burn down mode for getting the first version of the Windows app out to a few folks, I'm not exactly sure what's going on here, but have a personal todo to take a peek once I have more free time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @brianmichel, no problem at all! We're happy to turn it back on when possible, but definitely not urgent. Thanks for responding!

@mbrandonw mbrandonw merged commit 3870cfc into main Nov 15, 2023
8 checks passed
@mbrandonw mbrandonw deleted the macros branch November 15, 2023 18:05
@hadiidbouk
Copy link

hadiidbouk commented Nov 17, 2023

@mbrandonw @stephencelis Generating this part isn't included in the macro?

extension DependencyValues {
	public var myService: MyService {
		get { self[myService.self] }
		set { self[myService.self] = newValue }
	}
}

@mbrandonw
Copy link
Member

@hadiidbouk that part cannot be generated by the macro. Macros are not capable of extending types other than the one it is attached to.

@hadiidbouk
Copy link

hadiidbouk commented Nov 17, 2023

@mbrandonw Ohh I was hoping to avoid adding this boilerplate code, it's almost the same for all our dependencies.
Do you think something like #DependencyValues(myService.self) is doable?

@Sajjon
Copy link

Sajjon commented Nov 27, 2023

Oh I've missed this, this is great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants