wai-predicates (empty) → 0.1
raw patch · 19 files changed
+1390/−0 lines, 19 filesdep +HUnitdep +QuickCheckdep +attoparsecsetup-changed
Dependencies added: HUnit, QuickCheck, attoparsec, base, blaze-builder, bytestring, bytestring-from, case-insensitive, cookie, http-types, tasty, tasty-hunit, tasty-quickcheck, vector, wai, wai-predicates
Files
- LICENSE +373/−0
- README.md +5/−0
- Setup.hs +2/−0
- src/Data/Predicate.hs +134/−0
- src/Data/Predicate/Product.hs +104/−0
- src/Data/Predicate/Result.hs +62/−0
- src/Network/Wai/Predicate.hs +91/−0
- src/Network/Wai/Predicate/Accept.hs +46/−0
- src/Network/Wai/Predicate/Content.hs +49/−0
- src/Network/Wai/Predicate/Error.hs +19/−0
- src/Network/Wai/Predicate/MediaType.hs +31/−0
- src/Network/Wai/Predicate/Parser/MediaType.hs +76/−0
- src/Network/Wai/Predicate/Request.hs +119/−0
- src/Network/Wai/Predicate/Utility.hs +20/−0
- test/TestSuite.hs +11/−0
- test/Tests/Data/Predicate.hs +48/−0
- test/Tests/Wai/Predicate.hs +84/−0
- test/Tests/Wai/Util.hs +31/−0
- wai-predicates.cabal +85/−0
+ LICENSE view
@@ -0,0 +1,373 @@+Mozilla Public License Version 2.0+==================================++1. Definitions+--------------++1.1. "Contributor"+ means each individual or legal entity that creates, contributes to+ the creation of, or owns Covered Software.++1.2. "Contributor Version"+ means the combination of the Contributions of others (if any) used+ by a Contributor and that particular Contributor's Contribution.++1.3. "Contribution"+ means Covered Software of a particular Contributor.++1.4. "Covered Software"+ means Source Code Form to which the initial Contributor has attached+ the notice in Exhibit A, the Executable Form of such Source Code+ Form, and Modifications of such Source Code Form, in each case+ including portions thereof.++1.5. "Incompatible With Secondary Licenses"+ means++ (a) that the initial Contributor has attached the notice described+ in Exhibit B to the Covered Software; or++ (b) that the Covered Software was made available under the terms of+ version 1.1 or earlier of the License, but not also under the+ terms of a Secondary License.++1.6. "Executable Form"+ means any form of the work other than Source Code Form.++1.7. "Larger Work"+ means a work that combines Covered Software with other material, in+ a separate file or files, that is not Covered Software.++1.8. "License"+ means this document.++1.9. "Licensable"+ means having the right to grant, to the maximum extent possible,+ whether at the time of the initial grant or subsequently, any and+ all of the rights conveyed by this License.++1.10. "Modifications"+ means any of the following:++ (a) any file in Source Code Form that results from an addition to,+ deletion from, or modification of the contents of Covered+ Software; or++ (b) any new file in Source Code Form that contains any Covered+ Software.++1.11. "Patent Claims" of a Contributor+ means any patent claim(s), including without limitation, method,+ process, and apparatus claims, in any patent Licensable by such+ Contributor that would be infringed, but for the grant of the+ License, by the making, using, selling, offering for sale, having+ made, import, or transfer of either its Contributions or its+ Contributor Version.++1.12. "Secondary License"+ means either the GNU General Public License, Version 2.0, the GNU+ Lesser General Public License, Version 2.1, the GNU Affero General+ Public License, Version 3.0, or any later versions of those+ licenses.++1.13. "Source Code Form"+ means the form of the work preferred for making modifications.++1.14. "You" (or "Your")+ means an individual or a legal entity exercising rights under this+ License. For legal entities, "You" includes any entity that+ controls, is controlled by, or is under common control with You. For+ purposes of this definition, "control" means (a) the power, direct+ or indirect, to cause the direction or management of such entity,+ whether by contract or otherwise, or (b) ownership of more than+ fifty percent (50%) of the outstanding shares or beneficial+ ownership of such entity.++2. License Grants and Conditions+--------------------------------++2.1. Grants++Each Contributor hereby grants You a world-wide, royalty-free,+non-exclusive license:++(a) under intellectual property rights (other than patent or trademark)+ Licensable by such Contributor to use, reproduce, make available,+ modify, display, perform, distribute, and otherwise exploit its+ Contributions, either on an unmodified basis, with Modifications, or+ as part of a Larger Work; and++(b) under Patent Claims of such Contributor to make, use, sell, offer+ for sale, have made, import, and otherwise transfer either its+ Contributions or its Contributor Version.++2.2. Effective Date++The licenses granted in Section 2.1 with respect to any Contribution+become effective for each Contribution on the date the Contributor first+distributes such Contribution.++2.3. Limitations on Grant Scope++The licenses granted in this Section 2 are the only rights granted under+this License. No additional rights or licenses will be implied from the+distribution or licensing of Covered Software under this License.+Notwithstanding Section 2.1(b) above, no patent license is granted by a+Contributor:++(a) for any code that a Contributor has removed from Covered Software;+ or++(b) for infringements caused by: (i) Your and any other third party's+ modifications of Covered Software, or (ii) the combination of its+ Contributions with other software (except as part of its Contributor+ Version); or++(c) under Patent Claims infringed by Covered Software in the absence of+ its Contributions.++This License does not grant any rights in the trademarks, service marks,+or logos of any Contributor (except as may be necessary to comply with+the notice requirements in Section 3.4).++2.4. Subsequent Licenses++No Contributor makes additional grants as a result of Your choice to+distribute the Covered Software under a subsequent version of this+License (see Section 10.2) or under the terms of a Secondary License (if+permitted under the terms of Section 3.3).++2.5. Representation++Each Contributor represents that the Contributor believes its+Contributions are its original creation(s) or it has sufficient rights+to grant the rights to its Contributions conveyed by this License.++2.6. Fair Use++This License is not intended to limit any rights You have under+applicable copyright doctrines of fair use, fair dealing, or other+equivalents.++2.7. Conditions++Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted+in Section 2.1.++3. Responsibilities+-------------------++3.1. Distribution of Source Form++All distribution of Covered Software in Source Code Form, including any+Modifications that You create or to which You contribute, must be under+the terms of this License. You must inform recipients that the Source+Code Form of the Covered Software is governed by the terms of this+License, and how they can obtain a copy of this License. You may not+attempt to alter or restrict the recipients' rights in the Source Code+Form.++3.2. Distribution of Executable Form++If You distribute Covered Software in Executable Form then:++(a) such Covered Software must also be made available in Source Code+ Form, as described in Section 3.1, and You must inform recipients of+ the Executable Form how they can obtain a copy of such Source Code+ Form by reasonable means in a timely manner, at a charge no more+ than the cost of distribution to the recipient; and++(b) You may distribute such Executable Form under the terms of this+ License, or sublicense it under different terms, provided that the+ license for the Executable Form does not attempt to limit or alter+ the recipients' rights in the Source Code Form under this License.++3.3. Distribution of a Larger Work++You may create and distribute a Larger Work under terms of Your choice,+provided that You also comply with the requirements of this License for+the Covered Software. If the Larger Work is a combination of Covered+Software with a work governed by one or more Secondary Licenses, and the+Covered Software is not Incompatible With Secondary Licenses, this+License permits You to additionally distribute such Covered Software+under the terms of such Secondary License(s), so that the recipient of+the Larger Work may, at their option, further distribute the Covered+Software under the terms of either this License or such Secondary+License(s).++3.4. Notices++You may not remove or alter the substance of any license notices+(including copyright notices, patent notices, disclaimers of warranty,+or limitations of liability) contained within the Source Code Form of+the Covered Software, except that You may alter any license notices to+the extent required to remedy known factual inaccuracies.++3.5. Application of Additional Terms++You may choose to offer, and to charge a fee for, warranty, support,+indemnity or liability obligations to one or more recipients of Covered+Software. However, You may do so only on Your own behalf, and not on+behalf of any Contributor. You must make it absolutely clear that any+such warranty, support, indemnity, or liability obligation is offered by+You alone, and You hereby agree to indemnify every Contributor for any+liability incurred by such Contributor as a result of warranty, support,+indemnity or liability terms You offer. You may include additional+disclaimers of warranty and limitations of liability specific to any+jurisdiction.++4. Inability to Comply Due to Statute or Regulation+---------------------------------------------------++If it is impossible for You to comply with any of the terms of this+License with respect to some or all of the Covered Software due to+statute, judicial order, or regulation then You must: (a) comply with+the terms of this License to the maximum extent possible; and (b)+describe the limitations and the code they affect. Such description must+be placed in a text file included with all distributions of the Covered+Software under this License. Except to the extent prohibited by statute+or regulation, such description must be sufficiently detailed for a+recipient of ordinary skill to be able to understand it.++5. Termination+--------------++5.1. The rights granted under this License will terminate automatically+if You fail to comply with any of its terms. However, if You become+compliant, then the rights granted under this License from a particular+Contributor are reinstated (a) provisionally, unless and until such+Contributor explicitly and finally terminates Your grants, and (b) on an+ongoing basis, if such Contributor fails to notify You of the+non-compliance by some reasonable means prior to 60 days after You have+come back into compliance. Moreover, Your grants from a particular+Contributor are reinstated on an ongoing basis if such Contributor+notifies You of the non-compliance by some reasonable means, this is the+first time You have received notice of non-compliance with this License+from such Contributor, and You become compliant prior to 30 days after+Your receipt of the notice.++5.2. If You initiate litigation against any entity by asserting a patent+infringement claim (excluding declaratory judgment actions,+counter-claims, and cross-claims) alleging that a Contributor Version+directly or indirectly infringes any patent, then the rights granted to+You by any and all Contributors for the Covered Software under Section+2.1 of this License shall terminate.++5.3. In the event of termination under Sections 5.1 or 5.2 above, all+end user license agreements (excluding distributors and resellers) which+have been validly granted by You or Your distributors under this License+prior to termination shall survive termination.++************************************************************************+* *+* 6. Disclaimer of Warranty *+* ------------------------- *+* *+* Covered Software is provided under this License on an "as is" *+* basis, without warranty of any kind, either expressed, implied, or *+* statutory, including, without limitation, warranties that the *+* Covered Software is free of defects, merchantable, fit for a *+* particular purpose or non-infringing. The entire risk as to the *+* quality and performance of the Covered Software is with You. *+* Should any Covered Software prove defective in any respect, You *+* (not any Contributor) assume the cost of any necessary servicing, *+* repair, or correction. This disclaimer of warranty constitutes an *+* essential part of this License. No use of any Covered Software is *+* authorized under this License except under this disclaimer. *+* *+************************************************************************++************************************************************************+* *+* 7. Limitation of Liability *+* -------------------------- *+* *+* Under no circumstances and under no legal theory, whether tort *+* (including negligence), contract, or otherwise, shall any *+* Contributor, or anyone who distributes Covered Software as *+* permitted above, be liable to You for any direct, indirect, *+* special, incidental, or consequential damages of any character *+* including, without limitation, damages for lost profits, loss of *+* goodwill, work stoppage, computer failure or malfunction, or any *+* and all other commercial damages or losses, even if such party *+* shall have been informed of the possibility of such damages. This *+* limitation of liability shall not apply to liability for death or *+* personal injury resulting from such party's negligence to the *+* extent applicable law prohibits such limitation. Some *+* jurisdictions do not allow the exclusion or limitation of *+* incidental or consequential damages, so this exclusion and *+* limitation may not apply to You. *+* *+************************************************************************++8. Litigation+-------------++Any litigation relating to this License may be brought only in the+courts of a jurisdiction where the defendant maintains its principal+place of business and such litigation shall be governed by laws of that+jurisdiction, without reference to its conflict-of-law provisions.+Nothing in this Section shall prevent a party's ability to bring+cross-claims or counter-claims.++9. Miscellaneous+----------------++This License represents the complete agreement concerning the subject+matter hereof. If any provision of this License is held to be+unenforceable, such provision shall be reformed only to the extent+necessary to make it enforceable. Any law or regulation which provides+that the language of a contract shall be construed against the drafter+shall not be used to construe this License against a Contributor.++10. Versions of the License+---------------------------++10.1. New Versions++Mozilla Foundation is the license steward. Except as provided in Section+10.3, no one other than the license steward has the right to modify or+publish new versions of this License. Each version will be given a+distinguishing version number.++10.2. Effect of New Versions++You may distribute the Covered Software under the terms of the version+of the License under which You originally received the Covered Software,+or under the terms of any subsequent version published by the license+steward.++10.3. Modified Versions++If you create software not governed by this License, and you want to+create a new license for such software, you may create and use a+modified version of this License if you rename the license and remove+any references to the name of the license steward (except to note that+such modified license differs from this License).++10.4. Distributing Source Code Form that is Incompatible With Secondary+Licenses++If You choose to distribute Source Code Form that is Incompatible With+Secondary Licenses under the terms of this version of the License, the+notice described in Exhibit B of this License must be attached.++Exhibit A - Source Code Form License Notice+-------------------------------------------++ This Source Code Form is subject to the terms of the Mozilla Public+ License, v. 2.0. If a copy of the MPL was not distributed with this+ file, You can obtain one at http://mozilla.org/MPL/2.0/.++If it is not possible or desirable to put the notice in a particular+file, then You may include the notice in a location (such as a LICENSE+file in a relevant directory) where a recipient would be likely to look+for such a notice.++You may add additional accurate notices of copyright ownership.++Exhibit B - "Incompatible With Secondary Licenses" Notice+---------------------------------------------------------++ This Source Code Form is "Incompatible With Secondary Licenses", as+ defined by the Mozilla Public License, v. 2.0.
+ README.md view
@@ -0,0 +1,5 @@+Evaluate predicate functions against WAI requests and receive+metadata in addition to a success/failure indicator.++Predicates can be combined using logical connectives and the+metadata are passed on accordingly.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Data/Predicate.hs view
@@ -0,0 +1,134 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE TypeOperators #-}++module Data.Predicate+ ( Predicate+ , constant+ , failure+ , true+ , false+ , and+ , or+ , orElse+ , (.&.)+ , (.|.)+ , (|||)+ , opt+ , def+ , mapResult+ , mapOkay+ , mapFail+ , exec+ , module Data.Predicate.Product+ , module Data.Predicate.Result+ ) where++import Control.Monad+import Data.Predicate.Product+import Data.Predicate.Result+import Prelude hiding (and, or)++-- | A predicate is a function of some value of type @a@ to a 'Result',+-- i.e. a 'Bool'-like value with 'Okay' as 'True' and 'Fail' as 'False',+-- which carries additional data in each branch.+type Predicate a f t = a -> Result f t++-- | A predicate which always returns @Okay@ with the given+-- value as metadata.+constant :: t -> Predicate a f t+constant t _ = return t++true :: Predicate a f ()+true = constant ()++-- | A predicate which always returns @Fail@ with the given+-- value as metadata.+failure :: f -> Predicate a f t+failure f _ = Fail f++false :: Predicate a () t+false = failure ()++infixr 3 .&.+infixr 2 .|.+infixr 2 |||++-- | A predicate corresponding to the logical AND connective+-- of two predicate.+and, (.&.) :: Predicate a f t -> Predicate a f t' -> Predicate a f (t ::: t')+and f g x = f x `cmp` g x+ where+ cmp (Okay d y) (Okay w z) = Okay (d + w) (y ::: z)+ cmp (Okay _ _) (Fail y) = Fail y+ cmp (Fail y) _ = Fail y++-- | A predicate corresponding to the logical+-- OR connective of two predicates. It requires the+-- metadata of each @Okay@ branch to be of the same type.+--+-- If both arguments evaluate to @Okay@ the one with the+-- smaller \"delta\" will be preferred, or--if equal--the+-- left-hand argument.+or, (.|.) :: Predicate a f t -> Predicate a f t -> Predicate a f t+or f g x = f x `cmp` g x+ where+ cmp a@(Okay d _) b@(Okay w _) = if w < d then b else a+ cmp a@(Okay _ _) (Fail _) = a+ cmp (Fail _) b@(Okay _ _) = b+ cmp (Fail _) b@(Fail _) = b++-- | A predicate corresponding to the logical+-- OR connective of two predicates. The metadata of+-- each @Okay@ branch can be of different types.+--+-- If both arguments evaluate to @Okay@ the one with the+-- smaller \"delta\" will be preferred, or--if equal--the+-- left-hand argument.+orElse, (|||) :: Predicate a f t -> Predicate a f t' -> Predicate a f (Either t t')+orElse f g x = f x `cmp` g x+ where+ cmp (Okay d y) (Okay w z) = if w < d then Okay w (Right z) else Okay d (Left y)+ cmp (Okay d y) (Fail _) = Okay d (Left y)+ cmp (Fail _) (Okay d y) = Okay d (Right y)+ cmp (Fail _) (Fail y) = Fail y++(.&.) = and+(.|.) = or+(|||) = orElse++-- | Map the result of the given predicate to some other result.+mapResult :: (Result f t -> Result f' t') -> Predicate a f t -> Predicate a f' t'+mapResult f p = f . p++-- | Like 'mapResult', but only maps the @Okay@ metadata to another result.+mapOkay :: (t -> Result f t') -> Predicate a f t -> Predicate a f t'+mapOkay f p a =+ case p a of+ Okay _ x -> f x+ Fail x -> Fail x++-- | Like 'mapResult', but only maps the @Fail@ metadata to another result.+mapFail :: (f -> Result f' t) -> Predicate a f t -> Predicate a f' t+mapFail f p a =+ case p a of+ Fail x -> f x+ Okay d x -> Okay d x++-- | A predicate modifier which makes the given predicate optional,+-- i.e. the @Okay@ metadata type becomes a 'Maybe' and in the failure-case+-- 'Nothing' is returned.+opt :: Predicate a f t -> Predicate a f (Maybe t)+opt = mapResult (result (const $ return Nothing) (\d x -> Okay d (Just x)))++-- | A predicate modifier which returns as @Okay@ metadata the provided default+-- value if the given predicate fails.+def :: t -> Predicate a f t -> Predicate a f t+def t = mapResult (result (const $ return t) Okay)++exec :: Predicate a f t -> a -> (f -> b) -> (t -> b) -> b+exec p a g f = case p a of+ Okay _ x -> f x+ Fail x -> g x
+ src/Data/Predicate/Product.hs view
@@ -0,0 +1,104 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE TypeOperators #-}++module Data.Predicate.Product where++infixr 5 :::++-- | A data-type for combining results of predicate evaluations.+data a ::: b = a ::: b deriving (Eq, Show)++-- | @flip ($)@ - useful in combination with indexed access, e.g.+-- @('x' ::: True ::: False)#_2@ yields @True@.+(#) :: a -> (a -> b) -> b+(#) = flip ($)++hd :: a ::: b -> a+hd (a ::: _) = a+{-# INLINE hd #-}++tl :: a ::: b -> b+tl (_ ::: b) = b+{-# INLINE tl #-}++-----------------------------------------------------------------------------+-- Indexed access (except for last element)++_1 :: a ::: b -> a+_1 = _1'+{-# INLINE _1 #-}++_2 :: a ::: b ::: c -> b+_2 = hd . _2'+{-# INLINE _2 #-}++_3 :: a ::: b ::: c ::: d -> c+_3 = hd . _3'+{-# INLINE _3 #-}++_4 :: a ::: b ::: c ::: d ::: e -> d+_4 = hd . _4'+{-# INLINE _4 #-}++_5 :: a ::: b ::: c ::: d ::: e ::: f -> e+_5 = hd . _5'+{-# INLINE _5 #-}++_6 :: a ::: b ::: c ::: d ::: e ::: f ::: g -> f+_6 = hd . _6'+{-# INLINE _6 #-}++_7 :: a ::: b ::: c ::: d ::: e ::: f ::: g ::: h -> g+_7 = hd . _7'+{-# INLINE _7 #-}++_8 :: a ::: b ::: c ::: d ::: e ::: f ::: g ::: h ::: i -> h+_8 = hd . _8'+{-# INLINE _8 #-}++_9 :: a ::: b ::: c ::: d ::: e ::: f ::: g ::: h ::: i ::: j -> i+_9 = hd . _9'+{-# INLINE _9 #-}++-----------------------------------------------------------------------------+-- Access last element++_1' :: a ::: b -> a+_1' = hd+{-# INLINE _1' #-}++_2' :: a ::: b -> b+_2' = tl+{-# INLINE _2' #-}++_3' :: a ::: b ::: c -> c+_3' = tl . tl+{-# INLINE _3' #-}++_4' :: a ::: b ::: c ::: d -> d+_4' = tl . tl . tl+{-# INLINE _4' #-}++_5' :: a ::: b ::: c ::: d ::: e -> e+_5' = tl . tl . tl . tl+{-# INLINE _5' #-}++_6' :: a ::: b ::: c ::: d ::: e ::: f -> f+_6' = tl . tl . tl . tl . tl+{-# INLINE _6' #-}++_7' :: a ::: b ::: c ::: d ::: e ::: f ::: g -> g+_7' = tl . tl . tl . tl . tl . tl+{-# INLINE _7' #-}++_8' :: a ::: b ::: c ::: d ::: e ::: f ::: g ::: h -> h+_8' = tl . tl . tl . tl . tl . tl . tl+{-# INLINE _8' #-}++_9' :: a ::: b ::: c ::: d ::: e ::: f ::: g ::: h ::: i -> i+_9' = tl . tl . tl . tl . tl . tl . tl . tl+{-# INLINE _9' #-}+
+ src/Data/Predicate/Result.hs view
@@ -0,0 +1,62 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE TypeOperators #-}++module Data.Predicate.Result where++import Control.Applicative+import Control.Monad++-- | A 'Bool'-like type where each branch--@Fail@ and @Okay@--carries+-- some metadata.+data Result f t+ = Fail f+ | Okay !Double t+ deriving (Eq, Ord, Show)++instance Functor (Result f) where+ fmap f (Okay d x) = Okay d (f x)+ fmap _ (Fail x) = Fail x++instance Applicative (Result f) where+ pure = return+ (<*>) = ap++instance Monad (Result f) where+ return = Okay 0+ (Okay _ x) >>= k = k x+ (Fail x) >>= _ = Fail x++result :: (f -> a) -> (Double -> t -> a) -> Result f t -> a+result f _ (Fail x) = f x+result _ g (Okay d x) = g d x++newtype ResultT f m t = ResultT { runResultT :: m (Result f t) }++instance Monad m => Functor (ResultT f m) where+ fmap f = ResultT . liftM (fmap f) . runResultT++instance Monad m => Applicative (ResultT f m) where+ pure = return+ (<*>) = ap++instance Monad m => Monad (ResultT f m) where+ return = ResultT . return . Okay 0+ m >>= k = ResultT $ runResultT m >>= \a -> case a of+ Okay _ x -> runResultT (k x)+ Fail x -> return (Fail x)+ fail = ResultT . fail++resultT :: Monad m => (f -> m a) -> (Double -> t -> m a) -> ResultT f m t -> m a+resultT f g (ResultT m) = m >>= \a -> case a of+ Fail x -> f x+ Okay d x -> g d x++mapResultT :: (m (Result f t) -> n (Result f' t')) -> ResultT f m t -> ResultT f' n t'+mapResultT f m = ResultT $ f (runResultT m)++hoistResult :: Monad m => Result f t -> ResultT f m t+hoistResult = ResultT . return+
+ src/Network/Wai/Predicate.hs view
@@ -0,0 +1,91 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE OverloadedStrings #-}++module Network.Wai.Predicate+ ( module Data.Predicate+ , getRequest++ , query+ , hasQuery++ , header+ , hasHeader++ , segment+ , hasSegment++ , cookie+ , hasCookie++ , accept+ , contentType+ , module Network.Wai.Predicate.MediaType+ , module Network.Wai.Predicate.Error+ ) where++import Data.ByteString (ByteString)+import Data.ByteString.From+import Data.CaseInsensitive (mk)+import Data.List (find)+import Data.Monoid+import Data.Maybe (isJust)+import Data.Predicate+import Data.Word+import Network.HTTP.Types.Status+import Network.Wai.Predicate.Accept+import Network.Wai.Predicate.Content+import Network.Wai.Predicate.Error+import Network.Wai.Predicate.MediaType+import Network.Wai.Predicate.Request+import Network.Wai.Predicate.Utility+import Network.Wai++getRequest :: (HasRequest r) => Predicate r f Request+getRequest = return . request++query :: (HasQuery r, FromByteString a) => ByteString -> Predicate r Error a+query k r = case lookupQuery k r of+ [] -> Fail (err status400 ("Missing query '" <> k <> "'."))+ qq -> either (Fail . err status400) return (readValues qq)++hasQuery :: (HasQuery r) => ByteString -> Predicate r Error ()+hasQuery k r =+ if null (lookupQuery k r)+ then Fail (err status400 ("Missing query '" <> k <> "'."))+ else return ()++header :: (HasHeaders r, FromByteString a) => ByteString -> Predicate r Error a+header k r = case lookupHeader k r of+ [] -> Fail (err status400 ("Missing header '" <> k <> "'."))+ hh -> either (Fail . err status400) return (readValues hh)++hasHeader :: (HasHeaders r) => ByteString -> Predicate r Error ()+hasHeader k r =+ if isJust $ find ((mk k ==) . fst) (headers r)+ then return ()+ else Fail (err status400 ("Missing header '" <> k <> "'."))++segment :: (HasPath r, FromByteString a) => Word -> Predicate r Error a+segment i r = case lookupSegment i r of+ Nothing -> Fail (err status400 "Path segment index out of bounds.")+ Just s -> either (Fail . err status400) return (readValues [s])++hasSegment :: (HasPath r) => Word -> Predicate r Error ()+hasSegment i r =+ if isJust $ lookupSegment i r+ then return ()+ else Fail (err status400 "Path segment index out of bounds.")++cookie :: (HasCookies r, FromByteString a) => ByteString -> Predicate r Error a+cookie k r = case lookupCookie k r of+ [] -> Fail (err status400 ("Missing cookie '" <> k <> "'."))+ cc -> either (Fail . err status400) return (readValues cc)++hasCookie :: (HasCookies r) => ByteString -> Predicate r Error ()+hasCookie k r =+ if null (lookupCookie k r)+ then Fail (err status400 ("Missing cookie '" <> k <> "'."))+ else return ()
+ src/Network/Wai/Predicate/Accept.hs view
@@ -0,0 +1,46 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Wai.Predicate.Accept+ ( accept+ , module Network.Wai.Predicate.MediaType+ ) where++import Control.Monad+import Data.ByteString (ByteString)+import Data.Monoid hiding (All)+import Data.Maybe+import Data.Predicate+import GHC.TypeLits+import Network.HTTP.Types+import Network.Wai.Predicate.Error+import Network.Wai.Predicate.Request+import Network.Wai.Predicate.MediaType++import qualified Network.Wai.Predicate.Parser.MediaType as M++accept :: (HasHeaders r)+ => ByteString+ -> ByteString+ -> Predicate r Error (Media (t :: Symbol) (s :: Symbol))+accept t s r =+ let mtypes = M.readMediaTypes "accept" r in+ if null mtypes+ then return (Media t s 1.0 [])+ else case findMediaType t s mtypes of+ m:_ -> Okay (1.0 - mediaQuality m) m+ [] -> Fail (err status406 msg)+ where+ msg = "Expected 'Accept: " <> t <> "/" <> s <> "'."++findMediaType :: ByteString -> ByteString -> [M.MediaType] -> [Media t s]+findMediaType t s = mapMaybe (\m -> do+ let mt = M.medType m+ ms = M.medSubtype m+ guard ((mt == "*" || t == mt) && (ms == "*" || s == ms))+ return $ Media t s (M.medQuality m) (M.medParams m))
+ src/Network/Wai/Predicate/Content.hs view
@@ -0,0 +1,49 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Wai.Predicate.Content+ ( contentType+ , module Network.Wai.Predicate.MediaType+ ) where++import Control.Monad+import Data.ByteString (ByteString)+import Data.Monoid hiding (All)+import Data.Predicate+import GHC.TypeLits+import Data.Maybe+import Network.HTTP.Types.Status+import Network.Wai.Predicate.Error+import Network.Wai.Predicate.MediaType+import Network.Wai.Predicate.Request++import qualified Network.Wai.Predicate.Parser.MediaType as M++contentType :: (HasHeaders r)+ => ByteString+ -> ByteString+ -> Predicate r Error (Media (t :: Symbol) (s :: Symbol))+contentType t s r =+ let mtypes = M.readMediaTypes "content-type" r in+ case findContentType t s mtypes of+ m:_ -> Okay (1.0 - mediaQuality m) m+ [] -> Fail (err status415 msg)+ where+ msg = "Expected 'Content-Type: " <> t <> "/" <> s <> "'."++findContentType :: ByteString -> ByteString -> [M.MediaType] -> [Media t s]+findContentType t s = mapMaybe (\m -> do+ let mt = M.medType m+ ms = M.medSubtype m+ guard ((t == "*" || t == mt) && (s == "*" || s == ms))+ return $ Media mt ms (quality t s) (M.medParams m))+ where+ quality "*" "*" = 0+ quality "*" _ = 0.2+ quality _ "*" = 0.5+ quality _ _ = 1.0
+ src/Network/Wai/Predicate/Error.hs view
@@ -0,0 +1,19 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++module Network.Wai.Predicate.Error where++import Data.ByteString (ByteString)+import Network.HTTP.Types.Status++-- | The error type used as meta-data for @F@ in all WAI predicates.+data Error = Error+ { status :: !Status -- ^ HTTP status code+ , message :: Maybe ByteString -- ^ optional status message+ } deriving (Eq, Show)++-- | Convenience function to construct 'Error' values from+-- status code and body message.+err :: Status -> ByteString -> Error+err s = Error s . Just
+ src/Network/Wai/Predicate/MediaType.hs view
@@ -0,0 +1,31 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}++module Network.Wai.Predicate.MediaType where++import Data.ByteString (ByteString)+import Data.ByteString.Char8 (pack)+import GHC.TypeLits++data Media (t :: Symbol) (s :: Symbol) = Media+ { rawType :: !ByteString+ , rawSubTypes :: !ByteString+ , mediaQuality :: !Double+ , mediaParams :: ![(ByteString, ByteString)]+ } deriving (Eq, Show)++mediaType :: SingI t => Media t s -> ByteString+mediaType m = withSing (f m)+ where+ f :: Media t s -> Sing t -> ByteString+ f _ t = pack (fromSing t)++mediaSubType :: SingI s => Media t s -> ByteString+mediaSubType m = withSing (f m)+ where+ f :: Media t s -> Sing s -> ByteString+ f _ s = pack (fromSing s)
+ src/Network/Wai/Predicate/Parser/MediaType.hs view
@@ -0,0 +1,76 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE OverloadedStrings #-}++module Network.Wai.Predicate.Parser.MediaType+ ( MediaType (..)+ , readMediaTypes+ ) where++import Control.Applicative+import Data.Attoparsec.Char8+import Data.ByteString (ByteString)+import Data.List (sortBy)+import Network.Wai.Predicate.Request++import qualified Data.ByteString.Char8 as C++data MediaType = MediaType+ { medType :: !ByteString+ , medSubtype :: !ByteString+ , medQuality :: !Double+ , medParams :: ![(ByteString, ByteString)]+ } deriving (Eq, Show)++readMediaTypes :: (HasHeaders r) => ByteString -> r -> [MediaType]+readMediaTypes k r =+ sortBy q . concatMap parseMediaTypes $ lookupHeader k r+ where+ q a b = medQuality b `compare` medQuality a++parseMediaTypes :: ByteString -> [MediaType]+parseMediaTypes = either (const []) id . parseOnly mediaTypes++mediaTypes :: Parser [MediaType]+mediaTypes = mediaType `sepBy` char ','++mediaType :: Parser MediaType+mediaType =+ toMediaType <$> trim typ <*> (char '/' *> trim subtyp) <*> params+ where+ toMediaType t s p =+ case lookup "q" p >>= toDouble of+ Just q -> MediaType t s q (filter ((/= "q") . fst) p)+ Nothing -> MediaType t s 1.0 p++params :: Parser [(ByteString, ByteString)]+params = (trim (char ';') *> (element `sepBy` trim (char ';'))) <|> return []+ where+ element = (,) <$> trim key <*> (char '=' *> trim val)++typ, subtyp, key, val :: Parser ByteString+typ = takeTill (oneof "/ ")+subtyp = takeTill (oneof ",; ")++key = do+ c <- peekChar+ if c == Just ',' then fail "comma" else takeTill (oneof "= ")++val = takeTill (oneof ",; ")++toDouble :: ByteString -> Maybe Double+toDouble bs = toMaybe (parseOnly double bs)+ where+ toMaybe (Right x) = Just x+ toMaybe (Left _) = Nothing++spaces :: Parser ()+spaces = skipWhile (== ' ')++trim :: Parser a -> Parser a+trim p = spaces *> p <* spaces++oneof :: ByteString -> Char -> Bool+oneof s c = C.any (== c) s
+ src/Network/Wai/Predicate/Request.hs view
@@ -0,0 +1,119 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE OverloadedStrings #-}++module Network.Wai.Predicate.Request+ ( Req+ , HasMethod (..)+ , HasHeaders (..)+ , HasCookies (..)+ , HasQuery (..)+ , HasPath (..)+ , HasRequest (..)++ , fromRequest+ , lookupHeader+ , lookupQuery+ , lookupCookie+ , lookupSegment+ ) where++import Data.ByteString (ByteString)+import Data.CaseInsensitive (mk)+import Data.Maybe (mapMaybe)+import Data.Vector (Vector, (!?))+import Data.Word+import Network.HTTP.Types+import Network.Wai (Request)+import Web.Cookie++import qualified Data.ByteString as B+import qualified Network.Wai as Wai+import qualified Data.Vector as Vec++class HasRequest a where+ request :: a -> Wai.Request++class HasMethod a where+ method :: a -> Method++class HasHeaders a where+ headers :: a -> RequestHeaders++class HasCookies a where+ cookies :: a -> Cookies++class HasQuery a where+ queryItems :: a -> Query++class HasPath a where+ segments :: a -> Vector ByteString++data Req = Req+ { _request :: Request+ , _cookies :: Cookies+ , _segments :: Vector ByteString+ }++instance HasRequest Req where+ request = _request++instance HasMethod Req where+ method = Wai.requestMethod . request++instance HasMethod Wai.Request where+ method = Wai.requestMethod++instance HasHeaders Req where+ headers = Wai.requestHeaders . request++instance HasHeaders Wai.Request where+ headers = Wai.requestHeaders++instance HasQuery Req where+ queryItems = Wai.queryString . request++instance HasQuery Wai.Request where+ queryItems = Wai.queryString++instance HasCookies Req where+ cookies = _cookies++instance HasPath Req where+ segments = _segments++fromRequest :: Request -> Req+fromRequest rq =+ Req rq (concatMap parseCookies (getHeaders "Cookie" rq))+ (Vec.fromList . splitSegments . Wai.rawPathInfo $ rq)++lookupHeader :: HasHeaders r => ByteString -> r -> [ByteString]+lookupHeader name = getHeaders name++lookupSegment :: HasPath r => Word -> r -> Maybe ByteString+lookupSegment i r = segments r !? fromIntegral i++lookupCookie :: HasCookies r => ByteString -> r -> [ByteString]+lookupCookie name = map snd . filter ((name ==) . fst) . cookies++lookupQuery :: HasQuery r => ByteString -> r -> [ByteString]+lookupQuery name = mapMaybe snd . filter ((name ==) . fst) . queryItems++getHeaders :: HasHeaders r => ByteString -> r -> [ByteString]+getHeaders name = map snd . filter ((mk name ==) . fst) . headers++-----------------------------------------------------------------------------+-- Internal++splitSegments :: ByteString -> [ByteString]+splitSegments a+ | B.null a = []+ | "/" == a = []+ | otherwise = if B.head a == slash then go (B.tail a) else go a+ where+ go b =+ let (x, y) = B.breakByte slash b+ in urlDecode False x : if B.null y then [] else go (B.tail y)+ slash = 47
+ src/Network/Wai/Predicate/Utility.hs view
@@ -0,0 +1,20 @@+-- This Source Code Form is subject to the terms of the Mozilla Public+-- License, v. 2.0. If a copy of the MPL was not distributed with this+-- file, You can obtain one at http://mozilla.org/MPL/2.0/.++{-# LANGUAGE OverloadedStrings #-}++module Network.Wai.Predicate.Utility (readValues) where++import Data.ByteString (ByteString)+import Data.ByteString.From+import Data.List (foldl')+import Data.String (fromString)++readValues :: FromByteString a => [ByteString] -> Either ByteString a+readValues = foldl' res (Left "no parse") . map (runParser parser)+ where+ res (Left _) (Right x) = Right x+ res (Right x) _ = Right x+ res _ (Left x) = Left (fromString x)+
+ test/TestSuite.hs view
@@ -0,0 +1,11 @@+module Main where++import Test.Tasty+import qualified Tests.Data.Predicate as Predicate+import qualified Tests.Wai.Predicate as WaiPredicate++main :: IO ()+main = defaultMain $ testGroup "Tests"+ [ Predicate.tests+ , WaiPredicate.tests+ ]
+ test/Tests/Data/Predicate.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverlappingInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Tests.Data.Predicate (tests) where++import Control.Applicative hiding (Const, empty)+import Data.Predicate+import Test.QuickCheck hiding (Result, (.&.))+import Test.Tasty+import Test.Tasty.QuickCheck hiding ((.&.))++tests :: TestTree+tests = testGroup "Data.Predicate"+ [ testProperty ".&." testAnd+ , testProperty "|||" testOr+ , testProperty ".|." testOr'+ ]++testAnd :: Predicate () Int Char -> Predicate () Int Char -> Bool+testAnd a b = case (a (), b ()) of+ (Okay d x, Okay w y) -> (a .&. b) () == Okay (d + w) (x ::: y)+ (Okay _ _, Fail y) -> (a .&. b) () == Fail y+ (Fail x, Okay _ _) -> (a .&. b) () == Fail x+ (Fail x, Fail _) -> (a .&. b) () == Fail x++testOr :: Predicate () Int Char -> Predicate () Int Char -> Bool+testOr a b = case (a (), b ()) of+ (Okay d x, Okay e y) -> (a ||| b) () == if d <= e then Okay d (Left x) else Okay e (Right y)+ (Okay d x, Fail _) -> (a ||| b) () == Okay d (Left x)+ (Fail _, Okay d y) -> (a ||| b) () == Okay d (Right y)+ (Fail _, Fail y) -> (a ||| b) () == Fail y++testOr' :: Predicate () Int Char -> Predicate () Int Char -> Bool+testOr' a b = case (a (), b ()) of+ (Okay d x, Okay e y) -> (a .|. b) () == if d <= e then Okay d x else Okay e y+ (Okay d x, Fail _) -> (a .|. b) () == Okay d x+ (Fail _, Okay d y) -> (a .|. b) () == Okay d y+ (Fail _, Fail y) -> (a .|. b) () == Fail y++instance Arbitrary (Result Int Char) where+ arbitrary =+ oneof [ Okay <$> (arbitrary :: Gen Double) <*> (arbitrary :: Gen Char)+ , Fail <$> (arbitrary :: Gen Int)+ ]++instance Arbitrary (Predicate () Int Char) where+ arbitrary = (\r -> const r) <$> (arbitrary :: Gen (Result Int Char))
+ test/Tests/Wai/Predicate.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}++module Tests.Wai.Predicate (tests) where++import Data.ByteString (ByteString)+import Network.HTTP.Types.Status+import Network.Wai.Predicate+import Network.Wai.Predicate.Request+import Test.Tasty+import Test.Tasty.HUnit+import Tests.Wai.Util++tests :: TestTree+tests = testGroup "Wai.Predicate"+ [ testCase "Accept application/json" testAcceptJson+ , testCase "Accept application/thrift " testAcceptThrift+ , testCase "Accept application/*" testAcceptAll+ , testCase "Content-Type text/plain" testContentTypePlain+ , testCase "Content-Type text/*" testContentTypeAll+ , testCase "Query" testQuery+ , testCase "QueryOpt" testQueryOpt+ , testCase "exec" testExec+ ]++testAcceptJson :: IO ()+testAcceptJson = do+ let rq0 = fromRequest . json $ defRequest "/"+ Okay 0 (Media "application" "json" 1.0 []) @=? (accept "application" "json") rq0++ let rq1 = fromRequest . withHeader "Accept" "foo/bar" $ defRequest "/"+ Fail (err status406 ("Expected 'Accept: application/json'.")) @=? (accept "application" "json") rq1++testAcceptThrift :: IO ()+testAcceptThrift = do+ let rq0 = fromRequest . withHeader "Accept" "application/x-thrift" $ defRequest "/"+ Okay 0 (Media "application" "x-thrift" 1.0 []) @=? (accept "application" "x-thrift") rq0++ let rq1 = fromRequest . json $ defRequest "/"+ Fail (err status406 ("Expected 'Accept: application/x-thrift'.")) @=? (accept "application" "x-thrift") rq1++testAcceptAll :: IO ()+testAcceptAll = do+ let rq0 = fromRequest . withHeader "Accept" "application/*" $ defRequest "/"+ Okay 0 (Media "application" "*" 1.0 []) @=? (accept "application" "*") rq0+ Okay 0 (Media "application" "json" 1.0 []) @=? (accept "application" "json") rq0++testContentTypePlain :: IO ()+testContentTypePlain = do+ let rq0 = fromRequest . withHeader "Content-Type" "text/plain" $ defRequest "/"+ Okay 0 (Media "text" "plain" 1.0 []) @=? (contentType "text" "plain") rq0++ let rq1 = fromRequest . withHeader "Content-Type" "text/html" $ defRequest "/"+ Fail (err status415 ("Expected 'Content-Type: text/plain'.")) @=? (contentType "text" "plain") rq1++testContentTypeAll :: IO ()+testContentTypeAll = do+ let rq0 = fromRequest . withHeader "Content-Type" "text/plain" $ defRequest "/"+ Okay 0.5 (Media "text" "plain" 0.5 []) @=? (contentType "text" "*") rq0++testQuery :: IO ()+testQuery = do+ let rq0 = fromRequest . withQuery "x" "y" . withQuery "x" "z" $ defRequest "/"+ Okay 0 ("y" :: ByteString) @=? (query "x") rq0++ let rq1 = fromRequest $ defRequest "/"+ Fail (err status400 ("Missing query 'x'.")) @=? (query "x" :: Predicate Req Error ByteString) rq1++testQueryOpt :: IO ()+testQueryOpt = do+ let rq0 = fromRequest . withQuery "x" "y" . withQuery "x" "z" $ defRequest "/"+ Okay 0 (Just ("y" :: ByteString)) @=? (opt (query "x")) rq0++ let rq1 = fromRequest $ defRequest "/"+ Okay 0 Nothing @=? (opt (query "x" :: Predicate Req Error ByteString)) rq1++testExec :: IO ()+testExec = do+ let unit = const $ return ()+ let rq = fromRequest . withQuery "x" "42" . withHeader "a" "b" $ defRequest "/"+ exec (query "x" .&. header "a") rq unit $ \args -> do+ args#_1 @=? (42 :: Int)+ args#_2' @=? ("b" :: ByteString)+
+ test/Tests/Wai/Util.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE OverloadedStrings #-}++module Tests.Wai.Util where++import Data.ByteString (ByteString)+import Data.CaseInsensitive (CI)+import Network.Wai+import Network.Wai.Internal+import Network.HTTP.Types++import qualified Data.ByteString.Lazy as Lazy+import qualified Blaze.ByteString.Builder as Builder++defRequest :: ByteString -> Request+defRequest p = defaultRequest { rawPathInfo = p }++withHeader :: CI ByteString -> ByteString -> Request -> Request+withHeader k v r = r { requestHeaders = (k, v) : requestHeaders r }++withQuery :: ByteString -> ByteString -> Request -> Request+withQuery k v r = r { queryString = (k, Just v) : queryString r }++json :: Request -> Request+json = withHeader "Accept" "application/json"++responseBody :: Response -> Lazy.ByteString+responseBody (ResponseBuilder _ _ b) = Builder.toLazyByteString b+responseBody _ = ""++writeText :: Lazy.ByteString -> IO Response+writeText = return . responseLBS status200 []
+ wai-predicates.cabal view
@@ -0,0 +1,85 @@+name: wai-predicates+version: 0.1+synopsis: WAI request predicates+license: OtherLicense+license-file: LICENSE+author: Toralf Wittner+maintainer: Toralf Wittner <tw@dtex.org>+copyright: (c) 2014 Toralf Wittner+stability: experimental+category: Web+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+ README.md++description:+ Evaluate predicate functions against WAI requests and receive+ metadata in addition to a success/failure indicator.+ Predicates can be combined using logical connectives and the+ metadata are passed on accordingly.++source-repository head+ type: git+ location: git://github.com/twittner/wai-predicates.git++library+ default-language: Haskell2010+ hs-source-dirs: src+ ghc-options: -Wall -O2 -fwarn-tabs -funbox-strict-fields+ ghc-prof-options: -prof -auto-all++ exposed-modules:+ Data.Predicate+ Network.Wai.Predicate+ Network.Wai.Predicate.Error+ Network.Wai.Predicate.MediaType+ Network.Wai.Predicate.Request+ Network.Wai.Predicate.Utility++ other-modules:+ Data.Predicate.Product+ Data.Predicate.Result+ Network.Wai.Predicate.Accept+ Network.Wai.Predicate.Content+ Network.Wai.Predicate.Parser.MediaType++ build-depends:+ attoparsec >= 0.10 && < 0.12+ , base == 4.*+ , bytestring >= 0.9 && < 0.11+ , bytestring-from == 0.2.*+ , cookie == 0.4.*+ , case-insensitive == 1.1.*+ , http-types == 0.8.*+ , vector == 0.10.*+ , wai == 2.0.*++test-suite wai-predicate-tests+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: test+ main-is: TestSuite.hs+ ghc-options: -Wall -O2 -fwarn-tabs+ ghc-prof-options: -prof -auto-all++ other-modules:+ Tests.Data.Predicate+ Tests.Wai.Predicate+ Tests.Wai.Util++ build-depends:+ base == 4.*+ , blaze-builder == 0.3.*+ , bytestring >= 0.9 && < 0.11+ , case-insensitive == 1.1.*+ , http-types == 0.8.*+ , HUnit >= 1.2+ , QuickCheck >= 2.3+ , tasty >= 0.3+ , tasty-hunit >= 0.2+ , tasty-quickcheck >= 0.3+ , wai+ , wai-predicates+