-
-
Notifications
You must be signed in to change notification settings - Fork 331
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Part of #3611 --------- Co-authored-by: Li Haoyi <haoyi.sg@gmail.com>
- Loading branch information
1 parent
d43417e
commit d0cc6f8
Showing
11 changed files
with
406 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
//// SNIPPET:BUILD | ||
package build | ||
import mill._, kotlinlib._ | ||
|
||
object `package` extends RootModule with KotlinModule { | ||
|
||
def kotlinVersion = "1.9.24" | ||
|
||
def mainClass = Some("com.example.TodoMVCApplicationKt") | ||
|
||
val ktorVersion = "2.3.12" | ||
val exposedVersion = "0.53.0" | ||
|
||
def ivyDeps = Agg( | ||
ivy"io.ktor:ktor-server-core-jvm:$ktorVersion", | ||
ivy"io.ktor:ktor-server-netty-jvm:$ktorVersion", | ||
ivy"org.jetbrains.exposed:exposed-core:$exposedVersion", | ||
ivy"org.jetbrains.exposed:exposed-jdbc:$exposedVersion", | ||
ivy"com.h2database:h2:2.2.224", | ||
ivy"io.ktor:ktor-server-webjars-jvm:$ktorVersion", | ||
ivy"org.webjars:jquery:3.2.1", | ||
ivy"io.ktor:ktor-server-thymeleaf-jvm:$ktorVersion", | ||
ivy"org.webjars:webjars-locator:0.41", | ||
ivy"org.webjars.npm:todomvc-common:1.0.5", | ||
ivy"org.webjars.npm:todomvc-app-css:2.4.1", | ||
ivy"ch.qos.logback:logback-classic:1.4.14" | ||
) | ||
|
||
object test extends KotlinTests with TestModule.Junit5 { | ||
def ivyDeps = super.ivyDeps() ++ Agg( | ||
ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1", | ||
ivy"io.ktor:ktor-server-test-host-jvm:2.3.12" | ||
) | ||
} | ||
} | ||
|
||
|
||
// This example implementing the well known | ||
// https://todomvc.com/[TodoMVC] example app using Kotlin and Ktor. Apart from running a webserver, | ||
// this example also demonstrates: | ||
|
||
// * Serving HTML templates using Thymeleaf | ||
// * Serving static Javascript and CSS using Webjars | ||
// * Querying a SQL database using Exposed | ||
// * Testing using a H2 in-memory database | ||
|
||
/** Usage | ||
|
||
> mill test | ||
|
||
> mill runBackground | ||
|
||
> curl http://localhost:8091 | ||
...<h1>todos</h1>... | ||
|
||
> mill clean runBackground | ||
|
||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<configuration> | ||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||
<encoder> | ||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||
</encoder> | ||
</appender> | ||
<root level="trace"> | ||
<appender-ref ref="STDOUT"/> | ||
</root> | ||
<logger name="org.eclipse.jetty" level="INFO"/> | ||
<logger name="io.netty" level="INFO"/> | ||
</configuration> |
16 changes: 16 additions & 0 deletions
16
example/kotlinlib/web/2-todo-ktor/resources/templates/thymeleaf/fragments.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" lang="en"> | ||
<li th:fragment="todoItem(item)" th:classappend="${item.completed?'completed':''}"> | ||
<div class="view"> | ||
<form method="post" th:action="@{~/{id}/toggle(id=${item.id})}"> | ||
<input class="toggle" type="checkbox" | ||
onchange="this.form.submit()" | ||
th:attrappend="checked=${item.completed?'true':null}"> | ||
<label th:text="${item.title}">Taste JavaScript</label> | ||
</form> | ||
<form method="post" th:action="@{~/{id}/delete(id=${item.id})}"> | ||
<button class="destroy"></button> | ||
</form> | ||
</div> | ||
<input class="edit" value="Create a TodoMVC template"> | ||
</li> | ||
</html> |
84 changes: 84 additions & 0 deletions
84
example/kotlinlib/web/2-todo-ktor/resources/templates/thymeleaf/index.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<!doctype html> | ||
<html xmlns="http://www.w3.org/1999/xhtml" | ||
xmlns:th="http://www.thymeleaf.org" | ||
lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<title>Template • TodoMVC</title> | ||
<link rel="stylesheet" th:href="@{~/webjars/todomvc-common/base.css}"> | ||
<link rel="stylesheet" th:href="@{~/webjars/todomvc-app-css/index.css}"> | ||
</head> | ||
<body> | ||
<section class="todoapp"> | ||
<header class="header"> | ||
<h1>todos</h1> | ||
<p></p> | ||
<form th:action="@{~/save}" method="post" th:object="${item}"> | ||
<input class="new-todo" placeholder="What needs to be done?" autofocus | ||
id="item" name="title"> | ||
</form> | ||
</header> | ||
<!-- This section should be hidden by default and shown when there are todos --> | ||
<section class="main" th:if="${totalItemCount > 0}"> | ||
<form method="post" th:action="@{~/toggle-all}"> | ||
<input id="toggle-all" class="toggle-all" type="checkbox" | ||
onclick="this.form.submit()"> | ||
<label for="toggle-all">Mark all as complete</label> | ||
</form> | ||
<ul class="todo-list" th:remove="all-but-first"> | ||
<li th:insert="fragments :: todoItem(${item})" th:each="item : ${todoItems}" th:remove="tag"> | ||
</li> | ||
<!-- These are here just to show the structure of the list items --> | ||
<!-- List items should get the class `editing` when editing and `completed` when marked as completed --> | ||
<li class="completed"> | ||
<div class="view"> | ||
<input class="toggle" type="checkbox" checked> | ||
<label>Taste JavaScript</label> | ||
<button class="destroy"></button> | ||
</div> | ||
<input class="edit" value="Create a TodoMVC template"> | ||
</li> | ||
<li> | ||
<div class="view"> | ||
<input class="toggle" type="checkbox"> | ||
<label>Buy a unicorn</label> | ||
<button class="destroy"></button> | ||
</div> | ||
<input class="edit" value="Rule the web"> | ||
</li> | ||
</ul> | ||
</section> | ||
<!-- This footer should be hidden by default and shown when there are todos --> | ||
<footer class="footer" th:if="${totalItemCount > 0}"> | ||
<th:block th:unless="${activeItemCount == 1}"> | ||
<span class="todo-count"><strong th:text="${activeItemCount}">0</strong> items left</span> | ||
</th:block> | ||
<th:block th:if="${activeItemCount == 1}"> | ||
<span class="todo-count"><strong>1</strong> item left</span> | ||
</th:block> | ||
<ul class="filters"> | ||
<li> | ||
<a th:href="@{~/}" | ||
th:classappend="${todoFilter == 'ALL'?'selected':''}">All</a> | ||
</li> | ||
<li> | ||
<a th:href="@{~/active}" | ||
th:classappend="${todoFilter == 'ACTIVE'?'selected':''}">Active</a> | ||
</li> | ||
<li> | ||
<a th:href="@{~/completed}" | ||
th:classappend="${todoFilter == 'COMPLETED'?'selected':''}">Completed</a> | ||
</li> | ||
</ul> | ||
<form th:action="@{~/completed/delete}" | ||
th:if="${numberOfCompletedItems > 0}"> | ||
<button class="clear-completed">Clear completed</button> | ||
</form> | ||
</footer> | ||
</section> | ||
<footer class="info"> | ||
<p>Double-click to edit a todo</p> | ||
</footer> | ||
</body> | ||
</html> |
6 changes: 6 additions & 0 deletions
6
example/kotlinlib/web/2-todo-ktor/src/com/example/TodoItem.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.example | ||
|
||
import java.util.UUID | ||
|
||
data class TodoItem(val id: UUID , val title: String, val completed: Boolean = false) | ||
|
83 changes: 83 additions & 0 deletions
83
example/kotlinlib/web/2-todo-ktor/src/com/example/TodoItemController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package com.example | ||
|
||
import io.ktor.server.application.* | ||
import io.ktor.server.request.* | ||
import io.ktor.server.response.* | ||
import io.ktor.server.routing.* | ||
import io.ktor.server.thymeleaf.* | ||
import io.ktor.server.util.* | ||
import java.util.* | ||
|
||
enum class ListFilter { | ||
ALL, | ||
ACTIVE, | ||
COMPLETED | ||
} | ||
|
||
data class TodoItemFormData(var title: String? = null) | ||
|
||
fun modelContent( | ||
todos: List<TodoItem>, | ||
filter: ListFilter | ||
): Map<String, Any> { | ||
val activeItemCount = todos.count { !it.completed } | ||
val numberOfCompletedItems = todos.count { it.completed } | ||
|
||
val items = when (filter) { | ||
ListFilter.ALL -> todos | ||
ListFilter.ACTIVE -> todos.filterNot { it.completed } | ||
ListFilter.COMPLETED -> todos.filter { it.completed } | ||
} | ||
|
||
return mapOf( | ||
"item" to TodoItemFormData(), | ||
"todoItems" to items, | ||
"totalItemCount" to todos.size, | ||
"activeItemCount" to activeItemCount, | ||
"numberOfCompletedItems" to numberOfCompletedItems, | ||
"filter" to filter.name | ||
) | ||
} | ||
|
||
fun Application.configureRoutes(repository: TodoItemRepository) { | ||
routing { | ||
get("/") { | ||
val todos = repository.findAll() | ||
call.respond(ThymeleafContent("index", modelContent(todos, ListFilter.ALL))) | ||
} | ||
get("/active") { | ||
val todos = repository.findAll() | ||
call.respond(ThymeleafContent("index", modelContent(todos, ListFilter.ACTIVE))) | ||
} | ||
get("/completed") { | ||
val todos = repository.findAll() | ||
call.respond(ThymeleafContent("index", modelContent(todos, ListFilter.COMPLETED))) | ||
} | ||
get("/completed/delete") { | ||
repository.findAll().filter { it.completed } | ||
.forEach { repository.deleteById(it.id) } | ||
call.respondRedirect("/") | ||
} | ||
post("/save") { | ||
val title = call.receiveParameters().getOrFail("title") | ||
repository.save(TodoItem(UUID.randomUUID(), title)) | ||
call.respondRedirect("/") | ||
} | ||
post("/{id}/delete") { | ||
val id = call.parameters.getOrFail("id") | ||
repository.deleteById(UUID.fromString(id)) | ||
call.respondRedirect("/") | ||
} | ||
post("/{id}/toggle") { | ||
val id = call.parameters.getOrFail("id") | ||
val item = repository.findById(UUID.fromString(id)) | ||
repository.save(item.copy(completed = !item.completed)) | ||
call.respondRedirect("/") | ||
} | ||
post("/toggle-all") { | ||
repository.findAll().map { it.copy(completed = !it.completed) } | ||
.forEach { repository.save(it) } | ||
call.respondRedirect("/") | ||
} | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
example/kotlinlib/web/2-todo-ktor/src/com/example/TodoItemRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.example | ||
|
||
import kotlinx.coroutines.Dispatchers | ||
import org.jetbrains.exposed.sql.* | ||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq | ||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction | ||
import org.jetbrains.exposed.sql.transactions.transaction | ||
import java.util.UUID | ||
|
||
interface TodoItemRepository { | ||
suspend fun save(item: TodoItem) | ||
suspend fun findById(id: UUID): TodoItem | ||
suspend fun findAll(): List<TodoItem> | ||
suspend fun deleteById(id: UUID) | ||
} | ||
|
||
class TodoItemNotFound(id: String) : Exception("TodoItem $id not found") | ||
|
||
class TodoItemRepositoryImpl(database: Database) : TodoItemRepository { | ||
|
||
object TodoItems : Table() { | ||
val id = uuid("id") | ||
val title = varchar("title", length = 50) | ||
val completed = bool("completed") | ||
|
||
override val primaryKey = PrimaryKey(id) | ||
} | ||
|
||
init { | ||
transaction(database) { | ||
SchemaUtils.create(TodoItems) | ||
} | ||
} | ||
|
||
override suspend fun save(item: TodoItem): Unit = query { | ||
TodoItems.upsert { | ||
it[id] = item.id | ||
it[title] = item.title | ||
it[completed] = item.completed | ||
} | ||
} | ||
|
||
override suspend fun findById(id: UUID): TodoItem = query { | ||
TodoItems.selectAll().where(TodoItems.id eq id) | ||
.map { TodoItem(it[TodoItems.id], it[TodoItems.title], it[TodoItems.completed]) } | ||
.singleOrNull() ?: throw TodoItemNotFound(id.toString()) | ||
} | ||
|
||
|
||
override suspend fun findAll(): List<TodoItem> = query { | ||
TodoItems.selectAll() | ||
.map { TodoItem(it[TodoItems.id], it[TodoItems.title], it[TodoItems.completed]) } | ||
} | ||
|
||
|
||
override suspend fun deleteById(id: UUID): Unit = query { | ||
TodoItems.deleteWhere { TodoItems.id eq id } | ||
} | ||
|
||
private suspend fun <T> query(block: suspend () -> T): T = | ||
newSuspendedTransaction(Dispatchers.IO) { block() } | ||
} |
39 changes: 39 additions & 0 deletions
39
example/kotlinlib/web/2-todo-ktor/src/com/example/TodoMVCApplication.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package com.example | ||
|
||
import io.ktor.server.application.* | ||
import io.ktor.server.engine.* | ||
import io.ktor.server.netty.* | ||
import io.ktor.server.thymeleaf.* | ||
import io.ktor.server.webjars.* | ||
import org.jetbrains.exposed.sql.Database | ||
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver | ||
|
||
fun main(args: Array<String>) { | ||
val database = Database.connect("jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", "org.h2.Driver") | ||
val todoItemRepository = TodoItemRepositoryImpl(database) | ||
embeddedServer(Netty, port = 8091, host = "0.0.0.0"){ | ||
app(todoItemRepository) | ||
}.start(wait = true) | ||
} | ||
|
||
fun Application.configureTemplating() { | ||
install(Thymeleaf) { | ||
setTemplateResolver(ClassLoaderTemplateResolver().apply { | ||
prefix = "templates/thymeleaf/" | ||
suffix = ".html" | ||
characterEncoding = "utf-8" | ||
}) | ||
} | ||
} | ||
|
||
fun Application.configureWebjars() { | ||
install(Webjars) { | ||
path = "/webjars" //defaults to /webjars | ||
} | ||
} | ||
|
||
fun Application.app(todoItemRepository: TodoItemRepository) { | ||
configureTemplating() | ||
configureWebjars() | ||
configureRoutes(todoItemRepository) | ||
} |
Oops, something went wrong.