Skip to content

Structures and shapes backend interaction with pure idiomatic Kotlin

License

Notifications You must be signed in to change notification settings

cioccarellia/Kurl

Repository files navigation

Kurl

Download CircleCI Codacy Badge codecov Android Arsenal Kotlin License

Kurl is a library that implements, structures, modularizes and provides testability for backend APIs implementations. It is designed to turn basic strings (and URLs) operations into consistent and idiomatic Kotlin code.

Kurl is designed from the ground up to perform all the heavy and error-prone work, leaving you with higher level abstraction and logic. Kurl stands for Kotlin Url Repolishing Library. The name is inspired by cUrl.

Setup

Kurl is available on any JVM-targeted platform (Android, Desktop, Server).

Add dependency

Gradle
dependencies {
    implementation 'com.cioccarellia:kurl:1.0.4'
}
Maven
<dependency>
    <groupId>com.cioccarellia</groupId>
    <artifactId>kurl</artifactId>
    <version>1.0.4</version>
    <type>pom</type>
</dependency>

Introduction

If you have some backend interaction background, you can feel ok with skipping this part, it's just a technical brush up to make sure we're all on the same page.

An API url is where the server hosts the service you use. To create software which is capable of accessing a minimally structured API you have to know the API url and the scheme used by the server.

An URL is composed by several parts. Generally, we can group them into 3 categories:

https://api.github.com/users/AndreaCioccarelli/repos/Kurl
  • Protocol: https
  • Domain: api.github.com
  • Web Path: users/AndreaCioccarelli/repos/Kurl

In the case above the root API url matches with the domain itself, but if we take:

https://swapi.co/api/people/3

The general url composition will see swapi.co as the domain and api/people/3 as it's web path, and this is correct, but to be precise, the root API url is swapi.co/api (Because it is where all the requests are dispatched to), and the endpoint we want to access is people/1 (because it's the path we append to the API root to access our desired endpoint).

Kurl Docs

You use a kurl launcher function to start composing your request. Kurl provides a DSL with a set of methods to shape it up.

It returns an object of KurlRequest type. There are a bunch of functions and extensions you can invoke to start using Kurl:

val id = 3

val request = kurl("https://swapi.co/api") {
    endpoint("people/$id")
}
val r2d2Id = 3

val api = Api(
    domain = "swapi.co",
    path = "api"
)

val people = Endpoint("people")


val request1 = kurl(api, people) {
    endpoint("$r2d2Id")
}

val request2 = api.kurl(people) {
    endpoint("$r2d2Id")
}

Inside the lambda block you can invoke functions which will be used to compose the final request.

  • endpoint(): Appends the passed string to the request url.
  • parameters(): Sets the passed key-value pairs as the URL query.
  • headers(): Sets the header value for the header key for every entry you passed.
  • fragment(): Sets the fragment tag at the end of the url.

Once the lambda has been executed a request object is returned. It contains all the finalized information needed to extract the required url, plus, additional useful methods for testing and checking the url.

The core library can also be used alongside a kurl-ktx dependency for supported HTTP clients, in order to support a bunch of specific features and to reduce the code you need to write.

Api

An Api object describes a certain API root path. It can be created either namely or directly.

Namely creating an API instance means passing all the needed arguments inside the class primary constructor.

Api(
    domain = "swapi.co",
    path = "api"
)

This should be the default choice for your implementation, as it provides better readability. Kurl is smart enough to infer and stick together the url you want to create even if you don't pass in every parameter, and to perform basic input error checking and correction. The default protocol is HTTPS. All other fields are optional. The only mandatory parameter is domain.

Directly creating an API instance means that you pass in the url using the direct() function and that's it. No smart insertion or completion is performed, the precise exact string you type in is decomposed and used as the API root url.

Api.direct("https://swapi.com/api")

Endpoint

Endpoint is just a class representing a piece of an URL. Depending on the context, Kurl internally trims and concatenates different endpoints together, basing on how you input them.

The difference between an Api and an Endpoint is that the first one is unique, and the second one can be composed (You can stick together different endpoints to reach your target configuration).

Containers

You can make use of a KurlApiContainer to create a structured and specific API object. This will group your app's API components in one place, instead of having them all scattered around your codebase.

class StarWarsApiContainer : KurlApiContainer() {

    val api = Api(
        domain = "swapi.co",
        path = "/api/"
        // "api", "/api" and "api/" are ok too, Kurl will remove 
        // and add any needed slash since we're doing things namely.
    )

    val lukeUrl: KurlRequest
        get() = kurl(
            Endpoint("people/1")
        )

    fun getPersonById(id: Into): String = kurl {
        endpoint("people/$id")
    }.url()

    fun getRandomStarship(): KurlRequest = kurl {
        val rid = Random().nextInt(1, 9)
        endpoint("starships/$rid")
    }
}

Nice touches with a KurlApiContainer are:

  • You don't need to pass in an Api object every time you invoke kurl, it is automatically inferred by the context.
  • You can make use of inner classes to split in logical pieces your container

Kurl KTX

The Kurl core package is designed to build kurl requests. If your client is supported by Kurl, you may consider including its specific KTX module, in order to simplify even more the integration process.

Each extension module contains the same exact features, adapted to the specific client syntax.

  • Kurl injection on request builders.
  • Conversion from Kurl requests to client specific requests.
  • Library agnostic type conversion & support.

Ktor KTX

Download Ktor

Gradle
dependencies {
    implementation 'com.cioccarellia:kurl-ktx-ktor:1.0.4'
}
Maven
<dependency>
  <groupId>com.cioccarellia</groupId>
  <artifactId>kurl-ktx-ktor</artifactId>
  <version>1.0.4</version>
  <type>pom</type>
</dependency>

OkHttp KTX

Download OkHttp

Gradle
dependencies {
    implementation 'com.cioccarellia:kurl-ktx-okhttp:1.0.4'
}
Maven
<dependency>
  <groupId>com.cioccarellia</groupId>
  <artifactId>kurl-ktx-okhttp</artifactId>
  <version>1.0.4</version>
  <type>pom</type>
</dependency>