packages feed

json-tools-0.5.0: jq.hs

{-# LANGUAGE PatternGuards, OverloadedStrings #-}
import Control.Applicative as A
import Prelude hiding (filter)
import Data.Maybe
import Data.Char
import Data.Monoid
import Data.Aeson
import Data.Aeson.Parser (jstring, value)
import qualified Data.HashMap.Strict as H
import Data.HashMap.Strict (HashMap)
import qualified Data.Text as T
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.Vector as V
import Data.Vector (Vector)
import qualified Data.HashSet as S
import Data.Attoparsec.Char8 hiding (Result, parse)
import qualified Data.Attoparsec.Lazy as L
import System.Environment

type ValueOp = Value -> Value
type ValueBinOp = Value -> Value -> Value
type Filter = [Value] -> [Value]
type Obj = HashMap Text

err2 :: Value -> Value -> String -> a
err2 x y msg = error . unwords $ [show x, "and", show y, msg]

vecDiff :: Vector Value -> Vector Value -> Vector Value
x `vecDiff` y = V.filter p x
  where p = not . (`S.member`s)
        s = S.fromList (V.toList y)

(+|), (-|), (/|), ( *| ) :: ValueBinOp

Number x +| Number y = Number (x + y)
String x +| String y = String (x <> y)
Array  x +| Array  y = Array  (x <> y)
Object x +| Object y = Object (y <> x) -- Right biased
x        +| y        = err2 x y "cannot be added"

Number x -| Number y = Number (x - y)
Array  x -| Array  y = Array (x `vecDiff` y)
x        -| y        = err2 x y "cannot be subtracted"

Number x *| Number y = Number (x * y)
x        *| y        = err2 x y "cannot be multiplied"

Number x /| Number y = Number (x / y)
x        /| y        = err2 x y "cannot be divided"

lengthFi :: Value -> Int
lengthFi Null       = 0
lengthFi (Array v)  = V.length v
lengthFi (Object o) = length . H.toList $ o
lengthFi (String s) = B.length . encodeUtf8 $ s
lengthFi (Number _) = error "number has no length"
lengthFi (Bool _)   = error "boolean has no length"

lengthOp :: ValueOp
lengthOp = toJSON . lengthFi

keysOp :: ValueOp
keysOp (Array v)  = toJSON [0.. V.length v - 1]
keysOp (Object o) = toJSON $ H.keys o
keysOp Number{}   = error "number has no keys"
keysOp Null       = error "null has no keys"
keysOp Bool{}     = error "boolean has no keys"
keysOp String{}   = error "string has no keys"

at :: Value -> Value -> Maybe Value
Object o `at` String s = H.lookup s o
Array  a `at` Number mn
  | Success n <- fromJSON (Number mn) = a V.!? n
_        `at` _ = Nothing

atF :: Value -> Filter
atF key = fmap (fromMaybe Null . (`at` key))

allF :: Filter
allF xs = [ y | Array ys <- xs, y <- V.toList ys ]

bothF :: Filter -> Filter -> Filter
bothF f g xs = f xs ++ g xs

arrayF :: Filter -> Filter
arrayF f xs = [Array (V.fromList . f $ xs)]

objectF :: Obj Filter -> Filter
objectF o xs = [Object . H.fromList $ [ (k,y) | (k,f) <- H.toList o, y <- f xs ]]

{- Confusing:
$ jq -n -c '[(1,2,3,4) | .+.]'
[2,4,6,8]

$ jq -n -c '[(1,2,3,4) | .+1]'
[2,3,4,5]

But:

$ jq -n -c '[(1,2)+(2,1)]'
[3,4,2,3]

instead of [3,3]
-}
ap2F :: ValueBinOp -> Filter -> Filter -> Filter
-- ap2F op f g xs = [ op x y | x <- f xs, y <- g xs ]
ap2F op f g xs = zipWith op (f xs) (g xs)

emptyF :: Filter
emptyF _ = []

constF :: Value -> Filter
constF v xs = [ v | _ <- xs ]

-- Filter
data F = IdF             -- .
       | CompF F F       -- f | g
       | AtF Value       -- .[key]
       | AllF            -- .[]
       | BothF F F       -- f, g
       | ArrayF F        -- [f]
       | ObjectF (Obj F) -- {a: f, b: g}
       | EmptyF          -- empty
       | OpF Op          -- length, keys
       | Ap2F BinOp F F  -- f + g, f - g, f / g, f * g
       | ConstF Value    -- 1, "foo", null
       | ErrorF String
  deriving (Show)

data Op = Length | Keys
  deriving (Show)

data BinOp = Plus | Minus | Times | Div
  deriving (Show)

-- .key
keyF :: Text -> F
keyF = AtF . String

-- def map(f): [.[] | f];
mapF :: F -> F
mapF f = ArrayF (AllF `CompF` f)

-- TODO: deal properly with "f + g - h" and "f - g + h"
binOpF :: BinOp -> [F] -> F
binOpF _  []    = IdF
binOpF _  [x]   = x
binOpF op [x,y] = Ap2F op x y
binOpF _  _     = error "binOpF: not supported yet"

concatF, composeF, sumF, productF, minusF, divF :: [F] -> F

concatF [] = IdF
concatF xs = foldr1 BothF xs

composeF [] = IdF
composeF xs = foldr1 CompF xs

sumF     = binOpF Plus
productF = binOpF Times
minusF   = binOpF Minus
divF     = binOpF Div

valueBinOp :: BinOp -> ValueBinOp
valueBinOp Plus  = (+|)
valueBinOp Times = (*|)
valueBinOp Minus = (-|)
valueBinOp Div   = (/|)

valueOp :: Op -> ValueOp
valueOp Keys   = keysOp
valueOp Length = lengthOp

filter :: F -> Filter
filter IdF           = id
filter (CompF f g)   = filter g . filter f
filter (AtF key)     = atF key
filter AllF          = allF
filter (BothF f g)   = bothF (filter f) (filter g)
filter (ArrayF f)    = arrayF (filter f)
filter (ObjectF o)   = objectF (fmap filter o)
filter (Ap2F op f g) = ap2F (valueBinOp op) (filter f) (filter g)
filter EmptyF        = emptyF
filter (OpF op)      = fmap (valueOp op)
filter (ConstF v)    = constF v
filter (ErrorF err)  = error err

parseSimpleFilter, parseTimesFilter, parseDivFilter,
  parseMinusFilter, parsePlusFilter, parseCommaFilter,
  parseNoCommaFilter, parseFilter, parseDotFilter :: Parser F

parseDotFilter
  =  pure AllF <* string "[]"
 <|> AtF <$> (char '[' *> value <* tok ']')
 <|> keyF <$> bareWord
 <|> pure IdF

bareWord :: Parser Text
bareWord = T.pack <$> some (satisfy (\c -> isAscii c && isAlpha c))

parseConstFilter :: Parser Value
parseConstFilter
  =  String <$> jstring
 <|> Number <$> number
 <|> pure (Bool True)     <* string "true"
 <|> pure (Bool False)    <* string "false"
 <|> pure Null            <* string "null"
 <|> pure (Array mempty)  <* string "[]"
 <|> pure (Object mempty) <* string "{}"

parseOp :: Parser Op
parseOp =  pure Length <* string "length"
       <|> pure Keys   <* string "keys"

tok :: Char -> Parser Char
tok c = skipSpace *> char c

parseSimpleFilter
  = skipSpace *>
  (  char '.' *> skipSpace *> parseDotFilter
 <|> string "empty" *> pure EmptyF
 <|> OpF <$> parseOp
 <|> mapF <$> (string "map" *> tok '(' *> parseFilter <* tok ')')
 <|> ConstF <$> parseConstFilter
 <|> ArrayF <$> (char '[' *> parseFilter <* tok ']')
 <|> ObjectF <$> obj parseNoCommaFilter
 <|> char '(' *> parseFilter <* tok ')'
  )

parseTimesFilter = productF <$> parseSimpleFilter `sepBy` (tok '*')

parseDivFilter = divF <$> parseTimesFilter `sepBy` (tok '/')

parsePlusFilter = sumF <$> parseDivFilter `sepBy` (tok '+')

parseMinusFilter = minusF <$> parsePlusFilter `sepBy` (tok '-')

parseCommaFilter = concatF <$> parseMinusFilter `sepBy` (tok ',')

parseNoCommaFilter = composeF <$> parseMinusFilter `sepBy` (tok '|')

parseFilter = composeF <$> parseCommaFilter `sepBy1` (tok '|')

obj :: Parser a -> Parser (Obj a)
obj p = char '{' *> objectValues (skipSpace *> (bareWord <|> jstring)) p

-- From: https://github.com/bos/aeson/blob/master/Data/Aeson/Parser/Internal.hs
objectValues :: Parser Text -> Parser a -> Parser (Obj a)
objectValues str val = do
  skipSpace
  let pair = do
        a <- str <* skipSpace
        b <- char ':' *> skipSpace *> val
        return (a,b)
  vals <- ((pair <* skipSpace) `sepBy` (char ',' *> skipSpace)) <* char '}'
  return (H.fromList vals)
{-# INLINE objectValues #-}

parse :: Parser a -> L.ByteString -> Maybe a
parse p s =
    case L.parse p s of
      L.Done rest v | L.null rest -> Just v
      _ -> Nothing
{-# INLINE parse #-}

stream :: Parser [Value]
stream = value `sepBy` skipSpace

main :: IO ()
main = do [arg] <- getArgs
          let Just f = parse (parseFilter <* skipSpace) (L8.pack arg)
          -- print f
          input <- maybe (fail "JSON decoding") return . parse (stream <* skipSpace) =<< L.getContents
          mapM_ (L8.putStrLn . encode) $ filter f input