packages feed

aeson-diff 1.0.0.0 → 1.0.0.1

raw patch · 17 files changed

+369/−303 lines, 17 filesdep ~basePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- Data.Aeson.Diff: data Patch
- Data.Aeson.Diff: formatPatch :: Patch -> Text
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Diff.Key
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Diff.Operation
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Diff.Patch
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Diff.Pointer
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Diff.Key
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Diff.Operation
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Diff.Patch
- Data.Aeson.Diff: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Diff.Pointer
- Data.Aeson.Diff: instance GHC.Base.Monoid Data.Aeson.Diff.Patch
- Data.Aeson.Diff: instance GHC.Base.Monoid Data.Aeson.Diff.Pointer
- Data.Aeson.Diff: instance GHC.Classes.Eq Data.Aeson.Diff.Key
- Data.Aeson.Diff: instance GHC.Classes.Eq Data.Aeson.Diff.Operation
- Data.Aeson.Diff: instance GHC.Classes.Eq Data.Aeson.Diff.Patch
- Data.Aeson.Diff: instance GHC.Classes.Eq Data.Aeson.Diff.Pointer
- Data.Aeson.Diff: instance GHC.Classes.Ord Data.Aeson.Diff.Key
- Data.Aeson.Diff: instance GHC.Show.Show Data.Aeson.Diff.Key
- Data.Aeson.Diff: instance GHC.Show.Show Data.Aeson.Diff.Operation
- Data.Aeson.Diff: instance GHC.Show.Show Data.Aeson.Diff.Patch
- Data.Aeson.Diff: instance GHC.Show.Show Data.Aeson.Diff.Pointer
- Data.Aeson.Diff: patch' :: Patch -> Value -> Value
+ Data.Aeson.Diff: newtype Patch
+ Data.Aeson.Patch: Add :: Pointer -> Value -> Operation
+ Data.Aeson.Patch: Cpy :: Pointer -> Pointer -> Operation
+ Data.Aeson.Patch: Mov :: Pointer -> Pointer -> Operation
+ Data.Aeson.Patch: Patch :: [Operation] -> Patch
+ Data.Aeson.Patch: Rem :: Pointer -> Operation
+ Data.Aeson.Patch: Rep :: Pointer -> Value -> Operation
+ Data.Aeson.Patch: Tst :: Pointer -> Value -> Operation
+ Data.Aeson.Patch: [changePointer] :: Operation -> Pointer
+ Data.Aeson.Patch: [changeValue] :: Operation -> Value
+ Data.Aeson.Patch: [fromPointer] :: Operation -> Pointer
+ Data.Aeson.Patch: [patchOperations] :: Patch -> [Operation]
+ Data.Aeson.Patch: data Operation
+ Data.Aeson.Patch: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Patch.Operation
+ Data.Aeson.Patch: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Patch.Patch
+ Data.Aeson.Patch: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Patch.Operation
+ Data.Aeson.Patch: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Patch.Patch
+ Data.Aeson.Patch: instance GHC.Base.Monoid Data.Aeson.Patch.Patch
+ Data.Aeson.Patch: instance GHC.Classes.Eq Data.Aeson.Patch.Operation
+ Data.Aeson.Patch: instance GHC.Classes.Eq Data.Aeson.Patch.Patch
+ Data.Aeson.Patch: instance GHC.Show.Show Data.Aeson.Patch.Operation
+ Data.Aeson.Patch: instance GHC.Show.Show Data.Aeson.Patch.Patch
+ Data.Aeson.Patch: newtype Patch
+ Data.Aeson.Pointer: AKey :: Int -> Key
+ Data.Aeson.Pointer: OKey :: Text -> Key
+ Data.Aeson.Pointer: Pointer :: Path -> Pointer
+ Data.Aeson.Pointer: [pointerPath] :: Pointer -> Path
+ Data.Aeson.Pointer: data Key
+ Data.Aeson.Pointer: formatPointer :: Pointer -> Text
+ Data.Aeson.Pointer: get :: Pointer -> Value -> Result Value
+ Data.Aeson.Pointer: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Pointer.Key
+ Data.Aeson.Pointer: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Pointer.Key
+ Data.Aeson.Pointer: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Base.Monoid Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Classes.Eq Data.Aeson.Pointer.Key
+ Data.Aeson.Pointer: instance GHC.Classes.Eq Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Classes.Ord Data.Aeson.Pointer.Key
+ Data.Aeson.Pointer: instance GHC.Show.Show Data.Aeson.Pointer.Key
+ Data.Aeson.Pointer: instance GHC.Show.Show Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: newtype Pointer
+ Data.Aeson.Pointer: pointerFailure :: Path -> Value -> Result a
+ Data.Aeson.Pointer: type Path = [Key]
- Data.Aeson.Diff: Rem :: Pointer -> Value -> Operation
+ Data.Aeson.Diff: Rem :: Pointer -> Operation

