为典型的 REST 实现嵌套 Ktor 路由匹配器和处理程序的正确方法是什么?

What's the correct way to nest Ktor route matchers and handlers for a typical REST implementation?

我在理解为 request routing 使用 Ktor 的 DSL 的正确方法时遇到了一些麻烦。 问题是,当我测试我的 API 并尝试 GET /nomenclature/articles/categories 应该 return 所有文章类别的列表时,我得到 Invalid article specified 我 [=28] 的消息=] 用于 /nomenclature/articles/{articleId} 路由,当 articleId 参数无效时。

这是我的代码:

route("/nomenclature") {
    method(HttpMethod.Get) {
        handle { call.respondText("The resource you accessed is not a valid REST resource, but a parent node. Children nodes include articles, categories, subcategories and so on.") }
    }
    route("articles") {
        route("categories") {
            get("{categoryId?}") {
                val categoryIdParam = call.parameters["categoryId"]
                if (categoryIdParam != null) {
                    val categoryId = categoryIdParam.toIntOrNull()
                    if (categoryId != null) {
                        val category = articlesDAO.findCategoryById(categoryId)
                        if (category != null) call.respond(category)
                        else call.respondText("Category not found", status = HttpStatusCode.NotFound)
                    } else call.respondText("Invalid category ID specified", status = HttpStatusCode.BadRequest)
                } else {
                    val categories = articlesDAO.getCategories()
                    if (categories != null) call.respond(categories)
                    else call.respondText("No categories found", status = HttpStatusCode.NotFound)
                }
            }
        }
        route("subcategories") {
            get("{subcategoryId?}") {
                val subcategoryIdParam = call.parameters["subcategoryId"]
                if (subcategoryIdParam != null) {
                    val subcategoryId = subcategoryIdParam.toIntOrNull()
                    if (subcategoryId != null) {
                        val subcategory = articlesDAO.findSubcategoryById(subcategoryId)
                        if (subcategory != null) call.respond(subcategory)
                        else call.respondText("Subcategory not found", status = HttpStatusCode.NotFound)
                    } else call.respondText("Invalid subcategory ID specified", status = HttpStatusCode.BadRequest)
                } else {
                    val subcategories = articlesDAO.getCategories()
                    if (subcategories != null) call.respond(subcategories)
                    else call.respondText("No subcategories found", status = HttpStatusCode.NotFound)
                }
            }
        }
        get("types") {
            val articleTypes = articlesDAO.getArticleTypes()
            if (articleTypes != null) call.respond(articleTypes)
            else call.respondText("No article types found", status = HttpStatusCode.NotFound)
        }
        get("wholePackagings") {
            val wholePackagings = articlesDAO.getWholePackagings()
            if (wholePackagings != null) call.respond(wholePackagings)
            else call.respondText("No whole packagings found", status = HttpStatusCode.NotFound)
        }
        get("fractionsPackagings") {
            val fractionsPackagings = articlesDAO.getFractionPackagings()
            if (fractionsPackagings != null) call.respond(fractionsPackagings)
            else call.respondText("No fractions packagings found", status = HttpStatusCode.NotFound)
        }
        get("{articleId?}") {
            val articleIdParam = call.parameters["articleId"]
            if (articleIdParam != null) {
                val articleId = articleIdParam.toIntOrNull()
                if (articleId != null) {
                    val article = articlesDAO.findArticleById(articleId)
                    if (article != null) call.respond(article) else call.respondText("No article found", status = HttpStatusCode.NotFound)
                } else call.respondText("Invalid article ID specified", status = HttpStatusCode.BadRequest)
            } else {
                val articles = articlesDAO.getArticles()
                if (articles != null) call.respond(articles) else call.respondText("No articles found", status = HttpStatusCode.NotFound)
            }
        }
    }
}

如果有人能提供一个基本但有些全面的示例来帮助我说明如何使用所有 Ktor 路由匹配器和处理程序(包括以嵌套资源方式),那就太好了。

编辑: 我已经用不同的方法重写了路由,但我仍然想知道为什么我以前的版本没有按预期工作。 这是我的第二种方法,它似乎按预期工作(我已经测试过了):

