jsonsql 0.1.0.1 → 0.1.2.0
raw patch · 5 files changed
+92/−53 lines, 5 files
Files
- Main.hs +45/−30
- README.md +16/−22
- jsonsql.cabal +1/−1
- stack.yaml +6/−0
- test.json +24/−0
Main.hs view
@@ -18,6 +18,7 @@ import qualified Data.Attoparsec.Lazy as LA hiding (Result, parseOnly) import Data.Attoparsec.ByteString.Char8 (endOfLine, sepBy) import Data.Attoparsec.Text +import qualified Data.Attoparsec.Text as AttoT import qualified Data.HashMap.Lazy as HM import qualified Data.Vector as V import Data.Scientific @@ -58,8 +59,9 @@ chunks :: [Chunk] chunks = parseText template results = mconcat $ map (evalText chunks) vs- TL.putStrLn . B.toLazyText $ results+ TL.putStr . B.toLazyText $ results + decodeStream :: (FromJSON a) => BL.ByteString -> [a] decodeStream bs = case decodeWith json bs of (Just x, xs) | xs == mempty -> [x]@@ -76,18 +78,17 @@ _ -> (Nothing, r)) $ fromJSON v' -data ArrayFormat = ArrayFormat {- arrDelimiter :: Text- , arrPrefixStr :: Text -- can me mempty- , arrPostFixStr :: Text- } deriving (Show, Eq)+data ArrayFormat = ArrayFormat 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)+data KeyPath = KeyPath [Key] (Maybe ArrayFormat) + deriving (Show, Eq) +data Key = Key Text | Index Int | RootKey deriving (Eq, Show) evalText :: [Chunk] -> Value -> B.Builder-evalText xs v = mconcat $ map (B.fromText . evalChunk v) xs+evalText xs v = + let line = mconcat $ map (B.fromText . evalChunk v) xs+ in line <> B.fromText "\n" evalChunk :: Value -> Chunk -> Text evalChunk v (Pass s) = s@@ -101,11 +102,12 @@ -- evaluates the a JS key path against a Value context to a leaf Value evalKeyPath :: KeyPath -> Value -> Value+evalKeyPath (KeyPath [RootKey] _) x = x -- print the root object 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 [] _) x@(Object _) = x -- print object evalKeyPath (KeyPath (Key key:ks) a) (Object s) = case (HM.lookup key s) of Just x -> evalKeyPath (KeyPath ks a) x@@ -115,11 +117,14 @@ in case e of Just e' -> evalKeyPath (KeyPath ks a) e' Nothing -> Null -evalKeyPath k@(KeyPath _ ArrayFormat {..}) (Array v) = +evalKeyPath (KeyPath [] Nothing) x@(Array _) = x -- print literal object+evalKeyPath (KeyPath [] (Just (ArrayFormat a))) (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)+ in String $ mconcat . intersperse a $ map valToUnescapedText $ vs +evalKeyPath k@(KeyPath _ a) (Array v) = + let vs = V.toList v+ f = evalKeyPath k+ in evalKeyPath (KeyPath [] a) (Array $ V.fromList $ map f vs) evalKeyPath (KeyPath (Index _:_) _ ) _ = Null evalKeyPath _ _ = Null @@ -140,13 +145,20 @@ case floatingOrInteger x of Left float -> T.pack . show $ float Right int -> T.pack . show $ int-valToText x@(Object _) = error $ "Cannot interpolate " ++ show x+valToText x@(Object _) = literalJSON x+valToText x@(Array _) = literalJSON x escapeStringLiteral :: String -> String escapeStringLiteral ('\'':xs) = '\'': ('\'' : escapeStringLiteral xs) escapeStringLiteral (x:xs) = x : escapeStringLiteral xs escapeStringLiteral [] = [] ++literalJSON :: Value -> Text+literalJSON x = + let t = T.decodeUtf8 . Prelude.head . BL.toChunks. encode $ x+ in valToText . String $ t+ parseText :: Text -> [Chunk] parseText = either error id . parseOnly (many textChunk) @@ -158,7 +170,7 @@ exprChunk = do try (char ':') x <- notChar ':'- xs <- takeWhile1 identifierChar+ xs <- AttoT.takeWhile identifierChar arrFormat <- pArrayFormat let kp = parseKeyPath (T.singleton x <> xs) arrFormat return $ Expr kp@@ -166,30 +178,33 @@ passChunk :: Parser Chunk passChunk = Pass <$> takeWhile1 (notInClass ":") -parseKeyPath :: Text -> ArrayFormat -> KeyPath+parseKeyPath :: Text -> Maybe 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 ".[") + -- if the first character is '.', then it's an empty keypath+ -- which evaluates to the whole objject+ keys <- rootKey <|> (sepBy1 pKeyOrIndex (takeWhile1 $ inClass ".[")) return keys --- | syntax is {delimiter-string!prefix-string!postfix-string}--- immediately after last key--- e.g. {,!!}-pArrayFormat :: Parser ArrayFormat+rootKey :: Parser [Key]+rootKey = char '.' >> return [RootKey]++-- | syntax is [delimiter-string] immediately after last key++pArrayFormat :: Parser (Maybe ArrayFormat) pArrayFormat = do + let leftDelim = '<'+ rightDelim = '>' 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 "," "" ""+ char leftDelim+ delimiter <- T.pack <$> manyTill anyChar (char rightDelim)+ -- char rightDelim+ return . Just $ ArrayFormat delimiter)+ <|> pure Nothing pKeyOrIndex = pIndex <|> pKey
README.md view
@@ -63,12 +63,12 @@ ## Array joining -If a key path evaluates to an array of values, the values are converted-into strings, joined by a delimiter, and then output as a string. The-default delimiter is a comma:+If a key path evaluates to an array of values, specify a `<delimiter>` to have+the the values converted into strings, joined by the delimiter, and then+output as a string. ```-INSERT into titles (title, year, rating, stars, created) +INSERT into titles (title, year, rating, stars<,>, created) VALUES (:title, :year, :ratings.imdb, :stars.name, DEFAULT); ``` @@ -79,35 +79,29 @@ VALUES ('Interstellar', 2014, 8.9, 'Matthew McConaughey,Anne Hathaway', DEFAULT); ``` -A key path that terminates in an array can be followed by an array formatting expression:--```-{delimiter-string!prefix-sring!postfix-string}-```+## JSON literal interpolation +If a key path terminates in a JSON object or array (without a array delimiter+specified), a JSON literal will be interpolated: template: ```-INSERT into titles (title, year, rating, stars, created) -VALUES (:title, :year, :ratings.imdb, :stars.name{;!$!$}, DEFAULT);+INSERT into titles (title, stars) +VALUES (:title, :stars); ``` output: ```-INSERT into titles (title, year, rating, stars, created)-VALUES ('Terminator 2: ''Judgment Day''', 1991, 8.5, '$Arnold Schwarzenegger$;$Linda Hamilton$', DEFAULT);-INSERT into titles (title, year, rating, stars, created)-VALUES ('Interstellar', 2014, 8.9, '$Matthew McConaughey$;$Anne Hathaway$', DEFAULT);+INSERT into titles (title, stars)+VALUES ('Terminator 2: ''Judgment Day''', '[{"name":"Arnold Schwarzenegger"},{"name":"Linda Hamilton"}]');+INSERT into titles (title, stars) +VALUES ('Interstellar', '[{"name":"Matthew McConaughey"},{"name":"Anne Hathaway"}]'); ``` -Adding a prefix and postfix may be useful if you want to mark strings-for downstream pipeline processing with tools like `sed` before reaching-the database.+To interpolate the base object as a literal JSON string, use `:.` as the keypath. -The usefulness of this feature may be obscure. But the author needed it to-change an array of strings like `["apple","banana","pear"]` into a string field-containing a series of integer IDs like `'1,2,3'`. This type of field was then -indexed by the Sphinx search engine in a multi-valued attribute.+Do NOT put quotes around the placeholder for a literal JSON interpolation.+ ## Author
jsonsql.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: jsonsql-version: 0.1.0.1+version: 0.1.2.0 synopsis: Interpolate JSON object values into SQL strings -- description: homepage: https://github.com/danchoi/jsonsql
+ stack.yaml view
@@ -0,0 +1,6 @@+flags: {}+packages:+- '.'+extra-deps: +- string-qq-0.0.2+resolver: lts-2.16
+ test.json view
@@ -0,0 +1,24 @@+{+ "title": "Terminator 2: 'Judgment Day'",+ "year": 1991,+ "stars": [+ {"name": "Arnold Schwarzenegger"},+ {"name": "Linda Hamilton"}+ ],+ "ratings": {+ "imdb": 8.5+ },+ "created": "2014-12-04T10:10:10Z"+ +}+{+ "title": "Interstellar",+ "year": 2014,+ "stars": [+ {"name":"Matthew McConaughey"},+ {"name":"Anne Hathaway"}+ ],+ "ratings": {+ "imdb": 8.9+ }+}