yesod/Web/Restful/Resource.hs
2009-09-24 00:58:55 +03:00

189 lines
6.3 KiB
Haskell

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Resource
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Stable
-- Portability : portable
--
-- Defines the Resource class.
--
---------------------------------------------------------
module Web.Restful.Resource
( ResourceName (..)
, fromString
, checkPattern
, validatePatterns
, checkResourceName
-- * Testing
, testSuite
) where
import Data.List.Split (splitOn)
import Web.Restful.Definitions
import Web.Restful.Handler
import Data.List (intercalate)
import Test.Framework (testGroup, Test)
import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck (testProperty)
import Test.HUnit hiding (Test)
import Test.QuickCheck
data ResourcePatternPiece =
Static String
| Dynamic String
| Slurp String -- ^ take up the rest of the pieces. must be last
deriving Eq
instance Show ResourcePattern where
show = concatMap helper . unRP where
helper (Static s) = '/' : s
helper (Dynamic s) = '/' : '$' : s
helper (Slurp s) = '/' : '*' : s
isSlurp :: ResourcePatternPiece -> Bool
isSlurp (Slurp _) = True
isSlurp _ = False
newtype ResourcePattern = ResourcePattern { unRP :: [ResourcePatternPiece] }
deriving (Eq, Arbitrary)
fromString :: String -> ResourcePattern
fromString = ResourcePattern
. map fromString' . filter (not . null) . splitOn "/"
fromString' :: String -> ResourcePatternPiece
fromString' ('$':rest) = Dynamic rest
fromString' ('*':rest) = Slurp rest
fromString' x = Static x
class Show a => ResourceName a b | a -> b where
-- | Get the URL pattern for each different resource name.
-- Something like /foo/$bar/baz/ will match the regular expression
-- /foo/(\\w*)/baz/, matching the middle part with the urlParam bar.
--
-- Also, /foo/*bar/ will match /foo/[anything else], capturing the value
-- into the bar urlParam.
resourcePattern :: a -> String
-- | Get all possible values for resource names.
-- Remember, if you use variables ($foo) in your resourcePatterns you
-- can get an unlimited number of resources for each resource name.
allValues :: [a]
-- | Find the handler for each resource name/verb pattern.
getHandler :: b -> a -> Verb -> Handler
type SMap = [(String, String)]
data CheckPatternReturn = StaticMatch | DynamicMatch (String, String) | NoMatch
checkPattern :: ResourcePattern -> Resource -> Maybe SMap
checkPattern rp r = checkPattern'' (unRP rp) r
checkPattern'' :: [ResourcePatternPiece] -> Resource -> Maybe SMap
checkPattern'' rp r
| length rp /= 0 && isSlurp (last rp) = do
let rp' = init rp
(r1, r2) = splitAt (length rp') r
smap <- checkPattern'' rp' r1
let slurpValue = intercalate "/" r2
Slurp slurpKey = last rp
return $ (slurpKey, slurpValue) : smap
| length rp /= length r = Nothing
| otherwise = combine [] $ zipWith checkPattern' rp r
checkPattern' :: ResourcePatternPiece -> String -> CheckPatternReturn
checkPattern' (Static x) y = if x == y then StaticMatch else NoMatch
checkPattern' (Dynamic x) y = DynamicMatch (x, y)
checkPattern' (Slurp x) _ = error $ "Slurp pattern " ++ x ++ " must be last"
combine :: SMap -> [CheckPatternReturn] -> Maybe SMap
combine s [] = Just $ reverse s
combine _ (NoMatch:_) = Nothing
combine s (StaticMatch:rest) = combine s rest
combine s (DynamicMatch x:rest) = combine (x:s) rest
overlaps :: [ResourcePatternPiece] -> [ResourcePatternPiece] -> Bool
overlaps [] [] = True
overlaps [] _ = False
overlaps _ [] = False
overlaps (Slurp _:_) _ = True
overlaps _ (Slurp _:_) = True
overlaps (Dynamic _:x) (_:y) = overlaps x y
overlaps (_:x) (Dynamic _:y) = overlaps x y
overlaps (Static a:x) (Static b:y) = a == b && overlaps x y
checkResourceName :: (Monad m, ResourceName rn model) => rn -> m ()
checkResourceName rn = do
let avs@(y:_) = allValues
_ignore = asTypeOf rn y
let patterns = map (fromString . resourcePattern) avs
case validatePatterns patterns of
[] -> return ()
x -> fail $ "Overlapping patterns:\n" ++ unlines (map show x)
validatePatterns :: [ResourcePattern] -> [(ResourcePattern, ResourcePattern)]
validatePatterns [] = []
validatePatterns (x:xs) =
concatMap (validatePatterns' x) xs ++ validatePatterns xs where
validatePatterns' :: ResourcePattern
-> ResourcePattern
-> [(ResourcePattern, ResourcePattern)]
validatePatterns' a b =
if overlaps (unRP a) (unRP b)
then [(a, b)]
else []
---- Testing
testSuite :: Test
testSuite = testGroup "Web.Restful.Resource"
[ testCase "non-overlap" case_overlap1
, testCase "overlap" case_overlap2
, testCase "overlap-slurp" case_overlap3
, testCase "validatePatterns" case_validatePatterns
, testProperty "show pattern" prop_showPattern
]
case_overlap1 :: Assertion
case_overlap1 = assert $ not $ overlaps
(unRP $ fromString "/foo/$bar/")
(unRP $ fromString "/foo/baz/$bin")
case_overlap2 :: Assertion
case_overlap2 = assert $ overlaps
(unRP $ fromString "/foo/bar")
(unRP $ fromString "/foo/$baz")
case_overlap3 :: Assertion
case_overlap3 = assert $ overlaps
(unRP $ fromString "/foo/bar/baz/$bin")
(unRP $ fromString "*slurp")
case_validatePatterns :: Assertion
case_validatePatterns =
let p1 = fromString "/foo/bar/baz"
p2 = fromString "/foo/$bar/baz"
p3 = fromString "/bin"
p4 = fromString "/bin/boo"
p5 = fromString "/bin/*slurp"
in validatePatterns [p1, p2, p3, p4, p5] @?=
[ (p1, p2)
, (p4, p5)
]
prop_showPattern :: ResourcePattern -> Bool
prop_showPattern p = fromString (show p) == p
instance Arbitrary ResourcePatternPiece where
arbitrary = do
constr <- elements [Static, Dynamic, Slurp]
size <- elements [1..10]
s <- sequence (replicate size $ elements ['a'..'z'])
return $ constr s
coarbitrary = undefined