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
UIModel
hierarchy 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
DataMapper
protocols 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
DataMapper
API and token responses are mapped by theOAuthDataMapper
API. -
Update the corresponding
dataMapper
in 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