Halley provides a simple way on Android to serialize and deserialize models according
to JSON Hypertext Application Language specification also
known just as HAL.
Besides manual call sites that core package provides, Retrofit and Ktor integrations are included.
Halley is built on top of KotlinX Serialization.
- Getting started
- Usage
- Models
- Comments and limitations
- Deserialization options
- Requirements
- Contributing
- License
- Credits
There are several ways to include Halley in your project, depending on your use case.
In every case, you should include and apply Halley plugin first. Plugin will include core dependencies and apply KotlinX Serialization in
your project.
You have to add buildscript dependencies in your project level build.gradle
or build.gradle.kts
:
Groovy
buildscript {
repositories {
mavenCentral()
}
}
KotlinDSL
buildscript {
repositories {
mavenCentral()
}
}
To include plugin to your project, you have to add buildscript dependencies in your project level build.gradle
or build.gradle.kts
:
Groovy
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "com.infinum.halley:halley-plugin:0.0.6"
}
}
KotlinDSL
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("com.infinum.halley:halley-plugin:0.0.6")
}
}
Then apply the plugin in your app build.gradle
or build.gradle.kts
:
Groovy
apply plugin: "com.infinum.halley.plugin"
KotlinDSL
plugins {
...
id("com.infinum.halley.plugin")
}
Now you can sync your project and core features like serialization and deserialization will be automatically provided.
Groovy
implementation "com.infinum.halley:halley-core:0.0.6"
KotlinDSL
implementation("com.infinum.halley:halley-core:0.0.6")
Groovy
implementation "com.infinum.halley:halley-retrofit:0.0.6"
KotlinDSL
implementation("com.infinum.halley:halley-retrofit:0.0.6")
Groovy
implementation "com.infinum.halley:halley-ktor:0.0.6"
KotlinDSL
implementation("com.infinum.halley:halley-ktor:0.0.6")
Groovy
implementation "com.infinum.halley:halley-retrofit:0.0.6"
implementation "com.infinum.halley:halley-ktor:0.0.6"
KotlinDSL
implementation("com.infinum.halley:halley-retrofit:0.0.6")
implementation("com.infinum.halley:halley-ktor:0.0.6")
Serialization
// create or reuse default Halley instance
val halley = Halley()
// serialize Kotlin class to String
val result: String = halley.encodeToString(
value = ...
)
Deserialization
// create or reuse default Halley instance
val halley = Halley()
// deserialize String to Kotlin class
val actual: HalModel = halley.decodeFromString(
string = " ... "
)
Add converter factory
Retrofit.Builder()
.addCallAdapterFactory(RxJava3CallAdapterFactory.create()) // Optional only if you use RxJava
// Halley for Retrofit provides an extension on KotlinX Serialization Json
.addConverterFactory(Json.asHalleyConverterFactory())
.addConverterFactory(ScalarsConverterFactory.create())
.client(httpClient)
.baseUrl(baseUrl)
.build()
Deserialization
HttpClient(CIO) {
defaultRequest {
url {
protocol = URLProtocol.HTTP
host = "localhost"
}
port = 8080
// Halley for Ktor provides a ContentType header,
// this is a mandatory line to add for Ktor to resolve objects.
contentType(ContentType.HAL)
}
// Halley for Ktor provides a plugin to install with default configuration
install(HalleyPlugin) {
defaultConfiguration()
}
}
A typical HAL model class prepared for Halley consists of several parts.
@Serializable
data class Model(
@HalLink
@SerialName(value = "self")
val self: Link? = Link(href = "http://localhost:8008/api/Profile/self"),
@HalEmbedded
@SerialName(value = "user")
val user: OtherModel? = OtherModel(
self = Link(
href = "http://localhost:8008/api/User/self"
)
)
) : HalResource
Going from top to bottom, these classes must obey the following list of rules:
- A class must be annotated by KotlinX Serialization
@Serializable
. - A class must implement an empty
HalResource
interface. - Classes without
HalResource
interface will be treated like a simple plain JSON. - It's recommended to annotate each class property with
@SerialName(value = "...")
to avoid possible ProGuard issues. - HAL link properties must be annotated with
@HalLink
and beLink
type provided by Halley library. - HAL embedded properties must be annotated with
@HalEmbedded
- Objects under
@HalEmbedded
annotated property must also implementHalResource
interface.
A few notes about HalResource
models:
- Kotlin
object
classes are not supported in Halley. - Multiple
Link
classes in one property under one@HalLink
annotation are supported by usingList
,Iterable
,Set
,Collection
interfaces andArray
class only. - Multiple embedded classes in one property under one
@HalEmbedded
annotation are supported by usingList
,Iterable
,Set
,Collection
interfaces andArray
class only. - If a model class has
@HalEmbedded
annotated property but server response provides partial or no object in JSON, then link from _ links part of response JSON will be used to request that resource and populate the parent model before returning the complete parent model.
Please refer to more complex variations of HAL model classes in sample module or test source set in core module of this repository.
Halley has 2 ways of providing 3 different option arguments.
Developers can use imperative and annotated way, or both at the same time, to define option arguments depending on integration use
case.
If both ways are used, imperative way will override any existing and same keys in the annotations.
Option arguments are defined as common, query and template.
- common - will be appended as HTTP query parameters on every link executed by Halley
- query - will be appended as HTTP query parameters on every link with the matching key executed by Halley
- template - will be replaced on every link with the matching key executed by Halley according to RFC 6570 standard
// create or reuse default Halley instance
val halley = Halley()
// deserialize String to Kotlin class
val actual: HalModel = halley.decodeFromString(
string = " ... ",
options = Halley.Options(
common = Arguments.Common(
mapOf(
"device" to "Samsung",
"rooted" to "false"
)
),
query = Arguments.Query(
mapOf("animal" to mapOf("age" to "10"))
),
template = Arguments.Template(
mapOf("user" to mapOf("id" to "1"))
)
)
)
When using Halley with Retrofit, it is important to ensure that each Retrofit service interface method is annotated with @HalTag
.
The value (any String
you've chosen) set for the tag will later be used to match the method with the corresponding option arguments.
@GET("/Profile/self")
@HalCommonArguments(
arguments = [
HalArgumentEntry("device", "Alcatel")
]
)
@HalQueryArguments(
arguments = [
HalQueryArgument(
"animal",
[
HalArgumentEntry("country", "France")
]
)
]
)
@HalTemplateArguments(
arguments = [
HalTemplateArgument(
"animal",
[
HalArgumentEntry("id", "1")
]
)
]
)
@HalTag("profileWithAnnotatedOptions")
fun profileWithAnnotatedOptions(): Call<ProfileResource>
When setting options imperatively, it is important to ensure that you have used the same tag value as the one set in the Retrofit service interface method you intend to use the options for.
private fun fetchProfile() {
// Halley for Retrofit provides convenience halleyQueryOptions functions
halleyQueryOptions(tag = "profileWithImperativeOptions") {
mapOf("animal" to mapOf("country" to "Brazil"))
}
halleyTemplateOptions(tag = "profileWithImperativeOptions") {
mapOf("animal" to mapOf("id" to "1"))
}
webServer.client()?.service?.profileWithImperativeOptions()
?.enqueue(object : Callback<ProfileResource> {
override fun onResponse(
call: Call<ProfileResource>,
response: Response<ProfileResource>
) {
response.body()?.let {
showResult(it.prettyPrint())
}
}
override fun onFailure(call: Call<ProfileResource>, t: Throwable) {
showResult(t.message.toString())
}
})
}
Halley Retrofit converter factory works as a standalone factory or together with Kotlin Coroutines, RxJava1, RxJava2 and RxJava3
factories.
Various use cases can be examined in
the sample implementation.
HttpClient(CIO) {
defaultRequest {
...
contentType(ContentType.HAL)
// Halley provides extension methods for DefaultRequest
halOptions(
common = Arguments.Common(
mapOf(
"device" to "Motorola",
"rooted" to "false"
)
),
query = Arguments.Query(
mapOf("animal" to mapOf("country" to "Germany"))
),
template = Arguments.Template(
mapOf("animal" to mapOf("id" to "2"))
)
)
}
}
...
suspend fun profileWithOptions(): ProfileResource =
client.get {
url {
path("api", "Profile", "self")
}
// Halley provides extension methods for HttpRequest
halOptions(
query = Arguments.Query(
mapOf("animal" to mapOf("country" to "Italy"))
),
template = Arguments.Template(
mapOf("animal" to mapOf("id" to "1"))
)
)
}.body()
An example of a Ktor client can be examined here.
Halley is written entirely in Kotlin, for Kotlin models and projects.
We believe that the community can help us improve and build better a product. Please refer to our contributing guide to learn about the types of contributions we accept and the process for submitting them.
To ensure that our community remains respectful and professional, we defined a code of conduct that we expect all contributors to follow.
We appreciate your interest and look forward to your contributions.
Copyright 2022 Infinum
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Maintained and sponsored by Infinum.