routing {
    route("/v1") {
        route("stock") {
            get {
                val stock = stockDAO.getAllStock()
                if (stock != null) call.respond(stock) else call.respondText("No stock found", status = HttpStatusCode.NotFound)
            }
            get("{locationId}") {
                val locationIdParam = call.parameters["locationId"]
                if (locationIdParam != null) {
                    val locationId = locationIdParam.toIntOrNull()
                    if (locationId != null) {
                        val stock = stockDAO.getStockByLocationId(locationId)
                        if (stock != null) call.respond(stock) else call.respondText("No stock found", status = HttpStatusCode.NotFound)
                    } else call.respondText("ERROR: Invalid location ID specified.", status = HttpStatusCode.BadRequest)
                }
            }
        }

        route("nomenclature") {
            get {
                call.respondText("The resource you accessed is not a valid REST resource, but a parent node. Children nodes include articles, categories, subcategories and so on.")
            }

            route("articles") {
                get {
                    val articles = articlesDAO.getArticles()
                    if (articles != null) call.respond(articles) else call.respondText("No articles found", status = HttpStatusCode.NotFound)
                }
                get("{articleId}") {
                    val articleIdParam = call.parameters["articleId"]
                    if (articleIdParam != null) {
                        val articleId = articleIdParam.toIntOrNull()
                        if (articleId != null) {
                            val article = articlesDAO.findArticleById(articleId)
                            if (article != null) call.respond(article) else call.respondText("No article found", status = HttpStatusCode.NotFound)
                        } else call.respondText("Invalid article ID specified", status = HttpStatusCode.BadRequest)
                    }
                }

                route("categories") {
                    get {
                        val categories = articlesDAO.getCategories()
                        if (categories != null) call.respond(categories)
                        else call.respondText("No categories found", status = HttpStatusCode.NotFound)
                    }
                    get("{categoryId}") {
                        val categoryIdParam = call.parameters["categoryId"]
                        if (categoryIdParam != null) {
                            val categoryId = categoryIdParam.toIntOrNull()
                            if (categoryId != null) {
                                val category = articlesDAO.findCategoryById(categoryId)
                                if (category != null) call.respond(category)
                                else call.respondText("Category not found", status = HttpStatusCode.NotFound)
                            } else call.respondText("Invalid category ID specified", status = HttpStatusCode.BadRequest)
                        }
                    }
                }

                route("subcategories") {
                    get {
                        val subcategories = articlesDAO.getSubcategories()
                        if (subcategories != null) call.respond(subcategories)
                        else call.respondText("No subcategories found", status = HttpStatusCode.NotFound)
                    }
                    get("{subcategoryId}") {
                        val subcategoryIdParam = call.parameters["subcategoryId"]
                        if (subcategoryIdParam != null) {
                            val subcategoryId = subcategoryIdParam.toIntOrNull()
                            if (subcategoryId != null) {
                                val subcategory = articlesDAO.findSubcategoryById(subcategoryId)
                                if (subcategory != null) call.respond(subcategory)
                                else call.respondText("Subcategory not found", status = HttpStatusCode.NotFound)
                            } else call.respondText("Invalid subcategory ID specified", status = HttpStatusCode.BadRequest)
                        }
                    }
                }

                get("types") {
                    val articleTypes = articlesDAO.getArticleTypes()
                    if (articleTypes != null) call.respond(articleTypes)
                    else call.respondText("No article types found", status = HttpStatusCode.NotFound)
                }
                get("wholePackagings") {
                    val wholePackagings = articlesDAO.getWholePackagings()
                    if (wholePackagings != null) call.respond(wholePackagings)
                    else call.respondText("No whole packagings found", status = HttpStatusCode.NotFound)
                }
                get("fractionsPackagings") {
                    val fractionsPackagings = articlesDAO.getFractionPackagings()
                    if (fractionsPackagings != null) call.respond(fractionsPackagings)
                    else call.respondText("No fractions packagings found", status = HttpStatusCode.NotFound)
                }
            }
        }
    }
}

原因是您没有为GET /nomenclature/articles/categories指定任何处理程序。

你为/articles设置了路由,然后为/categories设置了路由,但是里面没有handler。 里面只有get("{categoryId?}"),不匹配,因为没有尾卡

得到/nomenclature/articles/{articleId}的原因是,它首先尝试匹配/articles,但由于它无法匹配/categories(没有处理程序),它继续寻找并最后找到最后一条路由,其中​​包含一个通配符参数并匹配。

如果您想为该特定路由设置处理程序,方法如下:

route("articles") {
    route("categories") {
        get("{categoryId?}") {
            ...
        }
        get {
            ... your code ...
        }
    }
}