packages feed

ot (empty) → 0.1.1.1

raw patch · 9 files changed

+530/−0 lines, 9 filesdep +QuickCheckdep +aesondep +attoparsecsetup-changed

Dependencies added: QuickCheck, aeson, attoparsec, base, binary, ot, test-framework, test-framework-quickcheck2, text

Files

+ LICENSE view
@@ -0,0 +1,20 @@+The MIT License (MIT)+Copyright © 2011-2012 Tim Baumann, http://timbaumann.info++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the “Software”), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ot.cabal view
@@ -0,0 +1,52 @@+Name:                ot+Version:             0.1.1.1+Synopsis:            Real-time collaborative editing with Operational Transformation+-- A longer description of the package.+-- Description:         +Homepage:            https://github.com/timjb/ot.hs+License:             MIT+License-file:        LICENSE+Author:              Tim Baumann+Maintainer:          tim@timbaumann.info++-- A copyright notice.+-- Copyright:           ++Category:            Text+Build-type:          Simple++-- Extra files to be distributed with the package, such as examples or+-- a README.+-- Extra-source-files:  ++-- Constraint on the version of Cabal needed to build this package.+Cabal-version:         >= 1.8+++Library+  Ghc-options:         -Wall+  Hs-source-dirs:      src+  Exposed-modules:     Control.OperationalTransformation, Control.OperationalTransformation.Text, Control.OperationalTransformation.Properties, Control.OperationalTransformation.Client, Control.OperationalTransformation.Server+  Build-depends:       base >= 4 && < 5,+                       text >= 0.11.1 && < 0.12,+                       aeson >= 0.6.0.2 && < 0.7,+                       attoparsec >= 0.10.1.1 && < 1,+                       QuickCheck >= 2.4.2 && < 2.6,+                       binary >= 0.5.1.0++  -- Modules not exported by this package.+  -- Other-modules:       ++Test-suite tests+  Ghc-options:         -Wall -rtsopts+  Hs-source-dirs:      test+  type:                exitcode-stdio-1.0+  main-is:             TestSuite.hs+  Build-depends:       ot,+                       QuickCheck,+                       base,+                       text,+                       aeson,+                       test-framework >= 0.6 && < 0.7,+                       test-framework-quickcheck2 >= 0.2.12.2 && < 0.3,+                       binary >= 0.5.1.0
+ src/Control/OperationalTransformation.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies #-}++module Control.OperationalTransformation+  ( OTOperation (..)+  , OTComposableOperation (..)+  , OTSystem (..)+  ) where++import Control.Monad (foldM)+import Control.Monad.Instances ()++class OTOperation op where+  -- | Transforms two concurrent operations /a/ and /b/, producing /a'/ and /b'/ +  -- such that @b' ∘ a == a' ∘ b@.+  transform :: op -> op -> Either String (op, op)++class (OTOperation op) => OTComposableOperation op where+  -- | Composes two operations /a/ and /b/, producing /c/, such that /c/ has the+  -- same effect when applied to a document as applying /a/ and /b/ one after+  -- another.+  compose :: op -> op -> Either String op++class (OTOperation op) => OTSystem doc op where+  -- | Apply an operation to a document, producing a new document.+  apply :: op -> doc -> Either String doc++instance (OTOperation op) => OTOperation [op] where+  transform = transformList2+    where+      transformList1 o [] = return (o, [])+      transformList1 o (p:ps) = do+        (o', p') <- transform o p+        (o'', ps') <- transformList1 o' ps+        return (o'', p':ps')++      transformList2 [] ps = return ([], ps)+      transformList2 (o:os) ps = do+        (o', ps') <- transformList1 o ps+        (os', ps'') <- transformList2 os ps'+        return (o':os', ps'')++instance (OTOperation op) => OTComposableOperation [op] where+  compose a b = return $ a ++ b++instance (OTSystem doc op) => OTSystem doc [op] where+  apply = flip $ foldM $ flip apply+++instance (OTOperation a, OTOperation b) => OTOperation (a, b) where+  transform (a1, a2) (b1, b2) = do+    (a1', b1') <- transform a1 b1+    (a2', b2') <- transform a2 b2+    return ((a1', a2'), (b1', b2'))++instance (OTComposableOperation a, OTComposableOperation b) => OTComposableOperation (a, b) where+  compose (a1, a2) (b1, b2) = do+    c1 <- compose a1 b1+    c2 <- compose a2 b2+    return (c1, c2)++instance (OTSystem doca a, OTSystem docb b) => OTSystem (doca, docb) (a, b) where+  apply (a, b) (doca, docb) = do+    doca' <- apply a doca+    docb' <- apply b docb+    return (doca', docb')+++instance (OTOperation a, OTOperation b, OTOperation c) => OTOperation (a, b, c) where+  transform (a1, a2, a3) (b1, b2, b3) = do+    (a1', b1') <- transform a1 b1+    (a2', b2') <- transform a2 b2+    (a3', b3') <- transform a3 b3+    return ((a1', a2', a3'), (b1', b2', b3'))++instance (OTComposableOperation a, OTComposableOperation b, OTComposableOperation c) => OTComposableOperation (a, b, c) where+  compose (a1, a2, a3) (b1, b2, b3) = do+    c1 <- compose a1 b1+    c2 <- compose a2 b2+    c3 <- compose a3 b3+    return (c1, c2, c3)++instance (OTSystem doca a, OTSystem docb b, OTSystem docc c) => OTSystem (doca, docb, docc) (a, b, c) where+  apply (a, b, c) (doca, docb, docc) = do+    doca' <- apply a doca+    docb' <- apply b docb+    docc' <- apply c docc+    return (doca', docb', docc')+++instance (OTOperation a, OTOperation b, OTOperation c, OTOperation d) => OTOperation (a, b, c, d) where+  transform (a1, a2, a3, a4) (b1, b2, b3, b4) = do+    (a1', b1') <- transform a1 b1+    (a2', b2') <- transform a2 b2+    (a3', b3') <- transform a3 b3+    (a4', b4') <- transform a4 b4+    return ((a1', a2', a3', a4'), (b1', b2', b3', b4'))++instance (OTComposableOperation a, OTComposableOperation b, OTComposableOperation c, OTComposableOperation d) => OTComposableOperation (a, b, c, d) where+  compose (a1, a2, a3, a4) (b1, b2, b3, b4) = do+    c1 <- compose a1 b1+    c2 <- compose a2 b2+    c3 <- compose a3 b3+    c4 <- compose a4 b4+    return (c1, c2, c3, c4)++instance (OTSystem doca a, OTSystem docb b, OTSystem docc c, OTSystem docd d) => OTSystem (doca, docb, docc, docd) (a, b, c, d) where+  apply (a, b, c, d) (doca, docb, docc, docd) = do+    doca' <- apply a doca+    docb' <- apply b docb+    docc' <- apply c docc+    docd' <- apply d docd+    return (doca', docb', docc', docd')+++instance (OTOperation a, OTOperation b, OTOperation c, OTOperation d, OTOperation e) => OTOperation (a, b, c, d, e) where+  transform (a1, a2, a3, a4, a5) (b1, b2, b3, b4, b5) = do+    (a1', b1') <- transform a1 b1+    (a2', b2') <- transform a2 b2+    (a3', b3') <- transform a3 b3+    (a4', b4') <- transform a4 b4+    (a5', b5') <- transform a5 b5+    return ((a1', a2', a3', a4', a5'), (b1', b2', b3', b4', b5'))++instance (OTComposableOperation a, OTComposableOperation b, OTComposableOperation c, OTComposableOperation d, OTComposableOperation e) => OTComposableOperation (a, b, c, d, e) where+  compose (a1, a2, a3, a4, a5) (b1, b2, b3, b4, b5) = do+    c1 <- compose a1 b1+    c2 <- compose a2 b2+    c3 <- compose a3 b3+    c4 <- compose a4 b4+    c5 <- compose a5 b5+    return (c1, c2, c3, c4, c5)++instance (OTSystem doca a, OTSystem docb b, OTSystem docc c, OTSystem docd d, OTSystem doce e) => OTSystem (doca, docb, docc, docd, doce) (a, b, c, d, e) where+  apply (a, b, c, d, e) (doca, docb, docc, docd, doce) = do+    doca' <- apply a doca+    docb' <- apply b docb+    docc' <- apply c docc+    docd' <- apply d docd+    doce' <- apply e doce+    return (doca', docb', docc', docd', doce')
+ src/Control/OperationalTransformation/Client.hs view
@@ -0,0 +1,66 @@+module Control.OperationalTransformation.Client+  ( ClientState (..)+  , initialClientState+  , applyClient+  , applyServer+  , serverAck+  ) where++import Control.OperationalTransformation++-- | At every moment, the client is in one of three states.+data ClientState op+  -- | All of the client's operations have been acknowledged by the server.+  = ClientSynchronized+  -- | The client has sent an operation to the server and it is still waiting+  -- for an acknowledgement.+  | ClientWaiting op+  -- | The client is waiting for an acknowledgement for a pending operation and+  -- the client is buffering local changes.+  | ClientWaitingWithBuffer op op+  deriving (Eq, Show, Read)++-- | The state a newly connected client starts in (synonym for+-- 'ClientSynchronized').+initialClientState :: ClientState op+initialClientState = ClientSynchronized++-- | Handle user-generated operations.+applyClient :: (OTComposableOperation op)+            => ClientState op+            -> op+            -- ^ The operation the user has performed on the local document.+            -> Either String (Bool, ClientState op)+            -- ^ Whether to send the operation to the server and the new client+            -- state (or an error).+applyClient ClientSynchronized op = Right (True, ClientWaiting op)+applyClient (ClientWaiting w) op = Right (False, ClientWaitingWithBuffer w op)+applyClient (ClientWaitingWithBuffer w b) op = case compose b op of+  Left err -> Left $ "operations couldn't be composed: " ++ err+  Right b' -> Right (False, ClientWaitingWithBuffer w b')++-- | Handle incoming operations from the server.+applyServer :: (OTComposableOperation op)+            => ClientState op+            -> op+            -> Either String (op, ClientState op)+            -- ^ The transformed operation that must be applied to local+            -- document and the new state (or an error).+applyServer ClientSynchronized op = Right (op, ClientSynchronized)+applyServer (ClientWaiting w) op = case transform w op of+  Left err -> Left $ "transform failed: " ++ err+  Right (w', op') -> Right (op', ClientWaiting w')+applyServer (ClientWaitingWithBuffer w b) op = case transform w op of+  Left err -> Left $ "transform failed: " ++ err+  Right (w', op') -> case transform b op' of+    Left err -> Left $ "transform failed: " ++ err+    Right (b', op'') -> Right (op'', ClientWaitingWithBuffer w' b')++-- | Handle acknowledgements.+serverAck :: ClientState op+          -> Maybe (Maybe op, ClientState op)+          -- ^ An optional operation that must be sent to the server if present+          -- and the new state.+serverAck ClientSynchronized            = Nothing+serverAck (ClientWaiting _)             = Just (Nothing, ClientSynchronized)+serverAck (ClientWaitingWithBuffer _ b) = Just (Just b, ClientWaiting b)
+ src/Control/OperationalTransformation/Properties.hs view
@@ -0,0 +1,45 @@+module Control.OperationalTransformation.Properties+  ( prop_compose_apply+  , prop_transform_apply+  ) where++import Control.OperationalTransformation+import Test.QuickCheck hiding (Result, reason)+import Test.QuickCheck.Property+import Control.Monad (liftM2, liftM3)++(==?) :: (Eq a, Show a) => a -> a -> Result+a ==? b | a == b    = succeeded+        | otherwise = failed { reason = "expected " ++ show a ++ " to be " ++ show b }++eitherProperty :: (Either String a) -> (a -> Property) -> Property+eitherProperty (Left err) _ = property $ failed { reason = err }+eitherProperty (Right res) prop = prop res++-- | @(b ∘ a)(d) = a(b(d))@ where /a/ and /b/ are two consecutive operations+-- and /d/ is the initial document.+prop_compose_apply :: (OTSystem doc op, OTComposableOperation op, Arbitrary doc, Show doc, Eq doc)+                   => (doc -> Gen op) -> Property+prop_compose_apply genOperation = do+  doc <- arbitrary+  a <- genOperation doc+  eitherProperty (apply a doc) $ \doc' -> do+    b <- genOperation doc'+    eitherProperty (apply b doc') $ \doc'' -> do+      eitherProperty (compose a b) $ \ab -> do+        property $ Right doc'' ==? apply ab doc++-- | @b'(a(d)) = b'(a(d))@ where /a/ and /b/ are random operations, /d/ is the+-- initial document and @(a', b') = transform(a, b)@.+prop_transform_apply :: (OTSystem doc op, Arbitrary doc, Show doc, Eq doc)+                     => (doc -> Gen op)+                     -> Property+prop_transform_apply genOperation = do+  doc <- arbitrary+  a <- genOperation doc+  b <- genOperation doc+  let res1 = liftM3 (,,) (apply a doc) (apply b doc) (transform a b)+  eitherProperty res1 $ \(doca, docb, (a', b')) -> do+    let res2 = liftM2 (,) (apply b' doca) (apply a' docb)+    eitherProperty res2 $ \(docab', docba') ->+      property $ docab' ==? docba'
+ src/Control/OperationalTransformation/Server.hs view
@@ -0,0 +1,44 @@+module Control.OperationalTransformation.Server+  ( Revision+  , ServerState (..)+  , initialServerState+  , applyOperation+  ) where++import Control.OperationalTransformation+import Control.Monad (foldM)++type Revision = Integer++-- | The server keeps the current revision number and a list of previous+-- operations to transform incoming operations against.+data ServerState doc op = ServerState Revision doc [op]++initialServerState :: doc -> ServerState doc op+initialServerState doc = ServerState 0 doc []++-- | Handles incoming operations.+applyOperation :: (OTSystem doc op)+               => ServerState doc op+               -> Revision+               -- ^ The latest operation that the client has received from the server when it sent the operation.+               -> op+               -- ^ The operation received from the client.+               -> Either String (op, ServerState doc op)+               -- ^ The operation to broadcast to all connected clients+               -- (except the client which has created the operation; that+               -- client must be sent an acknowledgement) and the new state+               -- (or an error).+applyOperation (ServerState rev doc ops) oprev op = do+  concurrentOps <- if oprev > rev || rev - oprev > fromIntegral (length ops)+    then Left "unknown revision number"+    else Right $ take (fromInteger $ rev - oprev) ops+  op' <- foldM transformFst op (reverse concurrentOps)+  doc' <- case apply op' doc of+    Left err -> Left $ "apply failed: " ++ err+    Right d -> Right d+  return $ (op', ServerState (rev+1) doc' (op':ops))+  where+    transformFst a b = case transform a b of+      Left err -> Left $ "transform failed: " ++ err+      Right (a', _) -> Right a'
+ src/Control/OperationalTransformation/Text.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE OverloadedStrings, MultiParamTypeClasses, GeneralizedNewtypeDeriving, DeriveDataTypeable #-}++module Control.OperationalTransformation.Text+  ( Action (..)+  , TextOperation (..)+  , invertOperation+  ) where++import Control.OperationalTransformation+import qualified Data.Text as T+import Data.Monoid (mappend)+import Data.Aeson (Value (..), FromJSON (..), ToJSON (..))+import Data.Binary (Binary (..), putWord8, getWord8)+import Data.Attoparsec.Number (Number (..))+import Data.Typeable (Typeable)+import Data.Text (pack, unpack)+import Control.Applicative ((<$>))+++-- | An action changes the text at the current position or advances the cursor.+data Action = Retain !Int    -- ^ Skip the next n characters.+            | Insert !T.Text -- ^ Insert the given text at the current position.+            | Delete !Int    -- ^ Delete the next n characters.+            deriving (Eq, Read, Show, Typeable)++instance Binary Action where+  put (Retain n) = putWord8 0 >> put n+  put (Insert i) = putWord8 1 >> put (unpack i)+  put (Delete n) = putWord8 2 >> put n+  get = do+    t <- getWord8+    case t of+      0 -> Retain <$> get+      1 -> Insert . pack <$> get+      _ -> Delete <$> get++instance ToJSON Action where+  toJSON (Retain n) = Number $ I (toInteger n)+  toJSON (Insert t) = String t+  toJSON (Delete n) = Number $ I (toInteger (-n))++instance FromJSON Action where+  parseJSON (Number (I n)) | n > 0 = return $ Retain (fromInteger n)+                           | n < 0 = return $ Delete (fromInteger (-n))+  parseJSON (String i) = return $ Insert i+  parseJSON _ = fail "expected a non-zero integer or a string"++-- | An edit on plain text documents. An operation consists of multiple actions+-- that change the document at the current cursor position or advance the+-- cursor. After applying all actions, the cursor must be at the end of the+-- document.+newtype TextOperation = TextOperation [Action] deriving (Eq, Read, Show, Binary, Typeable, FromJSON, ToJSON)++addRetain :: Int -> [Action] -> [Action]+addRetain n (Retain m : xs) = Retain (n+m) : xs+addRetain n xs = Retain n : xs++addInsert :: T.Text -> [Action] -> [Action]+addInsert s (Insert t : xs) = Insert (t `mappend` s) : xs+addInsert s xs = Insert s : xs++addDelete :: Int -> [Action] -> [Action]+addDelete n (Delete m : xs) = Delete (n+m) : xs+addDelete n xs = Delete n : xs++instance OTOperation TextOperation where+  transform (TextOperation o1) (TextOperation o2) = both (TextOperation . reverse) `fmap` loop o1 o2 [] []+    where+      both :: (a -> b) -> (a, a) -> (b, b)+      both f (a, b) = (f a, f b)++      loop [] [] xs ys = Right (xs, ys)+      loop aa@(a:as) bb@(b:bs) xs ys = case (a, b) of+        (Insert i, _) -> loop as bb (addInsert i xs) (addRetain (T.length i) ys)+        (_, Insert i) -> loop aa bs (addRetain (T.length i) xs) (addInsert i ys)+        (Retain n, Retain m) -> case compare n m of+          LT -> loop as (Retain (m-n) : bs) (addRetain n xs) (addRetain n ys)+          EQ -> loop as bs (addRetain n xs) (addRetain n ys)+          GT -> loop (Retain (n-m) : as) bs (addRetain m xs) (addRetain m ys)+        (Delete n, Delete m) -> case compare n m of+          LT -> loop as (Delete (m-n) : bs) xs ys+          EQ -> loop as bs xs ys+          GT -> loop (Delete (n-m) : as) bs xs ys+        (Retain r, Delete d) -> case compare r d of+          LT -> loop as (Delete (d-r) : bs) xs (addDelete r ys)+          EQ -> loop as bs xs (addDelete d ys)+          GT -> loop (Retain (r-d) : as) bs xs (addDelete d ys)+        (Delete d, Retain r) -> case compare d r of+          LT -> loop as (Retain (r-d) : bs) (addDelete d xs) ys+          EQ -> loop as bs (addDelete d xs) ys+          GT -> loop (Delete (d-r) : as) bs (addDelete r xs) ys+      loop [] (Insert i : bs) xs ys = loop [] bs (addRetain (T.length i) xs) (addInsert i ys)+      loop (Insert i : as) [] xs ys = loop as [] (addInsert i xs) (addRetain (T.length i) ys)+      loop _ _ _ _ = Left "the operations couldn't be transformed because they haven't been applied to the same document"++instance OTComposableOperation TextOperation where+  compose (TextOperation o1) (TextOperation o2) = (TextOperation . reverse) `fmap` loop o1 o2 []+    where+      loop [] [] xs = Right xs+      loop aa@(a:as) bb@(b:bs) xs = case (a, b) of+        (Delete d, _) -> loop as bb (addDelete d xs)+        (_, Insert i) -> loop aa bs (addInsert i xs)+        (Retain n, Retain m) -> case compare n m of+          LT -> loop as (Retain (m-n) : bs) (addRetain n xs)+          EQ -> loop as bs (addRetain n xs)+          GT -> loop (Retain (n-m) : as) bs (addRetain m xs)+        (Retain r, Delete d) -> case compare r d of+          LT -> loop as (Delete (d-r) : bs) (addDelete r xs)+          EQ -> loop as bs (addDelete d xs)+          GT -> loop (Retain (r-d) : as) bs (addDelete d xs)+        (Insert i, Retain m) -> case compare (T.length i) m of+          LT -> loop as (Retain (m - T.length i) : bs) (addInsert i xs)+          EQ -> loop as bs (addInsert i xs)+          GT -> let (before, after) = T.splitAt m i+                in loop (Insert after : as) bs (addInsert before xs)+        (Insert i, Delete d) -> case compare (T.length i) d of+          LT -> loop as (Delete (d - T.length i) : bs) xs+          EQ -> loop as bs xs+          GT -> loop (Insert (T.drop d i) : as) bs xs+      loop (Delete d : as) [] xs = loop as [] (addDelete d xs)+      loop [] (Insert i : bs) xs = loop [] bs (addInsert i xs)+      loop _ _ _ = Left "the operations couldn't be composed since their lengths don't match"++instance OTSystem T.Text TextOperation where+  apply (TextOperation actions) input = loop actions input ""+    where+      loop [] "" ot = Right ot+      loop (op:ops) it ot = case op of+        Retain r -> if T.length it < r+          then Left "operation can't be applied to the document: operation is longer than the text"+          else let (before, after) = T.splitAt r it+               in loop ops after (ot `mappend` before)+        Insert i -> loop ops it (ot `mappend` i)+        Delete d -> if d > T.length it+          then Left "operation can't be applied to the document: operation is longer than the text"+          else loop ops (T.drop d it) ot+      loop _ _ _ = Left "operation can't be applied to the document: text is longer than the operation"++-- | Computes the inverse of an operation. Useful for implementing undo.+invertOperation :: TextOperation               -- ^ An operation.+                -> T.Text                      -- ^ Document before the operation was applied.+                -> Either String TextOperation+invertOperation (TextOperation actions) doc = loop actions doc []+  where+    loop (op:ops) text inv = case op of+      (Retain n) -> loop ops (T.drop n text) (Retain n : inv)+      (Insert i) -> loop ops text (Delete (T.length i) : inv)+      (Delete d) -> let (before, after) = T.splitAt d text+                    in loop ops after (Insert before : inv)+    loop [] "" inv = Right . TextOperation . reverse $ inv+    loop [] _ _ = Left "invert failed: text is longer than the operation"
+ test/TestSuite.hs view
@@ -0,0 +1,10 @@+import Test.Framework++import qualified Control.OperationalTransformation.Text.Tests+import qualified Control.OperationalTransformation.ClientServerTests++main :: IO ()+main = defaultMain+  [ Control.OperationalTransformation.Text.Tests.tests+  , Control.OperationalTransformation.ClientServerTests.tests+  ]