Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package community.flock.wirespec.generator

import community.flock.wirespec.compiler.core.parse.AST
import community.flock.wirespec.compiler.core.parse.Endpoint
import community.flock.wirespec.compiler.core.parse.Endpoint.Segment

typealias Path = List<Segment>

data class MatchResult(
val endpoint: Endpoint,
val params: Map<String, String>,
val query: Map<String, String>
)

data class Route(
val endpoint: Endpoint,
val regex: Regex,
val params: List<String>
)

fun AST.router() = this
.filterIsInstance<Endpoint>()
.map { endpoint ->
Route(
endpoint = endpoint,
regex = endpoint.path.toRegex(),
params = endpoint.path.filterIsInstance<Segment.Param>().map { it.identifier.value }
)
}

fun Route.match(method: Endpoint.Method, path: String): MatchResult? {
val parts = path.split("?")
val p = parts[0]
val q = parts.getOrNull(1)
val match = regex.find(p)
println(regex)
return if (endpoint.method == method && match != null) {
MatchResult(
endpoint = endpoint,
params = params.associateWith { match.groups[it]?.value ?: error("parameter not found in matcher")},
query = q?.parseQuery().orEmpty()
)
} else {
null
}
}

fun List<Route>.match(method: Endpoint.Method, path: String): MatchResult? {
return firstNotNullOfOrNull { it.match(method, path) }
}

private fun String.parseQuery() = this
.split("&")
.map { it.split("=", limit = 2) }
.associate { (key, value) -> key to value }

