mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
11a454c2d6
This commit applies ormolu to the whole Haskell code base by running `make format`. For in-flight branches, simply merging changes from `main` will result in merge conflicts. To avoid this, update your branch using the following instructions. Replace `<format-commit>` by the hash of *this* commit. $ git checkout my-feature-branch $ git merge <format-commit>^ # and resolve conflicts normally $ make format $ git commit -a -m "reformat with ormolu" $ git merge -s ours post-ormolu https://github.com/hasura/graphql-engine-mono/pull/2404 GitOrigin-RevId: 75049f5c12f430c615eafb4c6b8e83e371e01c8e
276 lines
7.9 KiB
Haskell
276 lines
7.9 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
|
|
-- | A version of aeson that parses with key order preserved.
|
|
--
|
|
-- Copyright:
|
|
-- (c) 2011-2016 Bryan O'Sullivan
|
|
-- (c) 2011 MailRank, Inc.
|
|
module Data.Aeson.Ordered
|
|
( Value (..),
|
|
Object,
|
|
Array,
|
|
safeUnion,
|
|
value,
|
|
decode,
|
|
Data.Aeson.Ordered.toList,
|
|
fromList,
|
|
asObject,
|
|
object,
|
|
array,
|
|
insert,
|
|
delete,
|
|
adjust,
|
|
empty,
|
|
eitherDecode,
|
|
Data.Aeson.Ordered.lookup,
|
|
toOrdered,
|
|
fromOrdered,
|
|
)
|
|
where
|
|
|
|
import Control.Applicative hiding (empty)
|
|
import Control.Lens (prism)
|
|
import Data.Aeson qualified as J
|
|
import Data.Aeson.Lens (AsNumber (..))
|
|
import Data.Aeson.Parser (jstring)
|
|
import Data.Attoparsec.ByteString (Parser)
|
|
import Data.Attoparsec.ByteString qualified as A
|
|
import Data.Attoparsec.ByteString.Char8 qualified as A8
|
|
import Data.Bifunctor (second)
|
|
import Data.ByteString (ByteString)
|
|
import Data.ByteString.Lazy qualified as L
|
|
import Data.Data (Typeable)
|
|
import Data.HashMap.Strict qualified as Map
|
|
import Data.HashMap.Strict.InsOrd qualified as OMap
|
|
import Data.Hashable (Hashable (..))
|
|
import Data.Scientific (Scientific)
|
|
import Data.Text qualified as T
|
|
import Data.Vector (Vector)
|
|
import Data.Vector qualified as V
|
|
import Hasura.Prelude hiding (empty, first, second)
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Copied constants from aeson
|
|
|
|
#define BACKSLASH 92
|
|
#define CLOSE_CURLY 125
|
|
#define CLOSE_SQUARE 93
|
|
#define COMMA 44
|
|
#define DOUBLE_QUOTE 34
|
|
#define OPEN_CURLY 123
|
|
#define OPEN_SQUARE 91
|
|
#define C_0 48
|
|
#define C_9 57
|
|
#define C_A 65
|
|
#define C_F 70
|
|
#define C_a 97
|
|
#define C_f 102
|
|
#define C_n 110
|
|
#define C_t 116
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Our altered type
|
|
|
|
-- | A JSON \"object\" (key\/value map). This is where this type
|
|
-- differs to the 'aeson' package.
|
|
newtype Object = Object_ {unObject_ :: InsOrdHashMap Text Value}
|
|
deriving stock (Data, Eq, Generic, Read, Show, Typeable)
|
|
deriving newtype (Hashable)
|
|
|
|
-- | Union the keys, ordered, in two maps, erroring on duplicates.
|
|
safeUnion :: Object -> Object -> Either String Object
|
|
safeUnion (Object_ x) (Object_ y) =
|
|
fmap
|
|
Object_
|
|
( traverse
|
|
id
|
|
( OMap.unionWithKey
|
|
(\k _a _b -> Left ("Duplicate key: " ++ T.unpack k))
|
|
(fmap Right x)
|
|
(fmap Right y)
|
|
)
|
|
)
|
|
|
|
-- | Empty object.
|
|
empty :: Object
|
|
empty = Object_ mempty
|
|
|
|
-- | Insert before the element at index i. Think of it in terms of
|
|
-- 'splitAt', which is (take k, drop k). Deletes existing key, if any.
|
|
insert :: (Int, Text) -> Value -> Object -> Object
|
|
insert (idx, key) val =
|
|
Object_
|
|
. OMap.fromList
|
|
. uncurry (<>)
|
|
. second ((key, val) :)
|
|
. splitAt idx
|
|
. OMap.toList
|
|
. OMap.delete key
|
|
. unObject_
|
|
|
|
-- | Lookup a key.
|
|
lookup :: Text -> Object -> Maybe Value
|
|
lookup key (Object_ omap) = OMap.lookup key omap
|
|
|
|
-- | Delete a key.
|
|
delete :: Text -> Object -> Object
|
|
delete key (Object_ omap) = Object_ (OMap.delete key omap)
|
|
|
|
adjust :: (Value -> Value) -> Text -> Object -> Object
|
|
adjust f key (Object_ omap) = Object_ (OMap.adjust f key omap)
|
|
|
|
-- | ToList a key.
|
|
toList :: Object -> [(Text, Value)]
|
|
toList (Object_ omap) = OMap.toList omap
|
|
|
|
-- | FromList a key.
|
|
fromList :: [(Text, Value)] -> Object
|
|
fromList = Object_ . OMap.fromList
|
|
|
|
-- | A JSON \"array\" (sequence).
|
|
type Array = Vector Value
|
|
|
|
-- | A JSON value represented as a Haskell value. Intentionally
|
|
-- shadowing the 'Value' from the aeson package.
|
|
data Value
|
|
= Object !Object
|
|
| Array !Array
|
|
| String !Text
|
|
| Number !Scientific
|
|
| Bool !Bool
|
|
| Null
|
|
deriving stock (Data, Eq, Generic, Read, Show, Typeable)
|
|
|
|
instance Hashable Value where
|
|
-- Lifted from Aeson's implementation for 'Value'.
|
|
hashWithSalt s = \case
|
|
(Object o) -> s `hashWithSalt` (0 :: Int) `hashWithSalt` o
|
|
(Array a) -> foldl' hashWithSalt (s `hashWithSalt` (1 :: Int)) a
|
|
(String str) -> s `hashWithSalt` (2 :: Int) `hashWithSalt` str
|
|
(Number n) -> s `hashWithSalt` (3 :: Int) `hashWithSalt` n
|
|
(Bool b) -> s `hashWithSalt` (4 :: Int) `hashWithSalt` b
|
|
Null -> s `hashWithSalt` (5 :: Int)
|
|
|
|
-- Adapter instance for 'lens-aeson' which lets us write optics over the numeric
|
|
-- values in ordered JSON collections as if they were plain 'Scientific' types.
|
|
instance AsNumber Value where
|
|
_Number = prism upcast downcast
|
|
where
|
|
upcast = Number
|
|
downcast v = case v of
|
|
Number n -> Right n
|
|
_ -> Left v
|
|
|
|
-- | Value pairs to Value
|
|
object :: [(Text, Value)] -> Value
|
|
object = Object . fromList
|
|
|
|
-- | Value list to Value
|
|
array :: [Value] -> Value
|
|
array = Array . V.fromList
|
|
|
|
-- | Convert Aeson Value to Ordered Value
|
|
toOrdered :: (J.ToJSON a) => a -> Value
|
|
toOrdered v = case J.toJSON v of
|
|
J.Object obj -> Object $ fromList $ map (second toOrdered) $ Map.toList obj
|
|
J.Array arr -> Array $ V.fromList $ map toOrdered $ V.toList arr
|
|
J.String text -> String text
|
|
J.Number number -> Number number
|
|
J.Bool boolean -> Bool boolean
|
|
J.Null -> Null
|
|
|
|
-- | Convert Ordered Value to Aeson Value
|
|
fromOrdered :: Value -> J.Value
|
|
fromOrdered v = case v of
|
|
Object obj ->
|
|
J.Object $
|
|
Map.fromList $
|
|
map (second fromOrdered) $
|
|
Data.Aeson.Ordered.toList obj
|
|
Array arr -> J.Array $ V.fromList $ map fromOrdered $ V.toList arr
|
|
String text -> J.String text
|
|
Number number -> J.Number number
|
|
Bool boolean -> J.Bool boolean
|
|
Null -> J.Null
|
|
|
|
asObject :: IsString s => Value -> Either s Object
|
|
asObject = \case
|
|
Object o -> Right o
|
|
_ -> Left "expecting ordered object"
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Top-level entry points
|
|
|
|
eitherDecode :: L.ByteString -> Either String Value
|
|
eitherDecode = A.parseOnly value . L.toStrict
|
|
|
|
decode :: ByteString -> Maybe Value
|
|
decode = either (const Nothing) Just . A.parseOnly value
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Modified aeson parser
|
|
|
|
-- Copied from the aeson package.
|
|
arrayValues :: Parser Array
|
|
arrayValues = do
|
|
skipSpace
|
|
w <- A.peekWord8'
|
|
if w == CLOSE_SQUARE
|
|
then A.anyWord8 >> return V.empty
|
|
else loop [] 1
|
|
where
|
|
loop acc !len = do
|
|
v <- (value A.<?> "json list value") <* skipSpace
|
|
ch <- A.satisfy (\w -> w == COMMA || w == CLOSE_SQUARE) A.<?> "',' or ']'"
|
|
if ch == COMMA
|
|
then skipSpace >> loop (v : acc) (len + 1)
|
|
else return (V.reverse (V.fromListN len (v : acc)))
|
|
{-# INLINE arrayValues #-}
|
|
|
|
-- Copied from aeson package.
|
|
objectValues :: Parser (InsOrdHashMap Text Value)
|
|
objectValues = do
|
|
skipSpace
|
|
w <- A.peekWord8'
|
|
if w == CLOSE_CURLY
|
|
then A.anyWord8 >> return OMap.empty
|
|
else loop OMap.empty
|
|
where
|
|
-- Why use acc pattern here, you may ask? because 'H.fromList' use 'unsafeInsert'
|
|
-- and it's much faster because it's doing in place update to the 'HashMap'!
|
|
loop acc = do
|
|
k <- (jstring A.<?> "object key") <* skipSpace <* (A8.char ':' A.<?> "':'")
|
|
v <- (value A.<?> "object value") <* skipSpace
|
|
ch <- A.satisfy (\w -> w == COMMA || w == CLOSE_CURLY) A.<?> "',' or '}'"
|
|
let acc' = OMap.insert k v acc
|
|
if ch == COMMA
|
|
then skipSpace >> loop acc'
|
|
else pure acc'
|
|
{-# INLINE objectValues #-}
|
|
|
|
-- Copied from aeson package.
|
|
value :: Parser Value
|
|
value = do
|
|
skipSpace
|
|
w <- A.peekWord8'
|
|
case w of
|
|
DOUBLE_QUOTE -> String <$> jstring
|
|
OPEN_CURLY -> A.anyWord8 *> (Object . Object_ <$> objectValues)
|
|
OPEN_SQUARE -> A.anyWord8 *> (Array <$> arrayValues)
|
|
C_f -> A8.string "false" $> Bool False
|
|
C_t -> A8.string "true" $> Bool True
|
|
C_n -> A8.string "null" $> Null
|
|
_
|
|
| w >= 48 && w <= 57 || w == 45 ->
|
|
Number <$> A8.scientific
|
|
| otherwise -> fail "not a valid json value"
|
|
{-# INLINE value #-}
|
|
|
|
-- Copied from aeson package.
|
|
|
|
-- | The only valid whitespace in a JSON document is space, newline,
|
|
-- carriage return, and tab.
|
|
skipSpace :: Parser ()
|
|
skipSpace = A.skipWhile $ \w -> w == 0x20 || w == 0x0a || w == 0x0d || w == 0x09
|
|
{-# INLINE skipSpace #-}
|