hsp-0.2: HSP/Data.hs
{-# OPTIONS -fallow-overlapping-instances -fallow-undecidable-instances -fglasgow-exts #-}
-----------------------------------------------------------------------------
-- |
-- Module : HSP.Data
-- Copyright : (c) Niklas Broberg 2004,
-- License : BSD-style (see the file LICENSE.txt)
--
-- Maintainer : Niklas Broberg, d00nibro@dtek.chalmers.se
-- Stability : experimental
-- Portability : requires undecidable and overlapping instances
--
-- Datatypes and type classes comprising the basic model behind
-- the scenes of Haskell Server Pages tags.
-----------------------------------------------------------------------------
module HSP.Data (
-- * The 'XML' datatype
XML(..),
Domain,
Name,
Attributes,
Attribute,
AttrValue(..),
Children,
isTag, isCdata,
-- * The 'HSP' Monad
HSP,
HSPEnv,
runHSP,
unsafeRunHSP, -- dangerous!!
doIO,
-- * Type classes
IsXML(..),
IsXMLs(..),
IsAttrValue(..),
IsStyleValue(..),
IsStyle(..),
-- * Functions
renderXML,
mkTag,
genTag,
mkETag,
genETag,
pcdata,
cdata,
extract,
-- * Accessing the environment
getApplication,
getRequest,
getSession,
getResponse,
-- * Attribute manipulation
set,
Attr(..),
IsAttribute(..),
IsName,
-- * 'CSS' combinators
(<:>),
(<+>),
cssId,
cssClass,
cssElem,
pc,
css,
-- * Exceptions
throwHSP,
catch
) where
import Prelude hiding (catch)
import HSP.Exception
import Control.Exception (catchDyn, throwDyn)
import Control.Monad.Reader (ReaderT(..), ask, lift, runReaderT)
import Control.Monad.Trans (MonadIO(..))
import Data.Typeable
import Data.List (intersperse)
import HSP.Data.XML
import HSP.Data.PCDATA
import HSP.Data.CSS
import qualified HSP.Data.RequestEnv as Rq
import qualified HSP.Data.Response as Rp
import qualified HSP.Data.Application as A
import qualified HSP.Data.Session as S
--------------------------------------------------------------
-- The HSP Monad
-- | The HSP monad is simply a reader wrapper around
-- the IO monad.
data HSP a = MkHSP {baseC :: ReaderT HSPEnv IO a}
-- |
type HSPEnv = (A.Application, S.Session, Rq.RequestEnv, Rp.Response)
-- do NOT export this in the final version
dummyEnv :: HSPEnv
dummyEnv = undefined
instance Monad HSP where
return = MkHSP . return
c1 >>= k = MkHSP $ do
a <- baseC c1
baseC $ k a
instance MonadIO HSP where
liftIO c = doIO c
runHSP :: HSP a -> HSPEnv -> IO a
runHSP = runReaderT . baseC
unsafeRunHSP :: HSP a -> IO a
unsafeRunHSP hspf = runHSP hspf dummyEnv
doIO :: IO a -> HSP a
doIO = MkHSP . lift
getEnv :: HSP HSPEnv
getEnv = MkHSP ask
-----------------------------------------------------------------
-- Type classes
-------------
-- IsXML
-- | Instantiate this class to enable values of the given type
-- to appear as XML elements.
class IsXML a where
toXML :: a -> HSP XML
-- | XML can naturally be represented as XML.
instance IsXML XML where
toXML = return
-- | Strings should be represented as PCDATA.
instance IsXML String where
toXML = return . pcdata
-- | Anything that can be shown can always fall back on
-- that as a default behavior.
instance (Show a) => IsXML a where
toXML = toXML . show
-- | An IO computation returning something that can be represented
-- as XML can be lifted into an analogous HSP computation.
instance (IsXML a) => IsXML (IO a) where
toXML ioa = toXML $ doIO ioa
-- | An HSP computation returning something that can be represented
-- as XML simply needs to turn the produced value into XML.
instance (IsXML a) => IsXML (HSP a) where
toXML hspa = hspa >>= toXML
-- | CSS can appear as PCDATA of a style tag.
instance IsXML CSS where
toXML (CSS entries) = <style type="text/css">
<% unlines (map show entries) %>
</style>
-------------
-- IsXMLs
-- | Instantiate this class to enable values of the given type
-- to be represented as a list of XML elements. Note that for
-- any given type, only one of the two classes IsXML and IsXMLs
-- should ever be instantiated. Values of types that instantiate
-- IsXML can automatically be represented as a (singleton) list
-- of XML children.
class IsXMLs a where
toXMLs :: a -> HSP [XML]
-- | Anything that can be represented as a single XML element
-- can of course be represented as a (singleton) list of XML
-- elements.
instance (IsXML a) => IsXMLs a where
toXMLs a = do xml <- toXML a
return [xml]
-- | We must specify an extra rule for Strings even though the previous
-- rule would apply, because the rule for [a] would also apply and
-- would be more specific.
instance IsXMLs String where
toXMLs s = do xml <- toXML s
return [xml]
-- | If something can be represented as a list of XML, then a list of
-- that something can also be represented as a list of XML.
instance (IsXMLs a) => IsXMLs [a] where
toXMLs as = do xmlss <- mapM toXMLs as
return $ concat xmlss
-- | An IO computation returning something that can be represented
-- as a list of XML can be lifted into an analogous HSP computation.
instance (IsXMLs a) => IsXMLs (IO a) where
toXMLs = toXMLs . doIO
-- | An HSP computation returning something that can be represented
-- as a list of XML simply needs to turn the produced value into
-- a list of XML.
instance (IsXMLs a) => IsXMLs (HSP a) where
toXMLs hspa = hspa >>= toXMLs
-- | Of the base types, () stands out as a type that can only be
-- represented as a (empty) list of XML.
instance IsXMLs () where
toXMLs _ = return []
-- | Maybe types are handy for cases where you might or might not want
-- to generate XML, such as null values from databases
instance (IsXMLs a) => IsXMLs (Maybe a) where
toXMLs Nothing = return []
toXMLs (Just a) = toXMLs a
---------------
-- IsAttrValue
-- | Instantiate this class to enable values of the given type
-- to appear as XML attribute values.
class IsAttrValue a where
toAttrValue :: a -> HSP AttrValue
-- fromAttrValue :: AttrValue -> a
-- | An AttrValue is trivial.
instance IsAttrValue AttrValue where
toAttrValue = return
-- | Strings can be directly represented as values.
instance IsAttrValue String where
toAttrValue = return . Value . toPCDATA
-- | Anything that can be shown and read can always fall back on
-- that as a default behavior.
instance (Show a, Read a) => IsAttrValue a where
toAttrValue = toAttrValue . show
-- | An IO computation returning something that can be represented
-- as an attribute value can be lifted into an analogous HSP computation.
instance (IsAttrValue a) => IsAttrValue (IO a) where
toAttrValue = toAttrValue . doIO
-- | An HSP computation returning something that can be represented
-- as a list of XML simply needs to turn the produced value into
-- a list of XML.
instance (IsAttrValue a) => IsAttrValue (HSP a) where
toAttrValue hspa = hspa >>= toAttrValue
-- | The common way to present list data in attributes is as
-- a comma separated, unbracketed sequence
instance (IsAttrValue a) => IsAttrValue [a] where
toAttrValue as = do [/ (Value vs)* /] <- mapM toAttrValue as
return . Value $ concat $ intersperse "," vs
---------------
-- GetAttrValue
-- | Instantiate this class to enable values of the given type
-- to be retrieved through attribute patterns.
class GetAttrValue a where
fromAttrValue :: AttrValue -> a
-- | An AttrValue is trivial.
instance GetAttrValue AttrValue where
fromAttrValue = id
-- | Strings can be directly taken from values.
instance GetAttrValue String where
fromAttrValue (Value s) = s
-- | Anything that can be read can always fall back on
-- that as a default behavior.
instance (Read a) => GetAttrValue a where
fromAttrValue = read . fromAttrValue
-- | An IO computation returning something that can be represented
-- as an attribute value can be lifted into an analogous HSP computation.
instance (GetAttrValue a, Monad m) => GetAttrValue (m a) where
fromAttrValue = return . fromAttrValue
-- | The common way to present list data in attributes is as
-- a comma separated, unbracketed sequence
instance (GetAttrValue a) => GetAttrValue [a] where
fromAttrValue v@(Value str) = case str of
[/ v1+, (/ ',', vs@:_+ /)+ /] ->
map (fromAttrValue . Value) (v1:vs)
_ -> [fromAttrValue v]
-- All that for these two functions
extract :: (GetAttrValue a) => Name -> Attributes -> (Maybe a, Attributes)
extract _ [] = (Nothing, [])
extract name (p@(n, v):as)
| name == n = (Just $ fromAttrValue v, as)
| otherwise = let (val, attrs) = extract name as
in (val, p:attrs)
---------------
-- IsStyleValue
-- | Instantiate this class to enable values of the given type
-- to appear as CSS attribute values.
-- Instances mirroring the relevant ones of IsAttrValue are supplied
class IsStyleValue a where
toStyleValue :: a -> HSP StyleValue
-- | A StyleValue is trivial.
instance IsStyleValue StyleValue where
toStyleValue = return
-- | Strings can be directly represented as values.
instance IsStyleValue String where
toStyleValue = return . StyleValue
-- | Anything that can be shown and read can always fall back on
-- that as a default behavior.
instance (Show a, Read a) => IsStyleValue a where
toStyleValue = toStyleValue . show
-- | An IO computation returning something that can be represented
-- as a style value can be lifted into an analogous HSP computation.
instance (IsStyleValue a) => IsStyleValue (IO a) where
toStyleValue = toStyleValue . doIO
-- | An HSP computation returning something that can be represented
-- as a style value simply needs to turn the produced value into
-- a style value.
instance (IsStyleValue a) => IsStyleValue (HSP a) where
toStyleValue hspa = hspa >>= toStyleValue
-- | An Int is chosen to represent a value in pixels.
instance IsStyleValue Int where
toStyleValue x = toStyleValue $ show x ++ "px"
-- | A Float is chosen to represent a value in percent.
-- E.g. 0.33 yields 33%.
instance IsStyleValue Float where
toStyleValue x = toStyleValue $ show (round (x*100)) ++ "%"
-------------
-- IsStyle
-- | Instantiate this class to enable values of the given type
-- to appear as styles and be combined with other styles.
class IsStyle s where
toStyle :: s -> HSP Style
-- | A Style is trivial
instance IsStyle Style where
toStyle = return
-- | An IO computation returning something that can be represented
-- as a style can be lifted into an analogous HSP computation.
instance (IsStyle a) => IsStyle (IO a) where
toStyle = toStyle . doIO
-- | An HSP computation returning something that can be represented
-- as a style simply needs to turn the produced value into
-- a style.
instance (IsStyle a) => IsStyle (HSP a) where
toStyle hspa = hspa >>= toStyle
-----------------------------------------------------------------------
-- Manipulating attributes
data Attr = forall n a . (IsName n, IsAttrValue a) => n := a
class IsName n where
toName :: n -> Name
instance IsName Name where
toName = id
instance IsName String where
toName s = (Nothing, s)
instance IsName (String, String) where
toName (ns, s) = (Just ns, s)
class IsAttribute a where
toAttribute :: a -> HSP Attribute
instance IsAttribute Attribute where
toAttribute = return
instance IsAttribute Attr where
toAttribute (n := a) = do av <- toAttrValue a
return (toName n, av)
instance (IsAttribute a) => IsAttribute (HSP a) where
toAttribute hspa = hspa >>= toAttribute
instance (IsAttribute a) => IsAttribute (IO a) where
toAttribute ioa = doIO ioa >>= toAttribute
instance IsAttribute Style where
toAttribute s = toAttribute $ "style" := show s
set :: (IsXML a, IsAttribute at) => a -> at -> HSP XML
set x a = setAll x [a]
setAll :: (IsXML a, IsAttribute at) => a -> [at] -> HSP XML
setAll isxml ats = do
xml <- toXML isxml
attrs <- mapM toAttribute ats
case xml of
CDATA _ -> return xml
Tag n ats cs -> return $ Tag n (foldr insert ats attrs) cs
--------------------------------------------------------------------
-- CSS combinators
(<:>) :: IsStyleValue sv => String -> sv -> HSP Style
name <:> svc = do sv <- toStyleValue svc
return $ Style [(name, sv)]
(<+>) :: (IsStyle a, IsStyle b) => a -> b -> HSP Style
sc1 <+> sc2 = do Style set1 <- toStyle sc1
Style set2 <- toStyle sc2
return $ Style (set1 ++ set2)
cssId = CSSId
cssClass = CSSClass
cssElem = CSSElem
pc = CSSPseudoClass
css :: [HSP CSSEntry] -> HSP CSS
css ecs = do es <- sequence ecs
return $ CSS es
--------------------------------------------------------------------
-- Generation
-- | Generate a Tag from its components.
mkTag :: (IsName n, IsXMLs xmls, IsAttribute at) => n -> [at] -> xmls -> HSP XML
mkTag n attrs xmls = do
cxml <- toXMLs xmls
attribs <- mapM toAttribute attrs
return $ Tag (toName n)
(foldr insert eAttrs attribs)
(flattenCDATA cxml)
where flattenCDATA :: [XML] -> [XML]
flattenCDATA xmls =
case flP xmls [] of
[] -> []
[CDATA ""] -> []
xs -> xs
flP :: [XML] -> [XML] -> [XML]
flP [] bs = reverse bs
flP [x] bs = reverse (x:bs)
flP (x:y:xs) bs = case (x,y) of
(CDATA s1, CDATA s2) -> flP (CDATA (s1++s2) : xs) bs
_ -> flP (y:xs) (x:bs)
genTag :: Name -> [HSP Attribute] -> [HSP [XML]] -> HSP XML
genTag = mkTag
-- | Generate a Tag with no children from its components
mkETag :: (IsName n, IsAttribute at) => n -> [at] -> HSP XML
mkETag n attrs = mkTag n attrs ([] :: [XML])
genETag :: Name -> [HSP Attribute] -> HSP XML
genETag = mkETag
eAttrs :: Attributes
eAttrs = []
insert :: Attribute -> Attributes -> Attributes
insert = (:)
-----------------------------------------------------------------------
-- Exception handling
-- | Catch a user-caused exception
catch :: HSP a -> (Exception -> HSP a) -> HSP a
catch (MkHSP (ReaderT f)) handler = MkHSP $ ReaderT $ \e ->
f e `catchDyn` (\ex -> (let (MkHSP (ReaderT g)) = handler ex
in g e))
-- Will not be exported to the user
throwHSP :: Exception -> a
throwHSP = throwDyn
-----------------------------------------------------------------------
-- The Application object
-- Will not be exported
getApp :: HSP A.Application
getApp = do (app, _s, _rq, _rp) <- getEnv
return app
-- | Fetch the user-defined data held within the Application object
-- in the HSP environment.
getApplication :: Typeable a => HSP a
getApplication = do app <- getApp
case A.fromApplication app of
Nothing -> throwHSP WrongApplicationType
Just a -> return a
-----------------------------------------------------------------------
-- The Session object
-- Will not be exported
getSession :: HSP S.Session
getSession = do (_a, sess, _rq, _rp) <- getEnv
return sess
-----------------------------------------------------------------------
-- The Request object
-- Will not be exported
getRequest :: HSP Rq.RequestEnv
getRequest = do (_a, _s, req, _rp) <- getEnv
return req
-----------------------------------------------------------------------
-- The Response object
-- Will not be exported
getResponse :: HSP Rp.Response
getResponse = do (_a, _s, _rq, resp) <- getEnv
return resp