waypoint-0.1: lib/Waypoint/Path.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 'PathCodec'\'s underlying primitives.
-- Just import "Waypoint" instead.
module Waypoint.Path
( -- * PathValue
PathValue (..)
, ToPathValue (..)
, FromPathValue (..)
-- * PathF
, PathF (..)
, pathFDecode
, PathFDecodeState (..)
, pathFEncode
-- * PathCodec
, PathCodec (..)
, pathDecode
, pathEncode
, path
, pathLiteral
-- * ErrPath
, ErrPath (..)
)
where
import Control.Applicative
import Control.Applicative.Free.Fast
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.Trans.State.Strict
import Data.Monoid (Endo (..))
import Data.Profunctor
import Data.Text qualified as T
import Text.Read (readMaybe)
import Witherable qualified as W
import Prelude
--------------------------------------------------------------------------------
-- | How to encode and decode a single path segment.
-- (e.g., the @hello@ in @\/hello\/world@.
data PathValue i o = PathValue
{ encode :: i -> T.Text
, decode :: T.Text -> Maybe o
}
instance W.Filterable (PathValue i) where
mapMaybe f (PathValue i o) = PathValue i (o >=> f)
instance Functor (PathValue i) where
fmap = rmap
instance Profunctor PathValue where
dimap f g (PathValue i o) = PathValue (i . f) (fmap g . o)
-- | 'PathValue' based on 'ToPathValue' and 'FromPathValue'.
pathValue :: (ToPathValue i, FromPathValue o) => PathValue i o
pathValue = PathValue toPathValue fromPathValue
--------------------------------------------------------------------------------
-- | This datatype seems useless, seeing as we could use 'PathValue' directly.
-- But naming things is hard, so we add this name for consistency with 'QueryF'
-- and 'HeaderF'. Also, we will add metadata here someday.
newtype PathF i o = PathF
{ segment :: PathValue i o
}
deriving newtype (Functor, Profunctor, W.Filterable)
-- | See 'pathFDecode'.
data PathFDecodeState = PathFDecodeState
{ index :: Int
-- ^ Next index.
, input :: [T.Text]
-- ^ 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 'PathF' yourself.
pathFDecode
:: PathF x a -> PathFDecodeState -> Either ErrPath (a, PathFDecodeState)
pathFDecode (PathF ps) = \s ->
case s.input of
t : ts1
| Just a <- ps.decode t ->
Right (a, s{index = s.index + 1, input = ts1})
_ -> Left $ ErrPath s.index
-- | You probably don't need to use this unless you are building an
-- 'Applicative' on top of 'PathF' yourself.
pathFEncode :: PathF i o -> i -> Endo [T.Text]
pathFEncode p = \i -> Endo (p.segment.encode i :)
--------------------------------------------------------------------------------
-- | Bidirectional codec for encoding an @i@ into URL path segments, and
-- decoding URL path segments into an @o@.
--
-- Use 'path', 'pathLiteral' and 'Applicative' to construct.
newtype PathCodec i o = PathCodec (Ap (PathF i) o)
deriving newtype (Functor, Applicative)
instance Profunctor PathCodec where
rmap = fmap
lmap f (PathCodec a) = PathCodec $ hoistAp (lmap f) a
pathEncode :: PathCodec i o -> i -> [T.Text]
pathEncode (PathCodec af) = flip appEndo [] . runAp_ pathFEncode af
-- | Decodes the given path segments into @o@, if possible.
--
-- Parses from left to right, returning leftover segments, if any.
pathDecode :: PathCodec i o -> [T.Text] -> Either ErrPath (o, [T.Text])
pathDecode = \(PathCodec af) -> \ts0 -> do
let s0 = PathFDecodeState{index = 0, input = ts0}
(o, s1) <- runStateT (runAp (StateT . pathFDecode) af) s0
pure (o, s1.input)
data ErrPath
= -- | Error parsing the path element at the given index.
ErrPath Int
deriving stock (Eq, Show)
instance Exception ErrPath
--------------------------------------------------------------------------------
-- | Encode and decode a path segment.
path
:: (ToPathValue x, FromPathValue o)
=> (i -> x)
-- ^ @'path' f == 'lmap' f ('path' 'id')@, provided just for convenience.
-> PathCodec i o
path f = PathCodec $ liftAp $ PathF $ lmap f pathValue
-- | Literal segment in a URL path.
pathLiteral :: T.Text -> PathCodec i ()
pathLiteral t =
PathCodec $ liftAp $ PathF $ PathValue (const t) \x -> guard (x == t)
--------------------------------------------------------------------------------
class ToPathValue i where
-- | Render @i@ into a single path segment.
toPathValue :: i -> T.Text
default toPathValue :: (Show i) => i -> T.Text
{-# INLINE toPathValue #-}
toPathValue = T.pack . show
class FromPathValue o where
-- | Parse a single path segment into an @o@.
fromPathValue :: T.Text -> Maybe o
default fromPathValue :: (Read o) => T.Text -> Maybe o
{-# INLINE fromPathValue #-}
fromPathValue = readMaybe . T.unpack