I am trying to create a DSL for creating JSONObjects. Here is a builder class and a sample usage:
import org.json.JSONObject
fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
val builder = JsonObjectBuilder()
builder.build()
return builder.json
}
class JsonObjectBuilder {
val json = JSONObject()
infix fun <T> String.To(value: T) {
json.put(this, value)
}
}
fun main(args: Array<String>) {
val jsonObject =
json {
"name" To "ilkin"
"age" To 37
"male" To true
"contact" To json {
"city" To "istanbul"
"email" To "xxx#yyy.com"
}
}
println(jsonObject)
}
The output of the above code is :
{"contact":{"city":"istanbul","email":"xxx#yyy.com"},"name":"ilkin","age":37,"male":true}
It works as expected. But it creates an additional JsonObjectBuilder instance every time it creates a json object. Is it possible to write a DSL for creating json objects without extra garbage?
You can use a Deque as a stack to track your current JSONObject context with a single JsonObjectBuilder:
fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
return JsonObjectBuilder().json(build)
}
class JsonObjectBuilder {
private val deque: Deque<JSONObject> = ArrayDeque()
fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
deque.push(JSONObject())
this.build()
return deque.pop()
}
infix fun <T> String.To(value: T) {
deque.peek().put(this, value)
}
}
fun main(args: Array<String>) {
val jsonObject =
json {
"name" To "ilkin"
"age" To 37
"male" To true
"contact" To json {
"city" To "istanbul"
"email" To "xxx#yyy.com"
}
}
println(jsonObject)
}
Example output:
{"contact":{"city":"istanbul","email":"xxx#yyy.com"},"name":"ilkin","age":37,"male":true}
Calling json and build across multiple threads on a single JsonObjectBuilder would be problematic but that shouldn't be a problem for your use case.
Do you need a DSL? You lose the ability to enforce String keys, but vanilla Kotlin isn't that bad :)
JSONObject(mapOf(
"name" to "ilkin",
"age" to 37,
"male" to true,
"contact" to mapOf(
"city" to "istanbul",
"email" to "xxx#yyy.com"
)
))
Updated on Jan 11 2023:
Replaced infix fun String.to(json: Json -> Unit) with infix fun String.to(json: Json.() -> Unit) which uses Json block as receiver and invokes after a Json object is created. So no longe need to add Json key inside Json object.
I am not sure if I get the question correctly. You don't want a builder?
import org.json.JSONArray
import org.json.JSONObject
class Json() : JSONObject() {
constructor(init: Json.() -> Unit) : this() {
this.init()
}
infix fun String.to(json: Json.() -> Unit) {
put(this, Json().apply(json))
}
infix fun <T> String.to(value: T) {
put(this, value)
}
infix fun <T> String.to(values: List<T>) {
put(this, JSONArray().apply {
values.forEach { put(it) }
})
}
}
fun main(args: Array<String>) {
val json = Json {
"name" to "Roy"
"body" to {
"height" to 173
"weight" to 80
}
"cars" to listOf(
"Tesla"
"Porsche"
"BMW"
"Ferrari"
)
}
println(json)
}
You will get
{
"name": "Roy",
"body": {
"weight": 80,
"height": 173
},
"cars": [
"Tesla",
"Porsche",
"BMW",
"Ferrari"
]
}
Yes, it is possible if you don't need any intermediate representation of the nodes, and if the context is always the same (the recursive calls are no different from each other). This can be done by writing the output immediately.
However, this severely increases code complexity, because you have to process your DSL calls right away without storing them anywhere (again, to avoid redundant objects).
Example (see its demo here):
class JsonContext internal constructor() {
internal val output = StringBuilder()
private var indentation = 4
private fun StringBuilder.indent() = apply {
for (i in 1..indentation)
append(' ')
}
private var needsSeparator = false
private fun StringBuilder.separator() = apply {
if (needsSeparator) append(",\n")
}
infix fun String.to(value: Any) {
output.separator().indent().append("\"$this\": \"$value\"")
needsSeparator = true
}
infix fun String.toJson(block: JsonContext.() -> Unit) {
output.separator().indent().append("\"$this\": {\n")
indentation += 4
needsSeparator = false
block(this#JsonContext)
needsSeparator = true
indentation -= 4
output.append("\n").indent().append("}")
}
}
fun json(block: JsonContext.() -> Unit) = JsonContext().run {
block()
"{\n" + output.toString() + "\n}"
}
val j = json {
"a" to 1
"b" to "abc"
"c" toJson {
"d" to 123
"e" toJson {
"f" to "g"
}
}
}
If you don't need indentation but only valid JSON, this can be easily simplified, though.
You can make the json { } and .toJson { } functions inline to get rid even of the lambda classes and thus you achieve almost zero object overhead (one JsonContext and the StringBuilder with its buffers are still allocated), but that would require you to change the visibility modifiers of the members these functions use: public inline functions can only access public or #PublishedApi internal members.
Found another solution. You can just inherit JSONObject class without need to create other objects.
class Json() : JSONObject() {
constructor(init: Json.() -> Unit) : this() {
this.init()
}
infix fun <T> String.To(value: T) {
put(this, value)
}
}
fun main(args: Array<String>) {
val jsonObject =
Json {
"name" To "ilkin"
"age" To 37
"male" To true
"contact" To Json {
"city" To "istanbul"
"email" To "xxx#yyy.com"
}
}
println(jsonObject)
}
The output of code will be the same.
{"contact":{"city":"istanbul","email":"xxx#yyy.com"},"name":"ilkin","age":37,"male":true}
UPD: If you use gson library you can look at this awesome library. It doesn't create any garbage, source code is easy to read and understand.
You could use a library such as https://github.com/holgerbrandl/jsonbuilder to build json with
val myJson = json {
"size" to 0
"array" to arrayOf(1,2,3)
"aggs" to {
"num_destinations" to {
"cardinality" to {
"field" to "DestCountry"
}
}
}
}
Disclaimer: I'm the author of the library.
Related
i'm facing hard times updating list of Orders in real time from firestore using stateflow !!
class RepositoryImp : Repository {
private fun Query.snapshotFlow(): Flow<QuerySnapshot> = callbackFlow {
val snapshott = addSnapshotListener { value, error ->
if (error != null) {
close()
return#addSnapshotListener
}
if (value != null)
trySend(value)
}
awaitClose {
snapshott.remove()
}
}
override fun getAllOrders() = flow<State<List<OrderModel>>> {
emit(State.loading())
val snapshot = ORDER_COLLECTION_REF.snapshotFlow()
.mapNotNull { it.toObjects(OrderModel::class.java) }
emit(State.success(snapshot)) // **HERE** !!!!!!
}.catch {
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
}
i'm receiving the error from // emit(State.success(snapshot)) that says :
Type mismatch: inferred type is Flow<(Mutable)List<OrderModel!>> but List< OrderModel> was expected
sealed class State <T> {
class Loading <T> : State<T>()
data class Success <T> (val data: T) : State <T>()
data class Failed <T> (val message: String) : State <T>()
companion object {
fun <T> loading() = Loading <T>()
fun <T> success(data: T) = Success(data)
fun <T> failed(message: String) = Failed<T>(message)
}
}
My fun to LoadOrders :
private suspend fun loadOrders() {
viewModel.getAllOrders().collect { state ->
when (state) {
is State.Loading -> {
showToast("Loading")
}
is State.Success -> {
adapter.submitList(state.data)
}
is State.Failed -> showToast("Failed! ${state.message}")
}
}
}
Your snapshot variable is a Flow of lists, not a single List. If you want to just fetch the current list, you shouldn't use a flow for that. Instead use get().await().
override fun getAllOrders() = flow<State<List<OrderModel>>> {
emit(State.loading())
val snapshot = ORDER_COLLECTION_REF.get().await()
.let { it.toObjects(OrderModel::class.java) }
emit(State.success(snapshot))
}.catch {
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
The flowOn call is actually unnecessary because we aren't doing anything blocking. await() is a suspend function.
Based on comments discussion below, supposing we want to show a loading state only before the first item, then show a series of success states, and we want to show an error and stop emitting once there's an error, we could do:
override fun getAllOrders() = flow<State<List<OrderModel>>> {
emit(State.loading())
val snapshots = ORDER_COLLECTION_REF.snapshotFlow()
.mapNotNull { State.success(it.toObjects(OrderModel::class.java)) }
emitAll(snapshots)
}.catch {
emit(State.failed(it.message.toString()))
}
I use Ktor serialization in my app, below is a dependency in build.gradle:
dependencies {
// ...
implementation "io.ktor:ktor-serialization:$ktor_version"
}
And set up it in Application.kt:
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
#Suppress("unused")
fun Application.module(#Suppress("UNUSED_PARAMETER") testing: Boolean = false) {
// ...
install(ContentNegotiation) {
json(Json {
prettyPrint = true
})
}
// ...
}
All works perfectly but enumerations... For example, I have the next one:
enum class EGender(val id: Int) {
FEMALE(1),
MALE(2);
companion object {
fun valueOf(value: Int) = values().find { it.id == value }
}
}
If I will serialise this enum instance, Ktor will output a something like:
{
"gender": "MALE"
}
How to make it in a lower case without renaming enumeration members?
P.S. Also I can't change Int to String type cuz it represents database IDs.
You can add the SerialName annotation for enum constants to override names in JSON:
#kotlinx.serialization.Serializable
enum class EGender(val id: Int) {
#SerialName("female")
FEMALE(1),
#SerialName("male")
MALE(2);
companion object {
fun valueOf(value: Int) = values().find { it.id == value }
}
}
I have a model in Kotlin of a simple library of Books and Borrowers where a Book is checked out if it has a Borrower. I use Arrow Option to encode the absence/presence of a Borrower:
data class Borrower(val name: Name, val maxBooks: MaxBooks)
data class Book(val title: String, val author: String, val borrower: Option<Borrower> = None)
I am having trouble serializing/deserializing these objects to/from JSON in Gson - specifically the representation of an Option<Borrower> to a JSON null within a Book:
[
{
"title": "Book100",
"author": "Author100",
"borrower": {
"name": "Borrower100",
"maxBooks": 100
}
},
{
"title": "Book200",
"author": "Author200",
"borrower": null
}
]
My deserialize code:
fun jsonStringToBooks(jsonString: String): List<Book> {
val gson = Gson()
return try {
gson.fromJson(jsonString, object : TypeToken<List<Book>>() {}.type)
} catch (e: Exception) {
emptyList()
}
}
I get an empty list. The nearly identical jsonStringToBorrowers works fine.
Can someone point me in the right direction?
Would using a different JSON library like kotlinx.serialization or Klaxon be a better idea and how do they do the null <-> None thing?
Thank you!
The issue is a bit hidden by the fact that you don't log the exception before returning an empty list. If you logged that exception you would have gotten this:
java.lang.RuntimeException: Failed to invoke private arrow.core.Option() with no args
This means that Gson doesn't know how to create an Option class because it has no public empty constructor. Indeed, Option is a sealed class (hence abstract) having 2 concrete children classes: Some and None. In order to get an instance of Option you should use one of the factory methods, like Option.just(xxx) or Option.empty() among the others.
Now, in order to fix your code you need to tell Gson how to deserialize an Option class. To do that, you need to register a type adapter to your gson object.
A possible implementation is the following:
class OptionTypeAdapter<E>(private val adapter: TypeAdapter<E>) : TypeAdapter<Option<E>>() {
#Throws(IOException::class)
override fun write(out: JsonWriter, value: Option<E>) {
when (value) {
is Some -> adapter.write(out, value.t)
is None -> out.nullValue()
}
}
#Throws(IOException::class)
override fun read(input: JsonReader): Option<E> {
val peek = input.peek()
return if (peek != JsonToken.NULL) {
Option.just(adapter.read(input))
} else {
input.nextNull()
Option.empty()
}
}
companion object {
fun getFactory() = object : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val rawType = type.rawType as Class<*>
if (rawType != Option::class.java) {
return null
}
val parameterizedType = type.type as ParameterizedType
val actualType = parameterizedType.actualTypeArguments[0]
val adapter = gson.getAdapter(TypeToken.get(actualType))
return OptionTypeAdapter(adapter) as TypeAdapter<T>
}
}
}
}
You can use it in the following way:
fun main(args: Array<String>) {
val gson = GsonBuilder()
.registerTypeAdapterFactory(OptionTypeAdapter.getFactory())
.create()
val result: List<Book> = try {
gson.fromJson(json, TypeToken.getParameterized(List::class.java, Book::class.java).type)
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
println(result)
}
That code outputs:
[Book(title=Book100, author=Author100, borrower=Some(Borrower(name=Borrower100, maxBooks=100))), Book(title=Book200, author=Author200, borrower=None)]
I currently have this class with dsl like building ability
class GRLMessage {
var headerMap : MutableMap<String, String> = mutableMapOf()
lateinit var methodType : GRLMethod
lateinit var multipartObject : IGRLMultipart
fun message(closure: GRLMessage.() -> Unit) : GRLMessage {
closure()
return this
}
fun method(closure: GRLMessage.() -> GRLMethod) : GRLMessage {
methodType = closure()
return this
}
fun headers(closure: GRLMessage.() -> Unit) : GRLMessage {
closure()
return this
}
fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
var pair = closure()
headerMap.put(pair.first, pair.second)
return this
}
fun multipart(closure: GRLMessage.() -> IGRLMultipart) : GRLMessage {
multipartObject = closure()
return this
}
}
And I test it like this
class GRLMessageTest {
data class DummyMultipart(val field: String) : IGRLMultipart {
override fun getContent() {
this
}
}
#Test fun grlMessageBuilderTest() {
val grlMessage = GRLMessage().message {
method { GRLMethod.POST }
headers {
header { Pair("contentType", "object") }
header { Pair("objectType", "DummyMultipart") }
}
multipart { DummyMultipart("dummy") }
}
val multipart = DummyMultipart("dummy")
val headers = mapOf(
Pair("contentType", "object"),
Pair("objectType", "DummyMultipart")
)
val method = GRLMethod.POST
assertEquals(multipart, grlMessage.multipartObject)
assertEquals(method, grlMessage.methodType)
assertEquals(headers, grlMessage.headerMap)
}
}
But despite providing
header { Pair("contentType", "object") }
I still have to evaluate closure inside header method and directly put key and value into my MutableMap
fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
var pair = closure()
headerMap.put(pair.first, pair.second)
return this
}
Is there a better way adding entries to Map?
Does your headerMap need to be a var? If not, you can change it to a val and use headerMap += closure().
Adding an extension function makes your fluent methods more obviously fluent:
fun <T: Any> T.fluently(func: ()->Unit): T {
return this.apply { func() }
}
With that your fluent function is always clear about its return:
fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
return fluently { headerMap += closure() }
}
Which is really the same as:
fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
return this.apply { headerMap += closure() }
}
But the extension function adds a touch of readability.
Above I use the answer given in by #Ruckus for solving your specific question of adding a Pair to the headerMap. But you have other options that you might want to know about for other use cases of your DSL...
You can use let, apply or with which would allow any type of decomposition of the results of closure() call (maybe it is more complicated than Pair in the future). All of these are basically the same, minus their resulting value:
with(closure()) { headerMap.put(this.first, this.second) }
closure().apply { headerMap.put(this.first, this.second) }
closure().let { headerMap.put(it.first, it.second) }
Using let or apply is nice if you want to handle a case where closure() allows nullable return, in which case you might want to take action only if not null:
closure()?.apply { headerMap.put(this.first, this.second) }
closure()?.let { headerMap.put(it.first, it.second) }
Other notes about your code:
use val instead of var unless you have no other choice
lateinit (or the similar Delegates.notNull()) seem dangerous to use in an uncontrolled lifecycle where there is no guarantee it will be completed, because the error message will be confusing and happen at some unexpected time in the future. There are likely other ways to solve this with a DSL that chains calls to create more of a multi-step grammar
You can shorten code by only having types on one side of the assignment, for example:
val myMap = mutableMapOf<String, String>()
instead of
var myMap : MutableMap<String, String> = mutableMapOf()
Well for now as a solution I created extension for MutableMap
fun MutableMap<String, String>.put(pair : Pair<String, String>) {
this.put(pair.first, pair.second)
}
Which allowed me to write like this
fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
headerMap.put(closure())
return this
}
Is there possibility to get names of method's parameters inside method code? Can anyone show example of it?
I found only this solution at this time, but dislike it..:
class Greeter() {
fun greet(name: String) {
val c = Greeter::class;
for (m in c.memberFunctions) {
if (m.name == "greet") {
val p = m.parameters
println(p.toString())
}
}
println("Hello, ${name}");
}
}
fun main(args: Array<String>) {
Greeter().greet("UserName")
}
update: i found another solution, but now i have one more question:
How to get pairs of ParamName, ParamValue at function greet?
class Greeter() {
fun greet(name: String) {
val p = Greeter::greet.parameters
println(p.toString())
println("Hello, ${name}");
}
}
fun main(args: Array<String>) {
Greeter().greet("UserName")
}
Parameter names are available through the KParameter.name property:
class Greeter() {
fun greet(name: String) {
val p = Greeter::greet.parameters
println("Hello, ${p[0].name}")
}
}
Parameter values, on the other hand, cannot be obtained reflectively easily on JVM.