fun Path.toRegex() = Regex(this.joinToString("/", "^/", "/*$") {
when (it) {
is Segment.Param -> """(?<${it.identifier.value}>[^/]+)"""
is Segment.Literal -> """(${it.value})"""
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package community.flock.wirespec.generator

import arrow.core.getOrElse
import community.flock.wirespec.compiler.core.WirespecSpec
import community.flock.wirespec.compiler.core.parse
import community.flock.wirespec.compiler.core.parse.Endpoint
import community.flock.wirespec.compiler.utils.noLogger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull


private val src = """
type UUID /^[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}${'$'}/g

type Todo {
id: UUID,
name: String,
done: Boolean
}

endpoint GetTodos GET /todos -> {
200 -> Todo[]
}

endpoint GetTodosById GET Todo /todos/{id: UUID} -> {
200 -> Todo
404 -> Error
}

endpoint GetTodosByIdAndString GET Todo /todos/{id: UUID}/{string:String} -> {
200 -> Todo
404 -> Error
}

""".trimIndent()

class RouterTest {

private fun parser(source: String) = WirespecSpec
.parse(source)(noLogger)
.getOrElse { e -> error("Cannot parse: ${e.map { it.message }}") }

private val router = parser(src).router()

@Test
fun testGetTodos() {
val res = router.match(Endpoint.Method.GET, "/todos")
assertEquals("GetTodos", res?.endpoint?.name)
assertEquals(emptyMap(), res?.params)
assertEquals(emptyMap(), res?.query)
}

@Test
fun testGetTodosSlash() {
val res = router.match(Endpoint.Method.GET, "/todos/")
assertEquals("GetTodos", res?.endpoint?.name)
assertEquals(emptyMap(), res?.params)
assertEquals(emptyMap(), res?.query)
}

@Test
fun testGetTodosById() {
val res = router.match(Endpoint.Method.GET, "/todos/123")
assertEquals("GetTodosById", res?.endpoint?.name)
assertEquals(mapOf("id" to "123"), res?.params)
assertEquals(emptyMap(), res?.query)
}

@Test
fun testGetTodosByIdSlash() {
val res = router.match(Endpoint.Method.GET, "/todos/123/")
assertEquals("GetTodosById", res?.endpoint?.name)
assertEquals(mapOf("id" to "123"), res?.params)
assertEquals(emptyMap(), res?.query)
}

@Test
fun testGetTodosByIdAndString() {
val res = router.match(Endpoint.Method.GET, "/todos/123/hello")
assertEquals("GetTodosByIdAndString", res?.endpoint?.name)
assertEquals(mapOf("id" to "123", "string" to "hello"), res?.params)
assertEquals(emptyMap(), res?.query)
}

@Test
fun testGetTodosByIdAndStringSlash() {
val res = router.match(Endpoint.Method.GET, "/todos/123/hello/")
assertEquals("GetTodosByIdAndString", res?.endpoint?.name)
assertEquals(mapOf("id" to "123", "string" to "hello"), res?.params)
assertEquals(emptyMap(), res?.query)
}

@Test
fun testGetTodosByIdAndStringQuery() {
val res = router.match(Endpoint.Method.GET, "/todos/123/hello?foo=bar")
assertEquals("GetTodosByIdAndString", res?.endpoint?.name)
assertEquals(mapOf("id" to "123", "string" to "hello"), res?.params)
assertEquals(mapOf("foo" to "bar"), res?.query)
}


@Test
fun testNoMatch() {
val res = router.match(Endpoint.Method.GET, "/hello/world")
assertNull(res)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data class CompilerArguments(
sealed interface Operation {
data object Compile : Operation
data class Convert(val format: Format) : Operation
data class Serve(val port: Int) : Operation
}

enum class Format {
Expand Down
4 changes: 4 additions & 0 deletions src/plugin/cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ kotlin {
}
val desktopMain by creating {
dependsOn(commonMain)
dependencies {
implementation("io.ktor:ktor-server-core:2.3.10")
implementation("io.ktor:ktor-server-cio:2.3.10")
}
}
val macosX64Main by getting {
dependsOn(desktopMain)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.default
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.multiple
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int
import community.flock.wirespec.compiler.core.emit.common.DEFAULT_PACKAGE_STRING
import community.flock.wirespec.plugin.CompilerArguments
import community.flock.wirespec.plugin.Console
Expand Down Expand Up @@ -39,7 +41,7 @@ class WirespecCli : NoOpCliktCommand(name = "wirespec") {
fun provide(
compile: (CompilerArguments) -> Unit,
convert: (CompilerArguments) -> Unit,
): (Array<out String>) -> Unit = WirespecCli().subcommands(Compile(compile), Convert(convert))::main
): (Array<out String>) -> Unit = WirespecCli().subcommands(Compile(compile), Convert(convert), Serve(convert))::main
}
}

Expand Down Expand Up @@ -101,3 +103,26 @@ private class Convert(private val block: (CompilerArguments) -> Unit) : CommonOp
).let(block)
}
}

private class Serve(private val block: (CompilerArguments) -> Unit) : CommonOptions() {

private val port by option("--port", help = "Port").int().default(8080)
private val languages by option(*Options.Language.flags, help = "Language")
.choice(choices = Language.toMap(), ignoreCase = true)
.multiple(listOf(Wirespec))

override fun run() {
inputDir?.let { echo("To serve, please specify a file", err = true) }
CompilerArguments(
operation = Operation.Serve(port = port),
input = getInput(null),
output = Output(outputDir),
languages = languages.toSet().ifEmpty { setOf(Wirespec) },
packageName = PackageName(packageName),
shared = shared,
strict = strict,
debug = debug,
).let(block)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import community.flock.wirespec.plugin.cli.io.File
import community.flock.wirespec.plugin.cli.io.JavaFile
import community.flock.wirespec.plugin.cli.io.JsonFile
import community.flock.wirespec.plugin.cli.io.KotlinFile
import community.flock.wirespec.plugin.cli.io.Request
import community.flock.wirespec.plugin.cli.io.Response
import community.flock.wirespec.plugin.cli.io.ScalaFile
import community.flock.wirespec.plugin.cli.io.Server
import community.flock.wirespec.plugin.cli.io.TypeScriptFile
import community.flock.wirespec.plugin.cli.io.WirespecFile
import community.flock.wirespec.plugin.utils.orNull
Expand Down Expand Up @@ -97,6 +100,12 @@ fun compile(arguments: CompilerArguments) {
.let { it.wirespec(languages, packageName, it.path.out(packageName, output), logger) }
else error("Path $input is not a Wirespec file")
}

is Operation.Serve -> {
println("Start server")
fun handler(request: Request) = Response("Hello Wirespec!")
Server(::handler).start(operation.port)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package community.flock.wirespec.plugin.cli.io

import community.flock.wirespec.compiler.core.parse.Endpoint
import kotlin.reflect.KFunction1

data class Request(
val path:String
)

data class Response(
val body:String
)
expect class Server(handle: (req:Request) -> Response) {
fun start(port: Int)
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,5 @@ class CommandLineEntitiesTest {
it.debug shouldBe false
})(arrayOf("convert", "openapiv2", "-o", "output", "-l", "Kotlin"))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package community.flock.wirespec.plugin.cli.io

import io.ktor.server.application.call
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.request.path
import io.ktor.server.response.respond
import io.ktor.server.routing.route
import io.ktor.server.routing.routing

actual class Server actual constructor(val handle: (req:Request) -> Response) {
actual fun start(port: Int) {
embeddedServer(CIO, port = port) {
routing {
route("/{...}"){
handle {
val req = Request(call.request.path())
val res = handle(req)
call.respond(res.body)
}
}
}
}.start(wait = true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package community.flock.wirespec.plugin.cli.io

actual class Server actual constructor(handle: (req:Request) -> Response) {
actual fun start(port: Int) {
error("Not implemented yet")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package community.flock.wirespec.plugin.cli.io

import io.ktor.server.application.call
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.request.path
import io.ktor.server.response.respond
import io.ktor.server.routing.route
import io.ktor.server.routing.routing

actual class Server actual constructor(val handle: (req:Request) -> Response) {
actual fun start(port: Int) {
embeddedServer(CIO, port = port) {
routing {
route("/{...}"){
handle {
val req = Request(call.request.path())
val res = handle(req)
call.respond(res.body)
}
}
}
}.start(wait = true)
}
}