Skip to content

Commit

Permalink
Update TODO example
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Nov 14, 2018
1 parent cb7da43 commit e6f01ea
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 174 deletions.
17 changes: 11 additions & 6 deletions core/src/main/scala/io/finch/EndpointResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ sealed abstract class EndpointResult[F[_], +A] {

/**
* Returns the remainder of the [[Input]] after an [[Endpoint]] is matched.
*
* @return `Some(remainder)` if this endpoint was matched on a given input,
* `None` otherwise.
*/
final def remainder: Option[Input] = this match {
case EndpointResult.Matched(rem, _, _) => Some(rem)
Expand All @@ -42,9 +39,6 @@ sealed abstract class EndpointResult[F[_], +A] {

/**
* Returns the [[Trace]] if an [[Endpoint]] is matched.
*
* @return `Some(trace)` if this endpoint is matched on a given input,
* `None` otherwise.
*/
final def trace: Option[Trace] = this match {
case EndpointResult.Matched(_, trc, _) => Some(trc)
Expand Down Expand Up @@ -95,4 +89,15 @@ object EndpointResult {

def apply[F[_]]: NotMatched[F] = NotMatched.asInstanceOf[NotMatched[F]]
}

implicit class EndpointResultOps[F[_], A](val self: EndpointResult[F, A]) extends AnyVal {

/**
* Returns the [[Output]] if an [[Endpoint]] is matched.
*/
final def output: Option[F[Output[A]]] = self match {
case EndpointResult.Matched(_, _, out) => Some(out)
case _ => None
}
}
}
89 changes: 89 additions & 0 deletions examples/src/main/resources/todo/index.html
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>
62 changes: 62 additions & 0 deletions examples/src/main/resources/todo/main.js
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();
67 changes: 67 additions & 0 deletions examples/src/main/scala/io/finch/todo/App.scala
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
}
91 changes: 22 additions & 69 deletions examples/src/main/scala/io/finch/todo/Main.scala
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)
}
}
Loading

0 comments on commit e6f01ea

Please sign in to comment.