calamity-0.1.9.0: src/Calamity/Commands/Parser.hs
-- | Something that can parse user input
module Calamity.Commands.Parser
( Parser(..)
, KleeneConcat ) where
import Calamity.Cache.Eff
import Calamity.Commands.Context
import Calamity.Internal.Utils
import Calamity.Types.Model.Channel ( Channel )
import Calamity.Types.Model.Guild ( Emoji, Member, Role )
import Calamity.Types.Model.User ( User )
import Calamity.Types.Snowflake
import Control.Lens hiding ( Context )
import Control.Monad
import Data.Bifunctor
import Data.Char ( isSpace )
import Data.Kind
import Data.List.NonEmpty ( NonEmpty, nonEmpty )
import qualified Data.Text.Lazy as L
import Data.Text.Lazy ( Text )
import Data.Typeable
import qualified Polysemy as P
import Text.Megaparsec hiding ( parse )
import Text.Megaparsec.Char
import TextShow
class Parser (a :: Type) r where
type ParserResult a
type ParserResult a = a
parse :: (Context, Text) -> P.Sem r (Either Text (ParserResult a, Text))
instance Parser Text r where
parse (_ctx, msg) = pure $ runParserToCommandError item msg
instance Parser a r => Parser [a] r where
type ParserResult [a] = [ParserResult a]
parse (ctx, msg) = Right <$> go msg []
where
go :: Text -> [ParserResult a] -> P.Sem r ([ParserResult a], Text)
go t l = parse @a (ctx, t) >>= \case
Left _ -> pure (l, t)
Right (v, t') -> go t' (l <> [v])
instance (Parser a r, Typeable a) => Parser (NonEmpty a) r where
type ParserResult (NonEmpty a) = NonEmpty (ParserResult a)
parse (ctx, msg) = parse @[a] (ctx, msg)
<&> (\case
Right (res, rest) -> case nonEmpty res of
Just res' -> Right (res', rest)
Nothing -> Left ("Couldn't parse at least one of " <> (L.pack . show . typeRep $ Proxy @a))
Left e -> Left e)
data KleeneConcat a
instance (Monoid (ParserResult a), Parser a r) => Parser (KleeneConcat a) r where
type ParserResult (KleeneConcat a) = ParserResult a
parse (ctx, msg) = (first mconcat) <<$>> parse @[a] (ctx, msg)
instance {-# OVERLAPS #-}Parser (KleeneConcat Text) r where
type ParserResult (KleeneConcat Text) = ParserResult Text
-- consume rest on text just takes everything remaining
parse (_ctx, msg) = pure $ runParserToCommandError (someSingle) msg
instance Parser (Snowflake a) r where
parse (_ctx, msg) = pure $ runParserToCommandError snowflake msg
instance {-# OVERLAPS #-}Parser (Snowflake User) r where
parse (_ctx, msg) = pure $ runParserToCommandError (try (ping "@") <|> snowflake) msg
instance {-# OVERLAPS #-}Parser (Snowflake Member) r where
parse (_ctx, msg) = pure $ runParserToCommandError (try (ping "@") <|> snowflake) msg
instance {-# OVERLAPS #-}Parser (Snowflake Channel) r where
parse (_ctx, msg) = pure $ runParserToCommandError (try (ping "#") <|> snowflake) msg
instance {-# OVERLAPS #-}Parser (Snowflake Role) r where
parse (_ctx, msg) = pure $ runParserToCommandError (try (ping "@&") <|> snowflake) msg
instance {-# OVERLAPS #-}Parser (Snowflake Emoji) r where
parse (_ctx, msg) = pure $runParserToCommandError
(try emoji <|> snowflake) msg
instance Parser Member r where
parse (ctx, msg) = parse @(Snowflake Member) (ctx, msg)
<&> (>>= \(mid, rest) -> case ctx ^? #guild . _Just . #members . ix mid of
Just member -> Right (member, rest)
_ -> Left ("Couldn't find member with id: " <> showtl mid))
instance P.Member CacheEff r => Parser User r where
parse (ctx, msg) = do
r <- parse @(Snowflake User) (ctx, msg)
case r of
Right (uid, rest) -> getUser uid <&> \case
Just member -> Right (member, rest)
_ -> Left ("Couldn't find user with id: " <> showtl uid)
Left e -> pure $ Left e
instance ShowErrorComponent Text where
showErrorComponent = L.unpack
errorComponentLen = fromIntegral . L.length
runParserToCommandError :: Parsec Text Text a -> Text -> Either Text (a, Text)
runParserToCommandError m t = case runParser (space *> andRemaining m) "" t of
Right a -> Right a
Left s -> Left . L.pack . errorBundlePretty $ s
ping :: MonadParsec e Text m => Text -> m (Snowflake a)
ping c = chunk ("<" <> c) *> optional (chunk "!") *> snowflake <* chunk ">"
ping' :: MonadParsec e Text m => m () -> m (Snowflake a)
ping' m = chunk "<" *> m *> snowflake <* chunk ">"
snowflake :: MonadParsec e Text m => m (Snowflake a)
snowflake = (Snowflake . read) <$> some digitChar
emoji :: MonadParsec e Text m => m (Snowflake a)
emoji = ping' (optional (chunk "a") *> between (chunk ":") (chunk ":") (void $ takeWhileP Nothing $ not . (== ':')))
andRemaining :: MonadParsec e s m => m a -> m (a, Tokens s)
andRemaining m = do
a <- m
rest <- manySingle
pure (a, rest)
item :: MonadParsec e Text m => m Text
item = try quotedString <|> someNonWS
manySingle :: MonadParsec e s m => m (Tokens s)
manySingle = takeWhileP (Just "Any character") (const True)
someSingle :: MonadParsec e s m => m (Tokens s)
someSingle = takeWhile1P (Just "Any character") (const True)
quotedString :: MonadParsec e Text m => m Text
quotedString = try (between (chunk "'") (chunk "'") (takeWhileP Nothing $ not . (== '\''))) <|>
between (chunk "\"") (chunk "\"") (takeWhileP Nothing $ not . (== '"'))
-- manyNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
-- manyNonWS = takeWhileP (Just "Any Non-Whitespace") (not . isSpace)
someNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
someNonWS = takeWhile1P (Just "Any Non-Whitespace") (not . isSpace)