78 |
81 | $endif$
82 | $body$
83 |
92 |
93 | $for(include-after)$
94 | $include-after$
95 | $endfor$
96 |
97 |
98 |
--------------------------------------------------------------------------------
/docs/src/Site2.purs:
--------------------------------------------------------------------------------
1 | module Site2 where
2 |
3 | import Control.Monad.Indexed ((:*>))
4 | import Control.Monad.Error.Class (throwError)
5 | import Control.Monad.Except (ExceptT)
6 | import Data.Array (find)
7 | import Data.Maybe (Maybe(..), maybe)
8 | import Data.MediaType.Common (textHTML)
9 | import Data.Traversable (traverse_)
10 | import Effect (Effect)
11 | import Hyper.Node.Server (defaultOptions, runServer)
12 | import Hyper.Response (closeHeaders, contentType, respond, writeStatus)
13 | import Hyper.Status (statusNotFound)
14 | import Hyper.Trout.Router (RoutingError(..), router)
15 | import Text.Smolder.HTML (div, h1, li, p, ul)
16 | import Text.Smolder.Markup (text)
17 | import Type.Proxy (Proxy(..))
18 | import Type.Trout (type (:/), type (:<|>), type (:=), type (:>), Capture, Resource)
19 | import Type.Trout.ContentType.HTML (class EncodeHTML, HTML, linkTo)
20 | import Type.Trout.Links (linksTo)
21 | import Type.Trout.Method (Get)
22 | import Prelude hiding (div)
23 |
24 | -- start snippet resources-and-type
25 | data Home = Home
26 |
27 | data AllUsers = AllUsers (Array User)
28 |
29 | newtype User = User { id :: Int, name :: String }
30 |
31 | type Site2 =
32 | "home" := Resource (Get Home HTML)
33 | :<|> "users" := "users" :/ Resource (Get AllUsers HTML)
34 | :<|> "user" := "users" :/ Capture "user-id" Int
35 | :> Resource (Get User HTML)
36 |
37 | site2 :: Proxy Site2
38 | site2 = Proxy
39 | -- end snippet resources-and-type
40 |
41 | -- start snippet handlers
42 | homeResource :: forall m. Monad m => {"GET" :: ExceptT RoutingError m Home}
43 | homeResource = {"GET": pure Home}
44 |
45 | usersResource :: forall m. Monad m => {"GET" :: ExceptT RoutingError m AllUsers}
46 | usersResource = {"GET": AllUsers <$> getUsers}
47 |
48 | userResource :: forall m. Monad m => Int -> {"GET" :: ExceptT RoutingError m User}
49 | userResource id' =
50 | {"GET":
51 | find (\(User u) -> u.id == id') <$> getUsers >>=
52 | case _ of
53 | Just user -> pure user
54 | Nothing ->
55 | throwError (HTTPError { status: statusNotFound
56 | , message: Just "User not found."
57 | })
58 | }
59 | -- end snippet handlers
60 |
61 | -- start snippet encoding
62 | instance encodeHTMLHome :: EncodeHTML Home where
63 | encodeHTML Home =
64 | let {users} = linksTo site2
65 | in p do
66 | text "Welcome to my site! Go check out my "
67 | linkTo users (text "Users")
68 | text "."
69 |
70 | instance encodeHTMLAllUsers :: EncodeHTML AllUsers where
71 | encodeHTML (AllUsers users) =
72 | div do
73 | h1 (text "Users")
74 | ul (traverse_ linkToUser users)
75 | where
76 | linkToUser (User u) =
77 | let {user} = linksTo site2
78 | in li (linkTo (user u.id) (text u.name))
79 |
80 | instance encodeHTMLUser :: EncodeHTML User where
81 | encodeHTML (User { name }) =
82 | h1 (text name)
83 | -- end snippet encoding
84 |
85 | -- start snippet get-users
86 | getUsers :: forall m. Applicative m => m (Array User)
87 | getUsers =
88 | pure
89 | [ User { id: 1, name: "John Paul Jones" }
90 | , User { id: 2, name: "Tal Wilkenfeld" }
91 | , User { id: 3, name: "John Patitucci" }
92 | , User { id: 4, name: "Jaco Pastorious" }
93 | ]
94 | -- end snippet get-users
95 |
96 | -- start snippet main
97 | main :: Effect Unit
98 | main =
99 | let resources = { home: homeResource
100 | , users: usersResource
101 | , user: userResource
102 | }
103 |
104 | otherSiteRouter =
105 | router site2 resources onRoutingError
106 |
107 | onRoutingError status msg =
108 | writeStatus status
109 | :*> contentType textHTML
110 | :*> closeHeaders
111 | :*> respond (maybe "" identity msg)
112 |
113 | in runServer defaultOptions {} otherSiteRouter
114 | -- end snippet main
115 |
--------------------------------------------------------------------------------
/examples/Routing.purs:
--------------------------------------------------------------------------------
1 | module Examples.Routing where
2 |
3 | import Prelude
4 | import Control.Monad.Indexed ((:*>))
5 | import Control.Monad.Error.Class (throwError)
6 | import Control.Monad.Except (ExceptT)
7 | import Data.Argonaut (class EncodeJson, jsonEmptyObject, (:=), (~>))
8 | import Data.Argonaut.Encode.Generic.Rep (genericEncodeJson)
9 | import Data.Array (find, (..))
10 | import Data.Foldable (traverse_)
11 | import Data.Generic.Rep (class Generic)
12 | import Data.Maybe (Maybe(..), maybe)
13 | import Data.MediaType.Common (textHTML)
14 | import Effect (Effect)
15 | import Effect.Aff (Aff)
16 | import Hyper.Node.Server (defaultOptions, runServer)
17 | import Hyper.Response (closeHeaders, contentType, respond, writeStatus)
18 | import Hyper.Status (statusNotFound)
19 | import Hyper.Trout.Router (RoutingError(..), router)
20 | import Text.Smolder.HTML (h1, li, nav, p, section, ul)
21 | import Text.Smolder.Markup (text)
22 | import Type.Proxy (Proxy(..))
23 | import Type.Trout (type (:/), type (:<|>), type (:=), type (:>), Capture, Resource)
24 | import Type.Trout.ContentType.HTML (class EncodeHTML, HTML, linkTo)
25 | import Type.Trout.ContentType.JSON (JSON)
26 | import Type.Trout.Links (linksTo)
27 | import Type.Trout.Method (Get)
28 |
29 | type PostID = Int
30 |
31 | newtype Post = Post { id :: PostID
32 | , title :: String
33 | }
34 |
35 | derive instance genericPost :: Generic Post _
36 |
37 | instance encodeJsonPost :: EncodeJson Post where
38 | encodeJson (Post { id, title }) =
39 | "id" := id
40 | ~> "title" := title
41 | ~> jsonEmptyObject
42 |
43 | instance encodeHTMLPost :: EncodeHTML Post where
44 | encodeHTML (Post { id: postId, title}) =
45 | let {posts} = linksTo site
46 | in section do
47 | h1 (text title)
48 | p (text "Contents...")
49 | nav (linkTo posts (text "All Posts"))
50 |
51 | newtype PostsView = PostsView (Array Post)
52 |
53 | derive instance genericPostsView :: Generic PostsView _
54 |
55 | instance encodeJsonPostsView :: EncodeJson PostsView where
56 | encodeJson = genericEncodeJson
57 |
58 | instance encodeHTMLPostsView :: EncodeHTML PostsView where
59 | encodeHTML (PostsView posts) =
60 | let {post} = linksTo site
61 | postLink (Post { id: postId, title }) =
62 | li (linkTo (post postId) (text title))
63 | in section do
64 | h1 (text "Posts")
65 | ul (traverse_ postLink posts)
66 |
67 | type Site =
68 | "posts" := Resource (Get PostsView (HTML :<|> JSON))
69 | :<|> "post" := "posts" :/ Capture "id" PostID :> Resource (Get Post (HTML :<|> JSON))
70 |
71 | site :: Proxy Site
72 | site = Proxy
73 |
74 | type AppM a = ExceptT RoutingError Aff a
75 |
76 | -- This would likely be a database query in
77 | -- a real app:
78 | allPosts :: AppM (Array Post)
79 | allPosts = pure (map (\i -> Post { id: i, title: "Post #" <> show i }) (1..10))
80 |
81 | postsResource :: { "GET" :: AppM PostsView }
82 | postsResource = { "GET": PostsView <$> allPosts }
83 |
84 | postResource :: PostID -> { "GET" :: AppM Post }
85 | postResource postId =
86 | { "GET":
87 | find (\(Post p) -> p.id == postId) <$> allPosts >>=
88 | case _ of
89 | Just post -> pure post
90 | -- You can throw 404 Not Found in here as well.
91 | Nothing -> throwError (HTTPError { status: statusNotFound
92 | , message: Just "Post not found."
93 | })
94 | }
95 |
96 | main :: Effect Unit
97 | main =
98 | runServer defaultOptions {} siteRouter
99 | where
100 | siteRouter = router
101 | site
102 | { posts: postsResource
103 | , post: postResource
104 | }
105 | onRoutingError
106 | onRoutingError status msg = do
107 | writeStatus status
108 | :*> contentType textHTML
109 | :*> closeHeaders
110 | :*> respond (maybe "" identity msg)
111 |
--------------------------------------------------------------------------------
/docs/src/Site3.purs:
--------------------------------------------------------------------------------
1 | module Site3 where
2 |
3 | import Control.Monad.Indexed ((:*>))
4 | import Control.Monad.Error.Class (throwError)
5 | import Control.Monad.Except (ExceptT)
6 | import Data.Argonaut (class EncodeJson, encodeJson, fromArray, jsonEmptyObject, (:=), (~>))
7 | import Data.Array (find)
8 | import Data.Foldable (traverse_)
9 | import Data.Maybe (Maybe(..), maybe)
10 | import Data.MediaType.Common (textHTML)
11 | import Effect (Effect)
12 | import Hyper.Node.Server (defaultOptions, runServer)
13 | import Hyper.Response (closeHeaders, contentType, respond, writeStatus)
14 | import Hyper.Status (statusNotFound)
15 | import Hyper.Trout.Router (RoutingError(..), router)
16 | import Text.Smolder.HTML (div, h1, li, p, ul)
17 | import Text.Smolder.Markup (text)
18 | import Type.Proxy (Proxy(..))
19 | import Type.Trout (type (:/), type (:<|>), type (:=), type (:>), Capture, Resource)
20 | import Type.Trout.ContentType.HTML (class EncodeHTML, HTML, linkTo)
21 | import Type.Trout.ContentType.JSON (JSON)
22 | import Type.Trout.Links (linksTo)
23 | import Type.Trout.Method (Get)
24 | import Prelude hiding (div)
25 |
26 | data Home = Home
27 |
28 | newtype User = User { id :: Int, name :: String }
29 |
30 | instance encodeJsonUser :: EncodeJson User where
31 | encodeJson (User { id, name }) =
32 | "id" := show id
33 | ~> "name" := name
34 | ~> jsonEmptyObject
35 |
36 |
37 | data AllUsers = AllUsers (Array User)
38 |
39 | instance encodeJsonAllUsers :: EncodeJson AllUsers where
40 | encodeJson (AllUsers users) = fromArray (map encodeJson users)
41 |
42 | -- start snippet routing-type
43 | type Site3 =
44 | "home" := Resource (Get Home HTML)
45 | :<|> "users" := "users" :/ Resource (Get AllUsers (HTML :<|> JSON))
46 | :<|> "user" := "users" :/ Capture "user-id" Int
47 | :> Resource (Get User (HTML :<|> JSON))
48 | -- end snippet routing-type
49 |
50 | site3 :: Proxy Site3
51 | site3 = Proxy
52 |
53 | homeResource :: forall m. Monad m => {"GET" :: ExceptT RoutingError m Home}
54 | homeResource = {"GET": pure Home}
55 |
56 | usersResource :: forall m. Monad m => {"GET" :: ExceptT RoutingError m AllUsers}
57 | usersResource = {"GET": AllUsers <$> getUsers}
58 |
59 | userResource :: forall m. Monad m => Int -> {"GET" :: ExceptT RoutingError m User}
60 | userResource id' =
61 | {"GET":
62 | find (\(User u) -> u.id == id') <$> getUsers >>=
63 | case _ of
64 | Just user -> pure user
65 | Nothing ->
66 | throwError (HTTPError { status: statusNotFound
67 | , message: Just "User not found."
68 | })
69 | }
70 |
71 | instance encodeHTMLHome :: EncodeHTML Home where
72 | encodeHTML Home =
73 | let {users} = linksTo site3
74 | in p do
75 | text "Welcome to my site! Go check out my "
76 | linkTo users (text "Users")
77 | text "."
78 |
79 | instance encodeHTMLAllUsers :: EncodeHTML AllUsers where
80 | encodeHTML (AllUsers users) =
81 | div do
82 | h1 (text "Users")
83 | ul (traverse_ linkToUser users)
84 | where
85 | linkToUser (User u) =
86 | let {user} = linksTo site3
87 | in li (linkTo (user u.id) (text u.name))
88 |
89 | instance encodeHTMLUser :: EncodeHTML User where
90 | encodeHTML (User { name }) =
91 | h1 (text name)
92 |
93 | getUsers :: forall m. Applicative m => m (Array User)
94 | getUsers =
95 | pure
96 | [ User { id: 1, name: "John Paul Jones" }
97 | , User { id: 2, name: "Tal Wilkenfeld" }
98 | , User { id: 3, name: "John Patitucci" }
99 | , User { id: 4, name: "Jaco Pastorious" }
100 | ]
101 |
102 | main :: Effect Unit
103 | main =
104 | let resources = { home: homeResource
105 | , users: usersResource
106 | , user: userResource
107 | }
108 |
109 | site3Router =
110 | router site3 resources onRoutingError
111 |
112 | onRoutingError status msg =
113 | writeStatus status
114 | :*> contentType textHTML
115 | :*> closeHeaders
116 | :*> respond (maybe "" identity msg)
117 |
118 | in runServer defaultOptions {} site3Router
119 |
--------------------------------------------------------------------------------
/test/Hyper/Trout/RouterSpec.purs:
--------------------------------------------------------------------------------
1 | module Hyper.Trout.RouterSpec (spec) where
2 |
3 | import Prelude
4 | import Control.Monad.Indexed ((:*>))
5 | import Data.Either (Either(..))
6 | import Data.HTTP.Method (Method(POST, GET))
7 | import Data.Maybe (Maybe(..), maybe)
8 | import Data.MediaType.Common (textPlain)
9 | import Data.String (joinWith)
10 | import Data.Tuple (Tuple(..))
11 | import Foreign.Object (Object)
12 | import Foreign.Object as F
13 | import Hyper.Conn (Conn)
14 | import Hyper.Middleware (Middleware, evalMiddleware)
15 | import Hyper.Request (class Request)
16 | import Hyper.Response (class Response, contentType, headers, respond, class ResponseWritable, ResponseEnded, StatusLineOpen, closeHeaders, writeStatus)
17 | import Hyper.Status (statusBadRequest, statusMethodNotAllowed, statusOK)
18 | import Hyper.Test.TestServer (TestResponse(..), TestRequest(..), defaultRequest, testHeaders, testServer, testStatus, testStringBody)
19 | import Hyper.Trout.TestSite (Home(..), User(..), UserID(..), WikiPage(..), testSite)
20 | import Hyper.Trout.Router (router)
21 | import Test.Spec (Spec, describe, it)
22 | import Test.Spec.Assertions (shouldEqual)
23 |
24 | homeResource :: forall m. Monad m => {"GET" :: m Home}
25 | homeResource = {"GET": pure Home}
26 |
27 | profileResource :: forall m. Monad m => UserID -> {"GET" :: m User}
28 | profileResource userId = {"GET": pure (User userId)}
29 |
30 | friendsResource
31 | :: forall m
32 | . Monad m
33 | => UserID
34 | -> { "GET" :: m (Array User)
35 | , "POST" :: m User
36 | }
37 | friendsResource (UserID uid) =
38 | { "GET": pure [ User (UserID "foo")
39 | , User (UserID "bar")
40 | ]
41 | -- TODO: add ReqBody when supported
42 | , "POST": pure (User (UserID "new-user"))
43 | }
44 |
45 | wikiResource :: forall m. Monad m => Array String -> {"GET" :: m WikiPage}
46 | wikiResource segments = {"GET": pure (WikiPage (joinWith "/" segments))}
47 |
48 | aboutMiddleware
49 | :: forall m req res c r
50 | . Monad m
51 | => Request req m
52 | => Response res m r
53 | => ResponseWritable r m String
54 | => Middleware
55 | m
56 | (Conn req (res StatusLineOpen) c)
57 | (Conn req (res ResponseEnded) c)
58 | Unit
59 | aboutMiddleware = do
60 | writeStatus statusOK
61 | :*> contentType textPlain
62 | :*> closeHeaders
63 | :*> respond "This is a test."
64 |
65 | searchResource :: forall f m. Functor f => Monad m => f String -> {"GET" :: m (f User)}
66 | searchResource q =
67 | {"GET": pure $ User <<< UserID <$> q}
68 |
69 | spec :: Spec Unit
70 | spec =
71 | describe "Hyper.Routing.Router" do
72 | let userResources userId = { profile: profileResource userId
73 | , friends: friendsResource userId
74 | }
75 | resources = { home: homeResource
76 | , user: userResources
77 | , wiki: wikiResource
78 | , about: aboutMiddleware
79 | , search: searchResource
80 | , searchMany: searchResource
81 | }
82 |
83 | onRoutingError status msg = do
84 | writeStatus status
85 | :*> headers []
86 | :*> respond (maybe "" identity msg)
87 |
88 | makeRequestWithHeaders method path headers =
89 | { request: TestRequest defaultRequest { method = Left method
90 | , url = path
91 | , headers = headers
92 | }
93 | , response: TestResponse Nothing [] []
94 | , components: {}
95 | }
96 | # evalMiddleware (router testSite resources onRoutingError)
97 | # testServer
98 |
99 | makeRequest method path =
100 | makeRequestWithHeaders method path (F.empty :: Object String)
101 |
102 | describe "router" do
103 | it "matches root" do
104 | conn <- makeRequest GET "/"
105 | testStringBody conn `shouldEqual` "
Home
"
106 |
107 | it "considers Accept header for multi-content-type resources" do
108 | conn <- makeRequestWithHeaders GET "/" (F.singleton "accept" "application/json")
109 | testStatus conn `shouldEqual` Just statusOK
110 | testStringBody conn `shouldEqual` "{}"
111 |
112 | it "validates based on custom Capture instance" do
113 | conn <- makeRequest GET "/users/ /profile"
114 | testStatus conn `shouldEqual` Just statusBadRequest
115 | testStringBody conn `shouldEqual` "UserID must not be blank."
116 |
117 | it "matches nested routes" do
118 | conn <- makeRequest GET "/users/owi/profile"
119 | testStringBody conn `shouldEqual` "{\"userId\":\"owi\"}"
120 |
121 | it "ignores extraneous query string parameters" do
122 | conn <- makeRequest GET "/users/owi/profile?bugs=bunny"
123 | testStringBody conn `shouldEqual` "{\"userId\":\"owi\"}"
124 |
125 | it "supports arrays of JSON values" do
126 | conn <- makeRequest GET "/users/owi/friends"
127 | testStringBody conn `shouldEqual` "[{\"userId\":\"foo\"},{\"userId\":\"bar\"}]"
128 |
129 | it "supports second method of resource with different representation" do
130 | conn <- makeRequest POST "/users/owi/friends"
131 | testStringBody conn `shouldEqual` "{\"userId\":\"new-user\"}"
132 |
133 | it "matches CaptureAll route" do
134 | conn <- makeRequest GET "/wiki/foo/bar/baz.txt"
135 | testStringBody conn `shouldEqual` "Viewing page: foo/bar/baz.txt"
136 |
137 | it "matches QueryParam route" do
138 | conn <- makeRequest GET "/search?q=bunny"
139 | testStringBody conn `shouldEqual` "{\"userId\":\"bunny\"}"
140 |
141 | it "matches QueryParam route with empty value" do
142 | conn <- makeRequest GET "/search?q"
143 | testStringBody conn `shouldEqual` "{\"userId\":\"\"}"
144 |
145 | it "matches QueryParam route with missing key" do
146 | conn <- makeRequest GET "/search?r=bunny"
147 | testStringBody conn `shouldEqual` "null"
148 |
149 | it "matches QueryParams route" do
150 | conn <- makeRequest GET "/search-many?q=bugs&q=bunny"
151 | testStringBody conn `shouldEqual` "[{\"userId\":\"bugs\"},{\"userId\":\"bunny\"}]"
152 |
153 | it "matches QueryParams route with empty value" do
154 | conn <- makeRequest GET "/search-many?q&q=bunny"
155 | testStringBody conn `shouldEqual` "[{\"userId\":\"\"},{\"userId\":\"bunny\"}]"
156 |
157 | it "matches QueryParams route with missing key" do
158 | conn <- makeRequest GET "/search-many?p&q=bunny"
159 | testStringBody conn `shouldEqual` "[{\"userId\":\"bunny\"}]"
160 |
161 | it "matches Raw route" do
162 | conn <- makeRequest GET "/about"
163 | testHeaders conn `shouldEqual` [ Tuple "Content-Type" "text/plain" ]
164 | testStringBody conn `shouldEqual` "This is a test."
165 |
166 | it "checks HTTP method" do
167 | conn <- makeRequest POST "/"
168 | testStatus conn `shouldEqual` Just statusMethodNotAllowed
169 |
--------------------------------------------------------------------------------
/docs/src/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Hypertrout
3 | author: Oskar Wickström
4 | ---
5 |
6 | ## Purpose
7 |
8 | The purpose of this
9 | package,
10 | [Hypertrout](https://github.com/owickstrom/purescript-hypertrout), is
11 | writing web servers using the *type-level routing API*
12 | in [Trout](https://github.com/owickstrom/purescript-trout). It
13 | provides a router middleware which, together with records of handler
14 | functions for resources, and rendering instances, gives us a
15 | full-fledged server.
16 |
17 | ## A Single-Resource Example
18 |
19 | Let's say we want to render a home page as HTML. We start out by
20 | declaring the data type `Home`, and the structure of our site:
21 |
22 | ``` {.haskell language=purescript include=docs/src/Site1.purs snippet=routing-type}
23 | ```
24 |
25 | `Resource (Get Home HTML)` is a routing type with only one resource,
26 | responding to HTTP GET requests, rendering a `Home` value as HTML. So
27 | where does the `Home` value come from? We provide it using a *handler*
28 | inside a resource record. A resource record for `Site1` would be some
29 | value of the following type:
30 |
31 | ``` {.haskell}
32 | forall m. Monad m => {"GET" :: ExceptT RoutingError m Home}
33 | ```
34 |
35 | The resource record has fields for each supported HTTP method, with values
36 | being the corresponding handlers. A resource record type, supporting both GET
37 | and POST, could have the following type:
38 |
39 | ``` {.haskell}
40 | forall m. Monad m => { "GET" :: ExceptT RoutingError m SomeType
41 | , "POST" :: ExceptT RoutingError m SomeType
42 | }
43 | ```
44 |
45 | We can construct a resource record for the `Site1` routing type using `pure`
46 | and a `Home` value:
47 |
48 | ``` {.haskell language=purescript include=docs/src/Site1.purs snippet=handler}
49 | ```
50 |
51 | Nice! But what comes out on the other end? We need something that
52 | renders the `Home` value as HTML. By providing an instance of
53 | `EncodeHTML` for `Home`, we instruct the resource how to render.
54 |
55 | ``` {.haskell include=docs/src/Site1.purs snippet=encoding}
56 | ```
57 |
58 | The `HTML` type is a phantom type, only used as a marker type, and the
59 | actual markup is written in the `MarkupM` DSL from
60 | [purescript-smolder](https://github.com/bodil/purescript-smolder).
61 |
62 | We are getting ready to create the server. First, we need a value-level
63 | representation of the `Site1` type, to be able to pass it to the
64 | `router` function. For that we use
65 | [Proxy](https://pursuit.purescript.org/packages/purescript-proxy/1.0.0/docs/Type.Proxy).
66 | Its documentation describes it as follows:
67 |
68 | > The Proxy type and values are for situations where type information is
69 | > required for an input to determine the type of an output, but where it
70 | > is not possible or convenient to provide a value for the input.
71 |
72 | We create a top-level definition of the type `Proxy Site1` with the
73 | value constructor `Proxy`.
74 |
75 | ``` {.haskell include=docs/src/Site1.purs snippet=proxy}
76 | ```
77 |
78 | We pass the proxy, our handler, and the `onRoutingError` function for
79 | cases where no route matched the request, to the `router` function.
80 |
81 | ``` {.haskell include=docs/src/Site1.purs snippet=router}
82 | ```
83 |
84 | The value returned by `router` is regular middleware, ready to be passed
85 | to a server.
86 |
87 | ``` {.haskell include=docs/src/Site1.purs snippet=main}
88 | ```
89 |
90 | ## Routing Multiple Resources
91 |
92 | Real-world servers often need more than one resource. To combine
93 | multiple resources, resource routing types are separated using the
94 | `:<|>` operator, the type-level operator for separating
95 | *alternatives*.
96 |
97 | ``` {.haskell}
98 | RoutingType1 :<|> RoutingType2 :<|> ... :<|> RoutingTypeN
99 | ```
100 |
101 | When combining multiple resources in a routing type, each resource has
102 | to be named. The `:=` type-level operator names a resource, or another
103 | nested structure of resources, using a Symbol on the left-hand side,
104 | and a routing type on the right-hand side.
105 |
106 | ``` {.haskell}
107 | "" := RoutingType
108 | ```
109 |
110 | The following is a routing type for two resources, named `"foo"` and
111 | `"bar"`:
112 |
113 | ``` {.haskell}
114 | "foo" := Resource (Get Foo HTML)
115 | :<|> "bar" := Resource (Get Bar HTML)
116 | ```
117 |
118 | Named routes can be nested to create a structure of arbitrary depth, a
119 | practice useful for grouping related resources:
120 |
121 | ``` {.haskell}
122 | type UserResources =
123 | "profile" := Resource (Get UserProfile HTML)
124 | :<|> "settings" := Resource (Get UserSettings HTML)
125 |
126 | type AdminResources =
127 | "users" := Resource (Get Users HTML)
128 | :<|> "logs" := Resource (Get Logs HTML)
129 |
130 | type MyNestedResources =
131 | "user" := UserResources
132 | :<|> "admin" := AdminResources
133 | ```
134 |
135 | ### Example
136 |
137 | Let's define a router for an application that shows a home page with
138 | links, a page listing users, and a page rendering a specific user.
139 |
140 | ``` {.haskell include=docs/src/Site2.purs snippet=resources-and-type}
141 | ```
142 |
143 | There are some new things in this code that we haven't talked about,
144 | and some we touched upon a bit. Here's a walk-through of what's going
145 | on:
146 |
147 | - `:<|>` is the type-level operator that, in general, separates
148 | alternatives. In case of resources, a router will try each route
149 | in order until one matches.
150 | - `:=` names a route, where the left-hand argument is a Symbol, the
151 | name, and the right-hand argument is a routing type. Named routes
152 | are combined with `:<|>`, as explained previously.
153 | - `:/` separates a literal path segment and the rest of the routing
154 | type. Note that a named routing type, created with `:=`, has no relation
155 | to literal path segments. In other words, if want a resource named
156 | `"foo"` to be served under the path `/foo`, we write:
157 | ``` {.haskell}
158 | "foo" := "foo" :/ ...
159 | ```
160 | - `Capture` takes a descriptive string and a type. It takes the next
161 | available path segment and tries to convert it to the given type.
162 | Each capture in a routing type corresponds to an argument in the
163 | handler function.
164 | - `:>` separates a routing type modifier, like `Capture`, and the rest
165 | of the routing type.
166 |
167 | We define a resource record using regular functions on the specified data
168 | types, returning `ExceptT RoutingError m a` values, where `m` is the monad of
169 | our middleware, and `a` is the type to render for the resource and method.
170 |
171 | ``` {.haskell include=docs/src/Site2.purs snippet=handlers}
172 | ```
173 |
174 | As in the single-resource example, we want to render as HTML. Let's
175 | create instances for our data types. Notice how we can create links
176 | between routes in a type-safe manner.
177 |
178 | ``` {.haskell include=docs/src/Site2.purs snippet=encoding}
179 | ```
180 |
181 | The record destructuring on the value returned by `linksTo` extracts the
182 | correct link, based on the names from the routing type. Each link will have a
183 | type based on the corresponding resource. `user` in the previous code has
184 | type `Int -> URI`, while `users` has no captures and thus has type `URI`.
185 |
186 | We are still missing `getUsers`, our source of `User` values. In a real
187 | application it would probably be a database query, but for this example
188 | we simply hard-code some famous users of proper instruments.
189 |
190 | ``` {.haskell include=docs/src/Site2.purs snippet=get-users}
191 | ```
192 |
193 | Almost done! We just need to create the router, and start a server.
194 |
195 | ``` {.haskell include=docs/src/Site2.purs snippet=main}
196 | ```
197 |
198 | Notice how the `resources` record matches the names and structure of our
199 | routing type. If we fail to match the type we get a compile error.
200 |
201 | ## Multi-Method Resources
202 |
203 | So far we have just used a single method per resource, the `Get` method.
204 | By replacing the single method type with a sequence of alternatives,
205 | constructed with the type-level operator `:<|>`, we get a resource with
206 | multiple methods.
207 |
208 | ``` {.haskell include=docs/src/MultiMethodExample.purs snippet=routing-type}
209 | ```
210 |
211 | `MultiMethodExample` is a routing type with a *single resource*, named
212 | `"user"`, which has *multiple resource methods*. Handlers for the
213 | resource methods are provided as a record value, with field names
214 | matching the HTTP methods:
215 |
216 | ``` {.haskell include=docs/src/MultiMethodExample.purs snippet=resources}
217 | ```
218 |
219 | ## Content Negotiation
220 |
221 | By specifying alternative content types for a method, Hyper can choose
222 | a response and content type based on the request `Accept` header. This
223 | is called *content negotiation*. Instead of specifying a single type,
224 | like `HTML` or `JSON`, we provide alternatives using `:<|>`. All content
225 | types must have `MimeRender` instances for the response body type.
226 |
227 | ``` {.haskell include=docs/src/Site3.purs snippet=routing-type}
228 | ```
229 |
230 | By making requests to this site, using `Accept` headers, we can see how
231 | the router chooses the matching content type (output formatted and
232 | shortened for readability).
233 |
234 | ``` {.bash}
235 | $ curl -H 'Accept: application/json' http://localhost:3000/users
236 | [
237 | {
238 | "name": "John Paul Jones",
239 | "id": "1"
240 | },
241 | {
242 | "name": "Tal Wilkenfeld",
243 | "id": "2"
244 | },
245 | ...
246 | ]
247 | ```
248 |
249 | There is support for *wildcards* and *qualities* as well.
250 |
251 | ``` {.bash}
252 | $ curl -H 'Accept: text/*;q=1.0' http://localhost:3000/users
253 |
261 | ```
262 |
--------------------------------------------------------------------------------
/src/Hyper/Trout/Router.purs:
--------------------------------------------------------------------------------
1 | module Hyper.Trout.Router
2 | ( RoutingError(..)
3 | , class Router
4 | , route
5 | , router
6 | ) where
7 |
8 | import Prelude
9 | import Data.HTTP.Method as Method
10 | import Type.Trout as Trout
11 | import Type.Trout.Record as Record
12 | import Control.Monad.Error.Class (throwError)
13 | import Control.Monad.Except (ExceptT, runExceptT)
14 | import Control.Monad.Indexed (ibind, (:*>))
15 | import Data.Array (elem, filter, null, uncons)
16 | import Data.Either (Either(..), either)
17 | import Data.Generic.Rep (class Generic)
18 | import Data.Generic.Rep.Eq (genericEq)
19 | import Data.Generic.Rep.Show (genericShow)
20 | import Data.HTTP.Method (CustomMethod, Method)
21 | import Data.Lazy (force)
22 | import Data.Maybe (Maybe(..), fromMaybe)
23 | import Data.MediaType.Common (textPlain)
24 | import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
25 | import Data.Traversable (traverse)
26 | import Data.Tuple (Tuple(..), fst, lookup, snd)
27 | import Foreign.Object (Object)
28 | import Foreign.Object (lookup) as F
29 | import Hyper.Conn (Conn)
30 | import Hyper.ContentNegotiation (AcceptHeader, NegotiationResult(..), negotiateContent, parseAcceptHeader)
31 | import Hyper.Middleware (Middleware, lift')
32 | import Hyper.Request (class Request, getRequestData)
33 | import Hyper.Response (class Response, class ResponseWritable, ResponseEnded, StatusLineOpen, closeHeaders, contentType, end, respond, writeStatus)
34 | import Hyper.Status (Status, statusBadRequest, statusMethodNotAllowed, statusNotAcceptable, statusNotFound, statusOK)
35 | import Prim.Row (class Cons)
36 | import Type.Proxy (Proxy(..))
37 | import Type.Trout (type (:<|>), type (:=), type (:>), Capture, CaptureAll, QueryParam, QueryParams, Lit, Raw)
38 | import Type.Trout.ContentType (class AllMimeRender, allMimeRender)
39 | import Type.Trout.PathPiece (class FromPathPiece, fromPathPiece)
40 |
41 | type Method' = Either Method CustomMethod
42 |
43 | type RoutingContext = { path :: Array String
44 | , query :: Array (Tuple String (Maybe String))
45 | , method :: Method'
46 | }
47 |
48 | data RoutingError
49 | = HTTPError { status :: Status
50 | , message :: Maybe String
51 | }
52 |
53 | type Handler r = Either RoutingError r
54 |
55 | derive instance genericRoutingError :: Generic RoutingError _
56 |
57 | instance eqRoutingError :: Eq RoutingError where
58 | eq = genericEq
59 |
60 | instance showRoutingError :: Show RoutingError where
61 | show = genericShow
62 |
63 | class Router e h r | e -> h, e -> r where
64 | route :: Proxy e -> RoutingContext -> h -> Handler r
65 |
66 | orHandler :: forall r. Handler r -> Handler r -> Handler r
67 | orHandler h1 h2 =
68 | case h1 of
69 | Left err1 ->
70 | case h2 of
71 | -- The Error that's thrown depends on the errors' HTTP codes.
72 | Left err2 -> throwError (selectError err1 err2)
73 | Right handler -> pure handler
74 | Right handler -> pure handler
75 | where
76 | fallbackStatuses = [statusNotFound, statusMethodNotAllowed]
77 | selectError (HTTPError errL) (HTTPError errR) =
78 | case Tuple errL.status errR.status of
79 | Tuple s1 s2
80 | | s1 `elem` fallbackStatuses && s2 == statusNotFound -> HTTPError errL
81 | | s1 /= statusNotFound && s2 `elem` fallbackStatuses -> HTTPError errL
82 | | otherwise -> HTTPError errR
83 |
84 | instance routerAltNamed :: ( Router t1 h1 out
85 | , Router t2 (Record h2) out
86 | , IsSymbol name
87 | , Cons name h1 h2 hs
88 | )
89 | => Router (name := t1 :<|> t2) (Record hs) out where
90 | route _ context handlers =
91 | route (Proxy :: Proxy t1) context (Record.get name handlers)
92 | `orHandler`
93 | route (Proxy :: Proxy t2) context (Record.delete name handlers)
94 | where
95 | name = SProxy :: SProxy name
96 |
97 | instance routerNamed :: ( Router t h out
98 | , IsSymbol name
99 | , Cons name h () hs
100 | )
101 | => Router (name := t) (Record hs) out where
102 | route _ context handlers =
103 | route (Proxy :: Proxy t) context (Record.get (SProxy :: SProxy name) handlers)
104 |
105 | instance routerLit :: ( Router e h out
106 | , IsSymbol lit
107 | )
108 | => Router (Lit lit :> e) h out where
109 | route _ ctx r =
110 | case uncons ctx.path of
111 | Just { head, tail } | head == expectedSegment ->
112 | route (Proxy :: Proxy e) ctx { path = tail} r
113 | Just _ -> throwError (HTTPError { status: statusNotFound
114 | , message: Nothing
115 | })
116 | Nothing -> throwError (HTTPError { status: statusNotFound
117 | , message: Nothing
118 | })
119 | where expectedSegment = reflectSymbol (SProxy :: SProxy lit)
120 |
121 | instance routerCapture :: ( Router e h out
122 | , FromPathPiece v
123 | )
124 | => Router (Capture c v :> e) (v -> h) out where
125 | route _ ctx r =
126 | case uncons ctx.path of
127 | Nothing -> throwError (HTTPError { status: statusNotFound
128 | , message: Nothing
129 | })
130 | Just { head, tail } ->
131 | case fromPathPiece head of
132 | Left err -> throwError (HTTPError { status: statusBadRequest
133 | , message: Just err
134 | })
135 | Right x -> route (Proxy :: Proxy e) ctx { path = tail } (r x)
136 |
137 |
138 | instance routerCaptureAll :: ( Router e h out
139 | , FromPathPiece v
140 | )
141 | => Router (CaptureAll c v :> e) (Array v -> h) out where
142 | route _ ctx r =
143 | case traverse fromPathPiece ctx.path of
144 | Left err -> throwError (HTTPError { status: statusBadRequest
145 | , message: Just err
146 | })
147 | Right xs -> route (Proxy :: Proxy e) ctx { path = [] } (r xs)
148 |
149 |
150 | instance routerQueryParam :: ( IsSymbol k
151 | , Router e h out
152 | , FromPathPiece t
153 | )
154 | => Router (QueryParam k t :> e) (Maybe t -> h) out where
155 | route _ ctx r =
156 | let k = reflectSymbol (SProxy :: SProxy k)
157 | v = map (fromMaybe "") $ lookup k $ ctx.query in
158 | case fromPathPiece <$> v of
159 | Nothing -> go Nothing
160 | Just (Right v') -> go (Just v')
161 | Just (Left err) -> throwError (HTTPError { status: statusBadRequest
162 | , message: Just err
163 | })
164 | where go = route (Proxy :: Proxy e) ctx <<< r
165 |
166 |
167 | instance routerQueryParams :: ( IsSymbol k
168 | , Router e h out
169 | , FromPathPiece t
170 | )
171 | => Router (QueryParams k t :> e) (Array t -> h) out where
172 | route _ ctx r =
173 | let k = reflectSymbol (SProxy :: SProxy k)
174 | v = map (fromMaybe "" <<< snd) $ filter ((==) k <<< fst) $ ctx.query in
175 | case traverse fromPathPiece v of
176 | Right v' -> go v'
177 | Left err -> throwError (HTTPError { status: statusBadRequest
178 | , message: Just err
179 | })
180 | where go = route (Proxy :: Proxy e) ctx <<< r
181 |
182 |
183 | routeEndpoint :: forall e r method
184 | . IsSymbol method
185 | => Proxy e
186 | -> RoutingContext
187 | -> r
188 | -> SProxy method
189 | -> Either RoutingError r
190 | routeEndpoint _ context r methodProxy = do
191 | unless (null context.path) $
192 | throwError (HTTPError { status: statusNotFound
193 | , message: Nothing
194 | })
195 |
196 | let expectedMethod = Method.fromString (reflectSymbol methodProxy)
197 | unless (expectedMethod == context.method) $
198 | throwError (HTTPError { status: statusMethodNotAllowed
199 | , message: Just ("Method "
200 | <> show context.method
201 | <> " did not match "
202 | <> show expectedMethod
203 | <> ".")
204 | })
205 | pure r
206 |
207 | getAccept :: Object String -> Either String (Maybe AcceptHeader)
208 | getAccept m =
209 | case F.lookup "accept" m of
210 | Just a -> Just <$> parseAcceptHeader a
211 | Nothing -> pure Nothing
212 |
213 | instance routerAltMethod :: ( IsSymbol method
214 | , Router (Trout.Method method body ct) (Record hs) out
215 | , Router methods (Record hs) out
216 | )
217 | => Router
218 | (Trout.Method method body ct :<|> methods)
219 | (Record hs)
220 | out
221 | where
222 | route _ context handlers =
223 | route (Proxy :: Proxy (Trout.Method method body ct)) context handlers
224 | `orHandler`
225 | route (Proxy :: Proxy methods) context handlers
226 |
227 | instance routerMethod :: ( Monad m
228 | , Request req m
229 | , Response res m r
230 | , ResponseWritable r m b
231 | , IsSymbol method
232 | , AllMimeRender body ct b
233 | , Cons method (ExceptT RoutingError m body) hs' hs
234 | )
235 | => Router
236 | (Trout.Method method body ct)
237 | (Record hs)
238 | (Middleware
239 | m
240 | { request :: req, response :: (res StatusLineOpen), components :: c}
241 | { request :: req, response :: (res ResponseEnded), components :: c}
242 | Unit)
243 | where
244 | route proxy context handlers = do
245 | let handler = lift' (runExceptT (Record.get (SProxy :: SProxy method) handlers)) `ibind`
246 | case _ of
247 | Left (HTTPError { status }) -> do
248 | writeStatus status
249 | :*> contentType textPlain
250 | :*> closeHeaders
251 | :*> end
252 | Right body -> do
253 | { headers } <- getRequestData
254 | case getAccept headers of
255 | Left err -> do
256 | writeStatus statusBadRequest
257 | :*> contentType textPlain
258 | :*> closeHeaders
259 | :*> end
260 | Right parsedAccept -> do
261 | case negotiateContent parsedAccept (allMimeRender (Proxy :: Proxy ct) body) of
262 | Match (Tuple ct rendered) -> do
263 | writeStatus statusOK
264 | :*> contentType ct
265 | :*> closeHeaders
266 | :*> respond rendered
267 | Default (Tuple ct rendered) -> do
268 | writeStatus statusOK
269 | :*> contentType ct
270 | :*> closeHeaders
271 | :*> respond rendered
272 | NotAcceptable _ -> do
273 | writeStatus statusNotAcceptable
274 | :*> contentType textPlain
275 | :*> closeHeaders
276 | :*> end
277 | routeEndpoint proxy context handler (SProxy :: SProxy method)
278 | where bind = ibind
279 |
280 | instance routerRaw :: IsSymbol method
281 | => Router
282 | (Raw method)
283 | (Middleware
284 | m
285 | { request :: req
286 | , response :: (res StatusLineOpen)
287 | , components :: c
288 | }
289 | { request :: req
290 | , response :: (res ResponseEnded)
291 | , components :: c
292 | }
293 | Unit)
294 | (Middleware
295 | m
296 | { request :: req
297 | , response :: (res StatusLineOpen)
298 | , components :: c
299 | }
300 | { request :: req
301 | , response :: (res ResponseEnded)
302 | , components :: c
303 | }
304 | Unit)
305 | where
306 | route proxy context r =
307 | routeEndpoint proxy context r (SProxy :: SProxy method)
308 |
309 |
310 | instance routerResource :: ( Router methods h out
311 | )
312 | => Router (Trout.Resource methods) h out where
313 | route proxy = route (Proxy :: Proxy methods)
314 |
315 |
316 | router
317 | :: forall s r m req res c
318 | . Monad m
319 | => Request req m
320 | => Router s r (Middleware
321 | m
322 | (Conn req (res StatusLineOpen) c)
323 | (Conn req (res ResponseEnded) c)
324 | Unit)
325 | => Proxy s
326 | -> r
327 | -> (Status
328 | -> Maybe String
329 | -> Middleware
330 | m
331 | (Conn req (res StatusLineOpen) c)
332 | (Conn req (res ResponseEnded) c)
333 | Unit)
334 | -> Middleware
335 | m
336 | (Conn req (res StatusLineOpen) c)
337 | (Conn req (res ResponseEnded) c)
338 | Unit
339 | router site handler onRoutingError = do
340 | handler'
341 | -- Run the routing to get a handler.
342 | -- route (Proxy :: Proxy s) ctx handler
343 | -- Then, if successful, run the handler, possibly also generating an HTTPError.
344 | -- # either catch runHandler
345 | where
346 | context { parsedUrl, method } =
347 | let parsedUrl' = force parsedUrl in
348 | { path: parsedUrl'.path
349 | , query: either (const []) identity parsedUrl'.query
350 | , method: method
351 | }
352 | catch (HTTPError { status, message }) =
353 | onRoutingError status message
354 |
355 | handler' ∷ Middleware
356 | m
357 | (Conn req (res StatusLineOpen) c)
358 | (Conn req (res ResponseEnded) c)
359 | Unit
360 | handler' = do
361 | ctx <- context <$> getRequestData
362 | case route site ctx handler of
363 | Left err → catch err
364 | Right h → h
365 |
366 | bind = ibind
367 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hypertrout
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
Hypertrout
44 |
Build servers in Hyper using Trout
45 | By Oskar Wickström
46 |
47 |
Contents
48 |
59 |
Purpose
60 |
The purpose of this package, Hypertrout, is writing web servers using the type-level routing API in Trout. It provides a router middleware which, together with records of handler functions for resources, and rendering instances, gives us a full-fledged server.
61 |
A Single-Resource Example
62 |
Let’s say we want to render a home page as HTML. We start out by declaring the data type Home, and the structure of our site:
Resource (Get Home HTML) is a routing type with only one resource, responding to HTTP GET requests, rendering a Home value as HTML. So where does the Home value come from? We provide it using a handler inside a resource record. A resource record for Site1 would be some value of the following type:
67 |
forall m.Monad m => {"GET" ::ExceptTRoutingError m Home}
68 |
The resource record has fields for each supported HTTP method, with values being the corresponding handlers. A resource record type, supporting both GET and POST, could have the following type:
69 |
forall m.Monad m => { "GET" ::ExceptTRoutingError m SomeType
70 | , "POST" ::ExceptTRoutingError m SomeType
71 | }
72 |
We can construct a resource record for the Site1 routing type using pure and a Home value:
73 |
home :: forall m.Applicative m => {"GET" :: m Home}
74 | home = {"GET": pure Home}
75 |
Nice! But what comes out on the other end? We need something that renders the Home value as HTML. By providing an instance of EncodeHTML for Home, we instruct the resource how to render.
76 |
instance encodeHTMLHome ::EncodeHTMLHomewhere
77 | encodeHTML Home=
78 | p (text "Welcome to my site!")
79 |
The HTML type is a phantom type, only used as a marker type, and the actual markup is written in the MarkupM DSL from purescript-smolder.
80 |
We are getting ready to create the server. First, we need a value-level representation of the Site1 type, to be able to pass it to the router function. For that we use Proxy. Its documentation describes it as follows:
81 |
82 |
The Proxy type and values are for situations where type information is required for an input to determine the type of an output, but where it is not possible or convenient to provide a value for the input.
83 |
84 |
We create a top-level definition of the type Proxy Site1 with the value constructor Proxy.
85 |
site1 ::ProxySite1
86 | site1 =Proxy
87 |
We pass the proxy, our handler, and the onRoutingError function for cases where no route matched the request, to the router function.
88 |
onRoutingError status msg =
89 | writeStatus status
90 | :*> contentType textHTML
91 | :*> closeHeaders
92 | :*> respond (maybe "" id msg)
93 |
94 | siteRouter = router site1 home onRoutingError
95 |
The value returned by router is regular middleware, ready to be passed to a server.
96 |
main :: forall e.Eff (http ::HTTP, console ::CONSOLE, buffer ::BUFFER| e) Unit
97 | main =
98 | runServer defaultOptions {} siteRouter
99 |
Routing Multiple Resources
100 |
Real-world servers often need more than one resource. To combine multiple resources, resource routing types are separated using the :<|> operator, the type-level operator for separating alternatives.
When combining multiple resources in a routing type, each resource has to be named. The := type-level operator names a resource, or another nested structure of resources, using a Symbol on the left-hand side, and a routing type on the right-hand side.
103 |
"<resource-name>":=RoutingType
104 |
The following is a routing type for two resources, named "foo" and "bar":
There are some new things in this code that we haven’t talked about, and some we touched upon a bit. Here’s a walk-through of what’s going on:
136 |
137 |
:<|> is the type-level operator that, in general, separates alternatives. In case of resources, a router will try each route in order until one matches.
138 |
:= names a route, where the left-hand argument is a Symbol, the name, and the right-hand argument is a routing type. Named routes are combined with :<|>, as explained previously.
139 |
:/ separates a literal path segment and the rest of the routing type. Note that a named routing type, created with :=, has no relation to literal path segments. In other words, if want a resource named "foo" to be served under the path /foo, we write:
140 |
"foo":="foo":/...
141 |
Capture takes a descriptive string and a type. It takes the next available path segment and tries to convert it to the given type. Each capture in a routing type corresponds to an argument in the handler function.
142 |
:> separates a routing type modifier, like Capture, and the rest of the routing type.
143 |
144 |
We define a resource record using regular functions on the specified data types, returning ExceptT RoutingError m a values, where m is the monad of our middleware, and a is the type to render for the resource and method.
145 |
homeResource :: forall m.Monad m => {"GET" ::ExceptTRoutingError m Home}
146 | homeResource = {"GET": pure Home}
147 |
148 | usersResource :: forall m.Monad m => {"GET" ::ExceptTRoutingError m AllUsers}
149 | usersResource = {"GET":AllUsers<$> getUsers}
150 |
151 | userResource :: forall m.Monad m =>Int-> {"GET" ::ExceptTRoutingError m User}
152 | userResource id' =
153 | {"GET":
154 | find (\(User u) -> u.id == id') <$> getUsers >>=
155 | case _ of
156 | Just user -> pure user
157 | Nothing->
158 | throwError (HTTPError { status: statusNotFound
159 | , message:Just"User not found."
160 | })
161 | }
162 |
As in the single-resource example, we want to render as HTML. Let’s create instances for our data types. Notice how we can create links between routes in a type-safe manner.
163 |
instance encodeHTMLHome ::EncodeHTMLHomewhere
164 | encodeHTML Home=
165 | let {users} = linksTo site2
166 | in p do
167 | text "Welcome to my site! Go check out my "
168 | linkTo users (text "Users")
169 | text "."
170 |
171 | instance encodeHTMLAllUsers ::EncodeHTMLAllUserswhere
172 | encodeHTML (AllUsers users) =
173 | div do
174 | h1 (text "Users")
175 | ul (traverse_ linkToUser users)
176 | where
177 | linkToUser (User u) =
178 | let {user} = linksTo site2
179 | in li (linkTo (user u.id) (text u.name))
180 |
181 | instance encodeHTMLUser ::EncodeHTMLUserwhere
182 | encodeHTML (User { name }) =
183 | h1 (text name)
184 |
The record destructuring on the value returned by linksTo extracts the correct link, based on the names from the routing type. Each link will have a type based on the corresponding resource. user in the previous code has type Int -> URI, while users has no captures and thus has type URI.
185 |
We are still missing getUsers, our source of User values. In a real application it would probably be a database query, but for this example we simply hard-code some famous users of proper instruments.
186 |
getUsers :: forall m.Applicative m => m (ArrayUser)
187 | getUsers =
188 | pure
189 | [ User { id:1, name:"John Paul Jones" }
190 | , User { id:2, name:"Tal Wilkenfeld" }
191 | , User { id:3, name:"John Patitucci" }
192 | , User { id:4, name:"Jaco Pastorious" }
193 | ]
194 |
Almost done! We just need to create the router, and start a server.
Notice how the resources record matches the names and structure of our routing type. If we fail to match the type we get a compile error.
213 |
Multi-Method Resources
214 |
So far we have just used a single method per resource, the Get method. By replacing the single method type with a sequence of alternatives, constructed with the type-level operator :<|>, we get a resource with multiple methods.
MultiMethodExample is a routing type with a single resource, named "user", which has multiple resource methods. Handlers for the resource methods are provided as a record value, with field names matching the HTTP methods:
By specifying alternative content types for a method, Hyper can choose a response and content type based on the request Accept header. This is called content negotiation. Instead of specifying a single type, like HTML or JSON, we provide alternatives using :<|>. All content types must have MimeRender instances for the response body type.
By making requests to this site, using Accept headers, we can see how the router chooses the matching content type (output formatted and shortened for readability).