official JetBrains project
GitHub license

HTTP API

Содержание


Зависимости

Gradle:

dependencies {
    implementation "io.ktor:ktor-server-core:$ktor_version"
    implementation "io.ktor:ktor-server-netty:$ktor_version"
    implementation "ch.qos.logback:logback-classic:$logback_version"
    implementation "io.ktor:ktor-serialization:$ktor_version"

    testImplementation "io.ktor:ktor-server-test-host:$ktor_version"
    testImplementation "org.jetbrains.kotlin:kotlin-test"
}
  • ktor-server-core добавляет основные компоненты Ktor в наш проект.
  • ktor-server-netty добавляет движок Netty в наш проект, позволяя нам использовать функциональность сервера без необходимости полагаться на внешний контейнер приложения.
  • logback-classic предоставляет реализацию SLF4J, позволяющую нам видеть красиво отформатированные журналы в нашей консоли.
  • ktor-serialization обеспечивает удобный механизм преобразования объектов Kotlin в сериализованную форму, такую как JSON, и наоборот.
  • tor-server-test-host позволяет нам тестировать части нашего приложения Ktor без необходимости использовать весь стек HTTP в процессе.

Конфигурации: application.conf и logback.xml

Репозиторий также включает в себя базовый application.conf в формате HOCON, расположенный в resourcesпапке. Ktor использует этот файл для определения порта, на котором он должен работать, а также определяет точку входа нашего приложения.

Также в ту же папку включен logback.xml файл, который устанавливает базовую структуру ведения журнала для нашего сервера.

Точка входа

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {

}

Точка входа в наше приложение важна, потому что мы устанавливаем плагины Ktor и определяем маршрутизацию для нашего API

File: Application.kt

Маршруты клиентов

Модель и хранение клиентов

Модель клиентов:

import kotlinx.serialization.Serializable

@Serializable
data class Customer(val id: String, val firstName: String, val lastName: String, val email: String)

Хранение клиентов:

val customerStorage = mutableListOf<Customer>()

File: Customer.kt

Определение маршрута для клиентов

import io.ktor.routing.*

fun Route.customerRouting() {
    route("/customer") {
        get {

        }
        get("{id}") {

        }
        post {

        }
        delete("{id}") {

        }
    }
}

File: CustomerRoutes.kt

Регистрация маршрутов

fun Application.registerCustomerRoutes() {
    routing {
        customerRouting()
    }
}

Заказ маршрутов

Определение модели

Классы данных:

@Serializable
data class Order(val number: String, val contents: List<OrderItem>)

@Serializable
data class OrderItem(val item: String, val amount: Int, val price: Double)

Образцы заказов:

val orderStorage = listOf(Order(
    "2020-04-06-01", listOf(
        OrderItem("Ham Sandwich", 2, 5.50),
        OrderItem("Water", 1, 1.50),
        OrderItem("Beer", 3, 2.30),
        OrderItem("Cheesecake", 1, 3.75)
    )),
    Order("2020-04-03-01", listOf(
        OrderItem("Cheeseburger", 1, 8.50),
        OrderItem("Water", 2, 1.50),
        OrderItem("Coke", 2, 1.76),
        OrderItem("Ice Cream", 1, 2.35)
    ))
)

File: Order.kt

Определение маршрутов заказов

Перечисление всех и индивидуальных заказов:

import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*

fun Route.listOrdersRoute() {
    get("/order") {
        if (orderStorage.isNotEmpty()) {
            call.respond(orderStorage)
        }
    }
}

fun Route.getOrderRoute() {
    get("/order/{id}") {
        val id = call.parameters["id"] ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)
        val order = orderStorage.find { it.number == id } ?: return@get call.respondText(
            "Not Found",
            status = HttpStatusCode.NotFound
        )
        call.respond(order)
    }
}

Подведение итогов по заказу:

fun Route.totalizeOrderRoute() {
    get("/order/{id}/total") {
        val id = call.parameters["id"] ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)
        val order = orderStorage.find { it.number == id } ?: return@get call.respondText(
            "Not Found",
            status = HttpStatusCode.NotFound
        )
        val total = order.contents.map { it.price * it.amount }.sum()
        call.respond(total)
    }
}

Регистрация маршрутов:

fun Application.registerOrderRoutes() {
    routing {
        listOrdersRoute()
        getOrderRoute()
        totalizeOrderRoute()
    }
}

File: OrderRoutes.kt

Тестирование

Ручное тестирование

POST http://127.0.0.1:8080/customer
Content-Type: application/json

{
  "id": "100",
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "[email protected]"
}


###
POST http://127.0.0.1:8080/customer
Content-Type: application/json

{
  "id": "200",
  "firstName": "John",
  "lastName": "Smith",
  "email": "[email protected]"
}

###
POST http://127.0.0.1:8080/customer
Content-Type: application/json

{
  "id": "300",
  "firstName": "Mary",
  "lastName": "Smith",
  "email": "[email protected]"
}


###
GET http://127.0.0.1:8080/customer
Accept: application/json

###
GET http://127.0.0.1:8080/customer/200

###
GET http://127.0.0.1:8080/customer/500

###
DELETE http://127.0.0.1:8080/customer/100

###
DELETE http://127.0.0.1:8080/customer/500

File: CustomerTest.http and OrderTest.http

Автоматизированное тестирование

import com.jetbrains.handson.httpapi.module
import io.ktor.http.*
import io.ktor.server.testing.*
import org.junit.Test
import kotlin.test.assertEquals

class OrderRouteTests {
    @Test
    fun testGetOrder() {
        withTestApplication({ module(testing = true) }) {
            handleRequest(HttpMethod.Get, "/order/2020-04-06-01").apply {
                assertEquals(
                    """{"number":"2020-04-06-01","contents":[{"item":"Ham Sandwich","amount":2,"price":5.5},{"item":"Water","amount":1,"price":1.5},{"item":"Beer","amount":3,"price":2.3},{"item":"Cheesecake","amount":1,"price":3.75}]}""",
                    response.content
                )
                assertEquals(HttpStatusCode.OK, response.status())
            }
        }
    }
}

File: OrderRouteTests.kt


This repository is the code corresponding to the hands-on lab Creating HTTP APIs.