Files

CHANGELOG.md view
@@ -1,11 +1,20 @@+aeson-diff 1.0.0.1++    * Remove the `patch'` function before anyone gets attached to it.++    * Remove the 'Value' which was carried by the 'Rem' operation constructor.++    * Move 'Pointer' and 'Patch' types and operations into separate modules.+ aeson-diff 1.0      * aeson-diff now supports the operations and patch format described in-      RFC6902.+      RFC 6902. -    * Patch application can fail. The patch function now returns in the-      'Result' monad from the aeson package. The new patch' function throws an-      exception instead.+    * The `patch` function now returns in the 'Result' monad from the aeson+      package.++    * Add a `patch'` function throws an exception instead.      * The command line applications no longer pretend to support a non-JSON       patch format.
README.md view
@@ -81,9 +81,6 @@ - `diff :: Value -> Value -> Patch` examines source and target JSON `Value`s and constructs a new `Patch` describing the changes. -- `patch' :: Patch -> Value -> Result Value` applies the changes in a-`Path` to a JSON `Value`.- - `patch :: Patch -> Value -> Value` applies the changes in a `Patch` to a JSON `Value`. If an error results then an exception is thrown. 
aeson-diff.cabal view
@@ -1,5 +1,5 @@ name:                aeson-diff-version:             1.0.0.0+version:             1.0.0.1 synopsis:            Extract and apply patches to JSON documents. description:   .@@ -32,7 +32,9 @@   default-language:    Haskell2010   hs-source-dirs:      lib   exposed-modules:     Data.Aeson.Diff-  build-depends:       base >=4.5 && <4.9+                     , Data.Aeson.Patch+                     , Data.Aeson.Pointer+  build-depends:       base >=4.5 && <4.10                      , aeson                      , bytestring >= 0.10                      , edit-distance-vector
lib/Data/Aeson/Diff.hs view
@@ -1,7 +1,6 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE NamedFieldPuns             #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE NamedFieldPuns    #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}  -- | Description: Extract and apply patches on JSON documents. --@@ -18,10 +17,7 @@     -- * Functions     diff,     patch,-    patch',     applyOperation,--    formatPatch, ) where  import           Control.Applicative@@ -30,7 +26,6 @@ import           Data.Aeson import           Data.Aeson.Types           (modifyFailure, typeMismatch) import qualified Data.ByteString.Lazy.Char8 as BS-import           Data.Char                  (isNumber) import           Data.Foldable              (foldlM) import           Data.Hashable import           Data.HashMap.Strict        (HashMap)@@ -46,116 +41,15 @@  import Data.Vector.Distance --- | Describes the changes between two JSON documents.-data Patch = Patch-  { patchOperations :: [Operation] }-  deriving (Eq)+import Data.Aeson.Patch+import Data.Aeson.Pointer -instance Show Patch where-    show = T.unpack . formatPatch -instance Monoid Patch where-    mempty = Patch []-    mappend (Patch p1) (Patch p2) = Patch $ p1 <> p2--instance ToJSON Patch where-    toJSON (Patch ops) = toJSON ops--instance FromJSON Patch where-    parseJSON (Array v) = modifyFailure ("Could not parse patch: " <>) (Patch <$> mapM parseJSON (V.toList v))-    parseJSON v = modifyFailure ("Could not parse patch: " <> ) $ typeMismatch "Array" v--type Path = [Key]---- | Pointer to a location in a JSON document.------ Defined in RFC 6901 <http://tools.ietf.org/html/rfc6901>-newtype Pointer = Pointer { pointerPath :: Path }-  deriving (Eq, Show, Monoid)--pointerFailure :: Path -> Value -> Result a-pointerFailure [] value = Error ("UNPOSSIBLE!" <> show value)-pointerFailure path@(key:rest) value =-    fail . BS.unpack $ "Cannot follow pointer " <> pt <> ". Expected " <> ty <> " but got " <> doc-  where-    doc = encode value-    pt = encode (Pointer path)-    ty = case key of-           (AKey _) -> "array"-           (OKey _) -> "object"---- | An 'Operation' describes an atomic change to a JSON document.------ See RFC 6902 Section 4 <http://tools.ietf.org/html/rfc6902#section-4>.-data Operation-    = Add { changePointer :: Pointer, changeValue :: Value }-    -- ^ http://tools.ietf.org/html/rfc6902#section-4.1-    | Rem { changePointer :: Pointer, changeValue :: Value }-    -- ^ http://tools.ietf.org/html/rfc6902#section-4.2-    | Rep { changePointer :: Pointer, changeValue :: Value }-    -- ^ http://tools.ietf.org/html/rfc6902#section-4.3-    | Mov { changePointer :: Pointer, fromPointer :: Pointer }-    -- ^ http://tools.ietf.org/html/rfc6902#section-4.4-    | Cpy { changePointer :: Pointer, fromPointer :: Pointer }-    -- ^ http://tools.ietf.org/html/rfc6902#section-4.5-    | Tst { changePointer :: Pointer, changeValue :: Value }-    -- ^ http://tools.ietf.org/html/rfc6902#section-4.6-  deriving (Eq, Show)--instance ToJSON Operation where-    toJSON (Add p v) = object-        [ ("op", "add")-        , "path"  .= p-        , "value" .= v-        ]-    toJSON (Rem p v) = object-        [ ("op", "remove")-        , "path" .= p-        ]-    toJSON (Rep p v) = object-        [ ("op", "replace")-        , "path"  .= p-        , "value" .= v-        ]-    toJSON (Mov p f) = object-        [ ("op", "move")-        , "path" .= p-        , "from" .= f-        ]-    toJSON (Cpy p f) = object-        [ ("op", "copy")-        , "path" .= p-        , "from" .= f-        ]-    toJSON (Tst p v) = object-        [ ("op", "test")-        , "path" .= p-        , "value" .= v-        ]--instance FromJSON Operation where-    parseJSON o@(Object v)-        =   (op "add"     *> (Add <$> v .: "path" <*> v .: "value"))-        <|> (op "replace" *> (Rep <$> v .: "path" <*> v .: "value"))-        <|> (op "move"    *> (Mov <$> v .: "path" <*> v .: "from"))-        <|> (op "copy"    *> (Cpy <$> v .: "path" <*> v .: "from"))-        <|> (op "test"    *> (Tst <$> v .: "path" <*> v .: "value"))-        <|> (op "remove"  *> (Rem <$> v .: "path" <*> pure Null))-        <|> fail ("Expected a JSON patch operation, encountered: " <> BS.unpack (encode o))-      where-        op n = fixed v "op" (String n)-        fixed o n val = do-            v' <- o .: n-            if v' == val-              then return v'-              else mzero-    parseJSON v = typeMismatch "Operation" v- operationCost :: Operation -> Int operationCost op =     case op of       Add{} -> valueSize (changeValue op)-      Rem{} -> valueSize (changeValue op)+      Rem{} -> 1       Rep{} -> valueSize (changeValue op)       Mov{} -> 1       Cpy{} -> 1@@ -176,52 +70,7 @@           Cpy{} -> op { fromPointer = fn (fromPointer op) }           _     -> op --- | Traverse a single layer of a JSON document.-data Key-    = OKey Text -- ^ Traverse a 'Value' with an 'Object' constructor.-    | AKey Int  -- ^ Traverse a 'Value' with an 'Array' constructor.-  deriving (Eq, Ord, Show) -instance ToJSON Pointer where-    toJSON (Pointer path) = String ("/" <> T.intercalate "/" (fmap fmt path))-        where-          esc :: Char -> Text-          esc '~' = "~0"-          esc '/' = "~1"-          esc c = T.singleton c-          fmt :: Key -> Text-          fmt (OKey t) = T.concatMap esc t-          fmt (AKey i) = T.pack (show i)--instance FromJSON Pointer where-    parseJSON (String t) = 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)-    parseJSON _ = fail "A JSON pointer must be a string."--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."- -- * Atomic patches  -- | Construct a patch with a single 'Add' operation.@@ -230,7 +79,7 @@  -- | Construct a patch with a single 'Rem' operation. del :: Path -> Value -> Patch-del p v = Patch [Rem (Pointer p) v]+del p v = Patch [Rem (Pointer p)]  -- | Construct a patch which changes 'Rep' operation. rep :: Path -> Value -> Patch@@ -271,7 +120,7 @@             -- Deletions             del_keys = filter (not . (`elem` k2)) k1             deletions = Patch $ fmap-                (\k -> Rem (Pointer [OKey k]) . fromJust $ HM.lookup k o1)+                (\k -> Rem (Pointer [OKey k]))                 del_keys             -- Insertions             ins_keys = filter (not . (`elem` k1)) k2@@ -295,7 +144,7 @@         params :: Params Value [Operation] (Sum Int)         params = Params{..}         equivalent = (==)-        delete i v = [Rem (Pointer [AKey i]) v]+        delete i v = [Rem (Pointer [AKey i])]         insert i v = [Add (Pointer [AKey i]) v]         substitute i v v' =             let p = [AKey i]@@ -341,15 +190,6 @@         pos Tst{changePointer=Pointer path} = 0  -- | Apply a patch to a JSON document.------ If the patch cannot be cleanly applied an 'error' is thrown.-patch' :: Patch -> Value -> Value-patch' p d =-    case patch p d of-      Error e -> error $ "Failed to apply patch: " <> e <> "\n" <> BS.unpack (encode p)-      Success v -> v---- | Apply a patch to a JSON document. patch     :: Patch     -> Value@@ -362,15 +202,15 @@     :: Operation     -> Value     -> Result Value-applyOperation op j = case op of-    Add (Pointer path) v' -> applyAdd path v' j-    Rem (Pointer path) _  -> applyRem path    j-    Rep (Pointer path) v' -> applyRep path v' j-    Mov (Pointer path) (Pointer from) -> do-        v' <- get from j-        applyRem from j >>= applyAdd path v'-    Cpy (Pointer path) (Pointer from) -> applyCpy path from j-    Tst (Pointer path) v  -> applyTst path v j+applyOperation op json = case op of+    Add path v'   -> applyAdd path v' json+    Rem path      -> applyRem path    json+    Rep path v'   -> applyRep path v' json+    Tst path v    -> applyTst path v  json+    Cpy path from -> applyCpy path from json+    Mov path from -> do+        v' <- get from json+        applyRem from json >>= applyAdd path v'  -- | Apply an 'Add' operation to a document. --@@ -380,74 +220,78 @@ -- - A single 'OKey' inserts or replaces the corresponding member in an object. -- - A single 'AKey' inserts at the corresponding location. -- - Longer 'Paths' traverse if they can and fail otherwise.-applyAdd :: Path -> Value -> Value -> Result Value-applyAdd [] val _ =-    return val-applyAdd [AKey i] v' (Array v) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn _ = return (Just v')-    in return (Array $ vInsert i v' v)-applyAdd (AKey i : path) v' (Array v) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn Nothing = Error "Cannot insert beneath missing array index."-        fn (Just d) = Just <$> applyAdd path v' d-    in Array <$> vModify i fn v-applyAdd [OKey n] v' (Object m) =-    return . Object $ HM.insert n v' m-applyAdd (OKey n : path) v' (Object o) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn Nothing = Error ("Cannot insert beneath missing object index: " <> show n)-        fn (Just d) = Just <$> applyAdd path v' d-    in Object <$> hmModify n fn o-applyAdd (OKey n : path) v' array@(Array v)-    | n == "-" = applyAdd (AKey (V.length v) : path) v' array-applyAdd path _ v = pointerFailure path v+applyAdd :: Pointer -> Value -> Value -> Result Value+applyAdd from@(Pointer path) = go path+  where+    go [] val _ =+        return val+    go [AKey i] v' (Array v) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn _ = return (Just v')+        in return (Array $ vInsert i v' v)+    go (AKey i : path) v' (Array v) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn Nothing = cannot "insert" "array" i from+            fn (Just d) = Just <$> go path v' d+        in Array <$> vModify i fn v+    go [OKey n] v' (Object m) =+        return . Object $ HM.insert n v' m+    go (OKey n : path) v' (Object o) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn Nothing = cannot "insert" "object" n from+            fn (Just d) = Just <$> go path v' d+        in Object <$> hmModify n fn o+    go (OKey n : path) v' array@(Array v)+        | n == "-" = go (AKey (V.length v) : path) v' array+    go path _ v = pointerFailure path v  -- | Apply a 'Rem' operation to a document. -- -- http://tools.ietf.org/html/rfc6902#section-4.2 -- -- - The target location MUST exist.-applyRem :: Path -> Value -> Result Value-applyRem [] _ = return Null-applyRem [AKey i] d@(Array v) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn Nothing = fail $ "Cannot delete missing array member at " <> show i <> " " <> BS.unpack (encode d)-        fn (Just v) = return Nothing-    in Array <$> vModify i fn v-applyRem (AKey i : path) (Array v) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn Nothing = fail "Cannot traverse under missing array index."-        fn (Just o) = Just <$> applyRem path o-    in Array <$> vModify i fn v-applyRem [OKey n] (Object m) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn Nothing = fail "Cannot delete missing object member."-        fn (Just _) = return Nothing-    in Object <$> hmModify n fn m-applyRem (OKey n : path) (Object m) =-    let fn :: Maybe Value -> Result (Maybe Value)-        fn Nothing = fail "Cannot traverse under missing object index."-        fn (Just o) = Just <$> applyRem path o-    in Object <$> hmModify n fn m--- Dodgy hack for "-" key which means "the end of the array".-applyRem (OKey n : path) array@(Array v)-    | n == "-" = applyRem (AKey (V.length v) : path) array--- Type mismatch: clearly the thing we're deleting isn't here.-applyRem path value = pointerFailure path value+applyRem :: Pointer -> Value -> Result Value+applyRem from@(Pointer path) = go path+  where+    go [] _ = return Null+    go [AKey i] d@(Array v) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn Nothing = cannot "delete" "array" i from+            fn (Just v) = return Nothing+        in Array <$> vModify i fn v+    go (AKey i : path) (Array v) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn Nothing = cannot "traverse" "array" i from+            fn (Just o) = Just <$> go path o+        in Array <$> vModify i fn v+    go [OKey n] (Object m) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn Nothing = cannot "delete" "object" n from+            fn (Just _) = return Nothing+        in Object <$> hmModify n fn m+    go (OKey n : path) (Object m) =+        let fn :: Maybe Value -> Result (Maybe Value)+            fn Nothing = cannot "traverse" "object" n from+            fn (Just o) = Just <$> go path o+        in Object <$> hmModify n fn m+    -- Dodgy hack for "-" key which means "the end of the array".+    go (OKey n : path) array@(Array v)+        | n == "-" = go (AKey (V.length v) : path) array+    -- Type mismatch: clearly the thing we're deleting isn't here.+    go path value = pointerFailure path value  -- | Apply a 'Rep' operation to a document. -- -- http://tools.ietf.org/html/rfc6902#section-4.3 -- -- - Functionally identical to a 'Rem' followed by an 'Add'.-applyRep :: Path -> Value -> Value -> Result Value-applyRep path v doc = applyRem path doc >>= applyAdd path v+applyRep :: Pointer -> Value -> Value -> Result Value+applyRep from v doc = applyRem from doc >>= applyAdd from v  -- | Apply a 'Mov' operation to a document. -- -- http://tools.ietf.org/html/rfc6902#section-4.4-applyMov :: Path -> Path -> Value -> Result Value+applyMov :: Pointer -> Pointer -> Value -> Result Value applyMov path from doc = do   v <- get from doc   applyRem from doc >>= applyAdd path v@@ -458,7 +302,7 @@ -- -- - The location must exist. -- - Identical to an add with the appropriate value.-applyCpy :: Path -> Path -> Value -> Result Value+applyCpy :: Pointer -> Pointer -> Value -> Result Value applyCpy path from doc = do   v <- get from doc   applyAdd path v doc@@ -469,63 +313,18 @@ -- -- - The location must exist. -- - The value must be equal to the supplied value.-applyTst :: Path -> Value -> Value -> Result Value+applyTst :: Pointer -> Value -> Value -> Result Value applyTst path v doc = do     v' <- get path doc-    unless (v == v') (Error "Tested elements do not match.")+    unless (v == v') (Error . T.unpack $ "Element at \"" <> formatPointer path <> "\" fails test.")     return doc --- | Get the value at a 'Path'.------ - The path must exist.-get :: Path -> Value -> Result Value-get [] v = return v-get (AKey i : path) (Array v) =-    maybe (fail "") return (v V.!? i) >>= get path-get (OKey n : path) (Object v) =-    maybe (fail "") return (HM.lookup n v) >>= get path-get path value = pointerFailure path value---- * Formatting patches---- | Format a 'Patch' for reading by humans.------ For storing or exchanging 'Patch'es between systems using the JSON encoding--- implemented by the 'FromJSON' and 'ToJSON' instances.-formatPatch-    :: Patch-    -> Text-formatPatch (Patch ops) = T.unlines $ fmap formatOp ops-  where-    formatKey (OKey t) = "." <> t-    formatKey (AKey i) = "[" <> (T.pack . show $ i) <> "]"-    formatPath :: [Key] -> Text-    formatPath p = "@" <> (T.concat . fmap formatKey $ p)-    formatOp :: Operation -> Text-    formatValue :: Value -> Text-    formatValue v = case v of-        String t -> t-        Number s -> T.pack . show $ s-        Bool b -> T.pack . show $ b-        Null -> "Null"-        _ -> ":-("-    formatOp (Add (Pointer k) v) = formatPath k <> "\n" <> "+" <> formatValue v-    formatOp (Rem (Pointer k) _) = formatPath k <> "\n" <> "-"-    formatOp (Rep (Pointer k) v) = formatPath k <> "\n" <> "=" <> formatValue v-    formatOp (Mov (Pointer k) (Pointer f)) = formatPath k <> "\n" <> "<" <> formatPath f-    formatOp (Cpy (Pointer k) (Pointer f)) = formatPath k <> "\n" <> "~" <> formatPath f-    formatOp (Tst (Pointer k) v) = formatPath k <> "\n" <> "?" <> formatValue v---- | Parse a 'Patch'.-parsePatch :: Text -> Either Text Patch-parsePatch _t = throwError "Cannot parse"- -- * Utilities------ $ These are some utility functions used in the functions defined above. Mostly--- they just fill gaps in the APIs of the "Data.Vector" and "Data.HashMap.Strict"--- modules. +-- $ These are some utility functions used in the functions defined+-- above. Mostly they just fill gaps in the APIs of the "Data.Vector"+-- and "Data.HashMap.Strict" modules.+ -- | Estimate the size of a JSON 'Value'. -- -- This is used in the diff cost metric function.@@ -562,7 +361,11 @@ -- - delete an existing element; -- - insert a new element; or -- - replace an existing element.-vModify :: Int -> (Maybe a -> Result (Maybe a)) -> Vector a -> Result (Vector a)+vModify+    :: Int+    -> (Maybe a -> Result (Maybe a))+    -> Vector a+    -> Result (Vector a) vModify i f v =     let a = v V.!? i         a' = f a@@ -571,7 +374,7 @@         (Just _ , Success Nothing ) -> return (vDelete i v)         (Nothing, Success (Just n)) -> return (vInsert i n v)         (Just _ , Success (Just n)) -> return (V.update v (V.singleton (i, n)))-        (_      , Error   e       ) -> fail e+        (_      , Error   e       ) -> Error e  -- | Modify the value associated with a key in a 'HashMap'. --@@ -588,3 +391,15 @@     Error e -> Error e     Success Nothing  -> return $ HM.delete k m     Success (Just v) -> return $ HM.insert k v m++-- | Report an error about being able to use a pointer key.+cannot+    :: (Show ix)+    => String -- ^ Use to be made "delete", "traverse", etc.+    -> String -- ^ Type "array" "object"+    -> ix+    -> Pointer+    -> Result a+cannot op ty ix p =+    Error ("Cannot " <> op <> " missing " <> ty <> " member at index "+          <> show ix <> " in pointer \"" <> T.unpack (formatPointer p) <> "\".")
+ lib/Data/Aeson/Patch.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}++-- | Description: Represent RFC 6902 patches.+module Data.Aeson.Patch (+  Patch(..),+  Operation(..),+) where++import           Control.Applicative+import           Control.Monad+import           Data.Aeson+import           Data.Aeson.Types+import qualified Data.ByteString.Lazy.Char8 as BS+import           Data.Monoid+import           Data.Vector                (Vector)+import qualified Data.Vector                as V++import Data.Aeson.Pointer++-- * Patches++-- | Describes the changes between two JSON documents.+newtype Patch = Patch+    { patchOperations :: [Operation] }+  deriving (Eq, Show, Monoid)++instance ToJSON Patch where+    toJSON (Patch ops) = toJSON ops++instance FromJSON Patch where+    parseJSON = modifyFailure ("Could not parse patch: " <> ) . parsePatch+      where+        parsePatch (Array v) = Patch <$> mapM parseJSON (V.toList v)+        parsePatch v = typeMismatch "Array" v++-- * Operations++-- | An 'Operation' describes the operations which can appear as part of a JSON+-- Patch.+--+-- See RFC 6902 Section 4 <http://tools.ietf.org/html/rfc6902#section-4>.+data Operation+    = Add { changePointer :: Pointer, changeValue :: Value }+    -- ^ http://tools.ietf.org/html/rfc6902#section-4.1+    | Rem { changePointer :: Pointer }+    -- ^ http://tools.ietf.org/html/rfc6902#section-4.2+    | Rep { changePointer :: Pointer, changeValue :: Value }+    -- ^ http://tools.ietf.org/html/rfc6902#section-4.3+    | Mov { changePointer :: Pointer, fromPointer :: Pointer }+    -- ^ http://tools.ietf.org/html/rfc6902#section-4.4+    | Cpy { changePointer :: Pointer, fromPointer :: Pointer }+    -- ^ http://tools.ietf.org/html/rfc6902#section-4.5+    | Tst { changePointer :: Pointer, changeValue :: Value }+    -- ^ http://tools.ietf.org/html/rfc6902#section-4.6+  deriving (Eq, Show)++instance ToJSON Operation where+    toJSON (Add p v) = object+        [ ("op", "add")+        , "path"  .= p+        , "value" .= v+        ]+    toJSON (Rem p) = object+        [ ("op", "remove")+        , "path" .= p+        ]+    toJSON (Rep p v) = object+        [ ("op", "replace")+        , "path"  .= p+        , "value" .= v+        ]+    toJSON (Mov p f) = object+        [ ("op", "move")+        , "path" .= p+        , "from" .= f+        ]+    toJSON (Cpy p f) = object+        [ ("op", "copy")+        , "path" .= p+        , "from" .= f+        ]+    toJSON (Tst p v) = object+        [ ("op", "test")+        , "path" .= p+        , "value" .= v+        ]++instance FromJSON Operation where+    parseJSON = parse+      where+        parse o@(Object v)+            =   (op v "add"     *> (Add <$> v .: "path" <*> v .: "value"))+            <|> (op v "replace" *> (Rep <$> v .: "path" <*> v .: "value"))+            <|> (op v "move"    *> (Mov <$> v .: "path" <*> v .: "from"))+            <|> (op v "copy"    *> (Cpy <$> v .: "path" <*> v .: "from"))+            <|> (op v "test"    *> (Tst <$> v .: "path" <*> v .: "value"))+            <|> (op v "remove"  *> (Rem <$> v .: "path"))+            <|> fail ("Expected a JSON patch operation, encountered: " <> BS.unpack (encode o))+        parse v = typeMismatch "Operation" v+        op v n = fixed v "op" (String n)+        fixed o n val = do+            v' <- o .: n+            if v' == val+              then return v'+              else mzero+        fixed' o n val = (o .: n) >>= \v -> guard (v == n)
+ lib/Data/Aeson/Pointer.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+-- | Description: JSON Pointers as described in RFC 6901.++module Data.Aeson.Pointer (+  Pointer(..),+  Key(..),+  pointerFailure,+  Path,+  formatPointer,+  get,+) 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.Text                  (Text)+import qualified Data.Text                  as T+import qualified Data.Vector                as V++-- * Patch components++-- | Traverse a single layer of a JSON document.+data Key+    = OKey Text -- ^ Traverse a 'Value' with an 'Object' constructor.+    | AKey Int  -- ^ Traverse a 'Value' with an 'Array' constructor.+  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++-- * Pointers++-- | A sequence of 'Key's forms a path through a JSON document.+type Path = [Key]++-- | Pointer to a location in a JSON document.+--+-- Defined in RFC 6901 <http://tools.ietf.org/html/rfc6901>+newtype Pointer = Pointer { pointerPath :: Path }+  deriving (Eq, Show, Monoid)++-- | Format a 'Pointer' as described in RFC 6901.+formatPointer :: Pointer -> Text+formatPointer (Pointer path) = "/" <> T.intercalate "/" (formatKey <$> path)++-- | Report an error following a pointer.+pointerFailure :: Path -> Value -> Result a+pointerFailure [] value = Error ("UNPOSSIBLE!" <> show value)+pointerFailure path@(key:rest) value =+    fail . BS.unpack $ "Cannot follow pointer " <> pt <> ". Expected " <> ty <> " but got " <> doc+  where+    doc = encode value+    pt = encode (Pointer path)+    ty = case key of+           (AKey _) -> "array"+           (OKey _) -> "object"++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) = Pointer <$> mapM key (drop 1 $ T.splitOn "/" t)+        parse _ = fail "A JSON pointer must be a string."+        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)++-- | Get the value at a 'Path'.+get :: Pointer -> Value -> Result Value+get (Pointer p) = get' p+  where+    get' [] v = return v+    get' (AKey i : path) (Array v) =+        maybe (fail "") return (v V.!? i) >>= get' path+    get' (OKey n : path) (Object v) =+        maybe (fail "") return (HM.lookup n v) >>= get' path+    get' path value = pointerFailure path value
src/diff.hs view
@@ -19,20 +19,27 @@  -- | Command-line options. data Options = Options-    { optionOut  :: File+    { optionTst  :: Bool+    , optionOut  :: File     , optionFrom :: File     , optionTo   :: File     }  data Configuration = Configuration-    { cfgOut  :: Handle+    { cfgTst  :: Bool+    , cfgOut  :: Handle     , cfgFrom :: Handle     , cfgTo   :: Handle     }  optionParser :: Parser Options optionParser = Options-    <$> option fileP+    <$> switch+        (  long "test-before-remove"+        <> short 'T'+        <> help "Include a test before each remove."+        )+    <*> option fileP         (  long "output"         <> short 'o'         <> metavar "OUTPUT"@@ -72,7 +79,8 @@     load :: Options -> IO Configuration     load Options{..} =         Configuration-            <$> openw optionOut+            <$> pure  optionTst+            <*> openw optionOut             <*> openr optionFrom             <*> openr optionTo 
+ test/data/cases/case4-error.txt view
@@ -0,0 +1,1 @@+Cannot delete missing object member at index "missing" in pointer "/missing".
+ test/data/cases/case4-original.json view
@@ -0,0 +1,3 @@+{+    "a": 1+}
+ test/data/cases/case4-patch.json view
@@ -0,0 +1,2 @@+[ {"op": "remove", "path": "/missing"}+]
+ test/data/cases/case5-error.txt view
@@ -0,0 +1,1 @@+Cannot delete missing array member at index 1 in pointer "/1".
+ test/data/cases/case5-original.json view
@@ -0,0 +1,2 @@+[ "hello"+]
+ test/data/cases/case5-patch.json view
@@ -0,0 +1,2 @@+[ {"op": "remove", "path": "/1"}+]
test/data/rfc6902/a12-error.txt view
@@ -1,1 +1,1 @@-Cannot insert beneath missing object index: "baz"+Cannot insert missing object member at index "baz" in pointer "/baz/bat".
test/data/rfc6902/a15-error.txt view
@@ -1,1 +1,1 @@-Tested elements do not match.+Element at "/~01" fails test.
test/data/rfc6902/a9-error.txt view
@@ -1,1 +1,1 @@-Tested elements do not match.+Element at "/baz" fails test.
test/properties.hs view
@@ -8,7 +8,7 @@  import           Control.Applicative import           Control.Monad-import           Data.Aeson+import           Data.Aeson                 as A import qualified Data.ByteString.Lazy.Char8 as BL import           Data.Functor import           Data.HashMap.Strict        (HashMap)@@ -76,9 +76,13 @@     -> Bool diffApply f t =     let p = diff f t-    in (t == patch' p f) ||+    in (A.Success t == patch p f) ||        error ("BAD PATCH\n" <> BL.unpack (encode p) <> "\n"-                            <> BL.unpack (encode (patch' p f)))+                            <> result "<failure>" (BL.unpack . encode <$> patch p f))++result :: a -> A.Result a -> a+result _ (A.Success a) = a+result a _ = a  -- | Patch extracted from identical documents should be mempty. prop_diff_id