{-# LANGUAGE CPP , GADTs , KindSignatures , NoImplicitPrelude #-} #if MIN_VERSION_base(4,9,0) {-# OPTIONS_GHC -Wno-redundant-constraints #-} #endif -- | This module allows you to combine 'Resource's into an 'Api'. This -- can then be served using 'rest-happstack' or 'rest-snap', or used -- to generate clients or documentation using 'rest-gen'. module Rest.Api ( -- * Api data types. Api (..) , VersionSet , Router (..) , Some1 (..) -- * Defining routes. , route , compose , ( -/) , ( --/) , ( ---/) , ( ----/) , ( -----/) , (------/) , root -- * Api versioning. , Version (..) , mkVersion , latest , parseVersion , lookupVersion , lookupVersion' , withVersion ) where import Prelude.Compat import Data.Char import Data.Function (on) import Data.List (sortBy) import Data.List.Split import Data.Ord (comparing) import Rest.Resource import Rest.Schema (named, singleton) import Safe ------------------------------------------------------------------------------- -- A routing table of REST resources. -- | An existential where the second argument has kind @(* -> *)@. data Some1 f where Some1 :: f (a :: * -> *) -> Some1 f -- | A 'Router' is a 'Resource' and a list of subresources. data Router m s where Embed :: Resource m s sid mid aid -> [Some1 (Router s)] -> Router m s -- | Convenience constructor constructing a route without any -- subresource. route :: Monad s => Resource m s sid mid aid -> Router m s route = flip Embed [] -- | Add the second router as a subresource to the first. compose :: Router m s -> Router s t -> Router m s compose (Embed r xs) b = Embed r (xs ++ [Some1 b]) infixl 4 -/ infixl 5 --/ infixl 6 ---/ infixl 7 ----/ infixl 8 -----/ infixl 9 ------/ -- | Operators with the right fixities to allow you to define routes -- without using parentheses. Start with the shortest near the root. (-/), (--/), (---/), (----/), (-----/), (------/) :: Router m s -> Router s t -> Router m s ( -/) = compose ( --/) = compose ( ---/) = compose ( ----/) = compose ( -----/) = compose (------/) = compose -- | An empty router to use as the root for your API. root :: (Applicative m, Monad m) => Router m m root = route $ mkResourceId { schema = singleton () $ named [] } ------------------------------------------------------------------------------- -- | An API version has three parts. The first is two are used for API -- breaking changes, the last for non-API breaking changes. data Version = Version { full :: Int , major :: Int , minor :: Maybe Int } deriving (Eq, Ord) -- | Smart constructor for 'Version'. mkVersion :: Int -> Int -> Int -> Version mkVersion f m l = Version f m (Just l) instance Show Version where show v = show (full v) ++ "." ++ show (major v) ++ maybe "" (\x -> "." ++ show x) (minor v) -- | A version set is a list of versioned routers. type VersionSet m = [(Version, Some1 (Router m))] -- | An API can be versioned or unversioned. -- A versioned API is a set of versioned routers. -- An unversioned API is just a single router. data Api m where Unversioned :: Some1 (Router m) -> Api m Versioned :: VersionSet m -> Api m -- | Get the latest version of an API. latest :: VersionSet m -> Maybe (Version, Some1 (Router m)) latest = headMay . sortBy (flip compare `on` fst) -- | Parse a 'String' as a 'Version'. The string should contain two or -- three numbers separated by dots, e.g. @1.12.3@. parseVersion :: String -> Maybe Version parseVersion s = case map readMay . splitOn "." . filter (\c -> isDigit c || c == '.') $ s of [ Just a, Just b, Just c ] -> Just (Version a b (Just c)) [ Just a, Just b ] -> Just (Version a b Nothing) _ -> Nothing -- | Look up a version in an API. The string can either be a valid -- version according to 'parseVersion', or "latest". lookupVersion :: String -> VersionSet m -> Maybe (Some1 (Router m)) lookupVersion "latest" = fmap snd . latest lookupVersion str = (parseVersion str >>=) . flip lookupVersion' -- | Look up a version in the API. lookupVersion' :: Version -> VersionSet m -> Maybe (Some1 (Router m)) lookupVersion' v versions = best (filter (matches v . fst) versions) where best = fmap snd . headMay . sortBy (flip (comparing fst)) matches :: Version -> Version -> Bool matches (Version a b c) (Version x y z) | a == x && b == y && c <= z = True | otherwise = False -- | Given a version string, an API and a fallback, do the following: -- -- * Parse the version number or "latest". -- -- * Look up this version. -- -- * If ok, run the given function on it. -- -- * If not parsed or found, return the fallback. withVersion :: String -> Api m -> r -> (Version -> Some1 (Router m) -> r) -> r withVersion ver (Versioned vrs) err ok = maybe err (uncurry ok) $ case ver of "latest" -> latest vrs _ -> do pv <- parseVersion ver r <- lookupVersion' pv vrs return (pv, r) withVersion ver (Unversioned r) err ok = maybe err (uncurry ok) $ case ver of "latest" -> return (mkVersion 1 0 0, r) _ -> Nothing