fn 0.1.2.0 → 0.1.3.0
raw patch · 5 files changed
+84/−44 lines, 5 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Web.Fn: ParamMissing :: ParamError
+ Web.Fn: ParamOtherError :: Text -> ParamError
+ Web.Fn: ParamUnparsable :: ParamError
+ Web.Fn: anything :: Req -> a -> Maybe (Req, a)
+ Web.Fn: data ParamError
+ Web.Fn: instance GHC.Classes.Eq Web.Fn.ParamError
+ Web.Fn: instance GHC.Show.Show Web.Fn.ParamError
+ Web.Fn: paramMany :: FromParam p => Text -> Req -> ([p] -> a) -> Maybe (Req, a)
- Web.Fn: (==>) :: RequestContext ctxt => (Req -> k -> Maybe (Req, a)) -> (ctxt -> k) -> ctxt -> Maybe a
+ Web.Fn: (==>) :: RequestContext ctxt => (Req -> k -> Maybe (Req, ctxt -> a)) -> k -> ctxt -> Maybe a
- Web.Fn: fromParam :: FromParam a => Text -> Either Text a
+ Web.Fn: fromParam :: FromParam a => Text -> Either ParamError a
- Web.Fn: paramOpt :: FromParam p => Text -> Req -> (Either Text p -> a) -> Maybe (Req, a)
+ Web.Fn: paramOpt :: FromParam p => Text -> Req -> (Either ParamError [p] -> a) -> Maybe (Req, a)
Files
- CHANGELOG.md +11/−0
- README.md +2/−2
- fn.cabal +1/−1
- src/Web/Fn.hs +50/−30
- test/Spec.hs +20/−11
CHANGELOG.md view
@@ -1,3 +1,14 @@+* 0.1.3.0 Daniel Patterson <dbp@dbpmail.net> 2015-10-30++ - Allow nested calls to `route`, by changing `Request` in+ `ctxt`. This necesitated changing it so that the `ctxt` is passed+ to handlers _last_, instead of first, because we need to have+ completed matching before we can change the request.+ - Add `anything` route matcher that matches anything.+ - Add `paramMany` matcher that returns a list of values for the+ given query param.+ - Change `param` to fail if more than one value is in query string.+ * 0.1.2.0 Daniel Pattersion <dbp@dbpmail.net> 2015-10-27 Rename `paramOptional` to `paramOpt`, to match `fn-extra`'s `Heist`
README.md view
@@ -46,7 +46,7 @@ index :: IO (Maybe Response) index = okText "This is the index page! Try /foo/bar/baz?id=10" -handler :: Ctxt -> Text -> Int -> IO (Maybe Response)-handler _ fragment i = okText (fragment <> " - " <> T.pack (show i))+handler :: Text -> Int -> Ctxt -> IO (Maybe Response)+handler fragment i _ = okText (fragment <> " - " <> T.pack (show i)) ```
fn.cabal view
@@ -1,5 +1,5 @@ name: fn-version: 0.1.2.0+version: 0.1.3.0 synopsis: A functional web framework. description: Please see README. homepage: http://github.com/dbp/fn#readme
src/Web/Fn.hs view
@@ -33,9 +33,12 @@ , (/?) , path , end+ , anything , segment , FromParam(..)+ , ParamError(..) , param+ , paramMany , paramOpt -- * Responses , okText@@ -49,11 +52,9 @@ import qualified Blaze.ByteString.Builder.Char.Utf8 as B import Data.ByteString (ByteString)-import Data.List (find)+import Data.Either (rights) import Data.Maybe (fromJust)-import Data.Monoid ((<>)) import Data.Text (Text)-import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Text.Read (decimal, double) import Network.HTTP.Types@@ -137,15 +138,15 @@ -- patterns with varying numbers (and types) of parts with functions -- of the corresponding number of arguments and types. (==>) :: RequestContext ctxt =>- (Req -> k -> Maybe (Req, a)) ->- (ctxt -> k) ->+ (Req -> k -> Maybe (Req, ctxt -> a)) ->+ k -> ctxt -> Maybe a (match ==> handle) ctxt = let r = getRequest ctxt x = (pathInfo r, queryString r)- in case match x (handle ctxt) of+ in case match x handle of Nothing -> Nothing- Just (_, action) -> Just action+ Just ((pathInfo',_), action) -> Just (action (setRequest ctxt ((getRequest ctxt) { pathInfo = pathInfo' }))) -- | Connects two path segments. Note that when normally used, the -- type parameter r is 'Req'. It is more general here to facilitate@@ -184,6 +185,10 @@ [] -> Just (req, k) _ -> Nothing +-- | Matches anything.+anything :: Req -> a -> Maybe (Req, a)+anything req k = Just (req, k)+ -- | Captures a part of the path. It will parse the part into the type -- specified by the handler it is matched to. If there is no segment, or -- if the segment cannot be parsed as such, it won't match.@@ -195,53 +200,68 @@ Right p -> Just ((xs, snd req), k p) _ -> Nothing +data ParamError = ParamMissing | ParamUnparsable | ParamOtherError Text deriving (Eq, Show)+ -- | A class that is used for parsing for 'param', 'paramOpt', and -- 'segment'. class FromParam a where- fromParam :: Text -> Either Text a+ fromParam :: Text -> Either ParamError a instance FromParam Text where fromParam = Right instance FromParam Int where fromParam t = case decimal t of- Left msg -> Left (T.pack msg)+ Left _ -> Left ParamUnparsable Right m | snd m /= "" ->- Left ("Incomplete match: " <> T.pack (show m))+ Left ParamUnparsable Right (v, _) -> Right v instance FromParam Double where fromParam t = case double t of- Left msg -> Left (T.pack msg)+ Left _ -> Left ParamUnparsable Right m | snd m /= "" ->- Left ("Incomplete match: " <> T.pack (show m))+ Left ParamUnparsable Right (v, _) -> Right v --- | Matches on a query parameter of the given name. If there is no--- parameter, or it cannot be parsed into the type needed by the+-- | Matches on a single query parameter of the given name. If there is no+-- parameters, or it cannot be parsed into the type needed by the -- handler, it won't match. param :: FromParam p => Text -> Req -> (p -> a) -> Maybe (Req, a) param n req k =- let match = find ((== T.encodeUtf8 n) . fst) (snd req)- in case (maybe "" T.decodeUtf8 . snd) <$> match of- Nothing -> Nothing- Just p -> case fromParam p of- Left _ -> Nothing- Right p' -> Just (req, k p')+ let match = filter ((== T.encodeUtf8 n) . fst) (snd req)+ in case rights (map (fromParam . maybe "" T.decodeUtf8 . snd) match) of+ [x] -> Just (req, k x)+ _ -> Nothing --- | If the specified parameter is present, it will be parsed into the--- type needed by the handler, but if it isn't present or cannot be--- parsed, the handler will still be called (just with the 'Left'--- variant).+-- | Matches on query parameters of the given name. If there are no+-- parameters, or it cannot be parsed into the type needed by the+-- handler, it won't match.+paramMany :: FromParam p => Text -> Req -> ([p] -> a) -> Maybe (Req, a)+paramMany n req k =+ let match = filter ((== T.encodeUtf8 n) . fst) (snd req)+ in case map (maybe "" T.decodeUtf8 . snd) match of+ [] -> Nothing+ xs -> let ps = rights $ map fromParam xs in+ if length ps == length xs+ then Just (req, k ps)+ else Nothing++-- | If the specified parameters are present, they will be parsed into the+-- type needed by the handler, but if they aren't present or cannot be+-- parsed, the handler will still be called. paramOpt :: FromParam p => Text -> Req ->- (Either Text p -> a) ->+ (Either ParamError [p] -> a) -> Maybe (Req, a) paramOpt n req k =- let match = find ((== T.encodeUtf8 n) . fst) (snd req)- p = ((maybe "" T.decodeUtf8 . snd) <$> match)- in case p of- Nothing -> Just (req, k (Left "param missing"))- Just p' -> Just (req, k (fromParam p'))+ let match = filter ((== T.encodeUtf8 n) . fst) (snd req)++ in case map (maybe "" T.decodeUtf8 . snd) match of+ [] -> Just (req, k (Left ParamMissing))+ ps -> Just (req, k (foldLefts [] (map fromParam ps)))+ where foldLefts acc [] = Right (reverse acc)+ foldLefts _ (Left x : _) = Left x+ foldLefts acc (Right x : xs) = foldLefts (x : acc) xs returnText :: Text -> Status -> ByteString -> IO (Maybe Response) returnText text status content =
test/Spec.hs view
@@ -18,7 +18,7 @@ main :: IO () main = hspec $ do - describe "routing" $ do+ describe "matching" $ do it "should match first segment with path" $ do path "foo" (["foo", "bar"], []) () `shouldSatisfy` isJust path "foo" ([], []) () `shouldSatisfy` isNothing@@ -97,23 +97,23 @@ do (path "a" ==> const ()) (R (["a"], [])) `shouldSatisfy` isJust- (segment ==> \_ (_ :: Text) -> ())+ (segment ==> \(_ :: Text) _ -> ()) (R (["a"], [])) `shouldSatisfy` isJust- (segment // path "b" ==> \_ x -> x == ("a" :: Text))+ (segment // path "b" ==> \x _ -> x == ("a" :: Text)) (R (["a", "b"], [])) `shouldSatisfy` fromJust- (segment // path "b" ==> \_ x -> x == ("a" :: Text))+ (segment // path "b" ==> \x _ -> x == ("a" :: Text)) (R (["a", "a"], [])) `shouldSatisfy` isNothing- (segment // path "b" ==> \_ x -> x == ("a" :: Text))+ (segment // path "b" ==> \x _ -> x == ("a" :: Text)) (R (["a"], [])) `shouldSatisfy` isNothing it "should always pass a value with paramOpt" $- do paramOpt "id" ([], []) (isLeft :: Either Text Text -> Bool)+ do paramOpt "id" ([], []) (isLeft :: Either ParamError [Text] -> Bool) `shouldSatisfy` (snd . fromJust) paramOpt "id" ([], [("id", Just "foo")])- (== Right ("foo" :: Text))+ (== Right (["foo"] :: [Text])) `shouldSatisfy` (snd . fromJust) it "should match end against no further path segments" $ do end ([],[]) () `shouldSatisfy` isJust@@ -123,7 +123,16 @@ do (path "a" // end) (["a"],[]) () `shouldSatisfy` isJust (segment // end) (["a"],[]) (== ("a" :: Text)) `shouldSatisfy` isJust+ it "should match anything" $+ anything ([],[]) () `shouldSatisfy` isJust + describe "route" $ do+ it "should match route to parameter" $+ do r <- route (R (["a"], [])) [segment ==> (\a _ -> if a == ("a"::Text) then okText "" else return Nothing)]+ (responseStatus <$> r) `shouldSatisfy` isJust+ it "should match nested routes" $+ do r <- route (R (["a", "b"], [])) [path "a" ==> (\c -> route c [path "b" ==> const (okText "")])]+ (responseStatus <$> r) `shouldSatisfy` isJust describe "parameter parsing" $ do it "should parse Text" $@@ -132,13 +141,13 @@ do fromParam "1" `shouldBe` Right (1 :: Int) fromParam "2011" `shouldBe` Right (2011 :: Int) fromParam "aaa" `shouldSatisfy`- (isLeft :: Either Text Int -> Bool)+ (isLeft :: Either ParamError Int -> Bool) fromParam "10a" `shouldSatisfy`- (isLeft :: Either Text Int -> Bool)+ (isLeft :: Either ParamError Int -> Bool) it "should be able to parse Double" $ do fromParam "1" `shouldBe` Right (1 :: Double) fromParam "1.02" `shouldBe` Right (1.02 :: Double) fromParam "thr" `shouldSatisfy`- (isLeft :: Either Text Double -> Bool)+ (isLeft :: Either ParamError Double -> Bool) fromParam "100o" `shouldSatisfy`- (isLeft :: Either Text Double -> Bool)+ (isLeft :: Either ParamError Double -> Bool)