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:
- 
Create a class/struct that conforms to the desired
UIModelhierarchy protocol. - 
Inject your custom model mapping into the framework behavior. To do so, the client developer can choose from the following options:
- The recommended way is to provide custom mapping closures and register them into the internal DataMapper functionality using the framework provided DataMapperBuilder utilities.
 - Alternative, implement a class that conforms to one of the 
DataMapperprotocols and fully customize the model mappings behavior at will. 
Reminder: There are separate interfaces and utility APIs depending on the target object for the mapping. Haapi flow models are mapped by the
DataMapperAPI and token responses are mapped by theOAuthDataMapperAPI. - 
Update the corresponding
dataMapperin the application configuration by setting it using one of the above options. 
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