Skip to content

Commit

Permalink
Ktor: type safe routing (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
organize authored Oct 5, 2023
1 parent e17eb04 commit 43faa11
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 42 deletions.
8 changes: 6 additions & 2 deletions libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor"
ktor-server-tests = { module = "io.ktor:ktor-server-tests", version.ref = "ktor" }
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt", version.ref = "ktor" }
ktor-server-resources = { module = "io.ktor:ktor-server-resources", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-resources = { module = "io.ktor:ktor-client-resources", version.ref = "ktor" }
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-server-html = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
Expand All @@ -75,11 +77,13 @@ ktor-server = [
"ktor-server-defaultheaders",
"ktor-server-netty",
"ktor-server-auth",
"ktor-serialization"
"ktor-serialization",
"ktor-server-resources"
]
ktor-client = [
"ktor-client-content-negotiation",
"ktor-client-serialization"
"ktor-client-serialization",
"ktor-client-resources"
]
kotest = [
"kotest-assertionsCore",
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/io/github/nomisrev/env/ktor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.cors.maxAgeDuration
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import io.ktor.server.resources.Resources
import kotlin.time.Duration.Companion.days
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
Expand All @@ -22,6 +23,7 @@ val kotlinXSerializersModule = SerializersModule {

fun Application.configure() {
install(DefaultHeaders)
install(Resources) { serializersModule = kotlinXSerializersModule }
install(ContentNegotiation) {
json(
Json {
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/io/github/nomisrev/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.github.nomisrev.routes.tagRoutes
import io.github.nomisrev.routes.userRoutes
import io.ktor.server.application.Application
import io.ktor.server.netty.Netty
import io.ktor.server.routing.routing
import kotlinx.coroutines.awaitCancellation

fun main(): Unit = SuspendApp {
Expand All @@ -25,7 +26,7 @@ fun main(): Unit = SuspendApp {

fun Application.app(module: Dependencies) {
configure()
userRoutes(module.userService, module.jwtService)
routing { userRoutes(module.userService, module.jwtService) }
health(module.healthCheck)
tagRoutes(module.tagPersistence)
}
65 changes: 34 additions & 31 deletions src/main/kotlin/io/github/nomisrev/routes/users.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import io.github.nomisrev.service.RegisterUser
import io.github.nomisrev.service.Update
import io.github.nomisrev.service.UserService
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.resources.Resource
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.receive
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.put
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.server.resources.get
import io.ktor.server.resources.post
import io.ktor.server.resources.put
import io.ktor.server.routing.Routing
import io.ktor.util.pipeline.PipelineContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.MissingFieldException
Expand Down Expand Up @@ -48,32 +47,36 @@ data class User(

@Serializable data class LoginUser(val email: String, val password: String)

fun Application.userRoutes(
@Resource("/users")
data object UsersResource {
@Resource("/login") data class Login(val parent: UsersResource = UsersResource)
}

@Resource("/user") data object UserResource

fun Routing.userRoutes(
userService: UserService,
jwtService: JwtService,
) = routing {
route("/users") {
/* Registration: POST /api/users */
post {
either {
val (username, email, password) = receiveCatching<UserWrapper<NewUser>>().bind().user
val token = userService.register(RegisterUser(username, email, password)).bind().value
UserWrapper(User(email, token, username, "", ""))
}
.respond(HttpStatusCode.Created)
}
post("/login") {
either {
val (email, password) = receiveCatching<UserWrapper<LoginUser>>().bind().user
val (token, info) = userService.login(Login(email, password)).bind()
UserWrapper(User(email, token.value, info.username, info.bio, info.image))
}
.respond(HttpStatusCode.OK)
}
) {
/* Registration: POST /users */
post<UsersResource> {
either {
val (username, email, password) = receiveCatching<UserWrapper<NewUser>>().bind().user
val token = userService.register(RegisterUser(username, email, password)).bind().value
UserWrapper(User(email, token, username, "", ""))
}
.respond(HttpStatusCode.Created)
}

/* Get Current User: GET /api/user */
get("/user") {
post<UsersResource.Login> {
either {
val (email, password) = receiveCatching<UserWrapper<LoginUser>>().bind().user
val (token, info) = userService.login(Login(email, password)).bind()
UserWrapper(User(email, token.value, info.username, info.bio, info.image))
}
.respond(HttpStatusCode.OK)
}
/* Get Current User: GET /user */
get<UserResource> {
jwtAuth(jwtService) { (token, userId) ->
either {
val info = userService.getUser(userId).bind()
Expand All @@ -83,8 +86,8 @@ fun Application.userRoutes(
}
}

/* Update current user: PUT /api/user */
put("/user") {
/* Update current user: PUT /user */
put<UserResource> {
jwtAuth(jwtService) { (token, userId) ->
either {
val (email, username, password, bio, image) =
Expand Down
2 changes: 2 additions & 0 deletions src/test/kotlin/io/github/nomisrev/ktor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.github.nomisrev.env.Dependencies
import io.github.nomisrev.env.kotlinXSerializersModule
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.resources.Resources
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.TestApplication
Expand All @@ -19,6 +20,7 @@ suspend fun withServer(test: suspend HttpClient.(dep: Dependencies) -> Unit): Un
createClient {
expectSuccess = false
install(ContentNegotiation) { json(Json { serializersModule = kotlinXSerializersModule }) }
install(Resources) { serializersModule = kotlinXSerializersModule }
}
.use { client -> test(client, dependencies) }
}
Expand Down
16 changes: 8 additions & 8 deletions src/test/kotlin/io/github/nomisrev/routes/UserRouteSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import io.kotest.assertions.arrow.core.shouldBeRight
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.ktor.client.call.body
import io.ktor.client.plugins.resources.get
import io.ktor.client.plugins.resources.post
import io.ktor.client.plugins.resources.put
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
Expand All @@ -24,7 +24,7 @@ class UserRouteSpec :
"Can register user" {
withServer {
val response =
post("/users") {
post(UsersResource) {
contentType(ContentType.Application.Json)
setBody(UserWrapper(NewUser(validUsername, validEmail, validPw)))
}
Expand All @@ -46,7 +46,7 @@ class UserRouteSpec :
.shouldBeRight()

val response =
post("/users/login") {
post(UsersResource.Login()) {
contentType(ContentType.Application.Json)
setBody(UserWrapper(LoginUser(validEmail, validPw)))
}
Expand All @@ -68,7 +68,7 @@ class UserRouteSpec :
.register(RegisterUser(validUsername, validEmail, validPw))
.shouldBeRight()

val response = get("/user") { bearerAuth(expected.value) }
val response = get(UserResource) { bearerAuth(expected.value) }

response.status shouldBe HttpStatusCode.OK
with(response.body<UserWrapper<User>>().user) {
Expand All @@ -90,7 +90,7 @@ class UserRouteSpec :
val newUsername = "newUsername"

val response =
put("/user") {
put(UserResource) {
bearerAuth(expected.value)
contentType(ContentType.Application.Json)
setBody(UserWrapper(UpdateUser(username = newUsername)))
Expand All @@ -116,7 +116,7 @@ class UserRouteSpec :
val inalidEmail = "invalidEmail"

val response =
put("/user") {
put(UserResource) {
bearerAuth(token.value)
contentType(ContentType.Application.Json)
setBody(UserWrapper(UpdateUser(email = inalidEmail)))
Expand Down

0 comments on commit 43faa11

Please sign in to comment.