/ninjato

Flexible and type-safe inline HTTP client for Android and Kotlin

Primary LanguageKotlinApache License 2.0Apache-2.0

Ninjato

Bintray version Kotlin version badge codecov

Flexible and type-safe inline HTTP client for Android and Kotlin

What is Ninjato?

Ninjato is a library that lets you write simple yet powerful remote HTTP calls, whether it's RESTful services or any other. But at the same time the library gives you the complete control of the flow.

The library is written in Kotlin and tries to rely on reflection as low as possible. Most of the functionality is implemented with the help of inlining and reified types inference during compile time.

Why not Retrofit?

Retrofit is a great library, and people at Square do amazing and very important stuff. But for us at Agoda, after a long time with Retrofit we started to feel limited with the way how the library gives us opportunities to customize, intercept and implement more flexible behavior.

How to use?

Basic syntax

In a nutshell, Ninjato gives a possibility to execute any type of HTTP call through Api class. As such, you can have several approaches to define and use your remote services:

Extend the Api class directly:

class YourApi(client: HttpClient, config: Api.() -> Unit) : Api(client, config) {
    override val baseUrl = "https://yourApi.com"
    
    fun search(query: String): SearchResult = get {
        endpointUrl = "/search"
        parameters { "query" to query }
    }
}

Hide implementation behind the interface:

interface YourApi {
    fun updateArticle(article: Article)
}

class YourApiImpl(client: HttpClient, config: Api.() -> Unit) : Api(client, config), YourApi {
    override val baseUrl = "https://yourApi.com"
    
    fun updateArticle(article: Article) = post {
        endpointUrl = "/updateArticle"
        body = article
    }
}

Or implement your endpoints right in the interface:

interface YourApi {
    val api: Api
    
    fun putListing(listing: Listing) = api.put {
        endpointUrl = "/putListing"
        body = listing
    }
}

There are a lot more ways how you can organize your remote services' code, but we're not going to cover it here.

Main features

As you already understood from the examples below, Ninjato can infer the type of body and what is expected to be returned from the request. But there are a lot more features. All of them will be explained in the sections below with code examples.

DSL cascade: HttpClient, Api and Request

Ninjato is structured in a way that allows it's users to aggregate properties and/or behavior of their remote calls through the DSL cascade. This means that all main entities of library: HttpClient, Api and Request have the same configuration interface where you can configure your headers, parameters, interceptors, etc.

During the actual call configuration, all of these configurations will be aggregated from bottom to top, or replaced, if only one value is allowed.

Request body and return type inference

Thanks to Kotlin capabilities, Ninjato infers the type of body being set (if allowed) and expected return type.

Default types of body property include:

  • Body
  • String
  • ByteArray

Default types of return value include:

  • Unit
  • Response
  • Body
  • Stirng
  • ByteArray

For all custom types library has support of BodyConverter.

IMPORTANT NOTE: there is an issue with the Kotlin compiler and inlining of delegates with reified types is not working as expected. That means that current syntax of passing your body is inconsistent. Right now if you will pass an instance of generic class to a body property, type arguments will be lost:

fun foo(generic: Generic<String>) = post {
    body = generic // It will capture only class of the generic via generic.javaClass
}

That puts the responsibility of inferring the type arguments on your serializing library. Gson is doing this fine. If you want library to capture actual type and forward it to your BodyConverter.Factory, please consider using the newly introduced extension function:

fun foo(generic: Generic<Stirng>) = post {
    body = convert(generic)
}

BodyConverter

BodyConverter is a simple interface with a convert function from one to another. Library uses the provided BodyConverter.Factory instances to get the converter for a specific type. You can provide supplied or your own factory to any level of the DSL cascade:

val client = NinjatoOkHttpClient(okHttpClient) {
    converterFactories += GsonConverterFactory(Gson())
}

val api = YourApi(client) {
    converterFactories += MoshiConverterFactory()
}

val result: SearchResult = api.get {
    endpointUrl = "/get"
    converterFactories += JacksonConverterFactory()
}

List of body converters provided via extension artifacts can be found in Setup section.

Interceptors

Interceptors are instances that are executed prior (RequestInterceptor) the request is served by the HTTP client and after (ResponseInterceptor) the response was acquired. Interceptors have the ability to modify given instance of Request/Response or even provide completely another instances.

Interceptors can be added to any level of the DSL cascade. The following code can be used also in Api classes and in any call configuration:

val client = NinjatoOkHttpClient(okHttpClient) {
    interceptors += MyRequestInterceptor()
    interceptors += MyResponseInterceptor()
    
    interceptors {
        request { r ->
            Log.d(r)
            r
        }
        
        response { r ->
            Log.d(r)
            r
        }
    }
}

RetryPolicy

Simply put, RetryPolicy is a class that can allow you to decide, whether or not given request should try again and be executed. You get the Request and Throwable as input and should return one of the Retry sealed class children: DoNotRetry, WithoutDelay or WithDelay.

Retry policy can be added to any level of the DSL cascade. The following code can be used also in Api classes and in any call configuration:

val client = NinjatoOkHttpClient(okHttpClient) {
    retryPolicy = MyRetryPolicy()
    
    retryPolicy { request, throwable ->
        if (request.retries > 3) Retry.DoNotRetry else Retry.WithoutDelay
    }
}

FallbackPolicy

In case your RetryPolicy allowed a request to be retried, FallbackPolicy can help you with modifying you request before a retry attempt. For example, change the baseUrl of your request.

