diff --git a/helios-core/src/main/kotlin/helios/core/errors.kt b/helios-core/src/main/kotlin/helios/core/errors.kt new file mode 100644 index 0000000..09fffa4 --- /dev/null +++ b/helios-core/src/main/kotlin/helios/core/errors.kt @@ -0,0 +1,9 @@ +package helios.core + +sealed class DecodingError +data class StringDecodingError(val value: Json) : DecodingError() +data class BooleanDecodingError(val value: Json) : DecodingError() +data class NumberDecodingError(val value: Json) : DecodingError() +data class ArrayDecodingError(val value: Json) : DecodingError() +data class ObjectDecodingError(val value: Json) : DecodingError() +data class KeyNotFound(val name: String) : DecodingError() \ No newline at end of file diff --git a/helios-core/src/main/kotlin/helios/core/helios.kt b/helios-core/src/main/kotlin/helios/core/helios.kt index bb21662..e1bb6dc 100644 --- a/helios-core/src/main/kotlin/helios/core/helios.kt +++ b/helios-core/src/main/kotlin/helios/core/helios.kt @@ -9,7 +9,6 @@ import helios.instances.jsobject.eq.eq import helios.instances.json.eq.eq import helios.parser.Parser import helios.typeclasses.Decoder -import helios.typeclasses.DecodingError import java.io.File import java.math.BigDecimal import java.math.BigInteger @@ -21,6 +20,8 @@ const val MinLongString = "-9223372036854775808" sealed class Json { + inline val isNull inline get() = this === JsNull + companion object { fun fromValues(i: Iterable): JsArray = JsArray(i.toList()) @@ -123,45 +124,29 @@ data class JsString(val value: CharSequence) : Json() { sealed class JsNumber : Json() { - abstract fun toBigDecimal(): Option + abstract fun toBigDecimal(): BigDecimal - abstract fun toBigInteger(): Option + abstract fun toBigInteger(): BigInteger abstract fun toDouble(): Double - abstract fun toLong(): Option + abstract fun toFloat(): Float - fun toByte(): Option { - val ml = toLong() - return when (ml) { - is Some -> { - val asByte: Byte = ml.t.toByte() - if (asByte.compareTo(ml.t) == 0) Some(asByte) else None - } - is None -> None - } + abstract fun toLong(): Long + + open fun toInt(): Option = toLong().let { + val asInt: Int = it.toInt() + if (asInt.compareTo(it) == 0) Some(asInt) else None } - fun toShort(): Option { - val ml = toLong() - return when (ml) { - is Some -> { - val asShort: Short = ml.t.toShort() - if (asShort.compareTo(ml.t) == 0) Some(asShort) else None - } - is None -> None - } + open fun toShort(): Option = toLong().let { + val asShort: Short = it.toShort() + if (asShort.compareTo(it) == 0) Some(asShort) else None } - fun toInt(): Option { - val ml = toLong() - return when (ml) { - is Some -> { - val asInt: Int = ml.t.toInt() - if (asInt.compareTo(ml.t) == 0) Some(asInt) else None - } - is None -> None - } + open fun toByte(): Option = toLong().let { + val asByte: Byte = it.toByte() + if (asByte.compareTo(it) == 0) Some(asByte) else None } override fun equals(other: Any?): Boolean = JsNumber.eq().run { @@ -172,13 +157,21 @@ sealed class JsNumber : Json() { companion object { - operator fun invoke(value: Long): JsLong = JsLong(value) + operator fun invoke(value: BigDecimal): JsDecimal = JsDecimal(value.toString()) + + operator fun invoke(value: BigInteger): JsDecimal = JsDecimal(value.toString()) operator fun invoke(value: Double): JsDouble = JsDouble(value) + operator fun invoke(value: Float): JsFloat = JsFloat(value) + + operator fun invoke(value: Long): JsLong = JsLong(value) + operator fun invoke(value: Int): JsInt = JsInt(value) - operator fun invoke(value: Float): JsFloat = JsFloat(value) + operator fun invoke(value: Short): JsInt = JsInt(value.toInt()) + + operator fun invoke(value: Byte): JsInt = JsInt(value.toInt()) fun fromDecimalStringUnsafe(value: String): JsDecimal = JsDecimal(value) @@ -197,13 +190,16 @@ sealed class JsNumber : Json() { } data class JsDecimal(val value: String) : JsNumber() { - override fun toBigDecimal(): Option = value.toBigDecimal().some() - override fun toBigInteger(): Option = toBigDecimal().map { it.toBigInteger() } + override fun toBigDecimal(): BigDecimal = value.toBigDecimal() + + override fun toBigInteger(): BigInteger = value.toBigInteger() override fun toDouble(): Double = value.toDouble() - override fun toLong(): Option = value.toLong().some() + override fun toFloat(): Float = value.toFloat() + + override fun toLong(): Long = value.toLong() override fun toJsonString(): String = value @@ -217,13 +213,15 @@ data class JsDecimal(val value: String) : JsNumber() { } data class JsLong(val value: Long) : JsNumber() { - override fun toBigDecimal(): Option = value.toBigDecimal().some() + override fun toBigDecimal(): BigDecimal = value.toBigDecimal() - override fun toBigInteger(): Option = toBigDecimal().map { it.toBigInteger() } + override fun toBigInteger(): BigInteger = value.toBigInteger() override fun toDouble(): Double = value.toDouble() - override fun toLong(): Option = value.some() + override fun toFloat(): Float = value.toFloat() + + override fun toLong(): Long = value override fun toJsonString(): String = "$value" @@ -237,13 +235,16 @@ data class JsLong(val value: Long) : JsNumber() { } data class JsDouble(val value: Double) : JsNumber() { - override fun toBigDecimal(): Option = value.toBigDecimal().some() - override fun toBigInteger(): Option = toBigDecimal().map { it.toBigInteger() } + override fun toBigDecimal(): BigDecimal = value.toBigDecimal() + + override fun toBigInteger(): BigInteger = value.toBigDecimal().toBigInteger() override fun toDouble(): Double = value - override fun toLong(): Option = value.toLong().some() + override fun toFloat(): Float = value.toFloat() + + override fun toLong(): Long = value.toLong() override fun toJsonString(): String = "$value" @@ -258,13 +259,15 @@ data class JsDouble(val value: Double) : JsNumber() { data class JsFloat(val value: Float) : JsNumber() { - override fun toBigDecimal(): Option = value.toBigDecimal().some() + override fun toBigDecimal(): BigDecimal = value.toBigDecimal() - override fun toBigInteger(): Option = toBigDecimal().map { it.toBigInteger() } + override fun toBigInteger(): BigInteger = value.toBigDecimal().toBigInteger() override fun toDouble(): Double = value.toDouble() - override fun toLong(): Option = value.toLong().some() + override fun toFloat(): Float = value + + override fun toLong(): Long = value.toLong() override fun toJsonString(): String = "$value" @@ -278,13 +281,21 @@ data class JsFloat(val value: Float) : JsNumber() { } data class JsInt(val value: Int) : JsNumber() { - override fun toBigDecimal(): Option = value.toBigDecimal().some() + override fun toBigDecimal(): BigDecimal = value.toBigDecimal() - override fun toBigInteger(): Option = value.toBigInteger().some() + override fun toBigInteger(): BigInteger = value.toBigInteger() override fun toDouble(): Double = value.toDouble() - override fun toLong(): Option = value.toLong().some() + override fun toFloat(): Float = value.toFloat() + + override fun toLong(): Long = value.toLong() + + override fun toInt(): Option = value.some() + + override fun toShort(): Option = value.toShort().some() + + override fun toByte(): Option = value.toByte().some() override fun toJsonString(): String = "$value" diff --git a/helios-core/src/main/kotlin/helios/instances/instances.kt b/helios-core/src/main/kotlin/helios/instances/instances.kt index 1567cb7..fb2bc6f 100644 --- a/helios-core/src/main/kotlin/helios/instances/instances.kt +++ b/helios-core/src/main/kotlin/helios/instances/instances.kt @@ -1,18 +1,39 @@ package helios.instances import arrow.core.* -import arrow.core.extensions.eq -import arrow.data.extensions.list.foldable.forAll +import arrow.core.extensions.either.applicative.applicative +import arrow.core.extensions.either.applicative.map2 import arrow.extension -import arrow.higherkind -import arrow.typeclasses.Eq import helios.core.* -import helios.instances.jsarray.eq.eq -import helios.instances.jsnumber.eq.eq -import helios.instances.jsobject.eq.eq -import helios.instances.json.eq.eq import helios.typeclasses.* +fun Double.Companion.encoder() = object : Encoder { + override fun Double.encode(): Json = JsNumber(this) +} + +fun Double.Companion.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().map { it.toDouble() }.toEither { NumberDecodingError(value) } +} + +fun Float.Companion.encoder() = object : Encoder { + override fun Float.encode(): Json = JsNumber(this) +} + +fun Float.Companion.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().map { it.toFloat() }.toEither { NumberDecodingError(value) } +} + +fun Long.Companion.encoder() = object : Encoder { + override fun Long.encode(): Json = JsNumber(this) +} + +fun Long.Companion.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().map { it.toLong() }.toEither { NumberDecodingError(value) } +} + fun Int.Companion.encoder() = object : Encoder { override fun Int.encode(): Json = JsNumber(this) } @@ -22,13 +43,31 @@ fun Int.Companion.decoder() = object : Decoder { value.asJsNumber().flatMap { it.toInt() }.toEither { NumberDecodingError(value) } } +fun Short.Companion.encoder() = object : Encoder { + override fun Short.encode(): Json = JsNumber(this) +} + +fun Short.Companion.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().flatMap { it.toShort() }.toEither { NumberDecodingError(value) } +} + +fun Byte.Companion.encoder() = object : Encoder { + override fun Byte.encode(): Json = JsNumber(this) +} + +fun Byte.Companion.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().flatMap { it.toByte() }.toEither { NumberDecodingError(value) } +} + fun Boolean.Companion.encoder() = object : Encoder { override fun Boolean.encode(): Json = JsBoolean(this) } fun Boolean.Companion.decoder() = object : Decoder { override fun decode(value: Json): Either = - value.asJsBoolean().flatMap { it.value.some() }.toEither { BooleanDecodingError(value) } + value.asJsBoolean().map { it.value }.toEither { BooleanDecodingError(value) } } fun String.Companion.encoder() = object : Encoder { @@ -37,127 +76,114 @@ fun String.Companion.encoder() = object : Encoder { fun String.Companion.decoder() = object : Decoder { override fun decode(value: Json): Either = - value.asJsString().flatMap { - it.value.toString().some() - }.toEither { StringDecodingError(value) } + value.asJsString().map { it.value.toString() }.toEither { StringDecodingError(value) } } @extension -interface OptionEncoderInstance : Encoder> { +interface PairEncoderInstance : Encoder> { fun encoderA(): Encoder + fun encoderB(): Encoder + + override fun Pair.encode(): Json = JsArray( + listOf( + encoderA().run { first.encode() }, + encoderB().run { second.encode() } + ) + ) - override fun Option.encode(): Json = - fold({ JsNull }, { encoderA().run { it.encode() } }) + companion object { + operator fun invoke(encoderA: Encoder, encoderB: Encoder): Encoder> = + object : PairEncoderInstance { + override fun encoderA(): Encoder = encoderA + override fun encoderB(): Encoder = encoderB + } + } } @extension -interface Tuple2EncoderInstance : Encoder> { +interface PairDecoderInstance : Decoder> { - fun encoderA(): Encoder + fun decoderA(): Decoder + fun decoderB(): Decoder - fun encoderB(): Encoder + override fun decode(value: Json): Either> { + val arr = value.asJsArray().toList().flatMap { it.value } + return if (arr.size == 2) + decoderA().decode(arr.first()).map2(decoderB().decode(arr.last())) { it.toPair() }.fix() + else ArrayDecodingError(value).left() + } - override fun Tuple2.encode(): Json = JsArray( - listOf( - encoderA().run { a.encode() }, - encoderB().run { b.encode() } - ) - ) + companion object { + operator fun invoke(decoderA: Decoder, decoderB: Decoder): Decoder> = + object : PairDecoderInstance { + override fun decoderA(): Decoder = decoderA + override fun decoderB(): Decoder = decoderB + } + } } @extension -interface Tuple3EncoderInstance : Encoder> { +interface TripleEncoderInstance : Encoder> { fun encoderA(): Encoder - fun encoderB(): Encoder - fun encoderC(): Encoder - override fun Tuple3.encode(): Json = JsArray( + override fun Triple.encode(): Json = JsArray( listOf( - encoderA().run { a.encode() }, - encoderB().run { b.encode() }, - encoderC().run { c.encode() } + encoderA().run { first.encode() }, + encoderB().run { second.encode() }, + encoderC().run { third.encode() } ) ) -} - -private inline val Json.isNull inline get() = this === JsNull - -@extension -interface JsObjectEqInstance : Eq { - override fun JsObject.eqv(b: JsObject): Boolean = with(Json.eq()) { - this@eqv.value.entries.zip(b.value.entries) { aa, bb -> - aa.key == bb.key && aa.value.eqv(bb.value) - }.forAll { it } + companion object { + operator fun invoke( + encoderA: Encoder, + encoderB: Encoder, + encoderC: Encoder + ): Encoder> = + object : TripleEncoderInstance { + override fun encoderA(): Encoder = encoderA + override fun encoderB(): Encoder = encoderB + override fun encoderC(): Encoder = encoderC + } } -} -@extension -interface JsArrayEqInstance : Eq { - override fun JsArray.eqv(b: JsArray): Boolean = with(Json.eq()) { - this@eqv.value.zip(b.value) { a, b -> a.eqv(b) } - .forAll { it } - } } @extension -interface JsonEqInstance : Eq { - override fun Json.eqv(b: Json): Boolean = when { - this is JsObject && b is JsObject -> JsObject.eq().run { this@eqv.eqv(b) } - this is JsString && b is JsString -> String.eq().run { - this@eqv.value.toString().eqv(b.value.toString()) - } - this is JsNumber && b is JsNumber -> JsNumber.eq().run { this@eqv.eqv(b) } - this is JsBoolean && b is JsBoolean -> Boolean.eq().run { this@eqv.value.eqv(b.value) } - this is JsArray && b is JsArray -> JsArray.eq().run { this@eqv.eqv(b) } - else -> this.isNull && b.isNull +interface TripleDecoderInstance : Decoder> { + + fun decoderA(): Decoder + fun decoderB(): Decoder + fun decoderC(): Decoder + + override fun decode(value: Json): Either> { + val arr = value.asJsArray().toList().flatMap { it.value } + return if (arr.size == 3) + Either.applicative().map( + decoderA().decode(arr[0]), + decoderB().decode(arr[1]), + decoderC().decode(arr[2]) + ) { (a, b, c) -> Triple(a, b, c) }.fix() + else ArrayDecodingError(value).left() } -} - -@extension -interface JsNumberEqInstance : Eq { - override fun JsNumber.eqv(b: JsNumber): Boolean = when (this) { - is JsDecimal -> when (b) { - is JsDecimal -> String.eq().run { this@eqv.value.eqv(b.value) } - is JsLong -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } - is JsDouble -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } - is JsFloat -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } - is JsInt -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } - } - is JsLong -> when (b) { - is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } - is JsLong -> Long.eq().run { this@eqv.value.eqv(b.value) } - is JsDouble -> Double.eq().run { this@eqv.value.toDouble().eqv(b.value) } - is JsFloat -> Float.eq().run { this@eqv.value.toFloat().eqv(b.value) } - is JsInt -> Long.eq().run { this@eqv.value.eqv(b.value.toLong()) } - } - is JsDouble -> when (b) { - is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } - is JsLong -> Double.eq().run { this@eqv.value.eqv(b.value.toDouble()) } - is JsDouble -> Double.eq().run { this@eqv.value.eqv(b.value) } - is JsFloat -> Double.eq().run { this@eqv.value.eqv(b.value.toDouble()) } - is JsInt -> Double.eq().run { this@eqv.value.eqv(b.value.toDouble()) } - } - is JsFloat -> when (b) { - is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } - is JsLong -> Float.eq().run { this@eqv.value.eqv(b.value.toFloat()) } - is JsDouble -> Double.eq().run { this@eqv.value.toDouble().eqv(b.value) } - is JsFloat -> Float.eq().run { this@eqv.value.eqv(b.value) } - is JsInt -> Float.eq().run { this@eqv.value.eqv(b.value.toFloat()) } - } - is JsInt -> when (b) { - is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } - is JsLong -> Long.eq().run { this@eqv.value.toLong().eqv(b.value) } - is JsDouble -> Double.eq().run { this@eqv.value.toDouble().eqv(b.value) } - is JsFloat -> Float.eq().run { this@eqv.value.toFloat().eqv(b.value) } - is JsInt -> Int.eq().run { this@eqv.value.eqv(b.value) } - } + companion object { + operator fun invoke( + decoderA: Decoder, + decoderB: Decoder, + decoderC: Decoder + ): Decoder> = + object : TripleDecoderInstance { + override fun decoderA(): Decoder = decoderA + override fun decoderB(): Decoder = decoderB + override fun decoderC(): Decoder = decoderC + } } + } \ No newline at end of file diff --git a/helios-core/src/main/kotlin/helios/instances/instancesArrow.kt b/helios-core/src/main/kotlin/helios/instances/instancesArrow.kt new file mode 100644 index 0000000..85f4d26 --- /dev/null +++ b/helios-core/src/main/kotlin/helios/instances/instancesArrow.kt @@ -0,0 +1,80 @@ +package helios.instances + +import arrow.core.* +import arrow.core.extensions.either.applicative.applicative +import arrow.core.extensions.either.applicative.map2 +import arrow.extension +import helios.core.* +import helios.typeclasses.* + +fun Option.Companion.encoder(encoderA: Encoder) = object : Encoder> { + override fun Option.encode(): Json = + fold({ JsNull }, { encoderA.run { it.encode() } }) +} + +fun Option.Companion.decoder(decoderA: Decoder) = object : Decoder> { + override fun decode(value: Json): Either> = + if (value.isNull) None.right() else decoderA.decode(value).map { Some(it) } +} + +fun Either.Companion.encoder(encoderA: Encoder, encoderB: Encoder) = object : Encoder> { + override fun Either.encode(): Json = + fold({ encoderA.run { it.encode() } }, + { encoderB.run { it.encode() } }) +} + +fun Either.Companion.decoder(decoderA: Decoder, decoderB: Decoder) = object : Decoder> { + override fun decode(value: Json): Either> = + decoderB.decode(value).fold({ decoderA.decode(value).map { it.left() } }, + { v -> v.right().map { it.right() } }) +} + +fun Tuple2.Companion.encoder(encoderA: Encoder, encoderB: Encoder) = object : Encoder> { + override fun Tuple2.encode(): Json = JsArray( + listOf( + encoderA.run { a.encode() }, + encoderB.run { b.encode() } + ) + ) +} + +fun Tuple2.Companion.decoder(decoderA: Decoder, decoderB: Decoder) = object : Decoder> { + override fun decode(value: Json): Either> { + val arr = value.asJsArray().toList().flatMap { it.value } + return if (arr.size == 2) + decoderA.decode(arr.first()).map2(decoderB.decode(arr.last())) { it }.fix() + else ArrayDecodingError(value).left() + } +} + +fun Tuple3.Companion.encoder( + encoderA: Encoder, + encoderB: Encoder, + encoderC: Encoder +) = object : Encoder> { + override fun Tuple3.encode(): Json = JsArray( + listOf( + encoderA.run { a.encode() }, + encoderB.run { b.encode() }, + encoderC.run { c.encode() } + ) + ) +} + +fun Tuple3.Companion.decoder( + decoderA: Decoder, + decoderB: Decoder, + decoderC: Decoder +) = object : Decoder> { + override fun decode(value: Json): Either> { + val arr = value.asJsArray().toList().flatMap { it.value } + return if (arr.size == 3) + Either.applicative().map( + decoderA.decode(arr[0]), + decoderB.decode(arr[1]), + decoderC.decode(arr[2]) + ) { it }.fix() + else ArrayDecodingError(value).left() + } +} + diff --git a/helios-core/src/main/kotlin/helios/instances/instancesEq.kt b/helios-core/src/main/kotlin/helios/instances/instancesEq.kt new file mode 100644 index 0000000..7278da6 --- /dev/null +++ b/helios-core/src/main/kotlin/helios/instances/instancesEq.kt @@ -0,0 +1,85 @@ +package helios.instances + +import arrow.core.extensions.eq +import arrow.data.extensions.list.foldable.forAll +import arrow.extension +import arrow.typeclasses.Eq +import helios.core.* +import helios.instances.jsarray.eq.eq +import helios.instances.jsnumber.eq.eq +import helios.instances.jsobject.eq.eq +import helios.instances.json.eq.eq + + +@extension +interface JsObjectEqInstance : Eq { + override fun JsObject.eqv(b: JsObject): Boolean = with(Json.eq()) { + this@eqv.value.entries.zip(b.value.entries) { aa, bb -> + aa.key == bb.key && aa.value.eqv(bb.value) + }.forAll { it } + } +} + +@extension +interface JsArrayEqInstance : Eq { + override fun JsArray.eqv(b: JsArray): Boolean = with(Json.eq()) { + this@eqv.value.zip(b.value) { a, b -> a.eqv(b) } + .forAll { it } + } +} + +@extension +interface JsonEqInstance : Eq { + override fun Json.eqv(b: Json): Boolean = when { + this is JsObject && b is JsObject -> JsObject.eq().run { this@eqv.eqv(b) } + this is JsString && b is JsString -> String.eq().run { + this@eqv.value.toString().eqv(b.value.toString()) + } + this is JsNumber && b is JsNumber -> JsNumber.eq().run { this@eqv.eqv(b) } + this is JsBoolean && b is JsBoolean -> Boolean.eq().run { this@eqv.value.eqv(b.value) } + this is JsArray && b is JsArray -> JsArray.eq().run { this@eqv.eqv(b) } + else -> this.isNull && b.isNull + } + +} + +@extension +interface JsNumberEqInstance : Eq { + override fun JsNumber.eqv(b: JsNumber): Boolean = when (this) { + is JsDecimal -> when (b) { + is JsDecimal -> String.eq().run { this@eqv.value.eqv(b.value) } + is JsLong -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } + is JsDouble -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } + is JsFloat -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } + is JsInt -> String.eq().run { this@eqv.value.eqv(b.value.toString()) } + } + is JsLong -> when (b) { + is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } + is JsLong -> Long.eq().run { this@eqv.value.eqv(b.value) } + is JsDouble -> Double.eq().run { this@eqv.value.toDouble().eqv(b.value) } + is JsFloat -> Float.eq().run { this@eqv.value.toFloat().eqv(b.value) } + is JsInt -> Long.eq().run { this@eqv.value.eqv(b.value.toLong()) } + } + is JsDouble -> when (b) { + is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } + is JsLong -> Double.eq().run { this@eqv.value.eqv(b.value.toDouble()) } + is JsDouble -> Double.eq().run { this@eqv.value.eqv(b.value) } + is JsFloat -> Double.eq().run { this@eqv.value.eqv(b.value.toDouble()) } + is JsInt -> Double.eq().run { this@eqv.value.eqv(b.value.toDouble()) } + } + is JsFloat -> when (b) { + is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } + is JsLong -> Float.eq().run { this@eqv.value.eqv(b.value.toFloat()) } + is JsDouble -> Double.eq().run { this@eqv.value.toDouble().eqv(b.value) } + is JsFloat -> Float.eq().run { this@eqv.value.eqv(b.value) } + is JsInt -> Float.eq().run { this@eqv.value.eqv(b.value.toFloat()) } + } + is JsInt -> when (b) { + is JsDecimal -> String.eq().run { this@eqv.value.toString().eqv(b.value) } + is JsLong -> Long.eq().run { this@eqv.value.toLong().eqv(b.value) } + is JsDouble -> Double.eq().run { this@eqv.value.toDouble().eqv(b.value) } + is JsFloat -> Float.eq().run { this@eqv.value.toFloat().eqv(b.value) } + is JsInt -> Int.eq().run { this@eqv.value.eqv(b.value) } + } + } +} \ No newline at end of file diff --git a/helios-core/src/main/kotlin/helios/instances/instancesJava.kt b/helios-core/src/main/kotlin/helios/instances/instancesJava.kt index a4f0037..b806528 100644 --- a/helios-core/src/main/kotlin/helios/instances/instancesJava.kt +++ b/helios-core/src/main/kotlin/helios/instances/instancesJava.kt @@ -1,16 +1,49 @@ package helios.instances -import arrow.core.Either +import arrow.core.* import arrow.core.extensions.either.applicative.applicative -import arrow.core.fix +import arrow.core.extensions.either.applicative.map2 +import arrow.core.extensions.either.monoid.monoid +import arrow.data.extensions.list.foldable.fold +import arrow.data.extensions.list.foldable.foldLeft import arrow.data.extensions.list.traverse.sequence import arrow.data.fix -import helios.core.JsArray -import helios.core.Json -import helios.typeclasses.Decoder -import helios.typeclasses.DecodingError -import helios.typeclasses.Encoder +import arrow.extension +import arrow.typeclasses.Monoid +import helios.core.* +import helios.typeclasses.* +import java.math.BigDecimal +import java.math.BigInteger +import java.util.* +fun UUID.encoder() = object : Encoder { + override fun UUID.encode(): Json = JsString(this.toString()) +} + +fun UUID.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsString().map { UUID.fromString(it.value.toString()) }.toEither { StringDecodingError(value) } +} + +fun BigDecimal.encoder() = object : Encoder { + override fun BigDecimal.encode(): Json = JsNumber(this) +} + +fun BigDecimal.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().map { it.toBigDecimal() }.toEither { NumberDecodingError(value) } +} + +fun BigInteger.encoder() = object : Encoder { + override fun BigInteger.encode(): Json = JsNumber(this) +} + +fun BigInteger.decoder() = object : Decoder { + override fun decode(value: Json): Either = + value.asJsNumber().map { it.toBigInteger() }.toEither { NumberDecodingError(value) } +} + +@extension interface ListEncoderInstance : Encoder> { fun encoderA(): Encoder @@ -27,7 +60,8 @@ interface ListEncoderInstance : Encoder> { } -interface ListDecoderInstance : Decoder> { +@extension +interface ListDecoderInstance : Decoder> { fun decoderA(): Decoder @@ -45,3 +79,51 @@ interface ListDecoderInstance : Decoder> { } } + +@extension +interface MapEncoderInstance : Encoder> { + + fun keyEncoderA(): KeyEncoder + fun encoderB(): Encoder + + override fun Map.encode(): Json = + JsObject(this.map { (key, value) -> (keyEncoderA().run { key.keyEncode() } to encoderB().run { value.encode() }) }.toMap()) + + companion object { + operator fun invoke(keyEncoderA: KeyEncoder, encoderB: Encoder): Encoder> = + object : MapEncoderInstance, Encoder> { + override fun keyEncoderA(): KeyEncoder = keyEncoderA + override fun encoderB(): Encoder = encoderB + } + } + +} + +@extension +interface MapDecoderInstance : Decoder> { + + fun keyDecoderA(): KeyDecoder + fun decoderB(): Decoder + + override fun decode(value: Json): Either> = + value.asJsObject().fold({ ObjectDecodingError(value).left() }, { obj -> + obj.value.map { (key, value) -> + val maybeKey: Either = + Json.parseFromString(key).mapLeft { StringDecodingError(value) }.flatMap { keyDecoderA().keyDecode(it) } + val maybeValue: Either = decoderB().decode(value) + maybeKey.map2(maybeValue) { mapOf(it.toPair()) } + } + .foldLeft>, Either>>(mapOf().right()) { acc, either -> + acc.map2(either) { it.a + it.b } + } + }) + + companion object { + operator fun invoke(keyDecoderA: KeyDecoder, decoderB: Decoder): Decoder> = + object : MapDecoderInstance { + override fun keyDecoderA(): KeyDecoder = keyDecoderA + override fun decoderB(): Decoder = decoderB + } + } + +} diff --git a/helios-core/src/main/kotlin/helios/instances/instancesKey.kt b/helios-core/src/main/kotlin/helios/instances/instancesKey.kt new file mode 100644 index 0000000..88470ea --- /dev/null +++ b/helios-core/src/main/kotlin/helios/instances/instancesKey.kt @@ -0,0 +1,110 @@ +package helios.instances + +import arrow.core.Either +import helios.core.DecodingError +import helios.core.Json +import helios.core.StringDecodingError +import helios.typeclasses.KeyDecoder +import helios.typeclasses.KeyEncoder +import java.math.BigDecimal +import java.math.BigInteger +import java.util.* + +fun Double.Companion.keyEncoder() = object : KeyEncoder { + override fun Double.keyEncode(): String = this.toString() +} + +fun Double.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString().toDouble() }.toEither { StringDecodingError(value) } +} + +fun Float.Companion.keyEncoder() = object : KeyEncoder { + override fun Float.keyEncode(): String = this.toString() +} + +fun Float.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString().toFloat() }.toEither { StringDecodingError(value) } +} + +fun Long.Companion.keyEncoder() = object : KeyEncoder { + override fun Long.keyEncode(): String = this.toString() +} + +fun Long.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString().toLong() }.toEither { StringDecodingError(value) } +} + +fun Int.Companion.keyEncoder() = object : KeyEncoder { + override fun Int.keyEncode(): String = this.toString() +} + +fun Int.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString().toInt() }.toEither { StringDecodingError(value) } +} + +fun Short.Companion.keyEncoder() = object : KeyEncoder { + override fun Short.keyEncode(): String = this.toString() +} + +fun Short.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { (it.value.toString().toShort()) }.toEither { StringDecodingError(value) } +} + +fun Byte.Companion.keyEncoder() = object : KeyEncoder { + override fun Byte.keyEncode(): String = this.toString() +} + +fun Byte.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { (it.value.toString().toByte()) }.toEither { StringDecodingError(value) } +} + +fun Boolean.Companion.keyEncoder() = object : KeyEncoder { + override fun Boolean.keyEncode(): String = this.toString() +} + +fun Boolean.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { (it.value.toString().toBoolean()) }.toEither { StringDecodingError(value) } +} + +fun String.Companion.keyEncoder() = object : KeyEncoder { + override fun String.keyEncode(): String = this +} + +fun String.Companion.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString() }.toEither { StringDecodingError(value) } +} + +fun UUID.keyEncoder() = object : KeyEncoder { + override fun UUID.keyEncode(): String = this.toString() +} + +fun UUID.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { UUID.fromString(it.value.toString()) }.toEither { StringDecodingError(value) } +} + +fun BigDecimal.keyEncoder() = object : KeyEncoder { + override fun BigDecimal.keyEncode(): String = this.toString() +} + +fun BigDecimal.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString().toBigDecimal() }.toEither { StringDecodingError(value) } +} + +fun BigInteger.keyEncoder() = object : KeyEncoder { + override fun BigInteger.keyEncode(): String = this.toString() +} + +fun BigInteger.keyDecoder() = object : KeyDecoder { + override fun keyDecode(value: Json): Either = + value.asJsString().map { it.value.toString().toBigInteger() }.toEither { StringDecodingError(value) } +} diff --git a/helios-core/src/main/kotlin/helios/syntax/json/syntax.kt b/helios-core/src/main/kotlin/helios/syntax/json/syntax.kt index 09b431b..667c4d7 100644 --- a/helios-core/src/main/kotlin/helios/syntax/json/syntax.kt +++ b/helios-core/src/main/kotlin/helios/syntax/json/syntax.kt @@ -1,6 +1,7 @@ package helios.syntax.json import helios.core.* +import java.math.BigDecimal import java.math.BigInteger fun String.toJson(): JsString = JsString(this) @@ -19,3 +20,6 @@ fun BigInteger.toJson(): JsDecimal = JsNumber.fromDecimalStringUnsafe(this.toStr fun BigInteger.toJson(radix: Int): JsDecimal = JsNumber.fromDecimalStringUnsafe(this.toString(radix)) + +fun BigDecimal.toJson(): JsDecimal = JsNumber.fromDecimalStringUnsafe(this.toString()) + diff --git a/helios-core/src/main/kotlin/helios/typeclasses/Decoder.kt b/helios-core/src/main/kotlin/helios/typeclasses/Decoder.kt index 610c357..89bdbe3 100644 --- a/helios-core/src/main/kotlin/helios/typeclasses/Decoder.kt +++ b/helios-core/src/main/kotlin/helios/typeclasses/Decoder.kt @@ -2,14 +2,9 @@ package helios.typeclasses import arrow.core.Either import arrow.higherkind +import helios.core.DecodingError import helios.core.Json -sealed class DecodingError -data class StringDecodingError(val value: Json) : DecodingError() -data class BooleanDecodingError(val value: Json) : DecodingError() -data class NumberDecodingError(val value: Json) : DecodingError() -data class KeyNotFound(val name: String) : DecodingError() - @higherkind interface Decoder { fun decode(value: Json): Either diff --git a/helios-core/src/main/kotlin/helios/typeclasses/Encoder.kt b/helios-core/src/main/kotlin/helios/typeclasses/Encoder.kt index 02be0b1..036c8ae 100644 --- a/helios-core/src/main/kotlin/helios/typeclasses/Encoder.kt +++ b/helios-core/src/main/kotlin/helios/typeclasses/Encoder.kt @@ -5,5 +5,5 @@ import helios.core.Json interface Encoder { fun A.encode(): Json - fun Collection.toJson(): JsArray = JsArray(map { it.encode() }) -} \ No newline at end of file + fun Collection.encode(): JsArray = JsArray(map { it.encode() }) +} diff --git a/helios-core/src/main/kotlin/helios/typeclasses/KeyDecoder.kt b/helios-core/src/main/kotlin/helios/typeclasses/KeyDecoder.kt new file mode 100644 index 0000000..88af93f --- /dev/null +++ b/helios-core/src/main/kotlin/helios/typeclasses/KeyDecoder.kt @@ -0,0 +1,11 @@ +package helios.typeclasses + +import arrow.core.Either +import arrow.higherkind +import helios.core.DecodingError +import helios.core.Json + +@higherkind +interface KeyDecoder { + fun keyDecode(value: Json): Either +} \ No newline at end of file diff --git a/helios-core/src/main/kotlin/helios/typeclasses/KeyEncoder.kt b/helios-core/src/main/kotlin/helios/typeclasses/KeyEncoder.kt new file mode 100644 index 0000000..c118e88 --- /dev/null +++ b/helios-core/src/main/kotlin/helios/typeclasses/KeyEncoder.kt @@ -0,0 +1,5 @@ +package helios.typeclasses + +interface KeyEncoder { + fun A.keyEncode(): String +} \ No newline at end of file diff --git a/helios-meta-compiler/build.gradle b/helios-meta-compiler/build.gradle index 4741b4a..e6ad369 100644 --- a/helios-meta-compiler/build.gradle +++ b/helios-meta-compiler/build.gradle @@ -16,4 +16,5 @@ dependencies { testCompile "com.google.testing.compile:compile-testing:$googleTestingVersion" testCompile fileTree(dir: './src/test/libs', includes: ['*.jar']) testCompile files(Jvm.current().getToolsJar()) + compile "org.jetbrains.kotlin:kotlin-script-runtime:1.3.31" } diff --git a/helios-meta-compiler/src/main/kotlin/helios/meta/compiler/json/JsonFileGenerator.kt b/helios-meta-compiler/src/main/kotlin/helios/meta/compiler/json/JsonFileGenerator.kt index f287b90..02b81a9 100644 --- a/helios-meta-compiler/src/main/kotlin/helios/meta/compiler/json/JsonFileGenerator.kt +++ b/helios-meta-compiler/src/main/kotlin/helios/meta/compiler/json/JsonFileGenerator.kt @@ -61,28 +61,60 @@ class JsonFileGenerator( } } - //TODO FIXME - inline val String.encoder: String - get() = when { - this == "Boolean" -> "Boolean.Companion.encoder()" - this.startsWith("kotlin.collections.List") -> - "${Regex("kotlin.collections.List<(.*)>$").matchEntire(this)!!.groupValues[1]}.encoder()" + private inline val String.isComplex get() = this.contains('<') + private inline val String.notClosed get() = !(this.contains('<') && this.contains('>')) + + private inline val String.getTypeParameters + get() = { + val inside = this.substringAfter('<').substringBeforeLast('>') + inside.split(',').map { it.trim() }.fold(emptyList()) { acc: List, str: String -> + val maybeLast = acc.lastOrNull() + if (maybeLast != null && maybeLast.isComplex && maybeLast.notClosed) + acc.subList(0, acc.size - 1) + "$maybeLast, $str" + else acc + str + } + }() + + private fun String.complexEncoder(pre: String) = getTypeParameters.joinToString( + prefix = "$pre(", + postfix = ")" + ) { it.encoder() } + + private fun String.keyEncoder(): String = "$this.keyEncoder()" + + private fun String.encoder(): String = + when { + this.startsWith("kotlin.collections.List") -> complexEncoder("ListEncoderInstance") + this.startsWith("kotlin.collections.Map") -> + "MapEncoderInstance<${getTypeParameters.joinToString()}>(${getTypeParameters.first().keyEncoder()}, ${getTypeParameters.last().encoder()})" + this.contains('<') -> complexEncoder("${substringBefore('<')}.Companion.encoder") + this.contains('?') -> "arrow.core.Option<${substringBefore('?')}>".encoder() else -> "$this.encoder()" } private fun jsonProperties(je: JsonElement): String = je.pairs.joinToString(",", "JsObject(mapOf(", "))") { (p, r) -> """| - |"$p" to ${r.encoder}.run { $p.toJson() } + |"$p" to ${r.encoder()}.run { $p.encode() } |""".trimMargin() } - //TODO FIXME - inline val String.decoder: String - get() = when { - this == "Boolean" -> "Boolean.Companion.decoder()" + private fun String.complexDecoder(pre: String) = getTypeParameters.joinToString( + prefix = "$pre(", + postfix = ")" + ) { it.decoder() } + + private fun String.keyDecoder(): String = "$this.keyDecoder()" + + private fun String.decoder(): String = + when { this.startsWith("kotlin.collections.List") -> - "ListDecoderInstance(${Regex("kotlin.collections.List<(.*)>$").matchEntire(this)!!.groupValues[1]}.decoder())" + complexDecoder("ListDecoderInstance") + this.startsWith("kotlin.collections.Map") -> + "MapDecoderInstance<${getTypeParameters.joinToString()}>(${getTypeParameters.first().keyDecoder()}, ${getTypeParameters.last().decoder()})" + this.contains('<') -> + complexDecoder("${substringBefore('<')}.Companion.decoder") + this.contains('?') -> "arrow.core.Option<${substringBefore('?')}>".decoder() else -> "$this.decoder()" } @@ -90,7 +122,7 @@ class JsonFileGenerator( prefix = "\n\t", separator = ",\n\t", postfix = "\n" - ) { (p, t) -> "value[\"$p\"].fold({Either.Left(KeyNotFound(\"$p\"))}, { ${t.decoder}.run { decode(it) } })" } + ) { (p, t) -> "value[\"$p\"].fold({Either.Left(KeyNotFound(\"$p\"))}, { ${t.decoder()}.run { decode(it) } })" } private fun map(je: JsonElement): String = if (je.pairs.size == 1) "${parse(je)}.map(" else "Either.applicative().map(${parse(je)}, " diff --git a/helios-optics/src/main/kotlin/helios/optics/optics.kt b/helios-optics/src/main/kotlin/helios/optics/optics.kt index 49bf438..c5787e1 100644 --- a/helios-optics/src/main/kotlin/helios/optics/optics.kt +++ b/helios-optics/src/main/kotlin/helios/optics/optics.kt @@ -77,7 +77,7 @@ inline val JsDouble.Companion.value: Iso inline get() = doubleValue @PublishedApi -internal val float = Prism.fromOption({ JsFloat(it.toDouble().toFloat()).some() }, { identity(it) }) +internal val float = Prism.fromOption({ JsFloat(it.toFloat()).some() }, { identity(it) }) inline val JsNumber.Companion.jsFloat: Prism inline get() = float @@ -86,6 +86,16 @@ internal val floatValue = Iso(JsFloat::value, ::JsFloat) inline val JsFloat.Companion.value: Iso inline get() = floatValue +@PublishedApi +internal val long = Prism.fromOption({ JsLong(it.toLong()).some() }, { identity(it) }) +inline val JsNumber.Companion.jsLong: Prism + inline get() = long + +@PublishedApi +internal val longValue = Iso(JsLong::value, ::JsLong) +inline val JsLong.Companion.value: Iso + inline get() = longValue + @PublishedApi internal val int = Prism.fromOption({ it.toInt().map(::JsInt) }, { identity(it) }) inline val JsNumber.Companion.jsInt: Prism @@ -96,16 +106,6 @@ internal val intValue = Iso(JsInt::value, ::JsInt) inline val JsInt.Companion.value: Iso inline get() = intValue -@PublishedApi -internal val long = Prism.fromOption({ it.toLong().map(::JsLong) }, { identity(it) }) -inline val JsNumber.Companion.jsLong: Prism - inline get() = long - -@PublishedApi -internal val longValue = Iso(JsLong::value, ::JsLong) -inline val JsLong.Companion.value: Iso - inline get() = longValue - @PublishedApi internal val decimal = Prism.fromOption({ (it as? JsDecimal).toOption() }, { identity(it) }) inline val JsNumber.Companion.jsDecimal: Prism diff --git a/helios-optics/src/test/kotlin/helios/optics/instances.kt b/helios-optics/src/test/kotlin/helios/optics/instances.kt index 2965fa6..9017778 100644 --- a/helios-optics/src/test/kotlin/helios/optics/instances.kt +++ b/helios-optics/src/test/kotlin/helios/optics/instances.kt @@ -2,6 +2,7 @@ package helios.optics import arrow.core.None import arrow.core.Option +import arrow.core.extensions.option.applicative.applicative import arrow.core.extensions.option.eq.eq import arrow.core.orElse import helios.test.generators.alphaStr @@ -84,7 +85,7 @@ class InstancesTest : UnitSpec() { EQA = JsObject.eq(), EQB = Option.eq(Json.eq()), MB = object : Monoid> { - override fun Option.combine(b: Option) = orElse { b } + override fun Option.combine(b: Option) = flatMap { a -> b.map { a.merge(it) } } override fun empty(): Option = None } )) diff --git a/helios-sample/src/main/kotlin/helios/sample/domain.kt b/helios-sample/src/main/kotlin/helios/sample/domain.kt index 4325340..655819a 100644 --- a/helios-sample/src/main/kotlin/helios/sample/domain.kt +++ b/helios-sample/src/main/kotlin/helios/sample/domain.kt @@ -1,15 +1,17 @@ package helios.sample +import arrow.core.Either +import arrow.core.Option import arrow.optics.optics import helios.meta.json @json -data class Company(val name: String, val address: Address, val employees: List) { +data class Company(val name: String, val address: Address, val employees: List, val private: Boolean) { companion object } @json -data class Address(val city: String, val street: Street) { +data class Address(val city: String, val street: Street, val number: Option) { companion object } @@ -18,8 +20,19 @@ data class Street(val number: Int, val name: String) { companion object } +typealias MomSide = String +typealias DadSide = String + @json -data class Employee(val name: String, val lastName: String) { +data class Child(val name: String, val age: Int, val family: Map>){ + companion object +} + +typealias Wife = String +typealias Husband = String + +@json +data class Employee(val name: String, val lastName: String, val married: Either, val childs: Option>) { companion object } @@ -27,4 +40,4 @@ data class Employee(val name: String, val lastName: String) { @json data class Sibling(val first_name: String, val age: Int) { companion object -} \ No newline at end of file +} diff --git a/helios-sample/src/main/kotlin/helios/sample/sample.kt b/helios-sample/src/main/kotlin/helios/sample/sample.kt index 50e8fbd..168d131 100644 --- a/helios-sample/src/main/kotlin/helios/sample/sample.kt +++ b/helios-sample/src/main/kotlin/helios/sample/sample.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.getOrHandle import helios.core.* import helios.optics.* -import helios.typeclasses.DecodingError object Sample { const val companyJsonString = """ @@ -32,7 +31,7 @@ object Sample { @JvmStatic fun main(args: Array) { - val companyJson = + val companyJson: Json = Json.parseFromString(companyJsonString).getOrHandle { println("Failed creating the Json ${it.localizedMessage}, creating an empty one") JsString("")