Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move core to be mpp #20

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
88559c0
progress
i-walker Oct 24, 2021
b8e1bac
progress
i-walker Nov 1, 2021
b790c85
Merge remote-tracking branch 'origin/main' into mpp-core
i-walker Nov 3, 2021
be040bb
progress
i-walker Nov 3, 2021
6115b5f
progress
i-walker Nov 3, 2021
8484f2b
progress
i-walker Nov 3, 2021
e462f9f
fixed core mpp migration, except Uri
i-walker Nov 3, 2021
f7f7adb
further migrations
i-walker Jan 22, 2022
a121c0b
progress
i-walker Jan 24, 2022
cc72b47
clena up
i-walker Jan 24, 2022
5e8f52a
progress
i-walker Jan 24, 2022
23dedbf
progress
i-walker Jan 24, 2022
8445222
fixed core mpp migration, except Uri
i-walker Jan 24, 2022
e336eec
further migrations
i-walker Jan 24, 2022
9781ec0
Merge remote-tracking branch 'origin/mpp-core' into mpp-core
i-walker Jan 24, 2022
5de3b2a
Merge remote-tracking branch 'origin/main' into mpp-core
i-walker Jan 24, 2022
7d1f60b
remove ByteBuffer and inputstream references
i-walker Jan 24, 2022
7fdc953
clean up
i-walker Jan 24, 2022
f7c9c50
clean up
i-walker Jan 24, 2022
221759f
add Optin
i-walker Jan 24, 2022
e3bee82
add JvmInline
i-walker Jan 24, 2022
64cb6a3
clean up
i-walker Jan 24, 2022
aa20dc2
clean up
i-walker Jan 24, 2022
a81f90e
clean up
i-walker Jan 24, 2022
07bf507
add yarn lock
i-walker Jan 24, 2022
aebce42
small progress in uri parse
i-walker Jan 24, 2022
f43ef59
move to stdlibcommon
i-walker Jan 24, 2022
982a535
move to stdlibcommon
i-walker Jan 24, 2022
5b3bfa2
add mpp decoder
i-walker Jan 25, 2022
a78bd19
reorg and remove companion object of Uri
i-walker Jan 26, 2022
bc2ba9f
implement new parser, still need to authority
i-walker Jan 27, 2022
e343095
fix remaining errors
i-walker Jan 27, 2022
b9a2e0b
Merge remote-tracking branch 'origin/main' into mpp-core
i-walker Jan 27, 2022
7c5c3f4
mask testts
i-walker Jan 27, 2022
c7b7722
lowercase host nd unmask all tests
i-walker Jan 27, 2022
e4fbc9d
rm kotest dependency
i-walker Jan 27, 2022
c5dae11
implement js part
i-walker Jan 28, 2022
dd31440
clean up and add js related git ignore
i-walker Jan 28, 2022
564ec74
resolve missing dependiences, also
i-walker Jan 28, 2022
b618978
resolve Schema tests to be jvm only and compile and run jsTest
i-walker Jan 28, 2022
ca63cf2
fix js tests, besides a few encoding tests and change signature of Ur…
i-walker Jan 28, 2022
0a76fb3
make UrlencodedData mpp and simplify encodeQuery from Uricompatibily
i-walker Jan 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(kotlin("stdlib", Version.kotlin))
implementation(Libs.kotlinxCoroutines)
implementation(Libs.ktorio)
}
}

