-
Notifications
You must be signed in to change notification settings - Fork 221
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cb7da43
commit e6f01ea
Showing
7 changed files
with
316 additions
and
174 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<script type="text/javascript" src="main.js"></script> | ||
<style type="text/css"> | ||
body { | ||
background-color: #f0ffff; | ||
} | ||
|
||
.app { | ||
margin: auto; | ||
width: 50%; | ||
padding: 10px; | ||
} | ||
|
||
.form { | ||
bottom: 0; | ||
left: 0; | ||
right: 0; | ||
height: 60px; | ||
|
||
padding-top: 5px; | ||
} | ||
.header { | ||
height: 71px; | ||
padding: 5px; | ||
} | ||
.todos { | ||
top: 81px; | ||
left: 10px; | ||
right: 5px; | ||
padding: 10px; | ||
bottom: 70px; | ||
overflow-y: scroll; | ||
border: 1px solid black; | ||
|
||
background-color: lightyellow; | ||
} | ||
|
||
.form label { | ||
display: block; | ||
color: #f7861f; | ||
} | ||
|
||
.form input { | ||
height: 30px; | ||
} | ||
.form input[type=text] { | ||
display: block; | ||
width: 100%; | ||
box-sizing: border-box; | ||
} | ||
.form input[type=button] { | ||
float: right; | ||
width: 50px; | ||
|
||
/*border: 1px solid #d3d3d3;*/ | ||
border: 0; | ||
border-radius: 4px; | ||
background-color: #5e79cb; | ||
color: white; | ||
transition: background-color 500ms; | ||
} | ||
.form input[type=button]:hover { | ||
background-color: #679af5; | ||
} | ||
.form .input-container { | ||
margin-right: 60px; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div class="app"> | ||
<div class="header"> | ||
<h1>Todo List</h1> | ||
|
||
</div> | ||
<ul class="todos" id="todos"> | ||
</ul> | ||
|
||
<div class="form"> | ||
<input type="button" id="addButton" value="Add"> | ||
<div class="input-container"> | ||
<input type="text" id="todoInput"> | ||
</div> | ||
</div> | ||
</div> | ||
</body> | ||
</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,62 @@ | ||
// This is synchronous for simplicity. | ||
function client(method, endpoint, body) { | ||
var api = "http://" + window.location.host + endpoint; | ||
var xmlHttp = new XMLHttpRequest(); | ||
xmlHttp.open(method, api, false); | ||
|
||
if (body != null) { | ||
xmlHttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); | ||
} | ||
|
||
xmlHttp.send(body); | ||
return JSON.parse(xmlHttp.responseText); | ||
} | ||
|
||
function getTodos() { | ||
var todos = document.getElementById("todos"); | ||
todos.innerHTML = ""; | ||
|
||
var response = client("GET", "/todos", null); | ||
|
||
for (var i in response) | ||
{ | ||
var li = document.createElement("li"); | ||
var cb = document.createElement("input"); | ||
cb.setAttribute("type", "checkbox"); | ||
|
||
if (response[i].completed) { | ||
cb.setAttribute("checked", ""); | ||
} | ||
|
||
cb.setAttribute("todo-id", response[i].id); | ||
cb.onclick = toggleTodo; | ||
|
||
li.appendChild(cb); | ||
li.appendChild(document.createTextNode(response[i].title)); | ||
todos.appendChild(li); | ||
} | ||
} | ||
|
||
function postTodo() { | ||
var input = document.getElementById("todoInput"); | ||
if (input && input.value != "") { | ||
client("POST", "/todos", JSON.stringify({ "title": input.value })); | ||
input.value = ""; | ||
getTodos(); | ||
} | ||
} | ||
|
||
function toggleTodo() { | ||
client("PATCH", "/todos/" + this.getAttribute("todo-id"), JSON.stringify({ "completed": this.checked })); | ||
} | ||
|
||
function init() { | ||
if (document.getElementById("addButton")) { | ||
document.getElementById("addButton").onclick = postTodo; | ||
getTodos(); | ||
} else { | ||
setTimeout(init, 300); | ||
} | ||
} | ||
|
||
init(); |
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,67 @@ | ||
package io.finch.todo | ||
|
||
import cats.effect.{ContextShift, IO} | ||
import cats.effect.concurrent.Ref | ||
import com.twitter.finagle.Service | ||
import com.twitter.finagle.http.{Request, Response, Status} | ||
import io.circe.generic.auto._ | ||
import io.finch._ | ||
import io.finch.circe._ | ||
|
||
class App( | ||
idRef: Ref[IO, Int], | ||
storeRef: Ref[IO, Map[Int, Todo]] | ||
)( | ||
implicit S: ContextShift[IO] | ||
) extends Endpoint.Module[IO] { | ||
|
||
final val postedTodo: Endpoint[IO, Todo] = | ||
jsonBody[(Int, Boolean) => Todo].mapAsync(pt => idRef.modify(id => (id + 1, pt(id, false)))) | ||
|
||
final val patchedTodo: Endpoint[IO, Todo => Todo] = | ||
jsonBody[Todo => Todo] | ||
|
||
final val postTodo: Endpoint[IO, Todo] = post("todos" :: postedTodo) { t: Todo => | ||
storeRef.modify { store => | ||
(store + (t.id -> t), Created(t)) | ||
} | ||
} | ||
|
||
final val patchTodo: Endpoint[IO, Todo] = | ||
patch("todos" :: path[Int] :: patchedTodo) { (id: Int, pt: Todo => Todo) => | ||
storeRef.modify { store => | ||
store.get(id) match { | ||
case Some(currentTodo) => | ||
val newTodo = pt(currentTodo) | ||
(store + (id -> newTodo), Ok(newTodo)) | ||
case None => | ||
(store, Output.empty(Status.NotFound)) | ||
} | ||
} | ||
} | ||
|
||
final val getTodos: Endpoint[IO, List[Todo]] = get("todos") { | ||
storeRef.get.map(m => Ok(m.values.toList.sortBy(- _.id))) | ||
} | ||
|
||
final val deleteTodo: Endpoint[IO, Todo] = delete("todos" :: path[Int]) { id: Int => | ||
storeRef.modify { store => | ||
store.get(id) match { | ||
case Some(t) => (store - id, Ok(t)) | ||
case None => (store, Output.empty(Status.NotFound)) | ||
} | ||
} | ||
} | ||
|
||
final val deleteTodos: Endpoint[IO, List[Todo]] = delete("todos") { | ||
storeRef.modify { store => | ||
(Map.empty, Ok(store.values.toList.sortBy(- _.id))) | ||
} | ||
} | ||
|
||
final def toService: Service[Request, Response] = Bootstrap | ||
.serve[Application.Json](getTodos :+: postTodo :+: deleteTodo :+: deleteTodos :+: patchTodo) | ||
.serve[Text.Html](classpathAsset("/todo/index.html")) | ||
.serve[Application.Javascript](classpathAsset("/todo/main.js")) | ||
.toService | ||
} |
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 |
---|---|---|
@@ -1,100 +1,53 @@ | ||
package io.finch.todo | ||
|
||
import java.util.UUID | ||
|
||
import cats.effect.IO | ||
import cats.effect.concurrent.Ref | ||
import com.twitter.app.Flag | ||
import com.twitter.finagle.{Http, Service} | ||
import com.twitter.finagle.http.{Request, Response} | ||
import com.twitter.finagle.stats.Counter | ||
import com.twitter.finagle.Http | ||
import com.twitter.server.TwitterServer | ||
import com.twitter.util.Await | ||
import io.circe.generic.auto._ | ||
import io.finch._ | ||
import io.finch.circe._ | ||
import scala.concurrent.ExecutionContext | ||
|
||
/** | ||
* A simple Finch application implementing the backend for the TodoMVC project. | ||
* A simple Finch server serving a TODO application. | ||
* | ||
* Use the following sbt command to run the application. | ||
* | ||
* {{{ | ||
* $ sbt 'examples/runMain io.finch.todo.Main' | ||
* }}} | ||
* | ||
* Use the following HTTPie commands to test endpoints. | ||
* Open your browser at `http://localhost:8081/todo/index.html` or use the following HTTPie | ||
* commands to test endpoints. | ||
* | ||
* {{{ | ||
* $ http POST :8081/todos title=foo order:=0 completed:=false | ||
* $ http PATCH :8081/todos/<UUID> completed:=true | ||
* $ http POST :8081/todos title=foo | ||
* $ http PATCH :8081/todos/<ID> completed:=true | ||
* $ http :8081/todos | ||
* $ http DELETE :8081/todos/<UUID> | ||
* $ http DELETE :8081/todos/<ID> | ||
* $ http DELETE :8081/todos | ||
* }}} | ||
*/ | ||
object Main extends TwitterServer with Endpoint.Module[IO] { | ||
|
||
val port: Flag[Int] = flag("port", 8081, "TCP port for HTTP server") | ||
|
||
val todos: Counter = statsReceiver.counter("todos") | ||
|
||
def postedTodo: Endpoint[IO, Todo] = jsonBody[UUID => Todo].map(_(UUID.randomUUID())) | ||
|
||
def postTodo: Endpoint[IO, Todo] = post("todos" :: postedTodo) { t: Todo => | ||
todos.incr() | ||
Todo.save(t) | ||
object Main extends TwitterServer { | ||
|
||
Created(t) | ||
} | ||
|
||
def patchedTodo: Endpoint[IO, Todo => Todo] = jsonBody[Todo => Todo] | ||
private val port: Flag[Int] = flag("port", 8081, "TCP port for HTTP server") | ||
|
||
def patchTodo: Endpoint[IO, Todo] = | ||
patch("todos" :: path[UUID] :: patchedTodo) { (id: UUID, pt: Todo => Todo) => | ||
Todo.get(id) match { | ||
case Some(currentTodo) => | ||
val newTodo: Todo = pt(currentTodo) | ||
Todo.delete(id) | ||
Todo.save(newTodo) | ||
|
||
Ok(newTodo) | ||
case None => throw TodoNotFound(id) | ||
} | ||
} | ||
|
||
def getTodos: Endpoint[IO, List[Todo]] = get("todos") { | ||
Ok(Todo.list()) | ||
} | ||
|
||
def deleteTodo: Endpoint[IO, Todo] = delete("todos" :: path[UUID]) { id: UUID => | ||
Todo.get(id) match { | ||
case Some(t) => Todo.delete(id); Ok(t) | ||
case None => throw TodoNotFound(id) | ||
} | ||
} | ||
|
||
def deleteTodos: Endpoint[IO, List[Todo]] = delete("todos") { | ||
val all: List[Todo] = Todo.list() | ||
all.foreach(t => Todo.delete(t.id)) | ||
|
||
Ok(all) | ||
} | ||
|
||
val api: Service[Request, Response] = ( | ||
getTodos :+: postTodo :+: deleteTodo :+: deleteTodos :+: patchTodo | ||
).handle({ | ||
case e: TodoNotFound => NotFound(e) | ||
}).toServiceAs[Application.Json] | ||
|
||
def main(): Unit = { | ||
println("Serving the Todo application") //scalastyle:ignore | ||
println(s"Open your browser at http://localhost:${port()}/todo/index.html") //scalastyle:ignore | ||
|
||
val server = Http.server | ||
.withStatsReceiver(statsReceiver) | ||
.serve(s":${port()}", api) | ||
val server = for { | ||
id <- Ref[IO].of(0) | ||
store <- Ref[IO].of(Map.empty[Int, Todo]) | ||
} yield { | ||
val app = new App(id, store)(IO.contextShift(ExecutionContext.global)) | ||
val srv = Http.server.withStatsReceiver(statsReceiver) | ||
|
||
onExit { server.close() } | ||
srv.serve(s":${ port() }", app.toService) | ||
} | ||
|
||
val handle = server.unsafeRunSync() | ||
onExit { handle.close() } | ||
Await.ready(adminHttpServer) | ||
} | ||
} |
Oops, something went wrong.