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

@W-16589742: [iOS] REST wrappers for select SFAP APIs #3801

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

JohnsonEricAtSalesforce
Copy link
Contributor

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce commented Jan 13, 2025

🧱 Draft: Only one of the four endpoints is included. We'll review that for a quick look, then add the remaining three 🚧

This adds REST API clients for select sfap_api endpoints to match the Android version in forcedotcom/SalesforceMobileSDK-Android#2644

Only one of the four is included in the draft status. Here's a code sample from how I tested in iOSNativeSwiftTemplate's AppDelegate.

    /// The generation id from the `sfap_api` `generations` endpoint
    private var generationId: String? = nil
    
    /// Fetches generated text from the `sfap_api` "generations" endpoint.
    private func generateText() async {
        do {
            guard let userAccountCurrent = UserAccountManager.shared.currentUserAccount, let restClient = RestClient.restClient(for: userAccountCurrent) else { return }
            
            // Guards.            
            let generationsResponseBody = try await SfapApiClient(
                apiHostName: "dev.api.salesforce.com",
                modelName: "sfdc_ai__DefaultGPT35Turbo",
                restClient: restClient
            ).fetchGeneratedText(
                "Tell me a story about an action movie hero with really, really cool hair."
            )
            
            self.generationId = generationsResponseBody.generation?.id
            print("SFAP_API-TESTS: \(String(describing: generationsResponseBody.generation?.generatedText))")
            print("SFAP_API-TESTS: \(String(describing: generationsResponseBody.sourceJson))")
        } catch let error {
            SFSDKCoreLogger().e(
                SfapApiClient.classForCoder(),
                message: "Cannot fetch generated text due to an error with message '\(error.localizedDescription)'.")
        }
    }

// Guards.
guard
let userAccountCurrent = UserAccountManager.shared.currentUserAccount,
let restClient = RestClient.restClient(for: userAccountCurrent) else {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this getting a RestClient based on the current user vs using the RestClient from init?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. This guard lived somewhere else before I brought it to this file and I forgot to adjust it to scope. Thanks!

let restClient = RestClient.restClient(for: userAccountCurrent) else {
throw sfapApiErrorWithMessage("Cannot invoke sfap_api client without a current user account.")}

guard let modelNameUnwrapped = modelName else {
Copy link
Member

Choose a reason for hiding this comment

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

if you don't want to rename it, I think you could also do guard let modelName { here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍🏻

do {
return try sfapApiGenerationsResponse.asDecodable(type: SfapApiGenerationsResponseBody.self)
} catch let error {
throw sfapApiErrorWithMessage("Cannot JSON decode sfap_api generations response body due to a decoding error with description '\(error.localizedDescription)'.")
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little confused with the error handling, why does the error need to be caught and re-thrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like a re-throw here, also. I'll get that in. Thanks!

/// @param messageCode The `sfap_api` error code
/// @param source The original `sfap_api` error response body
@objc
public class SfapApiError : NSError, @unchecked Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Why does this use NSError? Was there an issue with Swift's error type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like it'd be alright with Swift's error type. I also made some matching changes I used on Android to make the sfap_api errorCode, message, messageCode and the original error response JSON available to the caller. Those were all pull request feedback items on that side, so hopefully those are valuable as matching developer experience here for iOS. Here's a screenshot of what catching an sfap_api error response looks like in the AppDelegate of iOSNativeSwiftTemplate. This specific case shows what would happen in the request had a malformed parameter, for example. sfap_api has a detailed response for that.

Screenshot 2025-01-14 at 10 59 11

underlyingError: _,
urlResponse: _
): if let errorResponseData = response as? Data {
let sfapApiErrorResponseBody = try JSONDecoder().decode(SfapApiErrorResponseBody.self, from: errorResponseData)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bbirman - I noticed we have an Error enum here on the iOS side. It looks like we could use .apiFailed as a chance to receive the Data and decode that to the documented error structure in the sfap_api doc and surface any valuable feedback to the client. The other cases could just re-throw. How does that look? On the Android side, we did create a type for the error responses much like this including the original error response's JSON source.

Copy link
Member

Choose a reason for hiding this comment

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

sounds good!

import Foundation

@objc(SFApApiClient)
public class SfapApiClient : NSObject {
Copy link
Member

Choose a reason for hiding this comment

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

not important for the draft but I'm wondering if "Api" should be all caps to match SFRestAPI

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.

2 participants