packages feed

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