packages feed

hyperbole-0.7.0: src/Web/Hyperbole/Data/Argument.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}

module Web.Hyperbole.Data.Argument where

import Control.Applicative ((<|>))
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..))
import Data.Aeson qualified as A
import Data.Aeson.KeyMap (KeyMap, (!?))
import Data.Aeson.Parser (json)
import Data.Attoparsec.ByteString qualified as AB
import Data.Attoparsec.ByteString qualified as Atto
import Data.Attoparsec.ByteString.Char8 (sepBy)
import Data.Attoparsec.ByteString.Char8 qualified as AC
import Data.Bifunctor (first)
import Data.Char (isAlphaNum, isUpper)
import Data.Maybe (fromMaybe)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Exts (IsList (..))
import GHC.Generics


data Argument
  = JSON Value
  deriving (Show, Eq, Ord, Generic)


-------------------------------------------------------------------------------
-- ARGUMENT ENCODING
-------------------------------------------------------------------------------

toArgument :: (ToJSON a) => a -> Argument
toArgument = JSON . toJSON


parseArgument :: forall a. (FromJSON a) => Argument -> Either String a
parseArgument (JSON v) = do
  case A.fromJSON v of
    A.Success a -> pure a
    A.Error e -> Left e


-- | Parses both json and our custom constructor format
argumentParser :: Atto.Parser Argument
argumentParser =
  JSON <$> (json <|> constructor <|> innerProduct)
 where
  innerProduct = do
    _ <- AC.char '('
    tag <- AC.takeWhile1 isAlphaNum
    contents :: KeyMap Value <- innerContents <|> pure []
    _ <- AC.char ')'
    pure $ Object $ [("tag", String $ cs tag)] <> contents

  innerContents = do
    _ <- AC.char ' '
    args <- json `sepBy` AC.space
    pure $ case args of
      [] -> []
      [a] -> [("contents", a)]
      as -> [("contents", Array $ fromList as)]

  constructor = do
    s <- AC.takeWhile1 isAlphaNum
    pure $ String (cs s)


-- decode a single argument
decodeToArgument :: Text -> Either String Argument
decodeToArgument inp =
  first (\e -> "decodeArgument: " <> cs inp <> " failed with error: " <> e) $ AB.parseOnly argumentParser (cs inp)


decodeArgument :: (FromJSON a) => Text -> Either String a
decodeArgument inp = do
  decodeToArgument inp >>= parseArgument


encodeArgument :: (ToJSON a) => a -> Text
encodeArgument a = encodeFromArgument $ toArgument a


-- | JSON Encode argument, but alter it slightly to use raw strings for constructors without parameters, and switch to two-element sum encoding
encodeFromArgument :: Argument -> Text
encodeFromArgument (JSON v) = encodeJSON v


-- | Encodes as JSON for simple values, but forces constructors into our custom show-like format
encodeJSON :: Value -> Text
encodeJSON = \case
  (String s) ->
    if isConstructorName s
      then s
      else encodeValue $ String s
  (Object km) ->
    simplifySumEncoding $ Object km
  v -> encodeValue v
 where
  encodeValue :: Value -> Text
  encodeValue = cs . A.encode

  isConstructorName :: Text -> Bool
  isConstructorName "" = False
  isConstructorName t =
    isUpper (T.head t) && T.all isAlphaNum t

  simplifySumEncoding :: Value -> Text
  simplifySumEncoding (Object o) = fromMaybe (encodeValue $ Object o) $ do
    innerProduct o <|> simpleTag o
  simplifySumEncoding v = encodeValue v

  innerProduct :: KeyMap Value -> Maybe Text
  innerProduct o = do
    String t <- o !? "tag"
    c <- o !? "contents"
    let contents = case c of
          Array a -> T.intercalate " " $ toList $ fmap encodeValue a
          other -> encodeValue other
    pure $ "(" <> t <> " " <> contents <> ")"

  simpleTag :: KeyMap Value -> Maybe Text
  simpleTag o = do
    t <- o !? "tag"
    case t of
      String s -> pure $ "(" <> s <> ")"
      v -> pure $ encodeValue v