diff --git a/.gitignore b/.gitignore index a1e004ac..1da82393 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,6 @@ common/build/ !gradle-wrapper.properties # Ignore local.properties file typically used for Android SDK path -local.properties \ No newline at end of file +local.properties + +.kotlin \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7aad810b..6d500eb8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ androidx-test-junit = "1.2.1" arrow = "1.2.4" junit = "4.13.2" kotlin = "2.0.21" -ktor = "2.3.10" +ktor = "3.0.1" ktor-client = "3.0.1" logback = "1.5.12" kotest = "5.9.1" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index a04db4b4..343b9f0a 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { implementation(libs.bundles.ktor.client.common) implementation(libs.ktor.client.java) - testImplementation(libs.ktor.server.tests) testImplementation(libs.ktor.server.test.host) testImplementation(libs.kotlin.test.junit) testImplementation(libs.bundles.test) diff --git a/server/src/main/kotlin/ivy/learn/api/AnalyticsApi.kt b/server/src/main/kotlin/ivy/learn/api/AnalyticsApi.kt index 09907cc3..c61f6f67 100644 --- a/server/src/main/kotlin/ivy/learn/api/AnalyticsApi.kt +++ b/server/src/main/kotlin/ivy/learn/api/AnalyticsApi.kt @@ -1,6 +1,5 @@ package ivy.learn.api -import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* import ivy.learn.api.common.Api diff --git a/server/src/main/kotlin/ivy/learn/api/CoursesApi.kt b/server/src/main/kotlin/ivy/learn/api/CoursesApi.kt index a7a77ce8..6fd2ebbb 100644 --- a/server/src/main/kotlin/ivy/learn/api/CoursesApi.kt +++ b/server/src/main/kotlin/ivy/learn/api/CoursesApi.kt @@ -4,7 +4,7 @@ import arrow.core.raise.ensureNotNull import io.ktor.server.routing.* import ivy.data.source.model.CourseResponse import ivy.learn.api.common.Api -import ivy.learn.api.common.endpoint +import ivy.learn.api.common.getEndpoint import ivy.learn.api.common.model.ServerError.BadRequest import ivy.learn.data.repository.CoursesRepository import ivy.learn.data.repository.LessonsRepository @@ -19,7 +19,7 @@ class CoursesApi( } private fun Routing.courseBy() { - get("/courses/{id}", endpoint { params -> + getEndpoint("/courses/{id}") { params -> val courseId = params["id"]?.let(::CourseId) ensureNotNull(courseId) { BadRequest("Course id is required.") } val course = coursesRepository.fetchCourseById(courseId) @@ -30,6 +30,6 @@ class CoursesApi( course = course, lessons = lessons ) - }) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/ivy/learn/api/LessonsApi.kt b/server/src/main/kotlin/ivy/learn/api/LessonsApi.kt index 593af3e4..99cd7d75 100644 --- a/server/src/main/kotlin/ivy/learn/api/LessonsApi.kt +++ b/server/src/main/kotlin/ivy/learn/api/LessonsApi.kt @@ -2,9 +2,8 @@ package ivy.learn.api import arrow.core.raise.ensureNotNull import io.ktor.server.routing.* -import io.ktor.utils.io.* import ivy.learn.api.common.Api -import ivy.learn.api.common.endpoint +import ivy.learn.api.common.getEndpoint import ivy.learn.api.common.model.ServerError import ivy.learn.api.common.model.ServerError.BadRequest import ivy.learn.data.repository.LessonsRepository @@ -19,15 +18,14 @@ class LessonsApi( lessonById() } - @KtorDsl private fun Routing.lessonById() { - get("/lessons/{courseId}/{lessonId}", endpoint { params -> + getEndpoint("/lessons/{courseId}/{lessonId}") { params -> val courseId = params["courseId"]?.let(::CourseId) val lessonId = params["lessonId"]?.let(::LessonId) ensureNotNull(courseId) { BadRequest("Course id is missing!") } ensureNotNull(lessonId) { BadRequest("Lesson id is missing!") } repository.fetchLesson(courseId, lessonId) .mapLeft(ServerError::Unknown).bind() - }) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/ivy/learn/api/StatusApi.kt b/server/src/main/kotlin/ivy/learn/api/StatusApi.kt index bb8429af..912b15cc 100644 --- a/server/src/main/kotlin/ivy/learn/api/StatusApi.kt +++ b/server/src/main/kotlin/ivy/learn/api/StatusApi.kt @@ -2,17 +2,17 @@ package ivy.learn.api import io.ktor.server.routing.* import ivy.learn.api.common.Api -import ivy.learn.api.common.endpoint +import ivy.learn.api.common.getEndpoint import kotlinx.serialization.Serializable class StatusApi : Api { override fun Routing.endpoints() { - get("/hello", endpoint { + getEndpoint("/hello") { HelloResponse( message = "Hello, world!", time = System.currentTimeMillis(), ) - }) + } } } diff --git a/server/src/main/kotlin/ivy/learn/api/TopicsApi.kt b/server/src/main/kotlin/ivy/learn/api/TopicsApi.kt index 84eccb7d..18a730bf 100644 --- a/server/src/main/kotlin/ivy/learn/api/TopicsApi.kt +++ b/server/src/main/kotlin/ivy/learn/api/TopicsApi.kt @@ -1,10 +1,9 @@ package ivy.learn.api import io.ktor.server.routing.* -import io.ktor.utils.io.* import ivy.data.source.model.TopicsResponse import ivy.learn.api.common.Api -import ivy.learn.api.common.endpoint +import ivy.learn.api.common.getEndpoint import ivy.learn.data.repository.CoursesRepository import ivy.learn.data.repository.TopicsRepository @@ -16,13 +15,12 @@ class TopicsApi( topics() } - @KtorDsl private fun Routing.topics() { - get("/topics", endpoint { + getEndpoint("/topics") { TopicsResponse( topics = topicsRepository.fetchTopics(), courses = coursesRepository.fetchCourses() ) - }) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/ivy/learn/api/common/ApiUtils.kt b/server/src/main/kotlin/ivy/learn/api/common/ApiUtils.kt index 03267294..a765506f 100644 --- a/server/src/main/kotlin/ivy/learn/api/common/ApiUtils.kt +++ b/server/src/main/kotlin/ivy/learn/api/common/ApiUtils.kt @@ -3,26 +3,32 @@ package ivy.learn.api.common import arrow.core.raise.Raise import arrow.core.raise.either import io.ktor.http.* -import io.ktor.server.application.* import io.ktor.server.response.* -import io.ktor.util.pipeline.* +import io.ktor.server.routing.* import ivy.learn.api.common.model.ServerError import ivy.learn.api.common.model.ServerErrorResponse -inline fun endpoint( +@IvyServerDsl +inline fun Routing.getEndpoint( + path: String, crossinline handler: suspend Raise.(Parameters) -> T -): suspend PipelineContext.(Unit) -> Unit = { - either { - handler(call.parameters) - }.onLeft { error -> - call.respond( - status = when (error) { - is ServerError.BadRequest -> HttpStatusCode.BadRequest - is ServerError.Unknown -> HttpStatusCode.InternalServerError - }, - message = ServerErrorResponse(error.msg) - ) - }.onRight { response -> - call.respond(HttpStatusCode.OK, response) +) { + get(path) { + either { + handler(call.parameters) + }.onLeft { error -> + call.respond( + status = when (error) { + is ServerError.BadRequest -> HttpStatusCode.BadRequest + is ServerError.Unknown -> HttpStatusCode.InternalServerError + }, + message = ServerErrorResponse(error.msg) + ) + }.onRight { response -> + call.respond(HttpStatusCode.OK, response) + } } -} \ No newline at end of file +} + +@DslMarker +annotation class IvyServerDsl \ No newline at end of file