├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── docs ├── .gitignore ├── 404.html ├── Makefile ├── README.md ├── bin │ ├── make-index-page.sh │ ├── make-latest-page.sh │ └── sort_by_semver.py ├── make.bat ├── requirements.txt └── src │ ├── _static │ ├── favicon.png │ ├── favicon.svg │ ├── hyper-inverse.png │ ├── hyper-inverse.svg │ ├── hyper-inverse@2x.png │ ├── hyper.png │ ├── hyper.svg │ ├── hyper@2x.png │ ├── icon-large.png │ └── pygments.css │ ├── conf.py │ ├── core-api │ ├── conn.rst │ ├── index.rst │ ├── middleware.rst │ ├── response-state-transitions.rst │ └── servers.rst │ ├── extensions │ ├── index.rst │ └── type-level-routing │ │ └── index.rst │ ├── faq.rst │ ├── index.rst │ ├── introduction │ └── index.rst │ ├── theme │ ├── layout.html │ ├── relbar.html │ ├── search.html │ ├── searchbox.html │ ├── searchresults.html │ ├── static │ │ ├── bootstrap │ │ │ ├── config.json │ │ │ ├── css │ │ │ │ ├── bootstrap-theme.css │ │ │ │ ├── bootstrap-theme.min.css │ │ │ │ ├── bootstrap.css │ │ │ │ └── bootstrap.min.css │ │ │ ├── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ └── js │ │ │ │ ├── bootstrap.js │ │ │ │ └── bootstrap.min.js │ │ ├── custom-highlight.css │ │ ├── overrides.css │ │ └── style.css │ └── theme.conf │ ├── topic-guides │ ├── FormSerialization.purs │ ├── NodeReaderT.purs │ ├── ReadBody.purs │ ├── forms.rst │ ├── index.rst │ ├── nodejs.rst │ ├── request-body-reading.rst │ └── testing.rst │ └── tutorials │ ├── getting-started-with-hyper.rst │ └── index.rst ├── examples ├── Authentication.purs ├── AuthenticationAndAuthorization.purs ├── Cookies.purs ├── FileServer.purs ├── FileServer │ ├── assets │ │ └── style.css │ └── index.html ├── FormParser.purs ├── HelloHyper.purs ├── NodeStreamRequest.purs ├── NodeStreamResponse.purs ├── QualifiedDo.purs ├── Sessions.purs └── StateT.purs ├── github.css ├── package.json ├── packages.dhall ├── spago.dhall ├── src └── Hyper │ ├── Authentication.purs │ ├── Authorization.purs │ ├── Conn.purs │ ├── ContentNegotiation.purs │ ├── Cookies.purs │ ├── Form.purs │ ├── Form │ └── Urlencoded.purs │ ├── Header.purs │ ├── Middleware.purs │ ├── Middleware │ └── Class.purs │ ├── Node │ ├── BasicAuth.purs │ ├── FileServer.purs │ ├── Server.purs │ ├── Server │ │ └── Options.purs │ ├── Session │ │ ├── InMemory.js │ │ └── InMemory.purs │ └── Test.purs │ ├── Request.purs │ ├── Response.purs │ ├── Session.purs │ ├── Status.purs │ └── Test │ └── TestServer.purs └── test ├── Hyper ├── ContentNegotiationSpec.purs ├── CookiesSpec.purs ├── FormSpec.purs ├── Node │ ├── Assertions.purs │ ├── BasicAuthSpec.purs │ ├── FileServerSpec.purs │ └── FileServerSpec │ │ ├── index.html │ │ └── some-file.txt └── RequestSpec.purs └── Main.purs /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /.psci* 6 | /src/.webpack.js 7 | /.psc-ide-port 8 | /.spago 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: required 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - texlive 9 | - texlive-fonts-recommended 10 | - texlive-latex-extra 11 | - texlive-fonts-extra 12 | - dvipng 13 | - texlive-latex-recommended 14 | - latex-xcolor 15 | - lmodern 16 | - latexmk 17 | 18 | node_js: 19 | - '11' 20 | 21 | env: 22 | matrix: 23 | - PATH=$HOME/purescript:$PATH 24 | global: 25 | - secure: "lAAcD9YPFC/yNql26yvr0llA4JY4TBPkSL0JdmbQ+i4KwQiw/o+sUSH+KPxoesD/1qcFBKwnv+dp11oggRDvlh+LRQzj2PH1/eSRVQ6r3aZ4/lgtQ6JHUWUpbbjTGqKc6Flx5+0kMR2U0l1FVUaSi0cj9QKYVcFsvg+Kk1QW7q8gltSwOV8Kmt2MPKbGkUzhMLF9VddaEwmBdVGZNw0n/ZKN+7fdwctfwYhhcVQVTvPmdOtgXlPioyVOGsAEUg96qZmBebTfEoDkmujYjaE9lueoSpFgArSxMpxoRPkq22bY7e/zG4Lfg2KgbPDbSNunZLqDZicpP20nV7ZT/4SfNsi535995xVs/ByyaK1aaNayFZU4CtxUolsib9wMvE7fo+jJCBET19LxxfJ324iUT0GBIoR5e9R1GSpiSifzThOBnzRsA2t1UGvVJri7QbxnRKaj7uEOBCgcs0u5eYRElfo50/zWoNcUBTSeW/8RzxBVPrcXlmz+QL8rSdtJuVi1Nmzsv/EJhdpW8qEO4sO6xu/7mSsHaVS0kTPOyNLvha4SGEDjLsz2NNqDz87pxYtazysAf+hFGBQoAjWMFaCArOy2YfVgdFpWK7pThjd33KYwrCBYGpa+IRMo2rCd8T2SHkMLgxciMyP09y2M+d7C9LJ0CcemdXX5zfjmPjBxxEs=" 26 | - secure: "K3g58P0HUnRLIa2te30XL8xmyzuvimzG0z5R4C2+fku/gC6kkYs2MT/Cc3MFYHt9FPGGOTfys3UR0aPkq2bSL+Y5WXbjutY9PNqsvA2MqMLxdpZirTW6N6yP2l/ilMnPnJUNxScnErqvk4JKvdybYrAxYRA1mC2x3UknCL7uFLowFB/GU/XyTBY3mhu5uIB5I//Lr/oKMxT1M98hA4T5WHbLEE6K5ozypbsX5yU8OOgf6+ZZO9YegBS7vD9zlZNZivObfX+N0tmWkGLp+1WZiI5x0BaYuNU2oFq+hQie/tHAQTvcmKfprreRPGr1R95878G8OpVuoVLX3yXEfznfV62RYJodHsOUDFOMRXbgoBPRGK3GsCts8UaOg0efG4Y67pbMYgXHROtrLxlBfZasrcu6Odh+ZlmV4eraMi6muJWkRwga03i7BtwiP3Aw47+ycoMympupRyDlzk9HCO7ZyhYXsN25RCOsY6HQ6U6k9WvG421KPEPGz5g4hBuVqq5svA8wAdT3XUGIkYkfPmSi2SK2xEDJhz/g5E5HazM8lbmZfFYCB8fthz3XuRuiQqHQov3VrBPMWYa4VxMsvbc2NFc+4DwenE6PaOBwrwcH1zdtlWZB4YpyCZPeXFrQwm5uvm3I3YKijBb4sNjzLCeUSR7QkuMiYa/sE/tb1yYPhFc=" 27 | 28 | install: 29 | - npm install -g bower pulp@13.0.0 purescript@^0.13.0 spago@0.8.3 30 | - spago install 31 | - bower install 32 | - pip install --user -r docs/requirements.txt 33 | 34 | script: 35 | - export VERSION=branch-job-$TRAVIS_JOB_NUMBER 36 | - if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then export VERSION=pull-request-job-$TRAVIS_JOB_NUMBER; 37 | fi 38 | - if [[ "$TRAVIS_TAG" != "" ]]; then export VERSION=$TRAVIS_TAG; fi 39 | - pulp build 40 | - rm -rf bower_components output 41 | - spago build 42 | - spago test 43 | - make examples 44 | - make docs 45 | 46 | before_deploy: pip install --user awscli 47 | 48 | deploy: 49 | - provider: s3 50 | access_key_id: "$AWS_ACCESS_KEY_ID" 51 | secret_access_key: "$AWS_SECRET_ACCESS_KEY" 52 | local_dir: docs/target/release 53 | bucket: hyper.wickstrom.tech 54 | upload-dir: docs/$VERSION 55 | region: eu-west-1 56 | skip_cleanup: true 57 | on: 58 | tags: true 59 | repo: owickstrom/hyper 60 | 61 | after_deploy: 62 | # regenerate and deploy the documentation index 63 | - make -C docs release-index 64 | # cache for one hour 65 | - aws s3 cp --region eu-west-1 --cache-control max-age=3600,public docs/target/index/index.html s3://hyper.wickstrom.tech/ 66 | - aws s3 cp --region eu-west-1 --cache-control max-age=3600,public docs/target/index/latest/index.html s3://hyper.wickstrom.tech/latest/index.html 67 | 68 | cache: 69 | directories: 70 | - output 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | * 0.11.1 4 | - Update for new Argonaut version. 5 | * 0.11.0 6 | - Update qualified-do usages to use indexed-monad exports. 7 | - Upgrade to node-buffer version 6. 8 | * 0.10.1 9 | - Add node-buffer as direct dependency to avoid incompatible upgrade 10 | to version 6. 11 | * 0.10.0 12 | - Updates for PureScript 0.13 13 | - Migration to Spago as primary package manager / build tool 14 | * 0.9.1 15 | - Remove redundant ```purescript-argonaut-*``` dependencies to 16 | resolve version conflicts. #76 17 | * 0.9.0 18 | - Node server changes: 19 | - Move ```ServerOptions``` from ```Hyper.Node.Server``` to 20 | ```Hyper.Node.Server.Options``` and rename to ```Options```. #40 21 | - Add a ```Hostname``` newtype for ```hostname``` option. #40 22 | - Use ```runAff``` instead of ```launchAff``` to correctly handle 23 | asynchronous exceptions. #44 24 | - In-memory session store: 25 | - Fix ```put```/```delete``` bug. #53 26 | - Use ```Ref``` instead of ```AVar```. #53 27 | - Enable random session ID generation. #53 28 | - File server: Detect content-type from file extension. #59 29 | - ```Set-Cookie``` attributes #60 30 | - ```Semigroup``` instance for ```Form``` type #52 31 | - Upgrade to ```purescript-aff``` v5. #50, #64 32 | - Migrate to PureScript 0.12. #63 33 | - Upgrade dependencies. #63 34 | - Add support for qualified-do syntax. #70 35 | - Various documentation updates. 36 | * 0.8.0 37 | - Add `MonadAff` instance for `Middleware` 38 | - Add Buffer instance for `ReadableBody` 39 | - Add `Readable ()` instance for `StreamableBody`, a new type class for 40 | streaming the request body, instead of reading the complete body as a 41 | value. 42 | * 0.7.3 43 | - Link to external packages in extensions docs 44 | * 0.7.2 45 | - Improve docs template, and index mobile support 46 | * 0.7.1 47 | - Deploy documentation for each tagged release 48 | - HTTPS for documentation site 49 | - No built docs in git repository 50 | * 0.7.0 51 | - PureScript 0.11.x compatibility (breaking changes!) 52 | * 0.6.0 53 | - Include early support for cookies and sessions 54 | - Add docs for new type-level routing API featuring the `Resource` construct 55 | - Use type classes instead of exposing record structure of `request` and 56 | `response` 57 | - The `Request` type class provides the `getRequestData` method 58 | - The `ReadableBody` type class provides overloaded ways of reading the 59 | request body as different types 60 | - Type signatures are generally much lighter and nicer to read! 61 | * 0.5.0 62 | - Separate out 63 | [purescript-hyper-routing-server](https://github.com/owickstrom/purescript-hyper-routing-server) 64 | into a package, cleaning up the routing type API 65 | - Minor changes to docs 66 | * 0.4.1 67 | - Fix issue with leaking type parameters in `runServer`, breaks backwards 68 | compatibility (type signatures changed) 69 | * 0.4.0 70 | - Separate out 71 | [purescript-hyper-routing](https://github.com/owickstrom/purescript-hyper-routing) 72 | into a package 73 | - Migrate to Sphinx for documentation 74 | - Fix bug caused by `stream.writeString` on empty string 75 | - Improve content negotiation (now in separate routing lib) 76 | * 0.3.0 77 | - Support usage of monad transformers together with Node server. 78 | - Move `onListening` and `onRequestError` into the Node server 79 | options. 80 | - Move the documentation to http://hyper.wickstrom.tech. 81 | - Support streaming responses with the Node server. 82 | - Restructure documentation and examples, more documentation. 83 | - Redesign of Middleware construct to use [indexed monads][indexed], 84 | instead of monadic functions. This breaks backwards compatibility! 85 | - Basic support for multiple content types in router (content negotiation 86 | is not at all well-tested, beware!) 87 | * 0.2.0 88 | - Use `purescript-http-methods` instead of custom Method type. Also use 89 | `Either Method CustomMethod` in middleware. 90 | * ... The dark ages. 91 | 92 | [indexed]: https://github.com/garyb/purescript-indexed-monad 93 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= $(shell git rev-parse --short HEAD) 2 | 3 | .PHONY: docs 4 | docs: 5 | make -C docs release 6 | 7 | .PHONY: examples 8 | examples: 9 | # Disabled! This part of the docs should be moved to the respective 10 | # repository instead. 11 | # make -C docs/src/extensions/type-level-routing/examples build 12 | # 13 | spago build -p docs/src/topic-guides/**/*.purs 14 | spago build -p examples/**/*.purs 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
10 | Type-safe, statically checked composition of HTTP servers 11 |
12 | 13 |14 | Getting Started 15 | | Documentation 16 | | FAQ 17 | | Examples 18 |
19 | 20 |35 | The page you are looking for is not here. It might have been relocated, 36 | so please go back to the documentation index, and find 38 | your way from there. 39 |
40 |93 | Type-safe, statically checked composition of HTTP servers 94 |
95 |96 | Hyper is an experimental middleware architecture for HTTP servers written in 97 | PureScript. Its main focus is correctness and type-safety, using type-level 98 | information to enforce correct composition and abstraction for web servers. 99 | The Hyper project is also a breeding ground for higher-level web server 100 | constructs, which tend to fall under the “framework” category. 101 |
102 |104 | Choose a version and a format of the documentation below, or just grab the latest. 106 |
107 | 108 |Version | Formats |
---|
Redirecting to latest release of Hyper, at ${url}...
18 | 21 | 22 | " 23 | -------------------------------------------------------------------------------- /docs/bin/sort_by_semver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import fileinput 4 | import semver 5 | 6 | 7 | def cmp_semver(a, b): 8 | try: 9 | return semver.compare(a, b) 10 | except: 11 | return cmp(a, b) 12 | 13 | 14 | def sort_versions(versions): return sorted(versions, cmp=cmp_semver, 15 | reverse=True) 16 | 17 | 18 | def main(): 19 | versions = map(lambda line: line.strip(), list(fileinput.input())) 20 | sorted_versions = sort_versions(versions) 21 | for version in sorted_versions: 22 | print(version) 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^113 | PDF 114 |
115 |27 | {% trans %}Please activate JavaScript to enable the search 28 | functionality.{% endtrans %} 29 |
30 |32 | {% trans %}From here you can search these documents. Enter your search 33 | words into the box below and click "search". Note that the search 34 | function will automatically search for all of the words. Pages 35 | containing fewer words won't appear in the result list.{% endtrans %} 36 |
37 | 38 | 45 | 46 | {% if search_performed %} 47 |{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}
50 | {% endif %} 51 | {% endif %} 52 |12 | From here you can search these documents. Enter your search 13 | words into the box below and click "search". 14 |
15 | 16 | 21 | 22 | {%- if search_performed %} 23 |Your search did not match any results.
26 | {%- endif %} 27 | {%- endif %} 28 |If this is green, then it is definitely working.
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/FormParser.purs: -------------------------------------------------------------------------------- 1 | module Examples.FormParser where 2 | 3 | import Prelude 4 | import Text.Smolder.HTML.Attributes as A 5 | import Control.Monad.Indexed.Qualified as Ix 6 | import Control.Monad.Indexed ((:>>=), (:*>)) 7 | import Effect (Effect) 8 | import Effect.Class (liftEffect) 9 | import Effect.Console (log) 10 | import Data.Either (Either(Right, Left)) 11 | import Data.HTTP.Method (Method(..)) 12 | import Data.Maybe (Maybe(Nothing, Just)) 13 | import Data.MediaType.Common (textHTML) 14 | import Data.String (length) 15 | import Hyper.Form (parseForm, required) 16 | import Hyper.Node.Server (defaultOptionsWithLogging, runServer) 17 | import Hyper.Request (getRequestData) 18 | import Hyper.Response (closeHeaders, contentType, respond, writeStatus) 19 | import Hyper.Status (statusBadRequest, statusMethodNotAllowed, statusOK) 20 | import Text.Smolder.HTML (button, form, input, label, p) 21 | import Text.Smolder.Markup (text, (!)) 22 | import Text.Smolder.Renderer.String (render) 23 | 24 | main :: Effect Unit 25 | main = 26 | let 27 | -- A view function that renders the name form. 28 | renderNameForm err = do 29 | form ! A.method "post" $ do 30 | formattedError 31 | formElements 32 | where 33 | formElements = do 34 | label ! A.for "firstName" $ text "Your Name:" 35 | p (input ! A.name "firstName" ! A.id "firstName") 36 | p (button (text "Send")) 37 | 38 | formattedError = 39 | case err of 40 | Just s -> p ! A.style "color: red;" $ text s 41 | Nothing -> pure unit 42 | 43 | htmlWithStatus status x = Ix.do 44 | writeStatus status 45 | contentType textHTML 46 | closeHeaders 47 | respond (render x) 48 | 49 | handlePost = 50 | parseForm :>>= 51 | case _ of 52 | Left err -> do 53 | liftEffect (log err) 54 | :*> htmlWithStatus 55 | statusBadRequest 56 | (p (text "Bad request, invalid form.")) 57 | Right form -> 58 | case required "firstName" form of 59 | Right name 60 | | length name > 0 -> 61 | htmlWithStatus 62 | statusOK 63 | (p (text ("Hi " <> name <> "!"))) 64 | | otherwise -> 65 | htmlWithStatus 66 | statusBadRequest 67 | (renderNameForm (Just "Name cannot be empty.")) 68 | Left err -> 69 | htmlWithStatus 70 | statusBadRequest 71 | (renderNameForm (Just err)) 72 | 73 | -- Our (rather primitive) router. 74 | router = 75 | _.method <$> getRequestData :>>= 76 | case _ of 77 | Left GET -> 78 | htmlWithStatus 79 | statusOK 80 | (renderNameForm Nothing) 81 | Left POST -> 82 | handlePost 83 | method -> 84 | htmlWithStatus 85 | statusMethodNotAllowed 86 | (text ("Method not supported: " <> show method)) 87 | 88 | -- Let's run it. 89 | in runServer defaultOptionsWithLogging {} router 90 | -------------------------------------------------------------------------------- /examples/HelloHyper.purs: -------------------------------------------------------------------------------- 1 | module Examples.HelloHyper where 2 | 3 | import Prelude 4 | import Control.Monad.Indexed.Qualified as Ix 5 | import Effect (Effect) 6 | import Hyper.Node.Server (defaultOptionsWithLogging, runServer) 7 | import Hyper.Response (closeHeaders, respond, writeStatus) 8 | import Hyper.Status (statusOK) 9 | 10 | main :: Effect Unit 11 | main = 12 | let app = Ix.do 13 | writeStatus statusOK 14 | closeHeaders 15 | respond "Hello, Hyper!" 16 | in runServer defaultOptionsWithLogging {} app 17 | -------------------------------------------------------------------------------- /examples/NodeStreamRequest.purs: -------------------------------------------------------------------------------- 1 | -- This example shows how you can stream request body data. It 2 | -- logs the size of each chunk it receives from the POST body. 3 | -- 4 | -- Test it out by first running the server, 5 | -- 6 | -- $ pulp run -I examples -m Examples.NodeStreamRequest 7 | -- 8 | -- and then, POST a large file with something like this command: 9 | -- 10 | -- $ curl -X POST --data-binary @/your/large/file localhost:3000 11 | -- 12 | module Examples.NodeStreamRequest where 13 | 14 | import Prelude 15 | import Node.Buffer as Buffer 16 | import Node.Stream as Stream 17 | import Control.Monad.Indexed.Qualified as Ix 18 | import Control.Monad.Indexed ((:>>=)) 19 | import Effect (Effect) 20 | import Effect.Class (class MonadEffect, liftEffect) 21 | import Effect.Console (log) 22 | import Effect.Exception (catchException, message) 23 | import Data.Either (Either(..), either) 24 | import Data.HTTP.Method (Method(..)) 25 | import Hyper.Node.Server (defaultOptionsWithLogging, runServer) 26 | import Hyper.Request (getRequestData, streamBody) 27 | import Hyper.Response (closeHeaders, respond, writeStatus) 28 | import Hyper.Status (statusMethodNotAllowed, statusOK) 29 | 30 | logRequestBodyChunks 31 | :: forall m 32 | . MonadEffect m 33 | => Stream.Readable () 34 | -> m Unit 35 | logRequestBodyChunks body = 36 | Stream.onData body (Buffer.size >=> (log <<< ("Got chunk of size: " <> _) <<< show)) 37 | # catchException (log <<< ("Error: " <> _) <<< message) 38 | # liftEffect 39 | 40 | main :: Effect Unit 41 | main = 42 | let 43 | app = 44 | getRequestData :>>= 45 | case _ of 46 | 47 | -- Only handle POST requests: 48 | { method: Left POST } -> Ix.do 49 | body <- streamBody 50 | logRequestBodyChunks body 51 | writeStatus statusOK 52 | closeHeaders 53 | respond "OK" 54 | 55 | -- Non-POST requests are not allowed: 56 | { method } -> Ix.do 57 | writeStatus statusMethodNotAllowed 58 | closeHeaders 59 | respond ("Method not allowed: " <> either show show method) 60 | 61 | in runServer defaultOptionsWithLogging {} app 62 | -------------------------------------------------------------------------------- /examples/NodeStreamResponse.purs: -------------------------------------------------------------------------------- 1 | -- This example shows how you can stream responses asynchronously. 2 | -- 3 | -- Test it out like so: 4 | -- $ curl -N http://localhost:3000 5 | module Examples.NodeStreamResponse where 6 | 7 | import Prelude 8 | import Control.Monad.Indexed.Qualified as Ix 9 | import Control.Monad.Indexed ((:*>)) 10 | import Effect.Aff as Aff 11 | import Effect.Aff.Class (class MonadAff, liftAff) 12 | import Effect (Effect) 13 | import Data.Int (toNumber) 14 | import Data.Newtype (wrap) 15 | import Data.Traversable (traverse_) 16 | import Data.Tuple (Tuple(..)) 17 | import Hyper.Middleware (Middleware, lift') 18 | import Hyper.Node.Server (defaultOptions, runServer, writeString) 19 | import Hyper.Response (closeHeaders, end, send, writeStatus) 20 | import Hyper.Status (statusOK) 21 | import Node.Encoding (Encoding(..)) 22 | 23 | delay :: forall m c. MonadAff m => Int -> Middleware m c c Unit 24 | delay n = lift' (liftAff (Aff.delay (wrap <<< toNumber $ n) *> pure unit)) 25 | 26 | main :: Effect Unit 27 | main = 28 | let 29 | -- These messages are streamed one at a time, regardless of their individual 30 | -- delays, i.e. they're sequenced. 31 | streamMessages = 32 | traverse_ 33 | (\(Tuple ms s) -> delay ms :*> send (writeString UTF8 s)) 34 | [ Tuple 2000 "Hello\n" 35 | , Tuple 1000 "Streaming\n" 36 | , Tuple 500 "Hyper\n" 37 | ] 38 | 39 | app = Ix.do 40 | writeStatus statusOK 41 | closeHeaders 42 | streamMessages 43 | end 44 | in runServer defaultOptions {} app 45 | -------------------------------------------------------------------------------- /examples/QualifiedDo.purs: -------------------------------------------------------------------------------- 1 | module Examples.QualifiedDo where 2 | 3 | import Prelude 4 | import Effect (Effect) 5 | import Control.Monad.Indexed.Qualified as Ix 6 | import Hyper.Node.Server (defaultOptionsWithLogging, runServer) 7 | import Hyper.Response (closeHeaders, respond, writeStatus) 8 | import Hyper.Status (statusOK) 9 | 10 | main :: Effect Unit 11 | main = runServer defaultOptionsWithLogging {} Ix.do 12 | writeStatus statusOK 13 | closeHeaders 14 | respond "Hello, Hyper!" 15 | -------------------------------------------------------------------------------- /examples/Sessions.purs: -------------------------------------------------------------------------------- 1 | module Examples.Sessions where 2 | 3 | import Prelude 4 | import Control.Monad.Indexed.Qualified as Ix 5 | import Control.Monad.Indexed ((:>>=)) 6 | import Effect.Aff (launchAff) 7 | import Effect (Effect) 8 | import Effect.Class (liftEffect) 9 | import Effect.Class.Console (log) 10 | import Data.Maybe (Maybe(..)) 11 | import Data.MediaType.Common (textHTML) 12 | import Hyper.Cookies (cookies) 13 | import Hyper.Middleware (lift') 14 | import Hyper.Node.Server (defaultOptionsWithLogging, runServer) 15 | import Hyper.Node.Session.InMemory (newInMemorySessionStore) 16 | import Hyper.Request (getRequestData) 17 | import Hyper.Response (closeHeaders, contentType, end, redirect, respond, writeStatus) 18 | import Hyper.Session (deleteSession, getSession, saveSession) 19 | import Hyper.Status (statusNotFound, statusOK) 20 | 21 | newtype MySession = MySession { userId :: Int } 22 | 23 | main :: Effect Unit 24 | main = void $ launchAff do 25 | store <- liftEffect newInMemorySessionStore 26 | liftEffect (runServer defaultOptionsWithLogging (components store) app) 27 | where 28 | components store = 29 | { sessions: { key: "my-session" 30 | , store: store 31 | } 32 | , cookies: unit 33 | } 34 | 35 | home = Ix.do 36 | writeStatus statusOK 37 | contentType textHTML 38 | closeHeaders 39 | getSession :>>= 40 | case _ of 41 | Just (MySession { userId }) -> Ix.do 42 | lift' (log "Session") 43 | respond ("You are logged in as user " <> show userId <> ". " 44 | <> "Logout if you're anxious.") 45 | Nothing -> Ix.do 46 | lift' (log "No Session") 47 | respond "Login to start a session." 48 | 49 | login = Ix.do 50 | redirect "/" 51 | saveSession (MySession { userId: 1 }) 52 | contentType textHTML 53 | closeHeaders 54 | end 55 | 56 | logout = Ix.do 57 | redirect "/" 58 | deleteSession 59 | closeHeaders 60 | end 61 | 62 | notFound = Ix.do 63 | writeStatus statusNotFound 64 | contentType textHTML 65 | closeHeaders 66 | respond "Not Found" 67 | 68 | -- Simple router for this example. 69 | router = 70 | getRequestData :>>= \{ url } -> 71 | case url of 72 | "/" -> home 73 | "/login" -> login 74 | "/logout" -> logout 75 | _ -> notFound 76 | 77 | app = Ix.do 78 | cookies 79 | router 80 | -------------------------------------------------------------------------------- /examples/StateT.purs: -------------------------------------------------------------------------------- 1 | module Examples.StateT where 2 | 3 | import Prelude 4 | import Control.Monad.Indexed.Qualified as Ix 5 | import Effect.Aff (Aff) 6 | import Effect (Effect) 7 | import Control.Monad.State (evalStateT, get, modify) 8 | import Control.Monad.State.Trans (StateT) 9 | import Data.String (joinWith) 10 | import Hyper.Middleware (lift') 11 | import Hyper.Node.Server (defaultOptionsWithLogging, runServer') 12 | import Hyper.Response (closeHeaders, respond, writeStatus) 13 | import Hyper.Status (statusOK) 14 | 15 | 16 | runAppM ∷ ∀ a. StateT (Array String) Aff a → Aff a 17 | runAppM = flip evalStateT [] 18 | 19 | 20 | main :: Effect Unit 21 | main = 22 | let 23 | -- Our application just appends to the state in between 24 | -- some operations, then responds with the built up state... 25 | app = Ix.do 26 | void $ lift' (modify (flip append ["I"])) 27 | writeStatus statusOK 28 | void $ lift' (modify (flip append ["have"])) 29 | closeHeaders 30 | void $ lift' (modify (flip append ["state."])) 31 | 32 | msgs ← lift' get 33 | respond (joinWith " " msgs) 34 | 35 | in runServer' defaultOptionsWithLogging {} runAppM app 36 | -------------------------------------------------------------------------------- /github.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:bold}.hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#008080}.hljs-string,.hljs-doctag{color:#d14}.hljs-title,.hljs-section,.hljs-selector-id{color:#900;font-weight:bold}.hljs-subst{font-weight:normal}.hljs-type,.hljs-class .hljs-title{color:#458;font-weight:bold}.hljs-tag,.hljs-name,.hljs-attribute{color:#000080;font-weight:normal}.hljs-regexp,.hljs-link{color:#009926}.hljs-symbol,.hljs-bullet{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-hyper", 3 | "description": "Type-safe, statically checked composition of HTTP servers", 4 | "main": " ", 5 | "directories": { 6 | "doc": "docs", 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "pulp test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/owickstrom/hyper.git" 15 | }, 16 | "author": "oskar.wickstrom@gmail.com", 17 | "license": "MPL-2.0", 18 | "homepage": "http://hyper.wickstrom.tech", 19 | "dependencies": { 20 | "bower-dependency-tree": "^0.1.2", 21 | "pulp": "^15.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20200615/packages.dhall sha256:5d0cfad9408c84db0a3fdcea2d708f9ed8f64297e164dc57a7cf6328706df93a 3 | 4 | let overrides = {=} 5 | 6 | let additions = {=} 7 | 8 | in upstream // overrides // additions 9 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | { sources = 2 | [ "src/**/*.purs", "test/**/*.purs" ] 3 | , name = 4 | "hyper" 5 | , dependencies = 6 | [ "aff" 7 | , "avar" 8 | , "argonaut" 9 | , "arrays" 10 | , "console" 11 | , "control" 12 | , "effect" 13 | , "foldable-traversable" 14 | , "generics-rep" 15 | , "http-methods" 16 | , "indexed-monad" 17 | , "media-types" 18 | , "node-buffer" 19 | , "node-fs-aff" 20 | , "node-http" 21 | , "ordered-collections" 22 | , "proxy" 23 | , "psci-support" 24 | , "random" 25 | , "smolder" 26 | , "spec" 27 | , "spec-discovery" 28 | , "strings" 29 | , "transformers" 30 | , "record-extra" 31 | ] 32 | , packages = 33 | ./packages.dhall 34 | } 35 | -------------------------------------------------------------------------------- /src/Hyper/Authentication.purs: -------------------------------------------------------------------------------- 1 | module Hyper.Authentication where 2 | 3 | import Hyper.Conn (Conn) 4 | 5 | setAuthentication :: forall a b req res c. 6 | b 7 | -> Conn req res { authentication :: a | c } 8 | -> Conn req res { authentication :: b | c } 9 | setAuthentication auth conn = 10 | conn { components { authentication = auth }} 11 | -------------------------------------------------------------------------------- /src/Hyper/Authorization.purs: -------------------------------------------------------------------------------- 1 | module Hyper.Authorization where 2 | 3 | import Control.Monad.Indexed.Qualified as Ix 4 | import Control.Monad (class Monad) 5 | import Data.Maybe (Maybe(Nothing, Just)) 6 | import Data.Unit (unit, Unit) 7 | import Hyper.Conn (Conn) 8 | import Hyper.Middleware (Middleware, lift') 9 | import Hyper.Middleware.Class (getConn, modifyConn) 10 | import Hyper.Response (class ResponseWritable, respond, headers, writeStatus, class Response, ResponseEnded, StatusLineOpen) 11 | import Hyper.Status (statusForbidden) 12 | 13 | withAuthorization :: forall a b req res c. 14 | b 15 | -> Conn req res { authorization :: a | c } 16 | -> Conn req res { authorization :: b | c } 17 | withAuthorization a conn = 18 | conn { components = (conn.components { authorization = a }) } 19 | 20 | 21 | authorized :: forall a m req res b c 22 | . Monad m 23 | => ResponseWritable b m String 24 | => Response res m b 25 | => (Conn req (res StatusLineOpen) { authorization :: Unit | c } -> m (Maybe a)) 26 | -> Middleware 27 | m 28 | (Conn req (res StatusLineOpen) { authorization :: a | c }) 29 | (Conn req (res ResponseEnded) { authorization :: a | c }) 30 | Unit 31 | -> Middleware 32 | m 33 | (Conn req (res StatusLineOpen) { authorization :: Unit | c }) 34 | (Conn req (res ResponseEnded) { authorization :: Unit | c }) 35 | Unit 36 | authorized authorizer mw = Ix.do 37 | conn ← getConn 38 | auth ← lift' (authorizer conn) 39 | case auth of 40 | Just a -> Ix.do 41 | modifyConn (withAuthorization a) 42 | mw 43 | modifyConn (withAuthorization unit) 44 | Nothing -> Ix.do 45 | writeStatus statusForbidden 46 | headers [] 47 | respond "You are not authorized." 48 | -------------------------------------------------------------------------------- /src/Hyper/Conn.purs: -------------------------------------------------------------------------------- 1 | module Hyper.Conn where 2 | 3 | -- | A `Conn` models the entirety of an HTTP connection, containing the fields 4 | -- | `request`, `response`, and the extensibility point `components`. 5 | type Conn req res components = 6 | { request :: req 7 | , response :: res 8 | , components :: components 9 | } 10 | -------------------------------------------------------------------------------- /src/Hyper/ContentNegotiation.purs: -------------------------------------------------------------------------------- 1 | -- This module implements, or aims to implement, Content Negotation, as 2 | -- described in