jvmMain {
dependencies {
// Needed for Uri MatchNamedGroupCollection, ties us to JDK8
// TODO https://app.clickup.com/t/kt7qd2
implementation(kotlin("stdlib-jdk8", Version.kotlin))
implementation(Libs.kotlinxCoroutines)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import arrow.endpoint.model.Header
import arrow.endpoint.model.Method
import arrow.endpoint.model.QueryParams
import arrow.endpoint.model.StatusCode
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import io.ktor.utils.io.ByteChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.charsets.Charset
import io.ktor.utils.io.charsets.Charsets

// Turn into top-level functions?
public object ArrowEndpoint {

public inline operator fun <A> invoke(f: ArrowEndpoint.() -> A): A = f(ArrowEndpoint)

@JvmName("queryList")
//@JvmName("queryList")
public fun <A> query(name: String, codec: Codec<List<String>, A, CodecFormat.TextPlain>): EndpointInput.Query<A> =
EndpointInput.Query(name, codec, EndpointIO.Info.empty())

Expand Down Expand Up @@ -60,26 +60,26 @@ public object ArrowEndpoint {
public fun stringBody(charset: String): EndpointIO.StringBody<String> =
stringBody(Charset.forName(charset))

public fun stringBody(charset: Charset = StandardCharsets.UTF_8): EndpointIO.StringBody<String> =
public fun stringBody(charset: Charset = Charsets.UTF_8): EndpointIO.StringBody<String> =
EndpointIO.StringBody(charset, Codec.string, EndpointIO.Info.empty())

public val htmlBodyUtf8: EndpointIO.StringBody<String> =
EndpointIO.StringBody(
StandardCharsets.UTF_8,
Charsets.UTF_8,
Codec.string.format(CodecFormat.TextHtml),
EndpointIO.Info.empty()
)

public fun <A> plainBody(
codec: PlainCodec<A>,
charset: Charset = StandardCharsets.UTF_8
charset: Charset = Charsets.UTF_8
): EndpointIO.StringBody<A> =
EndpointIO.StringBody(charset, codec, EndpointIO.Info.empty())

/** A body in any format, read using the given `codec`, from a raw string read using `charset`.*/
public fun <A, CF : CodecFormat> anyFromStringBody(
codec: Codec<String, A, CF>,
charset: Charset = StandardCharsets.UTF_8
charset: Charset = Charsets.UTF_8
): EndpointIO.StringBody<A> =
EndpointIO.StringBody(charset, codec, EndpointIO.Info.empty())

Expand All @@ -100,10 +100,10 @@ public object ArrowEndpoint {
public fun byteArrayBody(): EndpointIO.ByteArrayBody<ByteArray> =
EndpointIO.ByteArrayBody(Codec.byteArray, EndpointIO.Info.empty())

public fun byteBufferBody(): EndpointIO.ByteBufferBody<ByteBuffer> =
public fun byteBufferBody(): EndpointIO.ByteBufferBody<ByteChannel> =
EndpointIO.ByteBufferBody(Codec.byteBuffer, EndpointIO.Info.empty())

public fun inputStreamBody(): EndpointIO.InputStreamBody<InputStream> =
public fun inputStreamBody(): EndpointIO.InputStreamBody<ByteReadChannel> =
EndpointIO.InputStreamBody(Codec.inputStream, EndpointIO.Info.empty())

public fun <A> formBody(codec: Codec<String, A, CodecFormat.XWwwFormUrlencoded>): EndpointIO.StringBody<A> =
Expand Down
282 changes: 282 additions & 0 deletions core/src/commonMain/kotlin/arrow/endpoint/Codec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
@file:Suppress("MemberVisibilityCanBePrivate")

package arrow.endpoint

import arrow.core.Either
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.andThen
import arrow.endpoint.model.CodecFormat
import arrow.endpoint.model.Cookie
import arrow.endpoint.model.Uri
import arrow.endpoint.model.UriError

public typealias PlainCodec<A> = Codec<String, A, CodecFormat.TextPlain>
public typealias JsonCodec<A> = Codec<String, A, CodecFormat.Json>
public typealias XmlCodec<A> = Codec<String, A, CodecFormat.Xml>

public interface Codec<L, H, out CF : CodecFormat> : Mapping<L, H> {
public fun schema(): Schema<H>
public val format: CF

public fun <HH> map(codec: Codec<H, HH, @UnsafeVariance CF>): Codec<L, HH, CF> =
object : Codec<L, HH, CF> {
override fun rawDecode(l: L): DecodeResult<HH> =
this@Codec.rawDecode(l).flatMap(codec::rawDecode)

override fun encode(h: HH): L =
this@Codec.encode(codec.encode(h))

override val format: CF = this@Codec.format

override fun schema(): Schema<HH> =
codec.schema()
}

override fun <HH> map(codec: Mapping<H, HH>): Codec<L, HH, CF> =
object : Codec<L, HH, CF> {
override fun rawDecode(l: L): DecodeResult<HH> =
this@Codec.rawDecode(l).flatMap(codec::rawDecode)

override fun encode(h: HH): L =
this@Codec.encode(codec.encode(h))

override val format: CF = this@Codec.format

override fun schema(): Schema<HH> =
this@Codec.schema()
.map { v ->
when (val res = codec.decode(v)) {
is DecodeResult.Failure -> null
is DecodeResult.Value -> res.value
}
}
}

public fun <HH> mapDecode(rawDecode: (H) -> DecodeResult<HH>, encode: (HH) -> H): Codec<L, HH, CF> =
map(Mapping.fromDecode(rawDecode, encode))

public fun <HH> map(f: (H) -> HH, g: (HH) -> H): Codec<L, HH, CF> =
mapDecode(f.andThen { DecodeResult.Value(it) }, g)

public fun schema(s2: Schema<H>?): Codec<L, H, CF> =
s2?.let {
object : Codec<L, H, CF> {
override fun rawDecode(l: L): DecodeResult<H> = this@Codec.decode(l)
override fun encode(h: H): L = this@Codec.encode(h)
override fun schema(): Schema<H> = s2
override val format: CF = this@Codec.format
}
} ?: this@Codec

public fun modifySchema(modify: (Schema<H>) -> Schema<H>): Codec<L, H, CF> =
schema(modify(schema()))

public fun <CF2 : CodecFormat> format(f: CF2): Codec<L, H, CF2> =
object : Codec<L, H, CF2> {
override fun rawDecode(l: L): DecodeResult<H> = this@Codec.decode(l)
override fun encode(h: H): L = this@Codec.encode(h)
override fun schema(): Schema<H> = this@Codec.schema()
override val format: CF2 = f
}

override fun decode(l: L): DecodeResult<H> {
val res = super.decode(l)
val default = schema().info.default
return when {
res is DecodeResult.Failure.Missing && default != null ->
DecodeResult.Value(default.first)
else -> res
}
}

public companion object {
public fun <L, CF : CodecFormat> id(f: CF, s: Schema<L>): Codec<L, L, CF> =
object : Codec<L, L, CF> {
override fun rawDecode(l: L): DecodeResult<L> = DecodeResult.Value(l)
override fun encode(h: L): L = h
override fun schema(): Schema<L> = s
override val format: CF = f
}

public fun <L> idPlain(s: Schema<L> = Schema.string()): Codec<L, L, CodecFormat.TextPlain> =
id(CodecFormat.TextPlain, s)

public fun <T> stringCodec(schema: Schema<T>, parse: (String) -> T): Codec<String, T, CodecFormat.TextPlain> =
string.map(parse) { it.toString() }.schema(schema)

public val string: Codec<String, String, CodecFormat.TextPlain> =
id(CodecFormat.TextPlain, Schema.string)

public val byte: Codec<String, Byte, CodecFormat.TextPlain> = stringCodec(Schema.byte) { it.toByte() }
public val short: Codec<String, Short, CodecFormat.TextPlain> = stringCodec(Schema.short) { it.toShort() }
public val int: Codec<String, Int, CodecFormat.TextPlain> = stringCodec(Schema.int) { it.toInt() }
public val long: Codec<String, Long, CodecFormat.TextPlain> = stringCodec(Schema.long) { it.toLong() }
public val float: Codec<String, Float, CodecFormat.TextPlain> = stringCodec(Schema.float) { it.toFloat() }
public val double: Codec<String, Double, CodecFormat.TextPlain> = stringCodec(Schema.double) { it.toDouble() }
public val boolean: Codec<String, Boolean, CodecFormat.TextPlain> = stringCodec(Schema.boolean) { it.toBoolean() }

public val uri: PlainCodec<Uri> =
string.mapDecode(
{ raw ->
Uri.parse(raw).fold(
{ _: UriError -> DecodeResult.Failure.Error(raw, IllegalArgumentException(this.toString())) },
{ DecodeResult.Value(it) }
)
},
Uri::toString
)

public val byteArray: Codec<ByteArray, ByteArray, CodecFormat.OctetStream> =
id(CodecFormat.OctetStream, Schema.byteArray)

private fun <A, B, CF : CodecFormat> listBinarySchema(c: Codec<A, B, CF>): Codec<List<A>, List<B>, CF> =
id(c.format, Schema.binary<List<A>>())
.mapDecode({ aas -> aas.traverseDecodeResult(c::decode) }) { bbs -> bbs.map(c::encode) }

/**
* Create a codec which requires that a list of low-level values contains a single element. Otherwise a decode
* failure is returned. The given base codec `c` is used for decoding/encoding.
*
* The schema and validator are copied from the base codec.
*/
public fun <A, B, CF : CodecFormat> listFirst(c: Codec<A, B, CF>): Codec<List<A>, B, CF> =
listBinarySchema(c)
.mapDecode({ list ->
when (list.size) {
0 -> DecodeResult.Failure.Missing
1 -> DecodeResult.Value(list[0])
else -> DecodeResult.Failure.Multiple(list)
}
}) {
listOf(it)
}
.schema(c.schema())

/**
* Create a codec which requires that a list of low-level values contains a single element. Otherwise a decode
* failure is returned. The given base codec `c` is used for decoding/encoding.
*
* The schema and validator are copied from the base codec.
*/
public fun <A, B, CF : CodecFormat> listFirstOrNull(c: Codec<A, B, CF>): Codec<List<A>, B?, CF> =
listBinarySchema(c)
.mapDecode({ list ->
when (list.size) {
0 -> DecodeResult.Value(null)
1 -> DecodeResult.Value(list[0])
else -> DecodeResult.Failure.Multiple(list)
}
}) { listOfNotNull(it) }
.schema(c.schema().asNullable())

/**
* Create a codec which requires that a nullable low-level representation contains a single element.
* Otherwise a decode failure is returned. The given base codec `c` is used for decoding/encoding.
*
* The schema and validator are copied from the base codec.
*/
public fun <A, B, CF : CodecFormat> nullableFirst(c: Codec<A, B, CF>): Codec<A?, B, CF> =
id(c.format, Schema.binary<A?>())
.mapDecode({ option ->
when (option) {
null -> DecodeResult.Failure.Missing
else -> c.decode(option)
}
}) { us -> us?.let(c::encode) }
.schema(c.schema())

/**
* Create a codec which decodes/encodes a list of low-level values to a list of high-level values, using the given base codec `c`.
*
* The schema is copied from the base codec.
*/
public fun <A, B, CF : CodecFormat> list(c: Codec<A, B, CF>): Codec<List<A>, List<B>, CF> =
listBinarySchema(c).schema(c.schema().asList())

/**
* Create a codec which decodes/encodes an optional low-level value to an optional high-level value.
* The given base codec `c` is used for decoding/encoding.
*
* The schema and validator are copied from the base codec.
*/
public fun <A, B, CF : CodecFormat> option(c: Codec<A, B, CF>): Codec<Option<A>, Option<B>, CF> =
id(c.format, Schema.binary<Option<A>>())
.mapDecode({ option ->
when (option) {
None -> DecodeResult.Value(None)
is Some -> c.decode(option.value).map(::Some)
}
}) { us -> us.map(c::encode) }
.schema(c.schema().asOption())

/**
* Create a codec which decodes/encodes an nullable low-level value to an optional high-level value.
* The given base codec `c` is used for decoding/encoding.
*
* The schema and validator are copied from the base codec.
*/
public fun <A : Any, B : Any, CF : CodecFormat> nullable(c: Codec<A, B, CF>): Codec<A?, B?, CF> =
id(c.format, Schema.binary<A?>())
.mapDecode({ option ->
when (option) {
null -> DecodeResult.Value(null)
else -> c.decode(option)
}
}) { us -> us?.let(c::encode) }
.schema(c.schema().asNullable())

public fun <A> json(
schema: Schema<A>,
_rawDecode: (String) -> DecodeResult<A>,
_encode: (A) -> String
): JsonCodec<A> =
anyStringCodec(schema, CodecFormat.Json, _rawDecode, _encode)

public fun <A> xml(schema: Schema<A>, rawDecode: (String) -> DecodeResult<A>, encode: (A) -> String): XmlCodec<A> =
anyStringCodec(schema, CodecFormat.Xml, rawDecode, encode)

private fun decodeCookie(cookie: String): DecodeResult<List<Cookie>> =
when (val res = Cookie.parse(cookie)) {
is Either.Left -> DecodeResult.Failure.Error(cookie, RuntimeException(res.value))
is Either.Right -> DecodeResult.Value(res.value)
}

public val cookieCodec: Codec<String, List<Cookie>, CodecFormat.TextPlain> =
string.mapDecode(::decodeCookie) { cs -> cs.joinToString("; ") }

public val cookiesCodec: Codec<List<String>, List<Cookie>, CodecFormat.TextPlain> =
list(cookieCodec).map(List<List<Cookie>>::flatten) { listOf(it) }

public fun <L, H, CF : CodecFormat> fromDecodeAndMeta(
schema: Schema<H>,
cf: CF,
f: (L) -> DecodeResult<H>,
g: (H) -> L
): Codec<L, H, CF> =
object : Codec<L, H, CF> {
override fun rawDecode(l: L): DecodeResult<H> = f(l)
override fun encode(h: H): L = g(h)
override fun schema(): Schema<H> = schema
override val format: CF = cf
}

public fun <A, CF : CodecFormat> anyStringCodec(
schema: Schema<A>,
cf: CF,
rawDecode: (String) -> DecodeResult<A>,
encode: (A) -> String
): Codec<String, A, CF> =
fromDecodeAndMeta(
schema,
cf,
{ s: String ->
val toDecode = if (schema.isOptional() && s == "") "null" else s
rawDecode(toDecode)
}
) { t ->
if (schema.isOptional() && (t == null || t == None)) "" else encode(t)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import arrow.core.Tuple4
import arrow.core.Tuple5
import arrow.endpoint.model.StatusCode
import arrow.endpoint.server.ServerEndpoint
import kotlin.jvm.JvmName

/**
* An `Endpoint<Input, Error, Output>` for shape `suspend (Input) -> Either<Error, Output>` defines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import arrow.core.Tuple5
import arrow.core.Tuple6
import arrow.endpoint.model.CodecFormat
import arrow.endpoint.model.Method
import kotlin.jvm.JvmName
import arrow.endpoint.model.QueryParams as MQueryParams

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package arrow.endpoint
import arrow.core.Tuple4
import arrow.core.Tuple5
import arrow.endpoint.model.CodecFormat
import kotlin.jvm.JvmName
import arrow.endpoint.model.StatusCode as MStatusCode

// Elements that can occur as Output
Expand Down