Browse Source

Everywhere: Implement HTMX todo page

master
Riyyi 1 month ago
parent
commit
6b93045514
  1. 11
      Package.resolved
  2. 8
      Package.swift
  3. 4
      Public/css/style.css
  4. 0
      Public/js/site.js
  5. 10
      README.org
  6. 82
      Sources/App/Controllers/API/TodoAPIController.swift
  7. 81
      Sources/App/Controllers/TodoController.swift
  8. 12
      Sources/App/DTOs/TodoDTO.swift
  9. 24
      Sources/App/Extensions/Elementary.swift
  10. 35
      Sources/App/Views/Components/TodosFormComponent.swift
  11. 48
      Sources/App/Views/Components/TodosTableComponent.swift
  12. 82
      Sources/App/Views/Pages/IndexPage.swift
  13. 37
      Sources/App/Views/Shared/MainLayout.swift
  14. 5
      Sources/App/Views/Shared/NavMenu.swift
  15. 24
      Sources/App/routes.swift
  16. 26
      Tests/AppTests/AppTests.swift
  17. 10
      requests

11
Package.resolved

@ -1,5 +1,5 @@
{
"originHash" : "928a4b649897cc0b7c73d0862d9400289c0503386d6f44998334e327c5931856",
"originHash" : "d39a0a797e191acb95e23a306e38d51e93c56adbbcfd76f4476a7deecf94537b",
"pins" : [
{
"identity" : "async-http-client",
@ -37,6 +37,15 @@
"version" : "0.4.1"
}
},
{
"identity" : "elementary-htmx",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sliemeobn/elementary-htmx.git",
"state" : {
"revision" : "3e09519a605be410e8332627b260cebaf74bb9fa",
"version" : "0.3.0"
}
},
{
"identity" : "fluent",
"kind" : "remoteSourceControl",

8
Package.swift

@ -17,14 +17,16 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio.git", from: "2.76.1"),
//
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.2.0"),
//
.package(url: "https://github.com/sliemeobn/elementary-htmx.git", from: "0.3.0"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
// .product(name: "ElementaryHTMX", package: "elementary-htmx"),
// .product(name: "ElementaryHTMXSSE", package: "elementary-htmx"),
// .product(name: "ElementaryHTMXWS", package: "elementary-htmx"),
.product(name: "ElementaryHTMX", package: "elementary-htmx"),
.product(name: "ElementaryHTMXSSE", package: "elementary-htmx"),
.product(name: "ElementaryHTMXWS", package: "elementary-htmx"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"),
.product(name: "NIOCore", package: "swift-nio"),

4
Public/css/style.css

@ -0,0 +1,4 @@
tr.htmx-swapping td {
opacity: 0;
transition: opacity 0.5s ease-out;
}

0
Public/js/site.js

10
README.org

@ -0,0 +1,10 @@
* Website
Website, written in Swift 6.
* Libraries
- [[https://github.com/vapor/vapor][Vapor]] HTTP web framework
- [[https://github.com/vapor/fluent.git][Fluent]] ORM
- [[https://github.com/sliemeobn/elementary][Elementary]] HTML templating
- [[https://github.com/bigskysoftware/htmx][htmx]] font-end JS

82
Sources/App/Controllers/API/TodoAPIController.swift

@ -0,0 +1,82 @@
import Fluent
import Vapor
struct TodoAPIController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("todos")
todos.get(use: index)
todos.post(use: create)
todos.group(":id") { todo in
todo.get(use: show)
todo.put(use: update)
todo.delete(use: delete)
}
// todos.delete(":id", use: delete)
}
@Sendable
func index(req: Request) async throws -> [TodoDTO] {
try await Todo.query(on: req.db).all().map { $0.toDTO() }
}
@Sendable
func show(req: Request) async throws -> TodoDTO {
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
let todo = try await Todo.find(uuid, on: req.db) else {
throw Abort(.notFound)
}
return todo.toDTO()
}
@Sendable
func create(req: Request) async throws -> TodoDTO {
let todo = try req.content.decode(TodoDTO.self).toModel()
try await todo.save(on: req.db)
return todo.toDTO()
}
@Sendable
func update(req: Request) async throws -> TodoDTO {
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
let todo = try await Todo.find(uuid, on: req.db) else {
throw Abort(.notFound)
}
let updatedTodo = try req.content.decode(Todo.self)
todo.title = updatedTodo.title
try await todo.save(on: req.db)
return todo.toDTO()
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
let todo = try await Todo.find(uuid, on: req.db) else {
throw Abort(.notFound)
}
try await todo.delete(on: req.db)
return .noContent
}
// -------------------------------------
private func hexToUUID(hex: String) -> UUID? {
var uuid: String = hex.replacingOccurrences(of: "-", with: "")
guard uuid.count == 32 else { return nil }
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 8))
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 12 + 1))
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 16 + 2))
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 20 + 3))
return UUID(uuidString: uuid)
}
}

81
Sources/App/Controllers/TodoController.swift

@ -1,60 +1,63 @@
import Elementary
import ElementaryHTMX
import ElementaryHTMXSSE
import ElementaryHTMXWS
import Fluent
import Vapor
import VaporElementary
struct TodoController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("todos")
todos.get(use: index)
todos.post(use: create)
todos.group(":id") { todo in
todo.get(use: show)
todo.put(use: update)
todo.delete(use: delete)
routes.group("todos") { todos in
todos.get(use: index)
todos.post(use: create)
todos.group(":id") { todo in
todo.delete(use: delete)
}
}
// todos.delete(":todoID", use: delete)
}
@Sendable
func index(req: Request) async throws -> [TodoDTO] {
try await Todo.query(on: req.db).all().map { $0.toDTO() }
func index(req: Request) async throws -> HTMLResponse {
let todos = try await todos(db: req.db)
return HTMLResponse {
MainLayout(title: "Todos") {
TodosTableComponent(name: "todos", todos: todos)
TodosFormComponent(name: "todos-form", target: "todos")
}
}
}
@Sendable
func show(req: Request) async throws -> TodoDTO {
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
let todo = try await Todo.find(uuid, on: req.db) else {
throw Abort(.notFound)
func create(req: Request) async throws -> HTMLResponse {
do {
try TodoDTO.validate(content: req)
}
catch let error as ValidationsError {
return HTMLResponse {
TodosFormComponent(name: "todos-form", target: "todos", errors: ["title": error.description])
}
}
return todo.toDTO()
}
@Sendable
func create(req: Request) async throws -> TodoDTO {
let todo = try req.content.decode(TodoDTO.self).toModel()
try await todo.save(on: req.db)
return todo.toDTO()
}
@Sendable
func update(req: Request) async throws -> TodoDTO {
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
let todo = try await Todo.find(uuid, on: req.db) else {
throw Abort(.notFound)
}
let todos = try await todos(db: req.db)
let updatedTodo = try req.content.decode(Todo.self)
todo.title = updatedTodo.title
try await todo.save(on: req.db)
return HTMLResponse {
// Return the empty form
TodosFormComponent(name: "todos-form", target: "todos")
return todo.toDTO()
// Also update the todos table
TodosTableComponent(name: "todos", todos: todos, refresh: true) // TODO: Put component names inside variables
}
}
@Sendable
func delete(req: Request) async throws -> HTTPStatus {
func delete(req: Request) async throws -> HTMLResponse {
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
let todo = try await Todo.find(uuid, on: req.db) else {
throw Abort(.notFound)
@ -62,11 +65,16 @@ struct TodoController: RouteCollection {
try await todo.delete(on: req.db)
return .noContent
return HTMLResponse {} // TODO: Return 204 No Content
}
// -------------------------------------
@Sendable
private func todos(db: any Database) async throws -> [TodoDTO] {
try await Todo.query(on: db).all().map { $0.toDTO() }
}
private func hexToUUID(hex: String) -> UUID? {
var uuid: String = hex.replacingOccurrences(of: "-", with: "")
@ -79,4 +87,5 @@ struct TodoController: RouteCollection {
return UUID(uuidString: uuid)
}
}

12
Sources/App/DTOs/TodoDTO.swift

@ -4,10 +4,10 @@ import Vapor
struct TodoDTO: Content {
var id: UUID?
var title: String?
func toModel() -> Todo {
let model = Todo()
model.id = self.id
if let title = self.title {
model.title = title
@ -15,3 +15,11 @@ struct TodoDTO: Content {
return model
}
}
extension TodoDTO: Validatable {
static func validations(_ validations: inout Validations) {
validations.add(
"title", as: String.self, is: !.empty, required: true,
customFailureDescription: "Title is required")
}
}

24
Sources/App/Extensions/Elementary.swift

@ -0,0 +1,24 @@
import Elementary
import ElementaryHTMX
// -----------------------------------------
// Elementary
public extension HTMLAttribute {
static func empty() -> HTMLAttribute {
return HTMLAttribute(name: "", value: "")
}
}
// -----------------------------------------
// ElementaryHTMX
public extension HTMLAttribute.hx {
static func swap(_ value: HTMLAttributeValue.HTMX.ModifiedSwapTarget, _ spec: String? = nil) -> HTMLAttribute {
if let spec {
.init(name: "hx-swap", value: "\(value.rawValue) \(spec)")
} else {
.init(name: "hx-swap", value: value.rawValue)
}
}
}

35
Sources/App/Views/Components/TodosFormComponent.swift

@ -0,0 +1,35 @@
import Elementary
import Fluent
struct TodosFormComponent: HTML {
var name: String
var target: String
var errors: [String: String] = [:]
// -------------------------------------
var content: some HTML {
div(.id("cdiv_" + name)) {
form(
.id("\(name)-form"),
.hx.post("/\(target)"), .hx.target("#cdiv_" + name), .hx.swap(.outerHTML)
) {
div(.class("row")) {
div(.class("col-11")) {
input(
.class("form-control"), .type(.text), .id("\(name)-title"),
.name("title"), .placeholder("Title")) // .required
}
div(.class("col-1")) {
button(.class("btn btn-success"), .type(.submit)) { "Add" }
}
}
if let error = errors["title"] {
div(.class("form-text text-danger px-2")) { "\(error)." }
}
}
}
}
}

48
Sources/App/Views/Components/TodosTableComponent.swift

@ -0,0 +1,48 @@
import Elementary
import ElementaryHTMX
import Fluent
struct TodosTableComponent: HTML {
var name: String
var todos: [TodoDTO]
var refresh: Bool = false
// -------------------------------------
var content: some HTML {
div(.id("cdiv_" + name), refresh ? .hx.swapOOB(.outerHTML) : .empty()) {
table(.class("table")) {
thead {
tr {
th { "#" }
th { "ID" }
th { "Title" }
th { "Modifier" }
}
}
tbody(
.hx.confirm("Are you sure?"), .hx.target("closest tr"),
.hx.swap(.outerHTML, "swap:0.5s")
) {
for (index, todo) in todos.enumerated() {
tr {
td { "\(index)" }
td { todo.id?.uuidString ?? "" }
td { todo.title ?? "" }
td {
if let id = todo.id {
button(
.class("btn btn-danger"),
.hx.delete("/\(name)/\(id.uuidString)")
) { "Delete" }
}
}
}
}
}
}
}
}
}

82
Sources/App/Views/Pages/IndexPage.swift

@ -8,51 +8,51 @@ struct IndexPage: HTML {
}
p { "The navigation bar will stay at the top of the page as you scroll down." }
p {
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt arcu
sit amet leo rutrum luctus. Sed metus mi, consectetur vitae dui at, sodales
dignissim odio. Curabitur a nisi eros. Suspendisse semper ac justo non gravida.
Vestibulum accumsan interdum varius. Morbi at diam luctus, mattis mi nec, mollis
libero. Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Integer congue, nisl in tempus ultricies, ligula est
laoreet elit, sed elementum erat dui ac nisl. Aenean pulvinar arcu eget urna
venenatis, at dictum arcu ultrices. Etiam hendrerit, purus vitae sagittis
lobortis, nisi sapien vestibulum arcu, ut mattis arcu elit ut arcu. Nulla vitae
sem ac eros ullamcorper efficitur ut id arcu. Vestibulum euismod arcu eget
aliquet tempor. Nullam nec consequat magna. Etiam posuere, ipsum id condimentum
mollis, massa libero efficitur lectus, nec tempor mauris tellus ut ligula.
Quisque viverra diam velit, quis ultricies nisl lacinia vitae.
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt arcu
sit amet leo rutrum luctus. Sed metus mi, consectetur vitae dui at, sodales
dignissim odio. Curabitur a nisi eros. Suspendisse semper ac justo non gravida.
Vestibulum accumsan interdum varius. Morbi at diam luctus, mattis mi nec, mollis
libero. Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Integer congue, nisl in tempus ultricies, ligula est
laoreet elit, sed elementum erat dui ac nisl. Aenean pulvinar arcu eget urna
venenatis, at dictum arcu ultrices. Etiam hendrerit, purus vitae sagittis
lobortis, nisi sapien vestibulum arcu, ut mattis arcu elit ut arcu. Nulla vitae
sem ac eros ullamcorper efficitur ut id arcu. Vestibulum euismod arcu eget
aliquet tempor. Nullam nec consequat magna. Etiam posuere, ipsum id condimentum
mollis, massa libero efficitur lectus, nec tempor mauris tellus ut ligula.
Quisque viverra diam velit, quis ultricies nisl lacinia vitae.
Aliquam libero nibh, luctus vel augue at, congue feugiat risus. Mauris volutpat
eget eros ac congue. Duis venenatis, arcu vel sodales accumsan, diam mi posuere
mi, vitae rutrum ex mauris nec libero. Sed eleifend nulla magna, eu lobortis
mauris fringilla nec. In posuere dignissim eros, ut hendrerit quam lacinia eu.
Praesent vestibulum arcu enim, hendrerit convallis risus facilisis eu. Nunc
vitae mauris eu nulla laoreet rhoncus. Nullam ligula tellus, vulputate in
viverra nec, eleifend eu nulla. Praesent suscipit rutrum imperdiet.
Aliquam libero nibh, luctus vel augue at, congue feugiat risus. Mauris volutpat
eget eros ac congue. Duis venenatis, arcu vel sodales accumsan, diam mi posuere
mi, vitae rutrum ex mauris nec libero. Sed eleifend nulla magna, eu lobortis
mauris fringilla nec. In posuere dignissim eros, ut hendrerit quam lacinia eu.
Praesent vestibulum arcu enim, hendrerit convallis risus facilisis eu. Nunc
vitae mauris eu nulla laoreet rhoncus. Nullam ligula tellus, vulputate in
viverra nec, eleifend eu nulla. Praesent suscipit rutrum imperdiet.
Curabitur in lacus eu diam cursus viverra non eget turpis. Donec a ornare ipsum,
sed egestas orci. Vivamus congue gravida elementum. Pellentesque vitae mauris
magna. Phasellus blandit urna vitae auctor consectetur. Aenean iaculis eget arcu
vitae ultricies. Nunc maximus, massa hendrerit faucibus fringilla, eros quam
consequat enim, sit amet sodales erat quam eget massa.
Curabitur in lacus eu diam cursus viverra non eget turpis. Donec a ornare ipsum,
sed egestas orci. Vivamus congue gravida elementum. Pellentesque vitae mauris
magna. Phasellus blandit urna vitae auctor consectetur. Aenean iaculis eget arcu
vitae ultricies. Nunc maximus, massa hendrerit faucibus fringilla, eros quam
consequat enim, sit amet sodales erat quam eget massa.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum in venenatis
urna. Vestibulum lectus arcu, scelerisque in ipsum vitae, feugiat cursus tortor.
Maecenas aliquam nunc enim, id fringilla est mattis et. Vivamus vitae
ullamcorper erat. Sed quis vehicula felis, quis bibendum nunc. Duis semper
fermentum ante, id fermentum neque varius convallis. Donec dui leo, fringilla
nec massa nec, fermentum molestie purus. Donec eget feugiat velit. Nulla
facilisi. Cras maximus felis eu libero mollis consectetur. Nulla molestie vitae
neque venenatis porta.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum in venenatis
urna. Vestibulum lectus arcu, scelerisque in ipsum vitae, feugiat cursus tortor.
Maecenas aliquam nunc enim, id fringilla est mattis et. Vivamus vitae
ullamcorper erat. Sed quis vehicula felis, quis bibendum nunc. Duis semper
fermentum ante, id fermentum neque varius convallis. Donec dui leo, fringilla
nec massa nec, fermentum molestie purus. Donec eget feugiat velit. Nulla
facilisi. Cras maximus felis eu libero mollis consectetur. Nulla molestie vitae
neque venenatis porta.
Vestibulum nunc diam, mattis eu bibendum at, sagittis vitae nunc. Duis lacinia
sodales enim, et elementum libero posuere et. Donec ut fringilla orci. Donec at
aliquet ipsum, quis mattis risus. Donec malesuada enim in egestas blandit. Proin
sagittis mauris magna, elementum faucibus metus tempus eu. Sed in sem ut tellus
porta luctus et sed diam. Donec felis ante, euismod a est vitae, mollis
condimentum nulla.
"""
Vestibulum nunc diam, mattis eu bibendum at, sagittis vitae nunc. Duis lacinia
sodales enim, et elementum libero posuere et. Donec ut fringilla orci. Donec at
aliquet ipsum, quis mattis risus. Donec malesuada enim in egestas blandit. Proin
sagittis mauris magna, elementum faucibus metus tempus eu. Sed in sem ut tellus
porta luctus et sed diam. Donec felis ante, euismod a est vitae, mollis
condimentum nulla.
"""
}
}
}

37
Sources/App/Views/Shared/MainLayout.swift

@ -1,11 +1,12 @@
import Elementary
// https://www.srihash.org/
extension MainLayout: Sendable where Body: Sendable {}
struct MainLayout<Body: HTML>: HTMLDocument {
var title: String
@HTMLBuilder var pageContent: Body // This var name can't be changed!
// https://www.srihash.org/
var head: some HTML {
meta(.charset(.utf8))
meta(.name(.viewport), .content("width=device-width, initial-scale=1.0"))
@ -18,14 +19,17 @@ struct MainLayout<Body: HTML>: HTMLDocument {
.href("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"),
.integrity("sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"),
.crossorigin(.anonymous))
link(.rel(.stylesheet), .href("/style.css"))
link(.rel(.stylesheet), .href("/css/style.css"))
// ---------------------------------
// Style
style {
"""
body {
padding-top: 56px;
}
"""
"""
body {
padding-top: 56px;
}
"""
}
}
@ -38,10 +42,10 @@ struct MainLayout<Body: HTML>: HTMLDocument {
}
// ---------------------------------
// Body
// Content
main {
div(.class("cotainer mt-4")) {
div(.class("container mt-4")) {
div(.class("content px-4 pb-4")) {
pageContent
}
@ -57,6 +61,21 @@ struct MainLayout<Body: HTML>: HTMLDocument {
.integrity("sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"),
.crossorigin(.anonymous)
) {}
script(
.src("https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js"),
.integrity("sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq"),
.crossorigin(.anonymous)
) {}
script(
.src("https://cdn.jsdelivr.net/npm/htmx-ext-sse@2.2.2/sse.min.js"),
.integrity("sha384-yhS+rWHB2hwrHEg86hWiQV7XL6u+PH9X+3BlmS2+CNBaGYU8Nd7RZ2rZ9DWXgTdr"),
.crossorigin(.anonymous)
) {}
script(
.src("https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.1/ws.min.js"),
.integrity("sha384-yhWpPsq2os1hEnx1I8cH7Ius6rclwTm3G2fhXDLF6Pzv7UnSsXY7BAj4fB6PIgSz"),
.crossorigin(.anonymous)
) {}
script(.src("/js/site.js")) {}
}

5
Sources/App/Views/Shared/NavMenu.swift

@ -15,7 +15,10 @@ struct NavMenu: HTML {
div(.class("collapse navbar-collapse"), .id("navbarNav")) {
ul(.class("navbar-nav me-auto mb-2 mb-lg-0")) {
li(.class("nav-item")) {
a(.class("nav-link active"), .href("#")) { "Home" }
a(.class("nav-link active"), .href("/")) { "Home" }
}
li(.class("nav-item")) {
a(.class("nav-link active"), .href("/todos")) { "Todos" }
}
li(.class("nav-item")) {
a(.class("nav-link"), .href("#")) { "About" }

24
Sources/App/routes.swift

@ -6,27 +6,23 @@ import VaporElementary
func routes(_ app: Application) throws {
app.routes.caseInsensitive = true
app.get { req async throws in
let todo = Todo(title: "Test Todo")
try await todo.save(on: req.db)
return "It works!"
}
app.get("hello") { req async -> String in
"Hello, world!"
}
app.get("test") { _ in
app.get() { req async throws in
HTMLResponse {
MainLayout(title: "Test123") {
MainLayout(title: "Homepage") {
IndexPage()
}
}
}
app.get("hello") { req async -> String in
"Hello, world!"
}
try app.register(collection: TodoController())
try app.group("api") { api in
try api.register(collection: TodoAPIController())
}
}
/*

26
Tests/AppTests/AppTests.swift

@ -9,9 +9,9 @@ struct AppTests {
let app = try await Application.make(.testing)
do {
try await configure(app)
try await app.autoMigrate()
try await app.autoMigrate()
try await test(app)
try await app.autoRevert()
try await app.autoRevert()
}
catch {
try await app.asyncShutdown()
@ -19,7 +19,7 @@ struct AppTests {
}
try await app.asyncShutdown()
}
@Test("Test Hello World Route")
func helloWorld() async throws {
try await withApp { app in
@ -29,26 +29,26 @@ struct AppTests {
})
}
}
@Test("Getting all the Todos")
func getAllTodos() async throws {
try await withApp { app in
let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
try await sampleTodos.create(on: app.db)
try await app.test(.GET, "todos", afterResponse: { res async throws in
try await app.test(.GET, "api/todos", afterResponse: { res async throws in
#expect(res.status == .ok)
#expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} )
})
}
}
@Test("Creating a Todo")
func createTodo() async throws {
let newDTO = TodoDTO(id: nil, title: "test")
try await withApp { app in
try await app.test(.POST, "todos", beforeRequest: { req in
try await app.test(.POST, "api/todos", beforeRequest: { req in
try req.content.encode(newDTO)
}, afterResponse: { res async throws in
#expect(res.status == .ok)
@ -58,15 +58,15 @@ struct AppTests {
})
}
}
@Test("Deleting a Todo")
func deleteTodo() async throws {
let testTodos = [Todo(title: "test1"), Todo(title: "test2")]
try await withApp { app in
try await testTodos.create(on: app.db)
try await app.test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
try await app.test(.DELETE, "api/todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
#expect(res.status == .noContent)
let model = try await Todo.find(testTodos[0].id, on: app.db)
#expect(model == nil)

10
requests

@ -14,7 +14,7 @@
# ------------------------------------------
GET http://localhost:8080/todos
GET http://localhost:8080/api/todos
-> jq-set-var :id .[0].id
Accept: application/json
@ -22,12 +22,12 @@ Accept: application/json
# ------------------------------------------
GET http://localhost:8080/todos/:id
GET http://localhost:8080/api/todos/:id
Accept: application/json
# ------------------------------------------
POST http://localhost:8080/todos
POST http://localhost:8080/api/todos
-> run-hook (restclient-set-var ":id" (cdr (assq 'id (json-read))))
Accept: application/json
Content-Type: application/json
@ -38,7 +38,7 @@ Content-Type: application/json
# ------------------------------------------
PUT http://localhost:8080/todos/:id
PUT http://localhost:8080/api/todos/:id
-> run-hook (restclient-set-var ":id" (cdr (assq 'id (json-read))))
Accept: application/json
Content-Type: application/json
@ -55,7 +55,7 @@ Content-Type: application/json
# INSERT(INSERT(INSERT(INSERT(HEX(id), 9, 0, '-'), 14, 0, '-'), 19, 0, '-'), 24, 0, '-') AS id,
# title FROM riyyi.todos;
DELETE http://localhost:8080/todos/:id
DELETE http://localhost:8080/api/todos/:id
Accept: application/json
# ------------------------------------------

Loading…
Cancel
Save