Inject a gRPC (or any) client dependency into another dependency and utilize it within liveValue

I have an APIClient

struct APIClient {    
    var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
}

extension APIClient: DependencyKey {
    
    static let liveValue = Self(
        sendEmailOTP: { request in
            try! await Task.sleep(for: .seconds(1))
            
            return SendEmailOTPResponse(
                success: nil,
                error: .invalidEmail
            )
        }
    )
}

Here, I need to make a request using a GRPC client that shall be passed into the APIClient upon initialization.
How can I do so and access that value in my static liveValue field?

Expected Result:

struct APIClient {    
    var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
}

extension APIClient: DependencyKey {
    
    static let liveValue = Self(
        sendEmailOTP: { request in
            // try! await Task.sleep(for: .seconds(1))
            let result = grpcClient.makeAPICall(....)
            
            return SendEmailOTPResponse(
                success: nil,
                error: .invalidEmail
            )
        }
    )
}

And the usage of this API client in a reducer is as follows

@Reducer
struct MyFeature {
   @Dependency(\.apiClient) var apiClient: APIClient
   ...
}

Any help would be greatly appreciated :folded_hands:

You could introduce a @Dependency(\.grpcClient) and access it there. If the GRPCClient requires initial configuration you can use prepareDependencies at the entry point of your app:

@main
struct MyApp: App {
  init() {
    prepareDependencies {
      $0.grpcClient = GRPCClient(/* ... */)
    }
  }

  var body: some Scene { /* ... */ }
}

If you don't want layers of dependencies here, your APIClient could only conform to TestDependencyKey and you would do the same prep you need to do in liveValue but at the app entry point:

@main
struct MyApp: App {
  init() {
    prepareDependencies {
      // Custom initializer that uses the client to set up each endpoint
      $0.apiClient = APIClient(grpcClient: grpcClient)
    }
  }

  var body: some Scene { /* ... */ }
}

Hey @stephencelis, thanks for your response :)

While that shoud work,
but I won't be able to access the client inside the static let liveValue field.

Any suggestions on how can I do so?

That's the end goal.

In the tutorial, such as the basic Counter app, they have this NumberFactClient


struct NumberFactClient {
    var fetch: (Int) async throws -> String
}

extension NumberFactClient: DependencyKey {
    // A value used when your feature is run in simulators and devices,
    // and it’s the place where it is appropriate to make live network requests
    static let liveValue = Self(
        fetch: { number in
            let (data, _) = try await URLSession.shared.data(
                from: URL(string: "http://numbersapi.com/\(number)")!
            )
            return String(decoding: data, as: UTF8.self)
        }
    )
}

// This is what allows for the syntax @Dependency(\.numberFact) in the reducer.
extension DependencyValues {
    var numberFact: NumberFactClient {
        get { self[NumberFactClient.self] }
        set { self[NumberFactClient.self] = newValue }
    }
}

Here, they're using URLSession.shared.data so it's easy... but I want to inject the GRPC client as opposed to accessing it via singleton

Could you capture the client inside of the APIClient closures to maintain that strong reference and then use that for the rest of the app?

struct APIClient {    
    var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse

    // other initializers go here...

    /// Initialize the API Client backed by the GRPCClient.
    init(grpcClient: GRPCClient) {
        self.init(sendEmailOTP: { request in
            let result = grpcClient.makeAPICall(....)
            
            return SendEmailOTPResponse(
                success: nil,
                error: .invalidEmail
        })
    }
}

Edit: just realized this was pretty much Stephen's suggestion, but that's what the implementation could look like.

Hey lxalfonso, thank you!

Looks like I don't have to define my closure inside the static let liveValue?

I can just define it in the constructor and use it acorss my Reducers?

If so, what's the usage of liveValue then if I can just define the closures inside the init?

1 Like

Happy to help!

From my understanding the intent of the Dependencies package was that all dependencies had some default implementation (the "live" value of the UUIDGenerator for example was just UUID()) but they introduced prepareDependencies to allow you to globally introduce dependencies into your app that don't have a "default" implementation but need some initial configuration. The only quirk now is that you still have to define the liveValue of your DependencyKey.

In my app my default liveValue will, by default, assert a preconditionFailure but I don't know if that's the most recommended pattern to use. In the SharingGRDB framework the pointfree guys use an in-memory database but report what is going on to the developer. Just some ideas of what you could use as a default for your APIClient before it's initialized.

lxalfonso hopefully this would be my last question. I've went through the resoruces but I really need a single answer to my question. Perhaps stephencelis can also answer this.

So how should I be defining my APIClient?

This is my original APIClient

import ComposableArchitecture
import Foundation
import GRPC

struct APIClient {
    var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
    
    var loginWithEmailOTP: (LoginWithEmailOTPRequest) async -> LoginWithEmailOTPResponse
}

extension APIClient: DependencyKey {
    
    static let liveValue = Self(
        sendEmailOTP: { request in
            return SendEmailOTPResponse.invalid() // errorMessage: "APIClient's sendOTP must be defined in the constructor"
        },
        loginWithEmailOTP: { request in
            return LoginWithEmailOTPResponse.empty()
        }
    )
}

extension DependencyValues {
    var apiClient: APIClient {
        get { self[APIClient.self] }
        set { self[APIClient.self] = newValue }
    }
}

As per the suggestions (from my understanding), I should define my functions inside the init?

struct APIClient {

    let grpcClient: MyGRPCManager

    var sendEmailOTP: (SendEmailOTPRequest) async -> SendEmailOTPResponse
    
    var loginWithEmailOTP: (LoginWithEmailOTPRequest) async -> LoginWithEmailOTPResponse

    init(grpcClient: MyGRPCManager) {
        self.grpcClient = grpcClient

        self.sendEmailOTP =  { request in
            let result = grpcClient.process(request)
            // process it...
            return SendEmailOTPResponse(...)
        }

        self. loginWithEmailOTP = { request in ..... }
    }
}


extension APIClient: DependencyKey {
    static let liveValue = Self(grpcClient: MyGRPCManager.mock())
}

And then the usage would be

@main
struct MyApp: App {
  init() {
    prepareDependencies {
      $0.apiClient = APIClient(grpcClient: GRPCClient(/* ... */))
    }
  }

  var body: some Scene { /* ... */ }
}

Is that correct?

You usually don't want to define dependencies in terms of other dependencies. That is, APIClient shouldn't capture a MyGRPCManager directly. Instead, the live instance should grab it as a dependency.

static var liveValue: Self {
  @Dependency(\.grpcClient) var grpcClient
  return Self {
    // Use client and other network calls.
  } loginWithEmailOTP: {
    //...
  }
}

You can also call the dependency inside each closure, not sure if it matters.

Then you would prepare them at app launch.

@main
struct MyApp: App {
  init() {
    prepareDependencies {
      $0.grpcClient = GRPCClient...
      $0.apiClient = APIClient...
    }
  }

  var body: some Scene { /* ... */ }
}

@Jon_Shier, Thanks. I was looking for something like this, however...

This would lead to an error:

Extensions must not contain stored properties

You put it in the extension, you need to put it in the computed property itself, like my example.

Sry I'm confused...

@Jon_Shier Your example has it inside the extension.

Could you pls confirm?
Thanks! :slight_smile: