packages feed

jsonsql-0.1.0.1: Main.hs

{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
module Main where
import Data.Aeson
import Data.Monoid
import qualified Data.Map.Strict as M
import Data.Text (Text)
import qualified Data.Text.Encoding as T (decodeUtf8)
import Data.List (intersperse)
import qualified Data.List 
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Text.Lazy.IO as TL
import Data.Maybe (catMaybes)
import Control.Applicative
import Control.Monad (when)
import Data.ByteString.Lazy as BL hiding (map, intersperse)
import qualified Data.ByteString.Lazy.Char8 as BS
import qualified Data.Attoparsec.Lazy as LA hiding (Result, parseOnly)
import Data.Attoparsec.ByteString.Char8 (endOfLine, sepBy)
import Data.Attoparsec.Text 
import qualified Data.HashMap.Lazy as HM
import qualified Data.Vector as V
import Data.Scientific 
import System.Environment (getArgs)
import qualified Data.Text.Lazy.Builder as B
import qualified Data.Text.Lazy.Builder.Int as B
import qualified Data.Text.Lazy.Builder.RealFloat as B
import qualified Options.Applicative as O
import Data.String.QQ
import Test.HUnit 

data Options = Options {  
      template :: Template
    } deriving Show

data Template = TemplateFile FilePath | TemplateText Text deriving (Show)

parseOpts :: O.Parser Options
parseOpts = Options <$> (tmplText <|> tmplFile)

tmplText = TemplateText . T.pack <$> O.argument O.str (O.metavar "TEMPLATE")
tmplFile = TemplateFile 
      <$> O.strOption (O.metavar "FILE" <> O.short 'f' <> O.help "Template file")

opts = O.info (O.helper <*> parseOpts)
          (O.fullDesc 
          <> O.progDesc "Inject JSON into SQL template strings" 
          <> O.header "jsonsql")

main = do
  Options tmpl <- O.execParser opts
  template <- case tmpl of
                  TemplateFile fp -> T.readFile fp
                  TemplateText t -> return t
  x <- BL.getContents 
  let vs :: [Value]
      vs = decodeStream x
      chunks :: [Chunk] 
      chunks = parseText template
      results = mconcat $ map (evalText chunks) vs
  TL.putStrLn . B.toLazyText $ results

decodeStream :: (FromJSON a) => BL.ByteString -> [a]
decodeStream bs = case decodeWith json bs of
    (Just x, xs) | xs == mempty -> [x]
    (Just x, xs) -> x:(decodeStream xs)
    (Nothing, _) -> []

decodeWith :: (FromJSON a) => LA.Parser Value -> BL.ByteString -> (Maybe a, BL.ByteString)
decodeWith p s =
    case LA.parse p s of
      LA.Done r v -> f v r
      LA.Fail _ _ _ -> (Nothing, mempty)
  where f v' r = (\x -> case x of 
                      Success a -> (Just a, r)
                      _ -> (Nothing, r)) $ fromJSON v'


data ArrayFormat = ArrayFormat {
    arrDelimiter :: Text
  , arrPrefixStr :: Text  -- can me mempty
  , arrPostFixStr :: Text
  } deriving (Show, Eq)

data Chunk = Pass Text | Expr KeyPath deriving (Show, Eq)
data KeyPath = KeyPath [Key] ArrayFormat deriving (Show, Eq) -- Text fields are array item prefix and postfix strings, which is given at end
data Key = Key Text | Index Int deriving (Eq, Show)

evalText :: [Chunk] -> Value -> B.Builder
evalText xs v = mconcat $ map (B.fromText . evalChunk v) xs

evalChunk :: Value -> Chunk -> Text
evalChunk v (Pass s) = s
evalChunk v (Expr k) = evalToText k v

evalToText :: KeyPath -> Value -> Text
evalToText k v = valToText $ evalKeyPath k v

evalToUnescapedText :: KeyPath -> Value -> Text
evalToUnescapedText k v = valToUnescapedText $ evalKeyPath k v

