module Avers.Patching
( applyOperation
, opOT
, rebaseOperation
, resolvePathIn
) where
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.HashMap.Strict as M
import Data.Aeson
import Data.Aeson.Types
import Avers.Types (Patch(..), Path(..), Operation(..), PatchM, PatchError(..))
import Control.Monad
applyOperation :: Value -> Operation -> PatchM Value
applyOperation value Set{..}
| opPath == "", Just val <- opValue = return val
| otherwise = changeObject value opPath $ \key ->
maybe (M.delete key) (M.insert key) opValue
applyOperation value Splice{..} = changeArray value opPath $ \a -> do
when (V.length a < opIndex + opRemove) $
Left $ UnknownPatchError $ mconcat
[ "Index out of range ("
, T.pack (show $ V.length a)
, ","
, T.pack (show opIndex)
, ","
, T.pack (show opRemove)
, ")"
]
unless (isStructurallyEquivalent opInsert a) $
Left $ UnknownPatchError "Array doesn't match structure"
return $ V.take opIndex a V.++ V.fromList opInsert V.++ V.drop (opIndex + opRemove) a
where
isStructurallyEquivalent :: [Value] -> V.Vector Value -> Bool
isStructurallyEquivalent a b = strings a b || validObjects a b
strings a b = all isString a && V.all isString b
validObjects a b = all hasObjectId a && V.all hasObjectId b
isString :: Value -> Bool
isString (String _) = True
isString _ = False
hasObjectId :: Value -> Bool
hasObjectId (Object o) = M.member "id" o
hasObjectId _ = False
pathElements :: Path -> [Text]
pathElements = T.split ('.' ==) . unPath
changeObject :: Value -> Path -> (Text -> Object -> Object) -> PatchM Value
changeObject value path f = changeObjectAt value (init (pathElements path)) $ \x ->
case x of
Object o -> Right $ Object $ f (last (pathElements path)) o
_ -> Left $ UnknownPatchError "Can not change a non-object"
changeArray :: Value -> Path -> (Array -> PatchM Array) -> PatchM Value
changeArray value path f = changeObjectAt value (pathElements path) $ \x ->
case x of
Array a -> fmap Array $ f a
_ -> Left $ UnknownPatchError "Can not change a non-array"
changeObjectAt :: Value -> [Text] -> (Value -> PatchM Value) -> PatchM Value
changeObjectAt container [] f = f container
changeObjectAt (Object o) (x:xs) f =
case parse (const $ o .: x) o of
Error _ -> Left $ UnknownPatchError $ "Key '" <> T.pack (show x) <> "' does not exist inside the object"
Success a -> do
new <- changeObjectAt a xs f
return $ Object $ M.insert x new o
changeObjectAt (Array a) (x:xs) f =
case V.findIndex (matchObjectId x) a of
Nothing -> Left $ UnknownPatchError $ "Can not find item with id " <> T.pack (show x) <> " in the array"
Just index -> do
new <- changeObjectAt (a V.! index) xs f
return $ Array $ a V.// [(index, new)]
changeObjectAt _ _ _ = Left $ UnknownPatchError "Can not descend into primitive values"
matchObjectId :: Text -> Value -> Bool
matchObjectId itemId (Object o) = Just (String itemId) == M.lookup "id" o
matchObjectId _ _ = False
resolvePathIn :: Path -> Value -> Maybe Value
resolvePathIn path = go (pathElements path)
where
go [] value = Just value
go [""] value = Just value
go (x:xs) (Object o) =
case parse (const $ o .: x) o of
Error _ -> Nothing
Success a -> go xs a
go (x:xs) (Array a) =
maybe Nothing (go xs) $ V.find (matchObjectId x) a
go _ _ = Nothing
opOT :: Value -> Operation -> Operation -> Maybe Operation
opOT content base op
| base == op = Nothing
| not ((opPath base `isPrefixOf` opPath op) || (opPath op `isPrefixOf` opPath base)) =
Just op
| otherwise = case base of
Set{..} -> setOT opPath
Splice{..} -> spliceOT opPath
where
setOT path = case op of
Set{..}
| path == opPath -> Just op
| path `isPrefixOf` opPath -> Nothing
| otherwise -> Just op
Splice{..}
| path == opPath -> Nothing
| path `isPrefixOf` opPath -> Nothing
| otherwise -> Just op
spliceOT path = case op of
Set{..}
| path == opPath -> Just op
| path `isPrefixOf` opPath -> onlyIfPresent opPath
| otherwise -> Just op
Splice{..}
| path == opPath -> spliceOnSplice base op
| path `isPrefixOf` opPath -> onlyIfPresent opPath
| otherwise -> Nothing
onlyIfPresent path = case resolvePathIn path content of
Nothing -> Nothing
Just _ -> Just op
(Path a) `isPrefixOf` (Path b) = a `T.isPrefixOf` b
spliceOnSplice op1 op2
| opIndex op1 + opRemove op1 <= opIndex op2
= Just $ op2 { opIndex = opIndex op2 + (length $ opInsert op1) opRemove op2 }
| opIndex op2 + opRemove op2 < opIndex op1
= Just op2
| otherwise = Nothing
rebaseOperation :: Value -> Operation -> [Patch] -> Maybe Operation
rebaseOperation _ op [] = Just op
rebaseOperation content op (x:xs) = case applyOperation content (patchOperation x) of
Left e -> error $ "Unexpected failure: " ++ (show e)
Right newContent -> case opOT newContent (patchOperation x) op of
Nothing -> Nothing
Just op' -> rebaseOperation newContent op' xs