waypoint-0.1: lib/Waypoint/Query.hs
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_HADDOCK not-home #-}
-- | Usually, you don't need to import this module unless
-- you are trying to reuse some of 'QueryCodec'\'s underlying primitives.
-- Just import "Waypoint" instead.
module Waypoint.Query
( -- * QueryValue
QueryValue (..)
, ToQueryValue (..)
, FromQueryValue (..)
-- * QueryValues
, QueryValues
, queryValuesOne
, queryValuesMaybe
, queryValuesMany
, queryValuesSome
-- * QueryF
, QueryF (..)
, queryFDecode
, QueryFDecodeState(..)
, queryFEncode
, queryFKeys
-- * QueryCodec
, QueryCodec (..)
, queryDecode
, queryEncode
, queryKeys
, query
, queryMaybe
, querySome
, queryMany
-- * ErrQuery
, ErrQuery (..)
)
where
import Control.Applicative
import Control.Applicative.Free.Fast
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.Trans.State.Strict
import Data.List qualified as List
import Data.List.NonEmpty qualified as NEL
import Data.Monoid (Endo (..))
import Data.Profunctor
import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Tuple
import Network.HTTP.Types qualified as H
import Text.Read (readMaybe)
import Witherable qualified as W
import Prelude
--------------------------------------------------------------------------------
-- | How to encode and decode a single querystring value
-- (e.g., the @v@ in @?k=v@).
data QueryValue i o = QueryValue
{ encode :: i -> T.Text
, decode :: T.Text -> Maybe o
}
instance W.Filterable (QueryValue i) where
mapMaybe f (QueryValue i o) = QueryValue i (o >=> f)
instance Functor (QueryValue i) where
fmap = rmap
instance Profunctor QueryValue where
dimap f g (QueryValue i o) = QueryValue (i . f) (fmap g . o)
-- | 'QueryValue' based on 'ToQueryValue' and 'FromQueryValue'.
queryValue :: (ToQueryValue i, FromQueryValue o) => QueryValue i o
queryValue = QueryValue toQueryValue fromQueryValue
--------------------------------------------------------------------------------
-- | How to encode and decode potentially many querystring values
-- (e.g., both @v0@ and @v1@ in @?k=v0&k=v1@).
data QueryValues i o = QueryValues
{ encode :: i -> [T.Text]
, decode :: [T.Text] -> Maybe o
}
instance Functor (QueryValues i) where
fmap = rmap
instance Profunctor QueryValues where
dimap f g (QueryValues i o) = QueryValues (i . f) (fmap g . o)
instance W.Filterable (QueryValues i) where
mapMaybe f (QueryValues i o) = QueryValues i (o >=> f)
queryValuesOne :: QueryValue i o -> QueryValues i o
queryValuesOne v = QueryValues (pure . v.encode) \case
[t] -> v.decode t
_ -> Nothing
queryValuesMaybe :: QueryValue i o -> QueryValues (Maybe i) (Maybe o)
queryValuesMaybe v = QueryValues (maybe [] (pure . v.encode)) \case
[] -> Just Nothing
[t] -> Just <$> v.decode t
_ -> Nothing
queryValuesSome
:: QueryValue i o -> QueryValues (NEL.NonEmpty i) (NEL.NonEmpty o)
queryValuesSome v = QueryValues (fmap v.encode . NEL.toList) \case
t : ts -> traverse v.decode (t NEL.:| ts)
_ -> Nothing
queryValuesMany :: QueryValue i o -> QueryValues [i] [o]
queryValuesMany v = QueryValues (fmap v.encode) (traverse v.decode)
--------------------------------------------------------------------------------
data QueryF i o = QueryF
{ key :: T.Text
, values :: QueryValues i o
}
instance Functor (QueryF i) where
fmap = rmap
instance Profunctor QueryF where
dimap f g (QueryF k v) = QueryF k (dimap f g v)
instance W.Filterable (QueryF i) where
mapMaybe f (QueryF k v) = QueryF k (W.mapMaybe f v)
--------------------------------------------------------------------------------
-- | See 'queryFDecode'.
data QueryFDecodeState = QueryFDecodeState
{ seen :: Set.Set T.Text
-- ^ Previously seen keys.
, input :: H.QueryText
-- ^ Available raw input to decode.
}
deriving (Eq, Show)
-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'QueryF' yourself.
queryFDecode
:: QueryF i o
-> QueryFDecodeState
-> Either ErrQuery (o, QueryFDecodeState)
queryFDecode q = \s ->
if Set.member q.key s.seen
then Left $ ErrQueryBadDecoder q.key
else do
let (xs1, xs2) = List.partition (\(k, _) -> k == q.key) s.input
vs1 = fmap (maybe "" id . snd) xs1
case q.values.decode vs1 of
Just a -> Right (a, s{seen = Set.insert q.key s.seen, input = xs2})
Nothing -> Left $ ErrQueryDecode q.key
-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'QueryF' yourself.
queryFEncode :: QueryF i o -> i -> Endo H.QueryText
queryFEncode q = \i ->
foldMap
(\v -> Endo ((q.key, if T.null v then Nothing else Just v) :))
(q.values.encode i)
<> Endo (filter (\(k, _) -> k /= q.key))
-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'QueryF' yourself.
queryFKeys :: QueryF i o -> Endo [T.Text]
queryFKeys q = Endo ((q.key :) . filter (/= q.key))
--------------------------------------------------------------------------------
data ErrQuery
= -- | Error decoding the query value at the given key.
ErrQueryDecode T.Text
| -- | This error means the decoder itself is incorrect. It tried to
-- decode values associated to the same query string key more than
-- once.
ErrQueryBadDecoder T.Text
deriving stock (Eq, Show)
deriving anyclass (Exception)
--------------------------------------------------------------------------------
-- | Bidirectional codec for encoding an @i@ into a URL query string, and
-- decoding a URL query string into an @o@.
--
-- This works for @application/x-www-form-urlencoded@ bodies too.
--
-- Use 'query', 'queryMaybe', 'querySome', 'queryMany' and 'Applicative' to
-- construct.
newtype QueryCodec i o = QueryCodec (Ap (QueryF i) o)
deriving newtype (Functor, Applicative)
instance Profunctor QueryCodec where
rmap = fmap
lmap f (QueryCodec a) = QueryCodec $ hoistAp (lmap f) a
-- | Consumes as much of the 'H.QueryText' as necessary in order to produce @o@.
-- Returns any leftovers in the original order.
queryDecode :: QueryCodec i o -> H.QueryText -> Either ErrQuery (o, H.QueryText)
queryDecode (QueryCodec af) = \xs0 -> do
let s0 = QueryFDecodeState{seen = mempty, input = xs0}
(o, s1) <- runStateT (runAp (StateT . queryFDecode) af) s0
pure (o, s1.input)
queryEncode :: QueryCodec i o -> i -> H.QueryText
queryEncode (QueryCodec af) = flip appEndo [] . runAp_ queryFEncode af
-- | Keys in the order they are encoded and decoded.
queryKeys :: QueryCodec i o -> [T.Text]
queryKeys (QueryCodec af) = appEndo (runAp_ queryFKeys af) []
--------------------------------------------------------------------------------
-- | Encode and decode the one the query string value for a particular key.
--
-- E.g., @?__key__=__value__&...@.
query
:: (ToQueryValue x, FromQueryValue o)
=> T.Text
-- ^ Query string key.
-> (i -> x)
-- ^ @'query' k f == 'lmap' f ('query' k 'id')@, provided just for
-- convenience.
-> QueryCodec i o
query k f =
QueryCodec $ liftAp $ QueryF k $ lmap f $ queryValuesOne queryValue
-- | Encode and decode zero or one query string values for a particular key.
--
-- E.g., @?__key__=__value__&...@, or just no mention of @__key__@ altogether.
queryMaybe
:: (ToQueryValue x, FromQueryValue o)
=> T.Text
-- ^ Query string key.
-> (i -> Maybe x)
-- ^ @'queryMaybe' k f == 'lmap' f ('queryMaybe' k 'id')@, provided just for
-- convenience.
-> QueryCodec i (Maybe o)
queryMaybe k f =
QueryCodec $ liftAp $ QueryF k $ lmap f $ queryValuesMaybe queryValue
-- | Encode and decode zero or more query string values for a particular key.
--
-- E.g., @?__key__=__value0__&__key__=__value1__&...@.
queryMany
:: (ToQueryValue x, FromQueryValue o)
=> T.Text
-- ^ Query string key.
-> (i -> [x])
-- ^ @'queryMany' k f == 'lmap' f ('queryMany' k 'id')@, provided just for
-- convenience.
-> QueryCodec i [o]
queryMany k f =
QueryCodec $ liftAp $ QueryF k $ lmap f $ queryValuesMany queryValue
-- | Encode and decode one or more query string values for a particular key.
--
-- E.g., @?__key__=__value0__&__key__=__value1__&...@.
querySome
:: (ToQueryValue x, FromQueryValue o)
=> T.Text
-- ^ Query string key.
-> (i -> NEL.NonEmpty x)
-- ^ @'querySome' k f == 'lmap' f ('querySome' k 'id')@, provided just for
-- convenience.
-> QueryCodec i (NEL.NonEmpty o)
querySome k f =
QueryCodec $ liftAp $ QueryF k $ lmap f $ queryValuesSome queryValue
--------------------------------------------------------------------------------
class ToQueryValue i where
-- | Encode @i@ into a query string value.
toQueryValue :: i -> T.Text
default toQueryValue :: (Show i) => i -> T.Text
{-# INLINE toQueryValue #-}
toQueryValue = T.pack . show
class FromQueryValue o where
-- | Decode from one query string value into @o@.
--
-- E.g., from @?__key__=__value__&...@.
fromQueryValue :: T.Text -> Maybe o
default fromQueryValue :: (Read o) => T.Text -> Maybe o
{-# INLINE fromQueryValue #-}
fromQueryValue = readMaybe . T.unpack