• Usage • Issues • Contributing • License
Microya
This is the kotlin version of Microya. A network abstraction layer.
Usage
Step 1: Defining your Endpoints
Create an Api sealed class
with all supported endpoints as data class
with the request parameters/data specified as parameters.
In order to allow Microya-Kotlin serialize your request classes, you need to declare a custom SerializersModule
and register your classes as polymorphic subclasses of Any
.
object PostmanSerializer {
private val module = SerializersModule {
polymorphic(Any::class) {
subclass(FooBar::class)
}
}
val requestJsonFormatter = Json {
serializersModule = module
}
}
For example, when writing a client for the Postman Echo:
sealed class PostmanEchoEndpoint : Endpoint() {
data class Index(val sortedBy: String) : PostmanEchoEndpoint()
data class Post(val fooBar: FooBar) : PostmanEchoEndpoint()
data class Get(val fooBarID: String) : PostmanEchoEndpoint()
data class Patch(val fooBarID: String, val fooBar: FooBar) : PostmanEchoEndpoint()
object Delete : PostmanEchoEndpoint()
}
Endpoint
compliant by overriding variables in the Endpoint
abstract class.
Step 2: Making your Api sealed class PostmanEchoEndpoint : Endpoint() {
data class Index(val sortedBy: String) : PostmanEchoEndpoint()
data class Post(val fooBar: FooBar) : PostmanEchoEndpoint()
data class Get(val fooBarID: String) : PostmanEchoEndpoint()
data class Patch(val fooBarID: String, val fooBar: FooBar) : PostmanEchoEndpoint()
object Delete : PostmanEchoEndpoint()
override val subpath: String
get() = when (this) {
is Get -> "get/${fooBarID}"
is Index -> "get"
is Patch -> "patch/${fooBarID}"
is Post -> "post"
Delete -> "delete"
}
override val method: HttpMethod
get() = when (this) {
is Get -> HttpMethod.Get
is Index -> HttpMethod.Get
is Patch -> HttpMethod.Patch(fooBar)
is Post -> HttpMethod.Post(fooBar)
Delete -> HttpMethod.Delete
}
override val headers: Map<String, String>
get() = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json",
"Accept-Language" to Locale.getDefault().language
)
override val queryParameters: Map<String, String>
get() = when (this) {
is Index -> mapOf("sortedBy" to sortedBy)
else -> emptyMap()
}
override val mockedResponse: MockedResponse?
get() = null
}
Step 3a: Calling your API endpoint with the Result type
Call an API endpoint providing a response type and an also an error type.
performRequest<PostmanEchoResponse, PostmanEchoError>(PostmanEchoEndpoint.Index(sortedBy = "updatedAt"))
Here's a full example of a call you could make with Mircoya-Kotlin:
val provider = ApiProvider.Builder().baseUrl("https://postman-echo.com")
.client(OkHttpClient())
.plugins(listOf(HttpAuthPlugin(HttpAuthPlugin.Scheme.BASIC, "abc123")
.requestJsonFormatter(PostmanSerializer.requestJsonFormatter)
.responseJsonFormatter(PostmanSerializer.responseJsonFormatter)
.build()
provider.performRequest<PostmanEchoResponse, PostmanEchoError>(
PostmanEchoEndpoint.Index(sortedBy = "updatedAt")
).onSuccess { postmanEchoResponse: PostmanEchoResponse ->
// use the already decoded result
}.onFailure { jsonApiException: JsonApiException ->
// error handling
}
Note that you can use a throwing get()
function instead of onSuccess
and onFailure
callbacks or a when
statement.
val result = sampleApiProvider.performRequest<PostmanEchoResponse, PostmanEchoError>(PostmanEchoEndpoint.Index(sortedBy = "updatedAt")).get()
You can also use more functional methods like map()
, andThen()
, mapEither()
, mapError()
.
Microya-Kotlin returns a JsonApiException
when the request is unsuccessful. You can call .getBody()
after type casting the jsonApiException
to a ClientError
Step 3b: Multipart File Upload
Microya-Kotlin supports file uploads using multipart requests. To perform multipart request with Microya-kotlin, you'll need to use the performUploadRequest
function.
For example, here's how to upload an image to Imgur with Microya-Kotlin.
Model you request class. The request class takes a list of FileDataPart
. These are file parts to be included in multipart request.
Take note of the @Transient
annotation. This means the fileDataParts
won't be serialized. The fileDataParts
would be included in a multipart request by Microya-Kotlin.
@Serializable
data class UploadImageRequest(
val title: String,
val description: String,
@Transient
val fileDataParts: List<FileDataPart> = emptyList()
)
Next step is modeling the ImgurEndpoint. The only difference between endpoint with file uploads and normal endpoints is the fileDataParts
variable. Endpoints with uploads override this variable.
sealed class ImgurEndpoint : Endpoint() {
data class UploadImageEndpoint(val body: UploadImageRequest) : ImgurEndpoint()
override val subpath: String
get() = when (this) {
is UploadImageEndpoint -> "3/image"
}
override val method: HttpMethod
get() = when (this) {
is UploadImageEndpoint -> HttpMethod.Post(body)
}
override val headers: Map<String, String>
get() = emptyMap()
override val queryParameters: Map<String, String>
get() = emptyMap()
override val mockedResponse: MockedResponse?
get() = null
override val fileDataParts: List<FileDataPart>
get() = when (this) {
is UploadImageEndpoint -> body.fileDataParts
}
}
Make the uploadRequest using performUploadRequest
.
val result: ImgurResponse<ImgurSuccessData> =
uploadFileSampleApiProvider.performUploadRequest<ImgurResponse<ImgurSuccessData>, ImgurResponse<ImgurErrorData>>(
uploadEndpoint
).get()!!
Plugins
The builder of ApiProvider accepts a list of Plugin objects. You can implement your own plugins or use one of the existing ones in the Plugins directory. Here's are the callbacks a custom Plugin subclass can override:
// Called to modify a request before sending.
fun modifyRequest(request: Request, endpoint: Endpoint): Request
// Called immediately before a request is sent.
fun beforeRequest(request: Request)
// Called after a response has been received & decoded, but before calling the completion handler.
fun <T> afterRequest(response: Response, typedResult: T? = null, endpoint: Endpoint)
Toggle me to see a full custom plugin example
Here's a possible implementation of a RequestResponseLoggerPlugin that logs using `Log.d()`object RequestResponseLoggerPlugin : Plugin {
override fun <T> afterRequest(response: Response, typedResult: T?, endpoint: Endpoint) {
Log.d("Network Logger:", response.toString())
}
override fun beforeRequest(request: Request) {
Log.d("Network Logger:", request.toString())
}
override fun modifyRequest(request: Request, endpoint: Endpoint): Request = request
}
Testing
Microya-Kotlin supports mocking responses in your tests. To do that, just initialize a different ApiProvider
in your tests and specify with a given delay
and scheduler
as the mockingBehavior
parameter.
Now, instead of making actual calls, Microya-Kotlin will respond with the provided mockedResponse
computed property in your Endpoint
type.
ApiProvider.Builder().baseUrl("https://postman-echo.com")
.client(OkHttpClient())
.plugins(listOf(HttpAuthPlugin(HttpAuthPlugin.Scheme.BASIC, "abc123"))
.requestJsonFormatter(PostmanSerializer.requestJsonFormatter)
.responseJsonFormatter(PostmanSerializer.responseJsonFormatter)
.mockingBehaviour(MockingBehaviour(Duration.ZERO))
.build()
You can also define custom mockedResponse
with in your mockingBehavior
ApiProvider.Builder().baseUrl("https://postman-echo.com")
.client(OkHttpClient())
.plugins(listOf(HttpAuthPlugin(HttpAuthPlugin.Scheme.BASIC, "abc123")))
.requestJsonFormatter(PostmanSerializer.requestJsonFormatter)
.responseJsonFormatter(PostmanSerializer.responseJsonFormatter)
.mockingBehaviour(MockingBehaviour(Duration.ZERO) { endpoint: Endpoint ->
when (endpoint) {
is PostmanEchoEndpoint.Index -> {
endpoint.mockResponseObject(200, EmptyBodyResponse())
}
is PostmanEchoEndpoint.Post -> {
null
}
is PostmanEchoEndpoint.Get -> {
endpoint.mockResponseObject(200, FooBar("Sakata", "Gintoki"))
}
is PostmanEchoEndpoint.Patch -> {
endpoint.mockResponseObject(200, FooBar("test", "1234"))
}
else -> throw IllegalArgumentException("Endpoint doesn't exist.")
}
})
.build()
Contributing
See the file CONTRIBUTING.md.
License
This library is released under the MIT License. See LICENSE for details.