-- evaluates the a JS key path against a Value context to a leaf Value
evalKeyPath :: KeyPath -> Value -> Value
evalKeyPath (KeyPath [] _) x@(String _) = x
evalKeyPath (KeyPath [] _) x@Null = x
evalKeyPath (KeyPath [] _) x@(Number _) = x
evalKeyPath (KeyPath [] _) x@(Bool _) = x
evalKeyPath (KeyPath [] _) x@(Object _) = x
evalKeyPath (KeyPath (Key key:ks) a) (Object s) = 
    case (HM.lookup key s) of
        Just x          -> evalKeyPath (KeyPath ks a) x
        Nothing -> Null
evalKeyPath (KeyPath (Index idx:ks) a) (Array v) = 
      let e = (V.!?) v idx
      in case e of 
        Just e' -> evalKeyPath (KeyPath ks a) e'
        Nothing -> Null  
evalKeyPath k@(KeyPath _ ArrayFormat {..}) (Array v) = 
      let vs = V.toList v
          f =  (\v' -> escapeText $ arrPrefixStr <> evalToUnescapedText k v' <> arrPostFixStr)
          result = mconcat . intersperse arrDelimiter $ map f vs 
      in (String result)
evalKeyPath (KeyPath (Index _:_) _ ) _ = Null
evalKeyPath _ _ = Null

valToUnescapedText :: Value -> Text
valToUnescapedText (String x) = x
valToUnescapedText x = valToText x

escapeText = T.pack . escapeStringLiteral . T.unpack 

valToText :: Value -> Text
valToText (String x) = T.singleton '\'' 
    <> (escapeText x)
    <> T.singleton '\''
valToText Null = "NULL"
valToText (Bool True) = "TRUE"
valToText (Bool False) = "FALSE"
valToText (Number x) = 
    case floatingOrInteger x of
        Left float -> T.pack . show $ float
        Right int -> T.pack . show $ int
valToText x@(Object _) = error $ "Cannot interpolate " ++ show x

escapeStringLiteral :: String -> String
escapeStringLiteral ('\'':xs) = '\'': ('\'' : escapeStringLiteral xs)
escapeStringLiteral (x:xs) = x : escapeStringLiteral xs
escapeStringLiteral [] = []

parseText :: Text -> [Chunk]
parseText = either error id . parseOnly (many textChunk)

textChunk = exprChunk <|> passChunk

identifierChar = inClass "a-zA-Z_.[]0-9"

exprChunk :: Parser Chunk
exprChunk = do
    try (char ':')
    x <- notChar ':'
    xs <- takeWhile1 identifierChar
    arrFormat <- pArrayFormat
    let kp = parseKeyPath (T.singleton x <> xs) arrFormat
    return $ Expr kp

passChunk :: Parser Chunk
passChunk = Pass <$> takeWhile1 (notInClass ":")

parseKeyPath :: Text -> ArrayFormat -> KeyPath
parseKeyPath s a = case parseOnly pKeys s of
    Left err -> error $ "Error parsing key path: " ++ T.unpack s ++ " error: " ++ err 
    Right res -> KeyPath res a

pKeys :: Parser [Key]
pKeys = do
    keys <- sepBy1 pKeyOrIndex (takeWhile1 $ inClass ".[") 
    return keys 

-- | syntax is {delimiter-string!prefix-string!postfix-string}
-- immediately after last key
-- e.g. {,!!}
pArrayFormat :: Parser ArrayFormat
pArrayFormat = do 
  try (do
       char '{'
       delimiter <- T.pack <$> manyTill anyChar (char '!')
       pre <- T.pack <$> manyTill anyChar (char '!')
       post <- T.pack <$> manyTill anyChar (char '}')
       return $ ArrayFormat delimiter pre post)
  <|> pure defArrayFormat

defArrayFormat = ArrayFormat "," "" ""

pKeyOrIndex = pIndex <|> pKey

pKey = Key <$> takeWhile1 (notInClass " .[")

pIndex = Index <$> decimal <* char ']'

------------------------------------------------------------------------
-- Tests

runTests = runTestTT tests

tests = test [
    "testOne"          
        ~: []
        @=?   parseText "VALUES (:title, :year, :ratings.imdb)"
  , "test complex key"
        ~: []
        @=? parseText "values (:imdb, :versions.Rental.HD[0]);"
  ]