Initial version (Server, Compose clients work)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
@file:OptIn(ExperimentalWasmDsl::class)
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.serialization)
|
||||
}
|
||||
|
||||
val partialsDirectory = layout.buildDirectory.dir("generated/sources/partials")
|
||||
val versionDirectory = layout.buildDirectory.dir("generated/sources/version")
|
||||
val requestsDirectory = layout.projectDirectory.dir("src/commonMain/kotlin/com/jaytux/phoebench/common/")
|
||||
|
||||
val generatePartials = tasks.register<Exec>("generatePartials") {
|
||||
group = "generation"
|
||||
description = "Generate Partial classes (requests with all-nullable fields)"
|
||||
val scriptFile = project.file("partialize.main.kts")
|
||||
val targets = fileTree(requestsDirectory) {
|
||||
include("**/*.kt")
|
||||
}
|
||||
val lst = targets.map { it.absolutePath }
|
||||
|
||||
inputs.file(scriptFile)
|
||||
inputs.files(targets)
|
||||
outputs.dir(partialsDirectory)
|
||||
executable = "kotlin"
|
||||
doFirst {
|
||||
args(scriptFile.absolutePath, partialsDirectory.get().asFile.absolutePath, *lst.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
val generateVersion = tasks.register<Task>("protocolVersion") {
|
||||
doFirst {
|
||||
val outFile = versionDirectory.get().file("com/jaytux/phoebench/common/Version.kt").asFile
|
||||
outFile.parentFile.mkdirs()
|
||||
outFile.writeText("""
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
object ProtocolVersion {
|
||||
const val VERSION="${rootProject.version}"
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm("desktop")
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
kotlin {
|
||||
srcDir(generatePartials)
|
||||
srcDir(versionDirectory)
|
||||
}
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.auth)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targets.all {
|
||||
compilations.all {
|
||||
compileTaskProvider.configure {
|
||||
dependsOn(generatePartials)
|
||||
dependsOn(generateVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xcontext-parameters")
|
||||
optIn.add("kotlin.uuid.ExperimentalUuidApi")
|
||||
}
|
||||
}
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env kotlin
|
||||
|
||||
@file:DependsOn("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.21")
|
||||
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment
|
||||
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
|
||||
import org.jetbrains.kotlin.parsing.KotlinParserDefinition
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
fun process(file: Path, outputDir: Path, factory: KtPsiFactory) {
|
||||
if(!file.exists()) {
|
||||
System.err.println("Skipping $file: file does not exist")
|
||||
return
|
||||
}
|
||||
if(!file.isReadable()) {
|
||||
System.err.println("Skipping $file: file is not readable")
|
||||
return
|
||||
}
|
||||
|
||||
val ktFile = factory.createFile(file.readText())
|
||||
val pkg = ktFile.packageFqName.toString()
|
||||
val imports = ktFile.importDirectives.mapNotNull { it.importedFqName }.filter { !it.toString().contains("ToPartialize") }
|
||||
val classes = ktFile.declarations.mapNotNull { cls ->
|
||||
if(cls !is KtClass || !cls.isData()) return@mapNotNull null
|
||||
val annotations = cls.annotationEntries.map { it.text }
|
||||
if("@ToPartialize" !in annotations) return@mapNotNull null
|
||||
val ogName = cls.name ?: return@mapNotNull null
|
||||
|
||||
val newName = "Partial$ogName"
|
||||
|
||||
val nestedTypes = cls.declarations.filterIsInstance<KtClass>().mapNotNull { it.name }.toSet()
|
||||
|
||||
val props = cls.primaryConstructorParameters.map {
|
||||
val name = it.name
|
||||
val type = it.typeReference?.text
|
||||
if(name == null || type == null) return@mapNotNull null
|
||||
name to type
|
||||
}.joinToString(", ") { (n, t) ->
|
||||
val nnT = if(t.endsWith('?')) t.substring(startIndex = 0, endIndex = t.length - 1) else t
|
||||
val useT = if(nnT in nestedTypes) "$ogName.$nnT" else nnT
|
||||
"val $n: $useT? = null"
|
||||
}
|
||||
|
||||
"@Serializable\ndata class $newName($props)"
|
||||
}
|
||||
|
||||
val cnt = "package $pkg\n\n${imports.joinToString("\n") { "import $it" }}\n\n${classes.joinToString("\n\n")}"
|
||||
// println(cnt)
|
||||
|
||||
val writeDir = outputDir.resolve(pkg.replace('.', '/'))
|
||||
val fileName = file.fileName
|
||||
// println("Trying to write to $writeDir/$fileName")
|
||||
|
||||
if(!writeDir.exists()) writeDir.createDirectories()
|
||||
Files.writeString(Path("$writeDir/$fileName"), cnt)
|
||||
}
|
||||
|
||||
|
||||
// usage kotlin partialize.main.kts output-dir [input-file]+
|
||||
if(args.isEmpty()) {
|
||||
System.err.println("Usage: partialize.main.kts <output-dir> <input-file>+")
|
||||
exitProcess(-1)
|
||||
}
|
||||
|
||||
val outputDir = Path(args[0])
|
||||
if(outputDir.notExists()) {
|
||||
println("Creating output directory $outputDir...")
|
||||
Files.createDirectory(outputDir)
|
||||
}
|
||||
else if(!outputDir.isDirectory()) {
|
||||
System.err.println("Output directory $outputDir is not a directory.")
|
||||
exitProcess(-1)
|
||||
}
|
||||
else if(!outputDir.isWritable()) {
|
||||
System.err.println("Cannot write to output directory $outputDir")
|
||||
exitProcess(-1)
|
||||
}
|
||||
|
||||
val inputFiles = args.slice(1 until args.size)
|
||||
val disp = Disposer.newDisposable()
|
||||
val appEnv = KotlinCoreApplicationEnvironment.create(disp, KotlinCoreApplicationEnvironmentMode.Production)
|
||||
appEnv.registerParserDefinition(KotlinParserDefinition())
|
||||
val project = KotlinCoreProjectEnvironment(disp, appEnv)
|
||||
val factory = KtPsiFactory(project.project)
|
||||
inputFiles.forEach {
|
||||
process(Path(it), outputDir, factory)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.Parameters
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.util.reflect.TypeInfo
|
||||
import io.ktor.util.reflect.typeInfo
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
enum class Elevation {
|
||||
UN_AUTH, AUTH, ADMIN
|
||||
}
|
||||
|
||||
sealed class ApiRoute<TReq, TRes: Any>(val verb: String, val path: String, val elevation: Elevation, private val _resType: TypeInfo) {
|
||||
enum class ReqBodySource {
|
||||
BODY, PARAMS, QUERY, NON_BODY, NON_QUERY
|
||||
}
|
||||
|
||||
open val pattern = path
|
||||
open val bodySource = ReqBodySource.BODY
|
||||
|
||||
suspend fun extract(body: HttpResponse, meta: Any? = null): Either<ErrorResponse, TRes> = try {
|
||||
println("Extracting response for $verb $pattern [${body.status.value}] as ${_resType.type.simpleName} (${_resType.kotlinType}${meta?.let { "; meta = $it" } ?: ""})")
|
||||
if(body.status.isSuccess()) body.body<TRes>(_resType).value()
|
||||
else body.body<ErrorResponse>().error()
|
||||
} catch(e: Exception) {
|
||||
ErrorResponse("Failed to parse response: ${e.message}").error()
|
||||
}
|
||||
|
||||
abstract suspend fun makeCall(client: IClient, body: TReq): HttpResponse
|
||||
|
||||
suspend fun call(client: IClient, body: TReq, meta: Any? = null): Either<ErrorResponse, TRes> = extract(makeCall(client, body), meta)
|
||||
|
||||
open suspend fun parseParams(params: Parameters): TReq? = throw UnsupportedOperationException()
|
||||
open suspend fun parseQuery(params: Parameters): TReq = throw UnsupportedOperationException()
|
||||
open suspend fun parseNonBody(query: Parameters, params: Parameters): TReq? = throw UnsupportedOperationException()
|
||||
open suspend fun parseNonQuery(params: Parameters, receiver: suspend (bodyType: KClass<*>, baseReq: Any) -> TReq?): TReq? = throw UnsupportedOperationException()
|
||||
|
||||
class GetRoute<TRes: Any>(path: String, elevation: Elevation, resType: TypeInfo)
|
||||
: ApiRoute<EmptyRequest, TRes>("GET", path, elevation, resType) {
|
||||
override suspend fun makeCall(client: IClient, body: EmptyRequest): HttpResponse = client.client.get {
|
||||
url("${client.serverUrl}$path")
|
||||
}
|
||||
|
||||
suspend fun makeCall(client: IClient): HttpResponse = makeCall(client, EmptyRequest())
|
||||
}
|
||||
|
||||
class GetRoute1<TReq: Any, TRes: Any>(
|
||||
path: String, elevation: Elevation, resType: TypeInfo, val urlEncode: (TReq) -> String,
|
||||
val urlDecode: (String?) -> TReq?
|
||||
) : ApiRoute<TReq, TRes>("GET", path, elevation, resType) {
|
||||
override val pattern: String = "$path/{param}"
|
||||
override val bodySource: ReqBodySource = ReqBodySource.PARAMS
|
||||
|
||||
override suspend fun makeCall(client: IClient, body: TReq): HttpResponse = client.client.get {
|
||||
url("${client.serverUrl}$path/${urlEncode(body)}")
|
||||
}
|
||||
|
||||
override suspend fun parseParams(params: Parameters): TReq? = urlDecode(params["param"])
|
||||
}
|
||||
|
||||
class PostRoute<TReq: Any, TRes: Any>(path: String, elevation: Elevation, private val _reqType: TypeInfo, resType: TypeInfo)
|
||||
: ApiRoute<TReq, TRes>("POST", path, elevation, resType) {
|
||||
override suspend fun makeCall(client: IClient, body: TReq): HttpResponse = client.client.post {
|
||||
url("${client.serverUrl}$path")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(body, _reqType)
|
||||
}
|
||||
}
|
||||
|
||||
class PostRoute1<TReq: Any, TBody: Any, TRes: Any>(
|
||||
private val pathPre: String, private val pathPost: String, elevation: Elevation, private val _bodyType: TypeInfo,
|
||||
resType: TypeInfo, val urlEncode: (TReq) -> String, val urlDecode: (String?) -> TReq?
|
||||
) : ApiRoute<Pair<TReq, TBody>, TRes>("POST", "$pathPre/{param}/$pathPost", elevation, resType) {
|
||||
override val pattern: String = "$pathPre/{param}/$pathPost"
|
||||
override val bodySource: ReqBodySource = ReqBodySource.NON_QUERY
|
||||
|
||||
override suspend fun makeCall(client: IClient, body: Pair<TReq, TBody>): HttpResponse = client.client.post {
|
||||
url("${client.serverUrl}$pathPre/${urlEncode(body.first)}/$pathPost")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(body.second, _bodyType)
|
||||
}
|
||||
|
||||
override suspend fun parseNonQuery(params: Parameters, receiver: suspend (bodyType: KClass<*>, urlParam: Any) -> Pair<TReq, TBody>?): Pair<TReq, TBody>? {
|
||||
val param = urlDecode(params["param"]) ?: return null
|
||||
return receiver(_bodyType.type, param)
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteRoute1<TReq: Any, TRes: Any>(
|
||||
path: String, elevation: Elevation, resType: TypeInfo, val urlEncode: (TReq) -> String,
|
||||
val urlDecode: (String?) -> TReq?
|
||||
) : ApiRoute<TReq, TRes>("DELETE", path, elevation, resType) {
|
||||
override val pattern: String = "$path/{param}"
|
||||
override val bodySource: ReqBodySource = ReqBodySource.PARAMS
|
||||
|
||||
override suspend fun makeCall(client: IClient, body: TReq): HttpResponse = client.client.delete {
|
||||
url("${client.serverUrl}$path/${urlEncode(body)}")
|
||||
}
|
||||
|
||||
override suspend fun parseParams(params: Parameters): TReq? = urlDecode(params["param"])
|
||||
}
|
||||
|
||||
class PatchRoute1<TReq: Any, TBody: Any, TRes: Any>(
|
||||
path: String, elevation: Elevation, resType: TypeInfo, private val _bodyType: TypeInfo,
|
||||
val urlEncode: (TReq) -> String, val urlDecode: (String?) -> TReq?
|
||||
) : ApiRoute<Pair<TReq, TBody>, TRes>("PATCH", path, elevation, resType) {
|
||||
override val bodySource: ReqBodySource = ReqBodySource.NON_QUERY
|
||||
override val pattern: String = "$path/{param}"
|
||||
|
||||
override suspend fun makeCall(client: IClient, body: Pair<TReq, TBody>): HttpResponse = client.client.patch {
|
||||
url("${client.serverUrl}$path/${urlEncode(body.first)}")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(body.second, _bodyType)
|
||||
}
|
||||
|
||||
override suspend fun parseNonQuery(params: Parameters, receiver: suspend (bodyType: KClass<*>, urlParam: Any) -> Pair<TReq, TBody>?): Pair<TReq, TBody>? {
|
||||
val param = urlDecode(params["param"]) ?: return null
|
||||
return receiver(_bodyType.type, param)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parseUuid(str: String?) = str?.let {
|
||||
try {
|
||||
Uuid.parse(it)
|
||||
}
|
||||
catch(_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a GET route with no request parameters.
|
||||
*/
|
||||
inline fun <reified TRes: Any> get(path: String, elevation: Elevation) =
|
||||
GetRoute<TRes>(path, elevation, typeInfo<TRes>())
|
||||
|
||||
/**
|
||||
* Builds a GET route with one request parameter (encoded as URL parameter in the endpoint, like
|
||||
* `/endpoint/arg`).
|
||||
*/
|
||||
inline fun <reified TReq: Any, reified TRes: Any> get1(path: String, elevation: Elevation,
|
||||
noinline encode: (TReq) -> String = { it.toString() }, noinline decode: (String?) -> TReq?
|
||||
) = GetRoute1<TReq, TRes>(path, elevation, typeInfo<TRes>(), encode, decode)
|
||||
|
||||
/**
|
||||
* Builds a GET route with one UUID request parameter (encoded as URL parameter in the endpoint, like
|
||||
* `/endpoint/arg`).
|
||||
*/
|
||||
inline fun <reified TRes: Any> getUuid(path: String, elevation: Elevation) =
|
||||
get1<Uuid, TRes>(path, elevation, Uuid::toString, this::parseUuid)
|
||||
|
||||
/**
|
||||
* Builds a POST route with request and response types.
|
||||
*/
|
||||
inline fun <reified TReq: Any, reified TRes: Any> post(path: String, elevation: Elevation) =
|
||||
PostRoute<TReq, TRes>(path, elevation, typeInfo<TReq>(), typeInfo<TRes>())
|
||||
|
||||
/**
|
||||
* Builds a POST route with two hardcoded path segments and one request parameter (encoded as URL
|
||||
* parameter in the endpoint, like `/endpoint-pre/arg/endpoint-post`), and a request body.
|
||||
*/
|
||||
inline fun <reified TReq: Any, reified TBody: Any, reified TRes: Any> post1(
|
||||
pathPre: String, pathPost: String, elevation: Elevation, noinline encode: (TReq) -> String,
|
||||
noinline decode: (String?) -> TReq?
|
||||
) = PostRoute1<TReq, TBody, TRes>(pathPre, pathPost, elevation, typeInfo<TBody>(), typeInfo<TRes>(), encode, decode)
|
||||
|
||||
/**
|
||||
* Builds a POST route with two hardcoded path segments and one UUID request parameter (encoded as URL
|
||||
* parameter in the endpoint, like `/endpoint-pre/uuid/endpoint-post`), and a request body.
|
||||
*/
|
||||
inline fun <reified TBody: Any, reified TRes: Any> postUuid(pathPre: String, pathPost: String, elevation: Elevation) =
|
||||
post1<Uuid, TBody, TRes>(pathPre, pathPost, elevation, Uuid::toString, this::parseUuid)
|
||||
|
||||
/**
|
||||
* Builds a POST route with request type and no response (EmptyResponse).
|
||||
*/
|
||||
inline fun <reified TReq: Any> postNoRes(path: String, elevation: Elevation) =
|
||||
post<TReq, EmptyResponse>(path, elevation)
|
||||
|
||||
/**
|
||||
* Builds a DELETE route with one request parameter (encoded as URL parameter in the endpoint, like
|
||||
* `/endpoint/arg`).
|
||||
*/
|
||||
inline fun <reified TReq: Any, reified TRes: Any> delete1(path: String, elevation: Elevation,
|
||||
noinline encode: (TReq) -> String = { it.toString() }, noinline decode: (String?) -> TReq?
|
||||
) = DeleteRoute1<TReq, TRes>(path, elevation, typeInfo<TRes>(), encode, decode)
|
||||
|
||||
/**
|
||||
* Builds a DELETE route with one UUID request parameter (encoded as URL parameter in the endpoint, like
|
||||
* `/endpoint/arg`).
|
||||
*/
|
||||
inline fun <reified TRes: Any> deleteUuid(path: String, elevation: Elevation) =
|
||||
delete1<Uuid, TRes>(path, elevation, Uuid::toString, this::parseUuid)
|
||||
|
||||
/**
|
||||
* Builds a DELETE route with one request parameter and no response (EmptyResponse).
|
||||
*/
|
||||
inline fun <reified TReq: Any> delete1NoRes(path: String, elevation: Elevation,
|
||||
noinline encode: (TReq) -> String = { it.toString() }, noinline decode: (String?) -> TReq?
|
||||
) = delete1<TReq, EmptyResponse>(path, elevation, encode, decode)
|
||||
|
||||
/**
|
||||
* Builds a DELETE route with one UUID request parameter and no response (EmptyResponse).
|
||||
*/
|
||||
fun deleteUuidNoRes(path: String, elevation: Elevation) =
|
||||
delete1NoRes<Uuid>(path, elevation, Uuid::toString, this::parseUuid)
|
||||
|
||||
/**
|
||||
* Builds a PATCH route with one request parameter and a request body.
|
||||
*/
|
||||
inline fun <reified TReq: Any, reified TBody: Any, reified TRes: Any> patch1(
|
||||
path: String, elevation: Elevation, noinline encode: (TReq) -> String, noinline decode: (String?) -> TReq?
|
||||
) = PatchRoute1<TReq, TBody, TRes>(path, elevation, typeInfo<TRes>(), typeInfo<TBody>(), encode, decode)
|
||||
|
||||
/**
|
||||
* Builds a PATCH route with a UUID request parameter and a request body.
|
||||
*/
|
||||
inline fun <reified TBody: Any, reified TRes: Any> patchUuid(path: String, elevation: Elevation) =
|
||||
patch1<Uuid, TBody, TRes>(path, elevation, Uuid::toString, this::parseUuid)
|
||||
|
||||
/**
|
||||
* Builds a PATCH route with a UUID request parameter and a request body, but without response (EmptyResponse).
|
||||
*/
|
||||
inline fun <reified TBody: Any> patchUuidNoRes(path: String, elevation: Elevation) =
|
||||
patchUuid<TBody, EmptyResponse>(path, elevation)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
object Auth {
|
||||
const val JWT_CLAIM = "pb-user-id"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
sealed class Either<out E, out V> {
|
||||
class Error<E>(val errorData: E) : Either<E, Nothing>()
|
||||
class Value<V>(val value: V) : Either<Nothing, V>()
|
||||
}
|
||||
fun <E> E.error(): Either<E, Nothing> = Either.Error(this)
|
||||
fun <V> V.value(): Either<Nothing, V> = Either.Value(this)
|
||||
inline fun <reified E, reified V, reified V2> Either<E, V>.bind(f: (V) -> Either<E, V2>): Either<E, V2> = when(this) {
|
||||
is Either.Error<E> -> errorData.error()
|
||||
is Either.Value<V> -> f(value)
|
||||
}
|
||||
inline fun <reified E, reified V, reified V2> Either<E, V>.map(f: (V) -> V2): Either<E, V2> = bind { f(it).value() }
|
||||
inline fun <reified E, reified V, reified R> Either<E, V>.fold(fError: (E) -> R, fValue: (V) -> R) = when(this) {
|
||||
is Either.Error<E> -> fError(errorData)
|
||||
is Either.Value<V> -> fValue(value)
|
||||
}
|
||||
suspend inline fun <reified E, reified V, reified R> Either<E, V>.foldSuspend(fError: suspend (E) -> R, fValue: suspend (V) -> R) = when(this) {
|
||||
is Either.Error<E> -> fError(errorData)
|
||||
is Either.Value<V> -> fValue(value)
|
||||
}
|
||||
|
||||
inline fun <reified E, reified V> Either<E, V>.isError() = this is Either.Error<E>
|
||||
inline fun <reified E, reified V> Either<E, V>.isValue() = this is Either.Value<V>
|
||||
|
||||
inline fun <reified E, reified V> Either<E, V>.asError() = (this as? Either.Error<E>)?.errorData
|
||||
inline fun <reified E, reified V> Either<E, V>.asValue() = (this as? Either.Value<V>)?.value
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
interface IClient {
|
||||
val client: HttpClient
|
||||
val serverUrl: String
|
||||
|
||||
data class Default(override val client: HttpClient, override val serverUrl: String) : IClient
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
annotation class ToPartialize
|
||||
|
||||
@Serializable
|
||||
data class LoginRequest(val name: String, val pass: String)
|
||||
|
||||
@Serializable
|
||||
data class SignupRequest(val invite: Uuid, val name: String, val pass: String)
|
||||
|
||||
@Serializable
|
||||
class EmptyRequest
|
||||
|
||||
@Serializable
|
||||
data class RefreshRequest(val refreshToken: Uuid)
|
||||
|
||||
@Serializable @ToPartialize
|
||||
data class ProjectRequest(val name: String, var isPublic: Boolean)
|
||||
|
||||
@Serializable @ToPartialize
|
||||
data class LabelRequest(val name: String, val color: String, val projectId: Uuid)
|
||||
|
||||
@Serializable @ToPartialize
|
||||
data class EntryRequest(val label: Uuid, val timestamp: Instant, val projectId: Uuid, val warmups: List<Float>,
|
||||
val measurements: List<Float>, val unit: TimeUnit)
|
||||
|
||||
@Serializable
|
||||
data class LogoutRequest(val refresh: Uuid)
|
||||
|
||||
@Serializable
|
||||
data class UserUpdateRequest(val projectLimit: Int? = null, val isAdmin: Boolean? = null)
|
||||
|
||||
@Serializable
|
||||
data class InviteRequest(val asAdmin: Boolean)
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponse(val msg: String)
|
||||
|
||||
@Serializable
|
||||
class EmptyResponse
|
||||
|
||||
@Serializable
|
||||
data class TokenResponse(val access: String, val refresh: Uuid)
|
||||
|
||||
@Serializable
|
||||
data class UuidResponse(val uuid: Uuid)
|
||||
|
||||
@Serializable
|
||||
data class InviteListResponse(val uuids: List<Invite>) {
|
||||
@Serializable
|
||||
data class Invite(val code: Uuid, val expires: Instant, val asAdmin: Boolean)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserListResponse(val users: List<UserData>) {
|
||||
@Serializable
|
||||
data class UserData(val id: Uuid, val name: String, val isAdmin: Boolean, val projectLimit: Int, val usedProjects: Int)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NamedID(val name: String, val id: Uuid)
|
||||
|
||||
@Serializable
|
||||
data class HomeResponse(val username: String, val isAdmin: Boolean, val projectLimit: Int, val ownProjects: List<ProjectSummary>, val publicProjects: List<ProjectSummary>) {
|
||||
@Serializable
|
||||
data class ProjectSummary(val id: Uuid, val name: String, val isPublic: Boolean, val owner: NamedID)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ProjectResponse(val id: Uuid, val name: String, val owner: NamedID, val isPublic: Boolean, val isEditable: Boolean,
|
||||
val usedLabels: List<LabelResponse>, val entries: List<EntryResponse>)
|
||||
|
||||
@Serializable
|
||||
data class LabelResponse(val id: Uuid, val name: String, val color: String)
|
||||
|
||||
@Serializable
|
||||
data class EntryResponse(val id: Uuid, val labelId: Uuid, val timestamp: Instant, val warmups: List<Float>,
|
||||
val measurements: List<Float>, val unit: TimeUnit)
|
||||
|
||||
@Serializable
|
||||
data class HandshakeResponse(val version: String = ProtocolVersion.VERSION) {
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
object Routes {
|
||||
object Auth {
|
||||
val login = ApiRoute.post<LoginRequest, TokenResponse>("/login", Elevation.UN_AUTH)
|
||||
val signup = ApiRoute.post<SignupRequest, TokenResponse>("/signup", Elevation.UN_AUTH)
|
||||
val logout = ApiRoute.post<LogoutRequest, EmptyResponse>("/logout", Elevation.AUTH)
|
||||
val logoutEverywhere = ApiRoute.post<EmptyRequest, EmptyResponse>("/logout/all", Elevation.AUTH)
|
||||
val refresh = ApiRoute.post<RefreshRequest, TokenResponse>("/refresh", Elevation.UN_AUTH)
|
||||
object Invite {
|
||||
val new = ApiRoute.post<InviteRequest, UuidResponse>("/invite", Elevation.ADMIN)
|
||||
val list = ApiRoute.get<InviteListResponse>("/invite", Elevation.ADMIN)
|
||||
val delete = ApiRoute.deleteUuidNoRes("/invite", Elevation.ADMIN)
|
||||
}
|
||||
|
||||
object User {
|
||||
val list = ApiRoute.get<UserListResponse>("/user", Elevation.ADMIN)
|
||||
val update = ApiRoute.patchUuidNoRes<UserUpdateRequest>("/user", Elevation.ADMIN)
|
||||
val delete = ApiRoute.deleteUuidNoRes("/user", Elevation.ADMIN)
|
||||
}
|
||||
}
|
||||
|
||||
val handshake = ApiRoute.get<HandshakeResponse>("/", Elevation.UN_AUTH)
|
||||
val home = ApiRoute.get<HomeResponse>("/home", Elevation.AUTH)
|
||||
|
||||
object Project {
|
||||
val new = ApiRoute.post<ProjectRequest, ProjectResponse>("/project", Elevation.AUTH)
|
||||
val get = ApiRoute.getUuid<ProjectResponse>("/project", Elevation.AUTH)
|
||||
val update = ApiRoute.patchUuidNoRes<PartialProjectRequest>("/project", Elevation.AUTH)
|
||||
val delete = ApiRoute.deleteUuidNoRes("/project", Elevation.AUTH)
|
||||
}
|
||||
|
||||
object Label {
|
||||
val new = ApiRoute.post<LabelRequest, LabelResponse>("/label", Elevation.AUTH)
|
||||
val update = ApiRoute.patchUuidNoRes<PartialLabelRequest>("/label", Elevation.AUTH)
|
||||
val delete = ApiRoute.deleteUuidNoRes("/label", Elevation.AUTH)
|
||||
}
|
||||
|
||||
object Entry {
|
||||
val new = ApiRoute.post<EntryRequest, EntryResponse>("/entry", Elevation.AUTH)
|
||||
val update = ApiRoute.patchUuidNoRes<PartialEntryRequest>("/entry", Elevation.AUTH)
|
||||
val delete = ApiRoute.deleteUuidNoRes("/entry", Elevation.AUTH)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.jaytux.phoebench.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class TimeUnit(val disp: String, val mulToSec: Float) {
|
||||
NANOS("ns", 1e-9f),
|
||||
MICROS("μs", 1e-6f),
|
||||
MILLIS("ms", 1e-3f),
|
||||
SECONDS("s", 1f),
|
||||
MINUTES("min", 60f),
|
||||
HOURS("h", 3600f);
|
||||
|
||||
fun convertTo(other: TimeUnit, valueInThis: Float): Float = valueInThis * (mulToSec / other.mulToSec)
|
||||
}
|
||||
Reference in New Issue
Block a user