hyperbole-0.4.2: src/Web/Hyperbole/Data/QueryData.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE UndecidableInstances #-}
module Web.Hyperbole.Data.QueryData where
import Data.Bifunctor (bimap)
import Data.ByteString (ByteString)
import Data.Default (Default (..))
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Maybe (fromMaybe)
import Data.String (IsString)
import Data.String.Conversions (cs)
import Data.Text (Text, pack)
import Data.Time (UTCTime)
import Data.Word
import GHC.Generics
import Network.HTTP.Types (Query, renderQuery)
import Network.HTTP.Types qualified as HTTP
import Text.Read (readMaybe)
import Web.HttpApiData (parseQueryParam, toQueryParam)
import Prelude hiding (lookup)
newtype Param = Param {text :: Text}
deriving newtype (Show, Eq, Ord, IsString)
newtype ParamValue = ParamValue {text :: Text}
deriving newtype (Show, Eq, Ord, IsString)
-- | Key-value store for query params and sessions
newtype QueryData = QueryData (Map Param ParamValue)
deriving (Show)
deriving newtype (Monoid, Semigroup)
singleton :: (ToParam a) => Param -> a -> QueryData
singleton key a = QueryData $ M.singleton key (toParam a)
insert :: (ToParam a) => Param -> a -> QueryData -> QueryData
insert p a (QueryData m) =
let val = toParam a
in QueryData $ M.insert p val m
insertAll :: (ToQuery a) => a -> QueryData -> QueryData
insertAll a (QueryData m) =
let QueryData kvs = toQuery a
in QueryData $ M.union kvs m
delete :: Param -> QueryData -> QueryData
delete p (QueryData m) =
QueryData $ M.delete p m
lookup :: (FromParam a) => Param -> QueryData -> Maybe a
lookup k (QueryData m) = do
t <- M.lookup k m
either (const Nothing) pure $ parseParam t
require :: (FromParam a) => Param -> QueryData -> Either Text a
require p (QueryData m) = do
case M.lookup p m of
Nothing -> Left $ "Missing Key: " <> p.text
Just val -> parseParam val
filterKey :: (Param -> Bool) -> QueryData -> QueryData
filterKey p (QueryData m) =
QueryData $ M.filterWithKey (\k _ -> p k) m
member :: Param -> QueryData -> Bool
member k (QueryData qd) = M.member k qd
elems :: QueryData -> [ParamValue]
elems (QueryData m) = M.elems m
render :: QueryData -> ByteString
render (QueryData m) =
-- urlEncode True
renderQuery False (HTTP.toQuery $ fmap queryItem $ M.toList m)
where
queryItem (Param k, ParamValue val) = (k, val)
parse :: ByteString -> QueryData
parse =
-- urlDecode True
queryData . HTTP.parseQuery
queryData :: Query -> QueryData
queryData =
QueryData . M.fromList . map (bimap (Param . cs) value)
where
-- empty / missing values are encoded as empty strings
value Nothing = ""
value (Just v) = ParamValue (cs v)
fromList :: [(Param, ParamValue)] -> QueryData
fromList = QueryData . M.fromList
toList :: QueryData -> [(Param, ParamValue)]
toList (QueryData m) = M.toList m
{- | Decode a type from a 'QueryData'. Missing fields are set to 'defaultParam'
@
#EMBED Example/Docs/Encoding.hs data Filters
@
>>> parseQuery $ parse "active=true&search=asdf"
Right (Filters True "asdf")
>>> parseQuery $ parse "search=asdf"
Right (Filters False "asdf")
-}
class FromQuery a where
parseQuery :: QueryData -> Either Text a
default parseQuery :: (Generic a, GFromQuery (Rep a)) => QueryData -> Either Text a
parseQuery q = to <$> gParseQuery q
instance FromQuery QueryData where
parseQuery = pure
{- | A page can store state in the browser 'query' string. ToQuery and 'FromQuery' control how a datatype is encoded to a full query string
@
#EMBED Example/Docs/Encoding.hs data Filters
@
>>> render $ toQuery $ Filter True "asdf"
"active=true&search=asdf"
If the value of a field is the same as 'DefaultParam', it will be omitted from the query string
>>> render $ toQuery $ Filter True ""
"active=true"
>>> render $ toQuery $ Filter False ""
""
-}
class ToQuery a where
toQuery :: a -> QueryData
default toQuery :: (Generic a, GToQuery (Rep a)) => a -> QueryData
toQuery = gToQuery . from
instance ToQuery QueryData where
toQuery = id
instance ToQuery Query where
toQuery = queryData
{- | 'session's, 'form's, and 'query's all encode data as query strings. ToParam and FromParam control how a datatype is encoded to a parameter. By default it simply url-encodes the show instance.
@
#EMBED Example/Effects/Todos.hs data Todo = Todo
@
@
#EMBED Example/Docs/Encoding.hs data Tags
#EMBED Example/Docs/Encoding.hs instance ToParam Tags
@
-}
class ToParam a where
toParam :: a -> ParamValue
default toParam :: (Show a) => a -> ParamValue
toParam = showQueryParam
{- | Decode data from a 'query', 'session', or 'form' parameter value
@
#EMBED Example/Effects/Todos.hs data Todo = Todo
@
@
#EMBED Example/Docs/Encoding.hs data Tags
#EMBED Example/Docs/Encoding.hs instance FromParam Tags
@
-}
class FromParam a where
parseParam :: ParamValue -> Either Text a
default parseParam :: (Read a) => ParamValue -> Either Text a
parseParam = readQueryParam
instance ToParam Int where
toParam = ParamValue . toQueryParam
instance FromParam Int where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Integer where
toParam = ParamValue . toQueryParam
instance FromParam Integer where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Float where
toParam = ParamValue . toQueryParam
instance FromParam Float where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Double where
toParam = ParamValue . toQueryParam
instance FromParam Double where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Word where
toParam = ParamValue . toQueryParam
instance FromParam Word where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Word8 where
toParam = ParamValue . toQueryParam
instance FromParam Word8 where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Word16 where
toParam = ParamValue . toQueryParam
instance FromParam Word16 where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Word32 where
toParam = ParamValue . toQueryParam
instance FromParam Word32 where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Word64 where
toParam = ParamValue . toQueryParam
instance FromParam Word64 where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Bool where
toParam = ParamValue . toQueryParam
instance FromParam Bool where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Text where
toParam = ParamValue . toQueryParam
instance FromParam Text where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam Char where
toParam = ParamValue . toQueryParam
instance FromParam Char where
parseParam (ParamValue t) = parseQueryParam t
instance ToParam UTCTime where
toParam = ParamValue . toQueryParam
instance FromParam UTCTime where
parseParam (ParamValue t) = parseQueryParam t
instance (Show a) => ToParam [a] where
toParam = showQueryParam
instance (Read a) => FromParam [a] where
parseParam = readQueryParam
instance (Show k, Show v) => ToParam (Map k v) where
toParam = showQueryParam
instance (Read k, Read v, Ord k) => FromParam (Map k v) where
parseParam = readQueryParam
instance (ToParam a) => ToParam (Maybe a) where
toParam Nothing = ""
toParam (Just a) = toParam a
instance (FromParam a) => FromParam (Maybe a) where
parseParam "" = pure Nothing
parseParam t = Just <$> parseParam @a t
instance (ToParam a, ToParam b) => ToParam (Either a b) where
toParam (Left a) = toParam a
toParam (Right b) = toParam b
instance (FromParam a, FromParam b) => FromParam (Either a b) where
parseParam val =
case parseParam @a val of
Right a -> pure $ Left a
Left _ -> do
case parseParam @b val of
Left _ -> Left $ "Could not parseParam Either: " <> val.text
Right b -> pure $ Right b
-- | Encode a Show as a query param
showQueryParam :: (Show a) => a -> ParamValue
showQueryParam a = ParamValue $ toQueryParam $ show a
-- | Decode a Read as a query param
readQueryParam :: (Read a) => ParamValue -> Either Text a
readQueryParam (ParamValue t) = do
str <- parseQueryParam t
case readMaybe str of
Nothing -> Left $ pack $ "Could not read query param: " <> str
Just a -> pure a
-- | Parse a Traversable (list) of params
parseParams :: (Traversable t, FromParam a) => t ParamValue -> Either Text (t a)
parseParams = traverse parseParam
-- | Generic decoding of records from a Query
class GFromQuery f where
gParseQuery :: QueryData -> Either Text (f p)
instance (GFromQuery f, GFromQuery g) => GFromQuery (f :*: g) where
gParseQuery q = do
a <- gParseQuery q
b <- gParseQuery q
pure $ a :*: b
instance (GFromQuery f) => GFromQuery (M1 D d f) where
gParseQuery q = M1 <$> gParseQuery q
instance (GFromQuery f) => GFromQuery (M1 C c f) where
gParseQuery q = M1 <$> gParseQuery q
instance (Selector s, FromParam a, DefaultParam a) => GFromQuery (M1 S s (K1 R a)) where
gParseQuery q = do
let s = selName (undefined :: M1 S s (K1 R (f a)) p)
let mval = lookup (Param $ pack s) q
pure $ M1 $ K1 $ fromMaybe defaultParam mval
-- | Generic encoding of records to a Query
class GToQuery f where
gToQuery :: f p -> QueryData
instance (GToQuery f, GToQuery g) => GToQuery (f :*: g) where
gToQuery (f :*: g) = gToQuery f <> gToQuery g
instance (GToQuery f) => GToQuery (M1 D d f) where
gToQuery (M1 f) = gToQuery f
instance (GToQuery f) => GToQuery (M1 C d f) where
gToQuery (M1 f) = gToQuery f
instance (Selector s, ToParam a, Eq a, DefaultParam a) => GToQuery (M1 S s (K1 R a)) where
gToQuery (M1 (K1 a))
| a == defaultParam = mempty
| otherwise =
let sel = Param $ pack $ selName (undefined :: M1 S s (K1 R (f a)) p)
in singleton sel a
-- | Data.Default doesn't have a Text instance. This class does
class DefaultParam a where
defaultParam :: a
default defaultParam :: (Default a) => a
defaultParam = def
instance {-# OVERLAPPABLE #-} (Default a) => DefaultParam a where
defaultParam = def
instance {-# OVERLAPS #-} DefaultParam Text where
defaultParam = ""