{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Aeson.Pointer (
Pointer(..),
Key(..),
Path,
formatPointer,
parsePointer,
get,
pointerFailure,
) where
import Control.Applicative
import Data.Aeson
import Data.Aeson.Types
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.Char (isNumber)
import qualified Data.HashMap.Strict as HM
import Data.Monoid
import Data.Scientific
import Data.Semigroup (Semigroup)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Vector as V
data Key
= OKey Text
| AKey Int
deriving (Eq, Ord, Show)
instance ToJSON Key where
toJSON (OKey t) = String t
toJSON (AKey a) = Number . fromInteger . toInteger $ a
instance FromJSON Key where
parseJSON (String t) = return $ OKey t
parseJSON (Number n) =
case toBoundedInteger n of
Nothing -> fail "A numeric key must be a positive whole number."
Just n' -> return $ AKey n'
parseJSON _ = fail "A key element must be a number or a string."
formatKey :: Key -> Text
formatKey (AKey i) = T.pack (show i)
formatKey (OKey t) = T.concatMap esc t
where
esc :: Char -> Text
esc '~' = "~0"
esc '/' = "~1"
esc c = T.singleton c
type Path = [Key]
newtype Pointer = Pointer { pointerPath :: Path }
deriving (Eq, Ord, Show, Semigroup, Monoid)
formatPointer :: Pointer -> Text
formatPointer (Pointer []) = ""
formatPointer (Pointer path) = "/" <> T.intercalate "/" (formatKey <$> path)
parsePointer :: Text -> Parser Pointer
parsePointer t
| T.null t = return (Pointer [])
| otherwise = Pointer <$> mapM key (drop 1 $ T.splitOn "/" t)
where
step t
| "0" `T.isPrefixOf` t = T.cons '~' (T.tail t)
| "1" `T.isPrefixOf` t = T.cons '/' (T.tail t)
| otherwise = T.cons '~' t
unesc :: Text -> Text
unesc t =
let l = T.split (== '~') t
in T.concat $ take 1 l <> fmap step (tail l)
key t
| T.null t = fail "JSON components must not be empty."
| T.all isNumber t = return (AKey (read $ T.unpack t))
| otherwise = return $ OKey (unesc t)
instance ToJSON Pointer where
toJSON pointer =
String (formatPointer pointer)
instance FromJSON Pointer where
parseJSON = modifyFailure ("Could not parse JSON pointer: " <>) . parse
where
parse (String t) = parsePointer t
parse _ = fail "A JSON pointer must be a string."
get :: Pointer -> Value -> Result Value
get (Pointer []) v = return v
get (Pointer (AKey i : path)) (Array v) =
maybe (fail "") return (v V.!? i) >>= get (Pointer path)
get (Pointer (OKey n : path)) (Object v) =
maybe (fail "") return (HM.lookup n v) >>= get (Pointer path)
get pointer value = pointerFailure pointer value
pointerFailure :: Pointer -> Value -> Result a
pointerFailure (Pointer []) value = Error "Cannot follow empty pointer. This is impossible."
pointerFailure (Pointer path@(key:_)) value =
Error . BS.unpack $ "Cannot follow pointer " <> pt <> ". Expected " <> ty <> " but got " <> doc
where
doc = encode value
pt = encode path
ty = case key of
(AKey _) -> "array"
(OKey _) -> "object"