Chinook winds /ʃɪˈnʊk/, or simply chinooks, are foehn winds in the interior West of North America, where the Canadian Prairies and Great Plains meet various mountain ranges, although the original usage is in reference to wet, warm coastal winds in the Pacific Northwest.
Chinook is a Frege
(http://frege-lang.org) port of
SparkJava
(http://sparkjava.com) for creating web applications with
minimum effort.
1. Getting Started
Before going any further make sure you have installed JDK 7+ in your machine. |
Clone Chinook
project at github and from the project’s root folder
execute:
./gradlew :chinook-sample:run
If it is your first execution and you don’t have Gradle or
the project dependencies already installed, it may take a while before
executing Chinook .
|
Once the server has started:
[Thread-0] INFO org.eclipse.jetty.server.Server - Started @292ms
You can go to your browser a go to http://localhost:8080/hi You should
see the phrase Hello World from Chinook :-)
.
2. Versions
This is the compatibility version table of the current Chinook release:
name |
version |
chinook |
0.2.1 |
spark-java |
com.sparkjava:spark-core:2.5 |
frege-core |
org.frege-lang:frege:3.24-7.100 |
frege-repl |
org.frege-lang:frege-repl-core:1.3 |
jdk |
1.7+ |
Because Frege is evolving at fast pace, at least at the moment, Chinook is trying to follow latest versions of Frege. That’s why I thought it would be important to have a chapter where users would see a compatibility table of the current release. |
3. HTTP
3.1. Overview
Chinook is a tiny web framework aimed to create microservices. A basic
Chinook
application needs only three steps to expose an endpoint:
-
Handler
-
Endpoint registration
-
Main method
A whole Chinook
application could as simple as:
import chinook.Core
import chinook.Chinook
main _ = Chinook.run [] [
Get "/hello" $ \req -> do
return response.{ output = Just "Hello World" }
]
3.1.1. Handler
Handlers are the most important part of Chinook, is where client requests are handled. Chinook http handlers normally have the following signature:
IO Request -> IO Reponse
As you may see there’s no difference between handlers used with different http verbs:
helloWorldHandler :: IO Request -> IO Response
helloWorldHandler req = do
return $ response.{ output = Just "Hello World from Chinook :-)" }
deleteHandler :: IO Request -> IO Response
deleteHandler req = do
id <- req.path ":id"
return $ response.{ status = 204 }
What makes them different ? The endpoint registration.
3.1.2. Endpoint registration
The way Chinook
works at the moment is a main entry point where
handlers are registered for a specific http verb
and a path
.
It always follows the same pattern:
http-verb "/registered-path" handler
If you take a look at the chinook-sample
application, the main entry
point is located in the App.fr
file.
mappings = [
Before "/hi" securityHandler,
Get "/hi" helloWorldHandler,
Get "/hi/:name/:age" greetingsHandler,
"/json" + [
Get "/get" getJSONHandler,
Get "/form" getFormHandler,
Get "/sender" getJsonSenderHandler,
Post "/post" postJSONHandler,
Post "/html" postFormHandler
],
Get "/bye" goodbyeHandler,
Delete "/deleteme/:id" deleteHandler,
Put "/updateme/:id" putHandler
]
Here you can see all available endpoints in your application. For a given URL there is the related handler function. How you manage your handler files is completely up to you.
All HTTP
registration functions have the same structure, here is the
function to register a HTTP GET
handler:
data Resource = Get String Handler |
Post String Handler |
Put String Handler |
Delete String Handler |
Patch String Handler |
Options String Handler |
Trace String Handler |
Head String Handler |
Before String Handler |
After String Handler |
Family String [Resource]
It basically needs:
-
A
String
representing the URI resource A handler function to -
A
Handler
to deal with therequest
andresponse
originated in this invocation.
a handler is just an alias for a function with the following shape: |
type Handler = IO Request -> IO Response
A handler
basically receives a IO Request
and should return a IO
Response
. Both are available undere chinook.Core
:
import chinook.Core (Request, Response, response, haltingResponse)
Here’s a simple example:
helloWorldHandler :: IO Request -> IO Response
helloWorldHandler req = do
return $ response.{ output = Just "Hello World from Chinook :-)" }
Core.response is a way of creating a new
chinook.Core.Response without having to specify all possible
fields in Response . It creates a new copy from the default
Response value, but it lets you set different field values. This is
not a Chinook thing but Frege’s and it’s call value update .
|
If you want a GET
handler to return an output you should be setting
the response’s output
field.
Then the only thing remaining is to register the handler to receive a get call in a given URI:
Get "/hi" helloWorldHandler,
3.1.3. Main
To start a Chinook
application you only have to invoke the main
function containing all the endpoint registrations as we saw in the
previous chapter.
main = Chinook.run config routes
where config = [port 8080, staticFiles "/public"]
routes = mappings
This is independent of the tool used to call that method. For instance,
using gradle you can use the application
plugin.
apply plugin: 'java'
apply plugin: "org.frege-lang"
apply plugin: 'application' (1)
dependencies {
compile project (':chinook-core')
compile 'com.github.fregelab:diablo-groovy:0.1.1'
}
mainClassName = "chinook.App" (2)
1 | Gradle Application plugin |
2 | Namespace where the main function is located |
3.2. Request / Response
3.2.1. Basics
To return simple values
The first endpoint returns a typical hello world
message when
invoking the /hi
endpoint. Here is the handler implementation:
helloWorldHandler :: IO Request -> IO Response
helloWorldHandler req = do
return $ response.{ output = Just "Hello World from Chinook :-)" }
Changing response
Sometimes depending on the request we may want to change something about the response.
greetingsHandler :: IO Request -> IO Response
greetingsHandler req = do
name <- req.path ":name"
age <- req.path ":age"
return $ response.{ status = 200, (1)
output = createGreetings name age }
1 | Setting response status |
Here we’re setting the content type:
getJSONHandler :: IO Request -> IO Response
getJSONHandler req = do
code <- req.param "code"
desc <- req.param "desc"
return $ response.{ status = 200,
output = getLangAsJSON code desc,
headers = [("Content-Type", Just "application/json")] } (1)
1 | Setting response content type |
Instead setting content type headers manually you can use
chinook.util.ContentType , e.g. ContentType.json
|
There are many functions available for Request
and Response
abstractions, if you want to explore it please don’t hesitate to
explore the project’s Frege docs.
For the rest of HTTP verbs it doesn’t change. For instance in a POST
request it basically works the same way GET
did. The main difference
is that in POST
calls you would like to do something with the body
text sent from the client. Most of the times that body could be a
JSON
/XML
payload. You can get that information with the body
field from the request.
Here’s an example building a new Lang
instance out of the json
payload coming from the client:
postJSONHandler :: IO Request -> IO Response
postJSONHandler req = do
body <- req.body
return $ case (processJSON body) of
Just Lang { code, desc } -> createdResponse
Nothing -> badRequest
createdResponse :: Response
createdResponse = response.{ status = 201 , (1)
output = Just "Created",
headers = [ContentType.json] }
badRequest :: Response
badRequest = response.{ status = 400 , (2)
output = Just "Bad request",
headers = [ContentType.json] }
1 | Successful response sending a 201 (created) status response |
2 | Failure response sending a 400 (bad request) in case the payload
wasn’t correct. |
Of course it would be nice to send back more feedback about validation ;)
3.2.2. Request
A chinook.Core.Request
is a immutable data structure containing:
data Request = Request { headers :: [(String, Maybe String)],
queryParams :: [(String, [String])],
pathParams :: [(String, String)],
body :: Maybe String }
Headers
Headers are of type (String, Maybe String)
meaning (name of the
header, possible value)
. There are two utility methods to get headers
from a IO Request
.
allHeaders :: IO Request -> IO [(String, Maybe String)]
Returns a list of all headers from the request passed as first argument.
header :: IO Request -> String -> IO (Maybe String)
Returns a specific header. First argument is the request, then the name of the header.
Query Parameters
A typical URL containing a query string is as follows:
http://anysite.com/over/there?name=ferret
In order to get the name parameter we’ll be using the function:
param :: IO Request -> String -> IO (Maybe String)
First parameter is the request, and then the name of the parameter
(name
in this particular example).
Path Parameters
Bind the value of a path segment to a parameter. For instance:
http://localhost:4567/john/34
Is mapped:
Get "/hi/:name/:age" greetingsHandler,
So how can we retrieve the :name
and :age
path parameters ? With
the following functions:
allPaths :: IO Request -> IO [(String, String)]
This function receives the request as argument and it will return
a list of type (String, String)
.
Why (String, String) and not (String, Maybe String) ? Well
if it didn’t have any value it couldn’t match the path. So it’s safe
to assume everytime the URI is hit you will get a value.
|
path :: IO Request -> String -> IO (Maybe String)
path
should be use to get a specific path parameter. It receives the
request and the name of the path parameter, and it will return a
string representation of that parameter.
This way you could be able to use it like the following:
putHandler :: IO Request -> IO Response
putHandler req = do
id <- req.path ":id"
return $ response.{ status = 202 }
Body
Specially when trying to save or update data you will need to access
the request body
.
postJSONHandler :: IO Request -> IO Response
postJSONHandler req = do
body <- req.body
return $ case (processJSON body) of
Just Lang { code, desc } -> createdResponse
Nothing -> badRequest
createdResponse :: Response
createdResponse = response.{ status = 201 , (1)
output = Just "Created",
headers = [ContentType.json] }
badRequest :: Response
badRequest = response.{ status = 400 , (2)
output = Just "Bad request",
headers = [ContentType.json] }
3.2.3. Response
chinook.Core.Response
is just a data type:
data Response = Response { status :: Int,
halting :: Bool,
output :: Maybe String,
headers :: [(String, Maybe String)]} where
Unlike other web frameworks, you won’t be mutating the response via
functions modifiying internal fields. Chinook
forces you to create
an immutable structure.
Well, but, does it means I should be building from the scratch a
Response
every time ? Yes but Chinook
(well Frege
actually) will
help you in this task. Chinook
has a default Response
value
available through the constant chinook.Core.response
. This
value looks like this:
response = Response 200 false Nothing []
So e.g next time you wanted to return a message with no headers and
returning a 200 http status you don’t have to set everything, you
can take advantage of Frege’s value update
and write something
like this:
helloWorldHandler :: IO Request -> IO Response
helloWorldHandler req = do
return $ response.{ output = Just "Hello World from Chinook :-)" }
A Response
may contain:
Headers
(String, Maybe String)
When setting content type, or setting browser cache…etc we should be using
http response headers. A header is of type (String, Maybe String)
where
the first tuple argument is the name of the header and the second part
of the tuple is the possible value of the header.
Output
Maybe String
If you would like to respond back with a message, you can set the output field
with a Maybe String
. That string could be anything: text, json…
Status
Int
HTTP statuses are used often when doing Rest. For instance if you
would like to create a resource, you would send a message to a
resource using a POST endpoint and if it succeed it will return a 201
code meaning resource created
. A status code is of type Int
.
3.2.4. Interceptors
Sometimes you may need to check something for every request on a given path. An example could be a security interceptor, or just an audit trace.
For that purpose Chinook has two types of routes that don’t represent
a rest action but a handler capable of intercepting a given request,
or set of requests and do something about it. Those routes are
Before
and After
and as their name implies, one can be used to
intercept before the request has been processed and the latter once
the request has been processed.
Chinook interceptors are not filters . They can only
access to the request and modify only the response.
|
In the sample application, there’s a silly security interceptor that may help as a starting point.
Before "/hi" securityHandler,
All requests found under /hi
path will be intercepted by the
securityHandler
securityHandler :: IO Request -> IO Response
securityHandler req = do
token <- req.param "token"
return $ case token of
Just _ -> response
Nothing -> haltingResponse.{ status = 401,
output = Just "Please add a token to your query params '?token='" }
In an interceptor in you would like to stop the process and return
inmediately to the user, you should return a halting Response
with its halting
property set to true. There’s a shortcut which is
to make use of the haltingResponse
value. If you would like to allow
further execution just return a Response
value making sure its
halting property is set to false (which is the default value).
There are two alias for a normal response and a halting
response: response and haltingResponse .
|
3.3. Configuration
Settings are passed when bootstraping the application. Instead of
passing a list of tuples Chinook
expects values of type
chinook.Chinook.Configuration
.
Anyway Chinook
provides a set of functions as a shortcut to
produce these settings.
main = Chinook.run config routes
where config = [port 8080, staticFiles "/public"]
routes = mappings
3.3.1. Port
To produce server port settings you can use the port function:
port :: Int -> Configuration
3.3.2. Static Files
You can assign a folder in the classpath to serve static files, with
the staticFiles
function.
For instance staticFiles "/public"
will make files located at
"src/main/resources/public" be exposed at http://{server}:{port}
staticFiles :: String -> Configuration
3.4. Templating
Chinook
doesn’t have any templating solution by default
.
However chinook-sample application is using
Diablo . Diablo is a small
templating engine abstraction. It tries to expose different templating
engines with a unified api.
|
In the chinook-sample
application Diablo
is used to render html
using Groovy templates. These templates are located in the classpath,
precisely at src/main/resources
. In order to be able to use Diablo
first you need some import statements.
import diablo.Diablo (fromPath)
import diablo.Groovy (GroovyEngine)
Then a helper function is created to execute any template available in
the classpath (See Diablo
docs for more options):
render :: String -> IO (Maybe String)
render tplName = do
groovy <- GroovyEngine.new ()
Just <$> fromPath groovy tplName []
Finally we use the helper function in our handlers:
getFormHandler :: IO Request -> IO Response
getFormHandler req = do
html <- render "chinook/form.gtpl"
return response.{ status = 200,
output = html,
headers = [ContentType.html]
}
4. Development
4.1. License
Chinook
uses Apache 2.0 license
4.2. Frege docs
At the moment there is an issue with how FregeDoc that
prevents Chinook from publishing its fregedocs.
|
4.3. Github
Chinook
source code is hosted at Github