Providing custom model objects and overriding the mappings of objects passed to the UI elements for HAAPI

When faced with the need to customize the framework behavior for specific use cases, it is possible that the provided models don't contain all of the server's response content after mapping occurs. For this, the HaapiIdsvrUIKit framework provides a way to customize the model mapping behavior and inject custom logic/objects to enhance/change the mappings result output.

The requirements are:

The custom objects must conform to a UIModel hierarchy. The UIModel can be divided into 4 categories or main types:

Category/Type Description
UIInteractionModel An interaction model that requires user input or interaction.
UIOperationModel An operation model requires to launch a service inside or outside the application.
UIProblemModel A problem informs something wrong has happened.
OAuthModel The content of an OAuth2.0 response.

The following table demonstrates the default model mappings used internally by the framework.

HaapiResponse UIModel generated Type
AuthenticatorSelectorStep SelectorModel UIInteraction
ContinueSameStep ContinueSameModel UIInteraction
GenericRepresentationStep GenericModel UIInteraction
InteractiveFormStep FormModel UIInteraction
RedirectionStep FormModel UIInteraction
UserConsentStep FormModel UIInteraction
OAuthAuthorizationResponseStep AuthorizationRequestModel UIInteraction
PollingStep PollingOperationModel UIOperation
ExternalBrowserClientOperationStep FormOperationModel UIOperation
GenericClientOperationStep GenericOperationModel UIOperation
WebAuthnRegistrationClientOperationStep WebAuthnOperationModel UIOperation
WebAuthnAuthenticationClientOperationStep WebAuthnOperationModel UIOperation
Problem ProblemModel UIProblemModel
SuccessfulTokenResponse OAuthTokenModel OAuthModel
ErrorTokenResponse OAuthErrorModel OAuthModel

The following snippet illustrates a class which adds a new field for FormModel and override the InteractiveFormStep mapping.

struct MyFormModel: FormModel {
    var userInfo: String // New field
    var interactionItems: [InteractionItemModel]
    var linkItems: [LinkItemModel]
    var messageItems: [InfoMessageModel]
    var templateArea: String?
    var viewName: String?
}

The following snippet illustrates creating and registering a custom mapping factory block into the default DataMapper implementation via the DataMapperBuilder utility provided by the framework.

HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration)
.setDataMapper(
    DataMapperBuilder(redirectTo: haapiUIKitConfiguration.haapiConfiguration.appRedirect,
                        autoPollingDuration: haapiUIKitConfiguration.autoPollingDuration,
                        authSelectionPresentation: haapiUIKitConfiguration.authenticationSelectionPresentation)
    .customize(modelType: InteractiveFormStep.self, handler: { haapiModel in
        return MyFormModel(userInfo: "Custom data model", interactionItems: [], linkItems: [], messageItems: [])
    })
    .build()
)
.build()

The following snippet illustrates creating a custom of DataMapper implementation to use as replacement for the default data mapper.

class CustomDataMapper: DataMapper {
    let defaultMapper: DataMapper
    
    init(redirectTo: String, autoPollingDuration: TimeInterval, authSelectionPresentation: AuthenticatorSelectionPresentation) {
        defaultMapper = DataMapperBuilder(redirectTo: redirectTo,
                                          autoPollingDuration: autoPollingDuration,
                                          authSelectionPresentation: authSelectionPresentation)
                                          .build()
    }
    
    func mapHaapiRepresentationToInteraction(haapiRepresentation: any HaapiRepresentation) throws -> any UIInteractionModel {
        let defaultMappedObject = try defaultMapper.mapHaapiRepresentationToInteraction(haapiRepresentation: haapiRepresentation)
        switch haapiRepresentation {
        case is InteractiveFormStep:
            guard let formModel = (defaultMappedObject as? FormModel) else {
                return defaultMappedObject
            }
            return MyFormModel(userInfo: "Custom model data",
                               interactionItems: formModel.interactionItems,
                               linkItems: formModel.linkItems,
                               messageItems: formModel.messageItems,
                               templateArea: formModel.templateArea,
                               viewName: formModel.viewName)
        default:
            return defaultMappedObject
        }
    }
    
