hyperbole-0.6.0: src/Web/Hyperbole/Data/Encoded.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
module Web.Hyperbole.Data.Encoded where
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..))
import Data.Aeson qualified as A
import Data.Attoparsec.ByteString qualified as AB
import Data.Attoparsec.ByteString qualified as Atto
import Data.Attoparsec.ByteString.Char8 (isSpace, sepBy, takeWhile1)
import Data.Attoparsec.ByteString.Char8 qualified as AC
import Data.Bifunctor (first)
import Data.String (IsString)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics
import Web.Hyperbole.Data.Param
newtype ConName = ConName {text :: Text}
deriving newtype (Eq, Show, IsString, Ord)
instance Semigroup ConName where
-- Ignore the second constructor name
c1 <> _ = c1
instance Monoid ConName where
mempty = ConName ""
{- | Pretty Human Readable top-levelencoding for ViewAction and ViewId
For simple Sum and Product types it is equivalent to the Show/Read instance
MyConstructor 1 2 3
-}
data Encoded = Encoded ConName [ParamValue]
deriving (Show, Eq, Ord)
instance Semigroup Encoded where
Encoded c1 es1 <> Encoded c2 es2 =
Encoded (c1 <> c2) (es1 <> es2)
instance Monoid Encoded where
mempty = Encoded mempty mempty
instance ToJSON Encoded where
toJSON e = toJSON $ encode e
instance FromJSON Encoded where
parseJSON (String t) =
case decodeEither t of
Left e -> fail $ "Encoded " <> cs e
Right a -> pure a
parseJSON val = fail $ "Expected Encoded but got: " <> show val
encode :: (ToEncoded a) => a -> Text
encode a = encodedToText $ toEncoded a
decode :: (FromEncoded a) => Text -> Maybe a
decode t = either (const Nothing) Just $ decodeEither t
decodeEither :: (FromEncoded a) => Text -> Either String a
decodeEither t = do
enc <- encodedParseText t
parseEncoded enc
-- | Basic Encoding
encodedToText :: Encoded -> Text
encodedToText (Encoded con values) =
T.intercalate " " (con.text : fmap encodeParam values)
encodedParseText :: Text -> Either String Encoded
encodedParseText inp =
first cs $ AB.parseOnly encodedParser (cs inp)
where
encodedParser :: AB.Parser Encoded
encodedParser = do
con <- AC.takeTill AC.isSpace
AC.skipSpace
ps <- paramParser `sepBy` AC.char ' '
pure $ Encoded (ConName (cs con)) ps
genericToEncoded :: (Generic a, GToEncoded (Rep a)) => a -> Encoded
genericToEncoded a = gToEncoded (from a)
genericParseEncoded :: (Generic a, GFromEncoded (Rep a)) => Encoded -> Either String a
genericParseEncoded enc = do
(gen, _) <- gParseEncoded enc
pure $ to gen
genericDecode :: (Generic a, GFromEncoded (Rep a)) => Text -> Maybe a
genericDecode t = either (const Nothing) Just $ do
enc <- encodedParseText t
genericParseEncoded enc
-- | Custom Encoding for embedding into web documents. Noteably used for 'ViewId' and 'ViewAction'
class ToEncoded a where
toEncoded :: a -> Encoded
default toEncoded :: (Generic a, GToEncoded (Rep a)) => a -> Encoded
toEncoded = genericToEncoded
instance ToEncoded Encoded where
toEncoded = id
instance ToEncoded () where
toEncoded _ = mempty
instance ToEncoded ParamValue where
toEncoded p = Encoded mempty [toParam p]
instance ToEncoded Int where
toEncoded = toEncoded . toParam
instance ToEncoded Text where
toEncoded = toEncoded . toParam
-- | Custom Encoding for embedding into web documents. Noteably used for 'ViewId' and 'ViewAction'
class FromEncoded a where
parseEncoded :: Encoded -> Either String a
default parseEncoded :: (Generic a, GFromEncoded (Rep a)) => Encoded -> Either String a
parseEncoded = genericParseEncoded
instance FromEncoded Encoded where
parseEncoded = pure
instance FromEncoded () where
parseEncoded _ = pure ()
instance FromEncoded ParamValue where
parseEncoded (Encoded _ ps) = do
case ps of
[p] -> parseParam p
_ -> Left $ "Expected single param value [param] but got: " <> show ps
instance FromEncoded Int where
parseEncoded enc = parseEncoded enc >>= parseParam
instance FromEncoded Text where
parseEncoded enc = parseEncoded enc >>= parseParam
fromResult :: A.Result a -> Either String a
fromResult (A.Success a) = pure a
fromResult (A.Error e) = Left (cs e)
-------------------------------------------------------------------------------
-- PARAM ENCODING
-------------------------------------------------------------------------------
-- Params need to be sanitized and escaped, because we want to use spaces to separate our params
-- Data.Param by default does not sanitize spaces
paramParser :: Atto.Parser ParamValue
paramParser = do
t <- takeWhile1 (not . isSpace)
pure $ decodeParam $ cs t
decodeParam :: Text -> ParamValue
decodeParam = \case
"|" -> ParamValue ""
t -> ParamValue $ desanitizeParamText t
-- replace all underscores that are NOT "\\_" with spaces
desanitizeParamText :: Text -> Text
desanitizeParamText =
T.replace "\\ " "_" . T.replace "_" " " . T.replace "\\n" "\n"
-- | T.isSuffixOf "\\" seg = T.dropEnd 1 seg <> "_" <> txt
-- | otherwise = seg <> " " <> txt
-- foldr join "" $
-- where
--
-- join "" "" = " "
-- join "" " " = " "
-- join seg "" = seg
-- join seg txt
-- | T.isSuffixOf "\\" seg = T.dropEnd 1 seg <> "_" <> txt
-- | otherwise = seg <> " " <> txt
encodeParam :: ParamValue -> Text
encodeParam (ParamValue t) =
case t of
"" -> "|"
_ -> sanitizeParamText t
where
-- Q: Should we also sanitize \r\n?
sanitizeParamText :: Text -> Text
sanitizeParamText =
T.replace " " "_" . T.replace "_" "\\_" . T.replace "\n" "\\n"
-- decodeParamValue :: (FromParam a) => Text -> Either String a
-- decodeParamValue = parseParam . decodeParam
-- decodeParam :: Text -> ParamValue
-- decodeParam inp = do
-- case A.eitherDecode (cs inp) of
-- Left _ -> paramFromText inp
-- Right v -> ParamValue inp v
-------------------------------------------------------------------------------
-- GENERICS
-------------------------------------------------------------------------------
-- GToEncoded: Generic ViewAction Encoding
class GToEncoded f where
gToEncoded :: f p -> Encoded
instance (GToEncoded f, GToEncoded g) => GToEncoded (f :+: g) where
gToEncoded (L1 f) = gToEncoded f
gToEncoded (R1 f) = gToEncoded f
instance (GToEncoded f, GToEncoded g) => GToEncoded (f :*: g) where
gToEncoded (f :*: g) =
gToEncoded f <> gToEncoded g
instance GToEncoded U1 where
-- WARNING: not sure if this will work
gToEncoded U1 = mempty
instance (GToEncoded f) => GToEncoded (M1 D d f) where
gToEncoded (M1 f) = gToEncoded f
instance (Constructor c, GToEncoded f) => GToEncoded (M1 C c f) where
gToEncoded (M1 f) =
let con = cs (conName (undefined :: M1 C c f p))
in Encoded (ConName con) mempty <> gToEncoded f
instance (GToEncoded f) => GToEncoded (M1 S s f) where
gToEncoded (M1 f) = gToEncoded f
instance (ToParam a) => GToEncoded (K1 R a) where
gToEncoded (K1 a) = Encoded mempty [toParam a]
-- GFromEncoded: Generic ViewAction Decoding
class GFromEncoded f where
gParseEncoded :: Encoded -> Either String (f p, [ParamValue])
instance (GFromEncoded f, GFromEncoded g) => GFromEncoded (f :+: g) where
gParseEncoded enc@(Encoded con vals) = do
let el = gParseEncoded @f enc
let er = gParseEncoded @g enc
case (el, er) of
(Right (l, lvals), _) -> pure (L1 l, lvals)
(_, Right (r, rvals)) -> pure (R1 r, rvals)
(Left _, Left _) ->
Left $ "No matching sum constructor: " <> cs con.text <> " " <> cs (show vals)
instance (GFromEncoded f, GFromEncoded g) => GFromEncoded (f :*: g) where
gParseEncoded (Encoded con vals) = do
(a, rest) <- gParseEncoded @f (Encoded con vals)
(b, gone) <- gParseEncoded @g (Encoded con rest)
pure (a :*: b, gone)
instance GFromEncoded U1 where
gParseEncoded (Encoded _ vals) = pure (U1, vals)
instance (GFromEncoded f) => GFromEncoded (M1 D d f) where
gParseEncoded enc = do
first M1 <$> gParseEncoded enc
instance (Constructor c, GFromEncoded f) => GFromEncoded (M1 C c f) where
gParseEncoded enc@(Encoded cname _) = do
if cs cname.text == con
then first M1 <$> gParseEncoded @f enc
else Left $ "Mismatched Constructor " <> cs cname.text <> " /= " <> con
where
con = cs $ conName (undefined :: M1 C c f p)
instance (GFromEncoded f) => GFromEncoded (M1 S s f) where
gParseEncoded enc = do
(a, rest) <- gParseEncoded enc
pure (M1 a, rest)
instance (FromParam a) => GFromEncoded (K1 R a) where
gParseEncoded (Encoded con vals) = do
case vals of
(param : rest) -> do
case parseParam param of
-- consume one param
Right a -> pure (K1 a, rest)
Left e -> Left (cs e)
[] -> Left $ "Missing parameters for Encoded Constructor:" <> cs con.text