Fallback policy can be added to any level of the DSL cascade. The following code can be used also in Api classes and in any call configuration:

val client = NinjatoOkHttpClient(okHttpClient) {
    fallbackPolicy = MyFallbackPolicy()
    
    fallbackPolicy { request, throwable -> 
        request.also { it.baseUrl = "https://anotherServer.com" }
    }
}

Headers and query parameters

Headers and url query parameters are also part of the DSL cascade.

Headers and query parameters can be added to any level of the DSL cascade. The following code can be used also in HttpClient and Api classes:

val result: SearchResult = get {
    endpointUrl = "/search"
    headers += "A" to "B"
    
    headers {
        "B" to "C"
        
        cookie {
            "C" to "D"
            expires = 3600
            isSecure = true      
        }
    }
    
    parameters {
        "query" to query
    }
}

Request.Factory

To add possibility to extend Request entity and enrich it with use-case specific properties and/or logic, Ninjato has a factory that will return instances of Request which can be manipulated further in interceptors:

class YourRequestFactory : Request.Factory() {
    override fun create() = YourRequest()
}

class YourRequestInterceptor : RequestInterceptor() {
    fun intercept(request: Request): Request {
        if (request is YourRequest) {
            request.headers["request_id"] = request.id
        }
    }
}

val client = NinjatoOkHttpClient(okHttpClient, YourRequestFactory()) {
    interceptors += YourRequestInterceptor()
}

Response.Factory

To add possibility to extend Resposne entity and enrich it with use-case specific properties and/or logic, Ninjato has a factory that will return instances of Response which can be manipulated further in interceptors:

class YourResponseFactory : Response.Factory() {
    override fun create() = YourResponse()
}

// usage example
class YourResponseInterceptor {
    fun intercept(response: Response): Response {
        if (response is YourResponse) {
            response.token = response.headers["auth_token"]
        }
    }
}

val client = NinjatoOkHttpClient(okHttpClient, YourRequestFactory(), YourResponseFactory()) {
    interceptors += YourRequestInterceptor()
    interceptors += YourResponseInterceptor()
}

Wrappers

In case you prefer to use some sealed class type wrapper, or RxJava, for example, Ninjato has you covered as well. There are extension artifacts available that are providing extension wrapping functions for your Api classes. Here are few examples:

interface YourApi {
    val api: Api
    
    fun search(query: String): Call<SearchResult> = api.call {
        get {
            endpointUrl = "/serach"
            params { "query" to query }
        }
    }
    
    fun updateArticle(article: Article): Completable = api.completable {
        post {
            endpointUrl = "/articles"
            body = article
        }
    }
    
    fun putListing(listing: Listing): Flowable<Response> = api.flowable {
        put {
            endpointUrl = "/listings"
            body = listing
        }
    }
}

List of available extension artifacts can be found in Setup section.

More

For any additional information please refer to library's documentation.

Setup

Maven

<!-- Core library !-->
<dependency>
  <groupId>com.agoda.ninjato</groupId>
  <artifactId>ninjato-core</artifactId>
  <version>LATEST_VERSION</version>
  <type>pom</type>
</dependency>

<!-- OkHttp 3 client !-->
<dependency>
  <groupId>com.agoda.ninjato</groupId>
  <artifactId>client-okhttp</artifactId>
  <version>LATEST_VERSION</version>
  <type>pom</type>
</dependency>

<!-- Gson body converter !-->
<dependency>
  <groupId>com.agoda.ninjato</groupId>
  <artifactId>converter-gson</artifactId>
  <version>LATEST_VERSION</version>
  <type>pom</type>
</dependency>

<!-- Call wrapper !-->
<dependency>
  <groupId>com.agoda.ninjato</groupId>
  <artifactId>extension-call</artifactId>
  <version>LATEST_VERSION</version>
  <type>pom</type>
</dependency>

<!-- RxJava wrappers !-->
<dependency>
  <groupId>com.agoda.ninjato</groupId>
  <artifactId>extension-rxjava</artifactId>
  <version>LATEST_VERSION</version>
  <type>pom</type>
</dependency>

<!-- RxJava2 wrappers !-->
<dependency>
  <groupId>com.agoda.ninjato</groupId>
  <artifactId>extension-rxjava2</artifactId>
  <version>LATEST_VERSION</version>
  <type>pom</type>
</dependency>

or Gradle:

repositories {
    jcenter()
}

dependencies {
    // Core library
    implementation 'com.agoda.ninjato:ninjato-core:LATEST_VERSION'

    // OkHttp 3 client
    implementation 'com.agoda.ninjato:client-okhttp:LATEST_VERSION'

    // Gson body converter
    implementation 'com.agoda.ninjato:converter-gson:LATEST_VERSION'

    // Call wrapper
    implementation 'com.agoda.ninjato:extension-call:LATEST_VERSION'

    // RxJava wrappers
    implementation 'com.agoda.ninjato:extension-rxjava:LATEST_VERSION'

    // RxJava 2 wrappers
    implementation 'com.agoda.ninjato:extension-rxjava2:LATEST_VERSION'
}

Contribution Policy

Ninjato is an open source project, and depends on its users to improve it. We are more than happy to find you interested in taking the project forward.

Kindly refer to the Contribution Guidelines for detailed information.

Code of Conduct

Please refer to Code of Conduct document.

License

Ninjato is available under the Apache License, Version 2.0.

Thanks to