    func mapHaapiResultToUIModel(haapiResult: HaapiResult) throws -> any UIModel {
        try defaultMapper.mapHaapiResultToUIModel(haapiResult: haapiResult)
    }
    
    func mapRepresentationActionModelToUIInteractionModel(representationActionModel: any RepresentationActionModel) throws -> any UIInteractionModel {
        try defaultMapper.mapRepresentationActionModelToUIInteractionModel(representationActionModel: representationActionModel)
    }
}

// Use the custom DataMapper in HaapiUIKitApplication by setting it via the HaapiUIKitApplicationBuilder.
HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration)
    .setDataMapper(CustomDataMapper(redirectTo: haapiUIKitConfiguration.haapiConfiguration.appRedirect, 
                                    autoPollingDuration: haapiUIKitConfiguration.autoPollingDuration, 
                                    authSelectionPresentation: haapiUIKitConfiguration.authenticationSelectionPresentation))
    .build()

The OAuthModel mappings - a special UIModel that represent the Oauth 2.0 response, can also be customized so that when the server's OAuth response is mapped, custom logic can be applied to provide a different/richer model that is delivered to the client app code. The following snippet illustrates a mapping customization example targeting the SuccessfulTokenResponse mapping.

// Define a custom class/struct that conforms to the 
struct MySuccessToken: OAuthTokenModel {
    var userInfo: String // New field
    var accessToken: String
    var tokenType: String?
    var scope: String?
    var expiresIn: Int
    var refreshToken: String?
    var idToken: String?
}

// Injecting the mapper using the OAuth builder utility
HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration)
.setOAuthDataMapper(
    OAuthDataMapperBuilder().customize(modelType: SuccessfulTokenResponse.self, handler: { token in
        guard let tokenSuccess = token as? SuccessfulTokenResponse else {
            throw HaapiUIKitError.unsupportedMap(objName: String(describing: token),
                                                    expectedObjName: "SuccessfulTokenResponse")
        }
        return MySuccessToken(userInfo: "My custom token",
                                accessToken: tokenSuccess.accessToken,
                                expiresIn: tokenSuccess.expiresIn)
    })
    .build()
)
.build()

// Implementing a custom OAuthDataMapper
struct CustomOAuthDataMapper: OAuthDataMapper {
    let defaultMapper: OAuthDataMapper = OAuthDataMapperBuilder().build()
    
    func mapTokenResponseToOAuthModel(tokenResponse: TokenResponse) throws -> any OAuthModel {
        switch tokenResponse {
        case .successfulToken(let token):
            return MySuccessToken(userInfo: "My custom token", 
                                  accessToken: token.accessToken,
                                  expiresIn: token.expiresIn)
        default:
            break
        }
        return try defaultMapper.mapTokenResponseToOAuthModel(tokenResponse: tokenResponse)
    }
}

// Use the custom OAuth mapper in HaapiUIKitApplication by setting it via the HaapiUIKitApplicationBuilder.
HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration)
    .setOAuthDataMapper(CustomOAuthDataMapper())
    .build()

Then, it is possible to have a UIViewController that uses the new MyFormModel and can access the custom added state field like demonstrated below, as well as having an existing UIViewController that uses the FormModel accessing the inherited protocol properties and maintain the default functionality.

class UsingMyFormModelViewController: FormViewController {
    override init(uiModel: UIModel) {
        self.uiModel = uiModel
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        if let myModel = uiModel as? MyFormModel {
            NSLog("\(myModel.userInfo)")
        }
    }
}

// Update the class that implements HaapiUIKitViewControllerResolver to use the custom viewcontroller.
// TODO after rebase with changes to the HaapiUIViewController protocol