Skip to content

Commit

Permalink
Json optics dsl (arrow-kt#2)
Browse files Browse the repository at this point in the history
* First working POC

* Allow for extracting reasonable typed values from Json

* Bump version of Kotlin, Arrow and metadata.
Arrow-optics bumped to SNAPSHOT
Removed optics typeclasses.

* Add some docs and cleanup

* Complete genJson
Complete tests
  • Loading branch information
nomisRev authored and raulraja committed Feb 4, 2018
1 parent 0a19aaf commit 20042d4
Show file tree
Hide file tree
Showing 15 changed files with 555 additions and 12 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ buildscript {
gradleVersionsPluginVersion = '0.17.0'
javaVersion = JavaVersion.VERSION_1_7
kotlinTestVersion = '2.0.7'
kotlinVersion = '1.2.10'
kotlinVersion = '1.2.20'
kotlinxCoroutinesVersion = '0.20'
kotlinxCollectionsImmutableVersion = '0.1'
arrowVersion = '0.5.5'
arrowVersion = '0.6.1'
}

repositories {
Expand Down
22 changes: 13 additions & 9 deletions helios-core/src/main/kotlin/helios/core/helios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ const val MinLongString = "-9223372036854775808"

}

@lenses data class JsBoolean(val value: Boolean) : Json() {
@lenses @isos data class JsBoolean(val value: Boolean) : Json() {
override fun toJsonString(): String = "$value"
}

@lenses data class JsString(val value: CharSequence) : Json() {
@lenses @isos data class JsString(val value: CharSequence) : Json() {
override fun toJsonString(): String = """"$value""""
}

Expand Down Expand Up @@ -176,7 +176,7 @@ const val MinLongString = "-9223372036854775808"
}
}

@lenses data class JsDecimal(val value: String) : JsNumber() {
@lenses @isos data class JsDecimal(val value: String) : JsNumber() {
override fun toBigDecimal(): Option<BigDecimal> = value.toBigDecimal().some()

override fun toBigInteger(): Option<BigInteger> = toBigDecimal().map { it.toBigInteger() }
Expand All @@ -188,7 +188,7 @@ const val MinLongString = "-9223372036854775808"
override fun toJsonString(): String = value
}

@lenses data class JsLong(val value: Long) : JsNumber() {
@lenses @isos data class JsLong(val value: Long) : JsNumber() {
override fun toBigDecimal(): Option<BigDecimal> = value.toBigDecimal().some()

override fun toBigInteger(): Option<BigInteger> = toBigDecimal().map { it.toBigInteger() }
Expand All @@ -200,7 +200,7 @@ const val MinLongString = "-9223372036854775808"
override fun toJsonString(): String = "$value"
}

data class JsDouble(val value: Double) : JsNumber() {
@lenses @isos data class JsDouble(val value: Double) : JsNumber() {
override fun toBigDecimal(): Option<BigDecimal> = value.toBigDecimal().some()

override fun toBigInteger(): Option<BigInteger> = toBigDecimal().map { it.toBigInteger() }
Expand All @@ -212,7 +212,7 @@ data class JsDouble(val value: Double) : JsNumber() {
override fun toJsonString(): String = "$value"
}

@lenses data class JsFloat(val value: Float) : JsNumber() {
@lenses @isos data class JsFloat(val value: Float) : JsNumber() {

override fun toBigDecimal(): Option<BigDecimal> = value.toBigDecimal().some()

Expand All @@ -225,7 +225,7 @@ data class JsDouble(val value: Double) : JsNumber() {
override fun toJsonString(): String = "$value"
}

@lenses data class JsInt(val value: Int) : JsNumber() {
@lenses @isos data class JsInt(val value: Int) : JsNumber() {
override fun toBigDecimal(): Option<BigDecimal> = value.toBigDecimal().some()

override fun toBigInteger(): Option<BigInteger> = value.toBigInteger().some()
Expand All @@ -237,19 +237,23 @@ data class JsDouble(val value: Double) : JsNumber() {
override fun toJsonString(): String = "$value"
}

@lenses data class JsArray(val value: List<Json>) : Json() {
@lenses @isos data class JsArray(val value: List<Json>) : Json() {

operator fun get(index: Int): Option<Json> = Option.fromNullable(value.getOrNull(index))

override fun toJsonString(): String =
value.map { it.toJsonString() }.joinToString(prefix = "[", separator = ",", postfix = "]")

companion object
}
@lenses data class JsObject(val value: Map<String, Json>) : Json() {

@lenses @isos data class JsObject(val value: Map<String, Json>) : Json() {
fun toList(): List<Tuple2<String, Json>> = value.toList().map { it.first toT it.second }

override fun toJsonString(): String =
value.map { (k, v) -> """"$k":${v.toJsonString()}""" }.joinToString(prefix = "{", separator = ",", postfix = "}")

companion object
}

object JsNull : Json() {
Expand Down
2 changes: 1 addition & 1 deletion helios-meta-compiler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies {
compile project(":helios-meta")
compile "io.arrow-kt:arrow-annotations-processor:$arrowVersion"
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion"
compile 'me.eugeniomarletti:kotlin-metadata:1.2.0'
compile 'me.eugeniomarletti:kotlin-metadata:1.2.1'
compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
kapt 'com.google.auto.service:auto-service:1.0-rc3'

Expand Down
24 changes: 24 additions & 0 deletions helios-optics/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
dependencies {
compile project(":helios-core")
compile project(":helios-meta")
compile project(":helios-parser")
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
compile "io.arrow-kt:arrow-optics:0.6.2-SNAPSHOT"
compile "io.arrow-kt:arrow-typeclasses:$arrowVersion"
compileOnly project(':helios-meta-compiler')

kapt "io.arrow-kt:arrow-annotations-processor:$arrowVersion"
kapt project(':helios-meta-compiler')

testCompile "io.arrow-kt:arrow-test:$arrowVersion"
kaptTest project(':helios-meta-compiler')
kaptTest "io.arrow-kt:arrow-annotations-processor:$arrowVersion"
testCompileOnly "io.arrow-kt:arrow-annotations-processor:$arrowVersion"

testCompileOnly project(':helios-meta-compiler')
testCompile "io.kotlintest:kotlintest:$kotlinTestVersion"
}

apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
apply from: rootProject.file('gradle/generated-kotlin-sources.gradle')
apply plugin: 'kotlin-kapt'
4 changes: 4 additions & 0 deletions helios-optics/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Maven publishing configuration
POM_NAME=Helios Optics
POM_ARTIFACT_ID=helios-optics
POM_PACKAGING=jar
139 changes: 139 additions & 0 deletions helios-optics/src/main/kotlin/helios/optics/JsonDSL.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package helios.optics

import arrow.core.Option
import arrow.optics.*
import arrow.optics.typeclasses.At
import arrow.optics.typeclasses.Index
import arrow.optics.typeclasses.at
import arrow.optics.typeclasses.index
import helios.core.*
import helios.typeclasses.Decoder
import helios.typeclasses.Encoder
import helios.typeclasses.decoder
import helios.typeclasses.encoder

/**
* [JsonPath] is a Json DSL based on Arrow-Optics (http://arrow-kt.io/docs/optics/iso/).
*
* With [JsonPath] you can represent paths/relations within your [Json] and allows for working with [Json] in an elegant way.
*/
data class JsonPath(val json: Optional<Json, Json>) {

companion object {
/**
* [JsonPath] [root] which is the start of any path.
*/
val root = JsonPath(Optional.id())

/**
* Overload constructor to point to [root].
*/
operator fun invoke() = root
}

/**
* Extract value as [Boolean] from path.
*/
val boolean: Optional<Json, Boolean> = json compose jsonJsBoolean() compose jsBooleanIso()

/**
* Extract value as [String] from path.
*/
val string: Optional<Json, CharSequence> = json compose jsonJsString() compose jsStringIso()

/**
* Extract value as [JsNumber] from path.
*/
val number: Optional<Json, JsNumber> = json compose jsonJsNumber()

/**
* Extract value as [JsDecimal] from path.
*/
val decimal: Optional<Json, String> = number compose jsNumberJsDecimal() compose jsDecimalIso()

/**
* Extract value as [Long] from path.
*/
val long: Optional<Json, Long> = number compose jsNumberJsLong() compose jsLongIso()

/**
* Extract value as [Float] from path.
*/
val float: Optional<Json, Float> = number compose jsNumberJsFloat() compose jsFloatIso()

/**
* Extract value as [Int] from path.
*/
val int: Optional<Json, Int> = number compose jsNumberJsInt() compose jsIntIso()

/**
* Extract [JsArray] as `List<Json>` from path.
*/
val array: Optional<Json, List<Json>> = json compose jsonJsArray() compose jsArrayIso()

/**
* Extract [JsObject] as `Map<String, Json>` from path.
*/
val `object`: Optional<Json, Map<String, Json>> = json compose jsonJsObject() compose jsObjectIso()

/**
* Extract [JsNull] from path.
*/
val `null`: Optional<Json, JsNull> = json compose jsonJsNull()

/**
* Select field with [name] in [JsObject] from path.
*/
fun select(name: String): JsonPath = JsonPath(json compose jsonJsObject() compose Index.index(name))

/**
* Extract field with [name] from [JsObject] from path.
*/
fun at(field: String): Optional<Json, Option<Json>> = json compose jsonJsObject() compose At.at(field)

/**
* Get element at index [i] from [JsArray].
*/
operator fun get(i: Int): JsonPath = JsonPath(json compose jsonJsArray() compose Index.index(i))

/**
* Extract [A] from path.
*/
fun <A> extract(DE: Decoder<A>, EN: Encoder<A>): Optional<Json, A> =
json compose parse(DE, EN)

/**
* Select field with [name] in [JsObject] and extract as [A] from path.
*/
fun <A> selectExtract(DE: Decoder<A>, EN: Encoder<A>, name: String): Optional<Json, A> =
select(name).extract(DE, EN)

}

/**
* Extract [A] from path.
*/
inline fun <reified A> JsonPath.extract(EN: Encoder<A> = encoder(), DE: Decoder<A> = decoder()): Optional<Json, A> = extract(DE, EN)

/**
* Select field with [name] in [JsObject] and extract as [A] from path.
*/
inline fun <reified A> JsonPath.selectExtract(name: String, DE: Decoder<A> = decoder(), EN: Encoder<A> = encoder()): Optional<Json, A> =
selectExtract(DE, EN, name)

/**
* Unsafe optic: needs some investigation because it is required to extract reasonable typed values from Json.
* https://github.com/circe/circe/blob/master/modules/optics/src/main/scala/io/circe/optics/JsonPath.scala#L152
*/
@PublishedApi
internal fun <A> parse(DE: Decoder<A>, EN: Encoder<A>): Prism<Json, A> = Prism(
getOrModify = { json -> DE.decode(json).mapLeft { _ -> json } },
reverseGet = EN::encode
)

/**
* Unsafe optic: needs some investigation because it is required to extract reasonable typed values from Json.
* https://github.com/circe/circe/blob/master/modules/optics/src/main/scala/io/circe/optics/JsonPath.scala#L152
*/
@PublishedApi
internal inline fun <reified A> parse(EN: Encoder<A> = encoder(), DE: Decoder<A> = decoder()): Prism<Json, A> = parse(DE, EN)
49 changes: 49 additions & 0 deletions helios-optics/src/main/kotlin/helios/optics/instances.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package helios.optics

import arrow.core.Option
import arrow.data.getOption
import arrow.data.k
import arrow.data.updated
import arrow.instance
import arrow.optics.Lens
import arrow.optics.Optional
import arrow.optics.typeclasses.At
import arrow.optics.typeclasses.Index
import arrow.syntax.either.left
import arrow.syntax.either.right
import helios.core.JsArray
import helios.core.JsObject
import helios.core.Json

@instance(JsObject::class)
interface JsObjectIndexInstance : Index<JsObject, String, Json> {
override fun index(i: String): Optional<JsObject, Json> = Optional(
getOrModify = { it.value[i]?.right() ?: it.left() },
set = { js -> { jsObj -> jsObj.copy(jsObj.value.k().updated(i, js)) } }
)
}

@instance(JsObject::class)
interface JsObjectAtInstance : At<JsObject, String, Option<Json>> {
override fun at(i: String): Lens<JsObject, Option<Json>> = Lens(
get = { it.value.getOption(i) },
set = { optJs ->
{ js ->
optJs.fold({
js.copy(value = js.value - i)
}, {
js.copy(value = js.value + (i to it))
})
}
}
)

}

@instance(JsArray::class)
interface JsArrayIndexInstance : Index<JsArray, Int, Json> {
override fun index(i: Int): Optional<JsArray, Json> = Optional(
getOrModify = { it.value.getOrNull(i)?.right() ?: it.left() },
set = { js -> { jsArr -> jsArr.copy(jsArr.value.mapIndexed { index, t -> if (index == i) js else t }) } }
)
}
Loading

0 comments on commit 20042d4

Please sign in to comment.