Skip to content

Instantly share code, notes, and snippets.

@Revolucent
Created January 31, 2022 17:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Revolucent/fb3d03ec08e6e5a10c24afcc03100af6 to your computer and use it in GitHub Desktop.
Save Revolucent/fb3d03ec08e6e5a10c24afcc03100af6 to your computer and use it in GitHub Desktop.
Some combinators which implement a DSL for querying and deserializing Json from Strings. Extend or alter to your taste.
typealias Deserializer<I, O> = (I) -> O?
typealias JsonDeserializer<I, O> = (Json) -> Deserializer<I, O>
typealias JsonElementDeserializer = JsonDeserializer<JsonElement, JsonElement>
private fun <I, O> const(deserializer: Deserializer<I, O>): JsonDeserializer<I, O> = {
deserializer
}
infix fun <A, B, C> JsonDeserializer<A, B>.j(
next: JsonDeserializer<B, C>
): JsonDeserializer<A, C> = { json ->
{ a -> this(json)(a)?.let(next(json)) }
}
inline fun <reified T> js(json: Json): Deserializer<String, T> =
json::decodeFromString
inline fun <reified T> je(json: Json): Deserializer<JsonElement, T> =
json::decodeFromJsonElement
fun jse(json: Json): Deserializer<String, JsonElement> =
js(json)
fun ja(index: Int): JsonElementDeserializer = const { a ->
(a as? JsonArray)?.getOrNull(index)
}
fun jo(vararg keys: String): JsonElementDeserializer = const { o ->
var element: JsonElement? = o
for (key in keys) {
element = (element as? JsonObject)?.get(key)
}
element
}
infix fun <I> JsonDeserializer<I, JsonElement>.j(key: String): JsonDeserializer<I, JsonElement> =
this j jo(key)
infix fun <I> JsonDeserializer<I, JsonElement>.j(index: Int): JsonDeserializer<I, JsonElement> =
this j ja(index)
@Revolucent
Copy link
Author

Revolucent commented Jan 31, 2022

How to use this? Imagine we have the following JSON:

{
  "data": {
    "economists": [
      {"firstName": "Murray", "lastName": "Rothbard", "school": "Austrian"},
      {"firstName": "Friedrich", "lastName": "Hayek", "school": "Austrian"},
      {"firstName": "Milton", "lastName": "Friedman", "school": "Chicago"}
    ]
  }
}

We also have this class:

@Serializable
data class Economist(val firstName: String, val lastName: String, val school: String)

Let's say we just want the list of economists and nothing else. Here's how we do it:

val deserializer: JsonDeserializer<String, List<Economist>> = ::jse j "data" j "economists" ::je

If we get rid of the typealias, this gives us a function of the form (Json) -> ((String) -> List<Economist>?). In other words, it's a function which produces a function. Passing in a Json instance, we now get back a simple function which takes us from String to List<Economist> using the given Json instance.

val json: Json =val deserialize = deserializer(json)

The jse function converts a String into JsonElement. The je function deserializes a JsonElement into a class. The j combinator glues everything together. It's exactly equivalent to the | operator in shell scripting, where the output of the previous command is piped as the input of the next. However, if j is followed by a string and the output of the preceding command is a JsonElement, j assumes the JsonElement is a JsonObject and attempt to get the value of that attribute as a JsonElement. If any part of that fails, null is returned and the entire pipeline stops, producing the null. A very similar thing happens when the j is followed by an integer. Most pipelines start with ::jse, have one or more uses of j, and end with ::je.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment