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

chore: add bridging utils for core-rest packages communication #166

Merged
merged 28 commits into from
Feb 4, 2025

Conversation

OmarAlJarrah
Copy link
Contributor

@OmarAlJarrah OmarAlJarrah commented Jan 29, 2025

Situation

This is the third pull request of a sequence (#158, #164) following up on our new architectural decisions. This pull request addresses the need for utilities that ease communication with the sdk-core package.

The sdk-core package speaks the language of raw requests and responses, and is not involved in any serialization, deserialization, end-user API modeling or other web-based API specific needs. In order for the sdk-rest extension package to communicate with the sdk-core, a translation layer needs to be put in-place.

Task

1- Serialization & Deserialization

Build utilities and tools that can transform different data formats (support json for now) into POJOs and vice versa.

2- Request Translation

Build utilities and tools that translate Operations API-based requests into raw sdk-core requests. Translation includes:

  • Parsing endpoint paths, initializing requests URLs, mutating them based on any associated data available (e.g. query params) and verifying their schemas.
  • Parsing and verifying standard HTTP methods integrity.
  • Parsing and setting HTTP headers.
  • Parsing, serializing and setting HTTP request bodies, including setting their content types per request.

3- Response Translation

Build utilities and tools that translate raw sdk-core responses into our sdk-rest models. Translation includes:

  • Parse response headers.
  • Parse and deserialize response bodies when required.
  • Wrap responses with a consistent model that streamlines a the end-user experience.

Action

1- Serialization & Deserialization

Created a new DefaultJacksonBasedOperationDataMapper class which acts as wrapper for Jackson's ObjectMapper instances. The class adheres to the SerializeRequestBodyTrait and DeserializeResponseBodyTrait contracts.

class DefaultJacksonBasedOperationDataMapper(
    private val mapper: ObjectMapper = jacksonObjectMapper()
): SerializeRequestBodyTrait, DeserializeResponseBodyTrait {

    override fun <T> serialize(value: T): InputStream {...}

    override fun <T> deserialize(
        inputStream: InputStream,
        operation: OperationResponseBodyTrait<T>
    ): T {...}
}

Instances of this class or any other SerializeRequestBodyTrait and DeserializeResponseBodyTrait compliants are passed based on their traits (abstractions or interfaces), and are consumed later on.

fun <T> SDKCoreResponse.parseBodyAs(
    operation: OperationResponseBodyTrait<T>,
    deserializer: DeserializeResponseBodyTrait
): T { ... }

fun OperationRequestBodyTrait<*>.parseRequestBody(
    serializer: SerializeRequestBodyTrait
): RequestBody { ... }

2- Request Translation

Since traits can serve as markers that tell what data an operation instance holds, we made use of the Kotlin/Java typing system by associating extension-helper methods with the traits themselves.

internal fun OperationRequestBodyTrait<*>.parseRequestBody(
    serializer: SerializeRequestBodyTrait
): RequestBody

internal fun UrlPathTrait.parseURL(base: URL): URL

// Others

By assigning extension functions to trait types, we ensure a decoupled request translation system, and achieve a more compile-time code-safety since these functions and the data they consume will be blocked unless a trait-type check is performed.

fun consume(operation: OperationRequestTrait) {
  operation.parseRequestBody(...) // Code does not compile!
}

fun consume(operation: OperationRequestTrait) {
  if (operation is OperationRequestBodyTrait<*>) { // Check if trait is implemented
    operation.parseRequestBody(...) // Code compiles. More safety achieved!
  }
}

Of course, we have created an entry-point extension method that handles all the checks required, and calls other extension methods for you.

internal fun OperationRequestTrait.parseRequest(
    serverUrl: URL,
    serializer: SerializeRequestBodyTrait
): Request {
    require(this is HttpMethodTrait && this.getHttpMethod().isNotBlank()) { 
        "Operation must implement HttpMethodTrait trait!" 
    }

    if (this is HeadersTrait && this.getHeaders().entries().isNotEmpty()) {
        this.getHeaders()
    }

    if (this is OperationRequestBodyTrait<*> && getRequestBody() != null) {
        this.parseRequestBody(serializer)
    }

    if (this is UrlPathTrait  && this.getUrlPath().isNotBlank()) {
        this.parseURL(serverUrl)
    }

    // other code as needed...
}

It is also worth mentioning that all extension methods act as a guard for the sdk-core package by carrying any required checks that ensure data validity before moving forward with delegation to the sdk-core. These extension methods also follow a dependency injection model where dependencies such as serializers/deserializers are passed as params, adding more flexibility and extensibility to the workflow.

3- Response Translation

Response translation is designed around the same concepts used in request translation. Extension methods are designed and associated with trait types. Extension methods also handle any checks required before returning a response to the end-user.

internal fun <T> SDKCoreResponse.toRestResponse(
    operation: OperationResponseBodyTrait<T>,
    deserializer: DeserializeResponseBodyTrait
): Response<T>

internal fun <T> SDKCoreResponse.parseBodyAs(
    operation: OperationResponseBodyTrait<T>,
    deserializer: DeserializeResponseBodyTrait
): T {
    require(body != null) { "Response body is null!" }
    require(body!!.source().isOpen ) { "Response body is closed!" }
    require(body!!.contentLength() != 0L) { "Response body is empty!" }
    require(body!!.source().exhausted().not()) { "Response body is exhausted!" }
    // Other code...
}

// operation param unused in practice, but it makes use of the type system
internal fun SDKCoreResponse.toRestResponse(
    operation: OperationNoResponseBodyTrait 
): Response<Nothing?>

Testing

Changed Files

OperationToRequestExtension.kt

  • Cases Covered in OperationToRequestExtensionTest:
    • Parsing of operation requests into HTTP requests.
    • Exception handling for missing traits.
    • Correct URL parsing with and without path and query parameters.
    • Verification of headers and request bodies.

SDKCoreResponseExtension.kt

  • Cases Covered in SDKCoreResponseExtensionTest:
    • Parsing response bodies for operations with and without response body traits.
    • Validation of response headers.
    • Handling of various response body states (e.g., empty, closed, exhausted).

Response.kt

  • Cases Covered in ResponseTest:
    • Verification of correct data and headers in responses.
    • Handling of empty and null data in responses.
    • Ensuring headers are correctly managed.

DefaultJacksonBasedOperationDataMapper.kt

  • Cases Covered in DefaultJacksonBasedOperationDataMapperTest:
    • Serialization of values to InputStream.
    • Deserialization of InputStream to specified types.
    • Exception handling for unsupported operations.

OperationRequestTrait.kt

  • Cases Covered:
    • Correct inclusion of new traits.
    • Validation of request trait interfaces.

OperationResponseTrait.kt

  • Cases Covered:
    • Implementation checks for response traits.
    • Validation of type identifiers for response bodies.

Coverage Report

image

Results

Utilities that enable seamless communication with sdk-core package are now available for usage. Most of these utilities will be consumed in future pull requests.

Notes

Based on a conversation with @Mohammad-Dwairi, the logic for building and verifying URLs can be moved to the sdk-core package. We shall follow up on that in the future.

OmarAlJarrah and others added 16 commits January 29, 2025 08:25
@OmarAlJarrah OmarAlJarrah marked this pull request as ready for review January 30, 2025 12:02
@OmarAlJarrah OmarAlJarrah requested a review from a team as a code owner January 30, 2025 12:02
@OmarAlJarrah OmarAlJarrah requested review from jordan-n-schmidt and removed request for jordan-n-schmidt January 30, 2025 12:17
@anssari1 anssari1 self-requested a review January 30, 2025 14:46
@anssari1 anssari1 dismissed their stale review January 30, 2025 15:03

accidentally

OmarAlJarrah and others added 5 commits February 2, 2025 21:04
…t/extension/OperationToRequestExtension.kt

Co-authored-by: Mohammad Dwairi <[email protected]>
…t/extension/OperationToRequestExtension.kt

Co-authored-by: Mohammad Dwairi <[email protected]>
…t/extension/OperationToRequestExtension.kt

Co-authored-by: Mohammad Dwairi <[email protected]>
@OmarAlJarrah OmarAlJarrah merged commit 1032a1a into main Feb 4, 2025
2 checks passed
@OmarAlJarrah OmarAlJarrah deleted the OmarAlJarrah/sdk-rest-core-bridging-utils branch February 4, 2025 21:16
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.

None yet

3 participants