module Data.Lisp ( Lisp(..), lisp, encodeLisp, decodeLisp ) where import Prelude hiding (String, Bool) import qualified Prelude as P (String, Bool) import Data.Aeson (ToJSON(..), FromJSON(..), (.=)) import qualified Data.Aeson as A import Data.Aeson.Types (parseMaybe, parseEither) import Data.ByteString.Lazy (ByteString) import Data.Char (isAlpha, isDigit) import Data.Either (partitionEithers) import qualified Data.HashMap.Strict as HM import Data.List (unfoldr) import Data.Scientific import Data.String (fromString) import qualified Data.Text as T (unpack) import qualified Data.Text.Lazy as LT (pack, unpack) import qualified Data.Text.Lazy.Encoding as LT (encodeUtf8, decodeUtf8) import qualified Text.ParserCombinators.ReadP as R import Text.Read (readMaybe) import qualified Data.Vector as V data Lisp = Null | Bool P.Bool | Symbol P.String | String P.String | Number Scientific | List [Lisp] deriving (Eq) readable :: Read a => Int -> R.ReadP a readable = R.readS_to_P . readsPrec lisp :: Int -> R.ReadP Lisp lisp n = R.choice [ do s <- symbol return $ case s of "null" -> Null "true" -> Bool True "false" -> Bool False _ -> Symbol s, fmap String string, fmap Number number, fmap List list] where symbol :: R.ReadP P.String symbol = concat <$> sequence [ R.option [] (pure <$> R.char ':'), pure <$> R.satisfy isAlpha, R.munch (\ch -> isAlpha ch || isDigit ch || ch == '-')] string :: R.ReadP P.String string = (R.<++ R.pfail) $ do ('\"':_) <- R.look readable n number :: R.ReadP Scientific number = do s <- R.munch1 (\ch -> isDigit ch || ch `elem` ['e', 'E', '.', '+', '-']) maybe R.pfail return $ readMaybe s list :: R.ReadP [Lisp] list = R.between (R.char '(') (R.char ')') $ R.sepBy (lisp n) R.skipSpaces instance Read Lisp where readsPrec = R.readP_to_S . lisp instance Show Lisp where show Null = "null" show (Bool b) | b = "true" | otherwise = "false" show (Symbol s) = s show (String s) = show s show (Number n) = either show show (floatingOrInteger n :: Either Double Integer) show (List vs) = "(" ++ unwords (map show vs) ++ ")" instance ToJSON Lisp where toJSON Null = toJSON A.Null toJSON (Bool b) = toJSON b toJSON (Symbol s) = toJSON s toJSON (String s) = toJSON s toJSON (Number n) = toJSON n toJSON (List vs) | null keywords = toJSON $ map toJSON vals | null vals = keywordsObject | otherwise = toJSON $ map toJSON vals ++ [keywordsObject] where (vals, keywords) = partitionEithers $ unfoldr cutKeyword vs keywordsObject = A.object [fromString (dropColon k) .= v | (k, v) <- keywords] dropColon :: P.String -> P.String dropColon (':' : s) = s dropColon s = s cutKeyword :: [Lisp] -> Maybe (Either Lisp (P.String, Lisp), [Lisp]) cutKeyword [] = Nothing cutKeyword (Symbol s : []) = Just (Right (s, Null), []) cutKeyword (Symbol s : Symbol h : hs) = Just (Right (s, Null), Symbol h : hs) cutKeyword (Symbol s : h : hs) = Just (Right (s, h), hs) cutKeyword (h : hs) = Just (Left h, hs) instance FromJSON Lisp where parseJSON A.Null = return Null parseJSON (A.Bool b) = return $ Bool b parseJSON (A.String s) = return $ String $ T.unpack s parseJSON (A.Number n) = return $ Number n parseJSON (A.Array vs) = fmap List $ mapM parseJSON $ V.toList vs parseJSON (A.Object obj) = fmap (List . concat) $ mapM (\(k, v) -> sequence [pure $ Symbol (':' : T.unpack k), parseJSON v]) $ HM.toList obj decodeLisp :: FromJSON a => ByteString -> Either P.String a decodeLisp str = do sexp <- maybe (Left "Not a s-exp") Right . readMaybe . LT.unpack . LT.decodeUtf8 $ str parseEither parseJSON $ toJSON (sexp :: Lisp) encodeLisp :: ToJSON a => a -> ByteString encodeLisp r = LT.encodeUtf8 . LT.pack $ maybe "(:error \"can't convert to s-exp\")" (show :: Lisp -> P.String) (parseMaybe parseJSON (toJSON r))