BSD-3-Clause licensed by Allele Dev
Maintained by [email protected]
This version can be pinned in stack with:wai-request-spec-0.10.2.1@sha256:ec5b0e347b155fdcf885db0ca797733e78b3ec93a7b6edbe6aa2c161785fb4bd,2892

WAI Request Spec

WAI Request Spec is a declarative validation layer for HTTP requests. It aims to make error-handling for malformed requests as easy as taking the happy path.

A brief summary of the core features:

  • Can specify headers and query params as input sources
  • Support for parsing ints, floats, text, bytes (with encodings), and bools
  • A parser combinator interface for chaining together request requirements
  • Support for Alternatives
  • Support for optional parameters
  • Convenient and informative default error messages that let service consumers know what went wrong

It is built on WAI, so it is compatible with several Haskell web frameworks. All you need is the ability to access the Request object, and WAI Request Spec takes care of the rest!

Contributing

Contributions are welcome! Documentation, examples, code, and feedback - they all help.

Be sure to review the included code of conduct. This project adheres to the Contributor’s Covenant. By participating in this project you agree to abide by its terms.

How to Do

Here’s some code:

{-# LANGUAGE OverloadedStrings #-}
module Main (main) where

import Control.Applicative
import Data.Text
import Data.Text.Lazy (fromStrict)
import Network.Wai.RequestSpec
import Network.HTTP.Types (badRequest400)
import Web.Scotty hiding (param, header)

import Network.Wai.RequestSpec.Examples.Types

------------------------------------------------------------
-- Your Data Model
------------------------------------------------------------
data Query =
  Query ClientId Count Offset UserName Accepts
  deriving Show

------------------------------------------------------------
-- Request Spec: The Part You Write
------------------------------------------------------------

-- if offset is given, return that value; else, offset is 0
offset :: Maybe Int -> Offset
offset (Just n) = Offset n
offset Nothing  = Offset 0

-- if Accept is given, parse it; otherwise, default to plain text
accept :: Maybe Text -> Accepts
accept (Just "text/plain")       = PlainText
accept (Just "application/json") = JSON
accept _                         = PlainText

instance FromEnv Query where
  fromEnv e =
    Query <$> (ClientId <$> textQ "client_id" e)
          <*> (Count <$> intQ "count" e)
          <*> (offset <$> intQM "offset" e)
          <*> (UserName <$> textH "X-User-Name" e)
          <*> (accept <$> textHM "Accept" e)

------------------------------------------------------------
-- Application: Making Use of Request Spec
------------------------------------------------------------

main :: IO ()
main = scotty 3000 $
  get "/query" $ do
    -- Request spec in action: parse what you want, and ...
    req <- request
    let query = parse fromEnv (toEnv req) :: Result Query

    -- figure out what to do next based on whether the request was valid!
    case query of
      -- show user what's missing
      Failure e -> status badRequest400 >> (text . fromStrict . pack . show $ e)

      -- show user what Query they made
      Success v -> text . fromStrict . pack . show $ v

Here’s the API responses:

$ http get localhost:3000/query
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Date: Thu, 09 Apr 2015 21:30:43 GMT
Server: Warp/3.0.10
Transfer-Encoding: chunked

missing Header "X-User-Name"
missing Param "count"
missing Param "client_id"

$ http get 'localhost:3000/query?client_id&offset&count' x-user-name:taco
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Date: Thu, 09 Apr 2015 21:31:22 GMT
Server: Warp/3.0.10
Transfer-Encoding: chunked

missing Param "count"
missing Param "client_id"

$ http get 'localhost:3000/query?client_id=usually&offset=cat&count=not-a-typical-number' x-user-name:taco
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Date: Wed, 08 Apr 2015 19:33:47 GMT
Server: Warp/3.0.10
Transfer-Encoding: chunked

could not parse "cat": "Could not parse integer"
could not parse "not-a-typical-number": "Could not parse integer"

$ http get 'localhost:3000/query?client_id=usually&offset=5&count=10' x-user-name:taco
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 09 Apr 2015 21:37:29 GMT
Server: Warp/3.0.10
Transfer-Encoding: chunked

Query (ClientId "usually") (Count 10) (Offset 5) (UserName "taco") PlainText

$ http get 'localhost:3000/query?client_id=usually&count=10' x-user-name:taco accept:application/json
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 09 Apr 2015 21:38:24 GMT
Server: Warp/3.0.10
Transfer-Encoding: chunked

Query (ClientId "usually") (Count 10) (Offset 0) (UserName "taco") JSON

Developer Setup

Here’s a few handy steps in order to get the project set up locally:

  1. Install Haskell

  2. Build the project

    $ git clone https://gitlab.com/cpp.cabrera/wai-request-spec.git
    $ cd wai-request-spec
    $ cabal sandbox init
    $ cabal install --dependencies-only
    $ cd examples
    $ cabal sandbox init
    $ cabal sandbox add-source ..  # Until this is published on Hackage
    $ cabal install --dependencies-only
    
  3. Play with the project! I give examples below using the lovely httpie HTTP CLI client.

    $ cabal run scotty-request-spec
    # new terminal
    $ http get 'localhost:3000/auth?client_id=cats'\
               Authorization:"Basic eW91J3JlOmF3ZXNvbWU="
    
  4. (Coming soon) Run tests:

    # From project root, not examples/
    $ cabal configure --enable-tests
    $ cabal install --dependencies-only
    $ cabal test
    
  5. (Coming soon) Run benchmarks:

    # From project root, not examples/
    $ cabal configure --enable-benchmarks
    $ cabal install --dependencies-only
    $ cabal benchmark
    

Todo

Benchmarking

What’s slow? What’s fast? It’d be nice to track regressions and improvements in performance over time.

Documentation/Examples

There’s examples available for a few web frameworks already. It’d be great to see more types of examples.

Testing

A combination of spec-style and property tests are needed. There’s a lot of churn right now, but capturing expected core behavior would be awesome.

Licensing

This project is distrubted under a BSD3 license. See the included LICENSE file for more details.

Changes

0.10.2.0 (Sep. 10, 2015)

  • stack support
  • GHC 7.10

0.10.0.1 (May 29. 2015)

  • handle decoding form parameter fields correctly
    • failed when content-type header contained char-encoding data

0.10.0.0 (May 29. 2015)

  • toEnv now parses a request w/o form data
  • toEnvRaw parses a request + bytestring body for form data
  • toEnvRaw parses a request + [(Text, Text)] for form data

0.9.1.0 (May 28, 2015)

  • Export Env type

0.9.0.1 (May 28, 2015)

  • Remember to update changelog

0.9.0.0 (May 28, 2015)

Features

  • Add form parameter parsing

  • If the content-type is application/x-www-form-urlencoded, then parameters are automatically parsed from the request body

Interface Changes

  • all query param functions xP are renamed to xQ
  • new form param functions are named xF
  • toEnv now takes ByteString (requestBody) parameter

0.8.5.2 (Apr. 29, 2015)

  • Fix description in cabal file

0.8.5.1 (Apr. 29, 2015)

  • Fix description in cabal file

0.8.5.0 (Apr. 29, 2015)

  • Initial Hackage release
  • Improved Error Show instance and new helpers:
    • toList to convert Error to [String]
    • toTextList to convert Error to [Text]
  • Inline annotations for Parser internals
  • Basic documentation

0.8.2.0

  • Correct option combinators
    • Maybe (P a) to P (Maybe a)
    • Former construction made them impossible to use in P context

0.8.1.1

  • Actually export new combinators

0.8.1.0

  • Add parser combinators for optional inputs
    • int[PH]M, bool[PH]M, float[PH]M, text[PH]M, bytes[PH]M

0.8.0.0

  • Reorganized internals/exports
    • Added RequestSpec.Combinators
    • Added RequestSpec.Parser
  • Added derived combinators for handling data from headers/params
    • int[PH], bool[PH], float[PH], text[PH], bytes[PH]

0.7.0.1

  • Remove RequestSpec.Internal as everything stable is re-exported in RequestSpec

0.7.0.0

  • Moved away from using native HTTP rep towards Text based
    • HeaderName -> CI Text
    • Param -> ParamValue (Text)
  • Assumes utf8 encodings

0.6.1.0

  • Changed representation of query params
    • Maybe ByteString -> ParamValue (ByteString)

0.6.0.0

  • Brand new parser
    • Accumulating errors
    • Continuation passing style
    • Primitive and derived combinators
    • Functor, Applicative, Monad, Monad+, Alternative, Monoid instances
  • fromParsed -> fromEnv
  • Separate Spec step removed
    • The spec is now the sequence of combinators given in fromEnv

0.5.0.2

  • This changelog came to be!
  • Initially available on Gitlab
  • Core functions in place:
    • Required and Optional, Headers and Params
    • Parsing thereof
  • Lots of examples
    • Bare WAI/Warp
    • Scotty
    • Spock