safecopy

Intro

SafeCopy extends the parsing and serialization capabilities of Data.Serialize to include nested version control. Nested version control means that you can change the defintion and binary format of a type nested deep within other types without problems.

SafeCopy and acid-state.

The packages safecopy and acid-state go hand-in-hand. You’ll never lose your data with acid-state but any changes in your data schema can easily make it unreadable until those schema changes are reverted. SafeCopy picks up the ball from here and delivers it securely over the goal line.

Extension and migrations

Let’s consider a simple example where we migrate from one type to another.

-- We thought Int would be enough but doesn't seem to be the case.
-- To be safe, let's use Integers instead.
data OldType = OldType Int     deriving (Show)
data NewType = NewType Integer deriving (Show)

-- Notice that putCopy/getCopy are pretty much identical in both instances.
instance SafeCopy OldType where
  putCopy (OldType n) = contain $ safePut n
  getCopy = contain $ OldType <$> safeGet

-- A key feature here is that we don't mention the type of the previous version.
instance SafeCopy NewType where
  version = 2
  kind = extension
  putCopy (NewType n) = contain $ safePut n
  getCopy = contain $ NewType <$> safeGet

instance Migrate NewType where
  type MigrateFrom NewType = OldType
  migrate (OldType n) = NewType (fromIntegral n)

-- note: b should be (Either String b)
-- keeping just b because it is easier to read
safeCoerce :: (SafeCopy a, SafeCopy b) => a -> b
safeCoerce a
  = let serialized = runPut (safePut a)
    in runGet safeGet serialized

The above code allows us to parse old serialization in the following way:

> safeCoerce (OldType 1337) :: NewType
NewType 1337

> safeCoerce (NewType 1337) :: NewType
NewType 1337

Chains

Migrations are not limited to a single step. You can build up long chains of migrations and SafeCopy will dutifully do the migrations for you.

data X1 = X1 Word8  deriving (Show)
data X2 = X2 Word16 deriving (Show)
data X3 = X3 Word32 deriving (Show)

instance SafeCopy X1 where
  putCopy (X1 n) = contain (safePut n); getCopy = contain $ X1 <$> safeGet
instance SafeCopy X2 where
  version = 2
  kind = extension
  putCopy (X1 n) = contain (safePut n); getCopy = contain $ X1 <$> safeGet
instance SafeCopy X2 where
  version = 3
  kind = extension
  putCopy (X1 n) = contain (safePut n); getCopy = contain $ X1 <$> safeGet

instance Migrate X2 where
  type MigrateFrom X2 = X1
  migrate (X1 n) = X2 (fromIntegral n)

instance Migrate X3 where
  type MigrateFrom X3 = X2
  migrate (X2 n) = X3 (fromIntegral n)

We now have a chain of extensions: X1 -> X2 -> X3.

> safeCoerce (X1 42) :: X2
X2 42

> safeCoerce (X1 42) :: X3
X3 42

> safeCoerce (X2 42) :: X3
X3 42

> safeCoerce (X3 42) :: X3
X3 42

Branches

Types can only extend a single type but each type can have multiple extensions. Consider the following code:

data OldType = OldType Int deriving (Show)
data LeftBranch = LeftBranch String deriving (Show)
data RightBranch = RightBranch Integer deriving (Show)

instance SafeCopy OldType where
  putCopy (OldType n) = contain $ safePut n
  getCopy = contain $ OldType <$> safeGet

instance SafeCopy LeftBranch where
  version = 2
  kind = extension
  putCopy (LeftBranch str) = contain $ safePut str
  getCopy = contain $ LeftBranch <$> safeGet

-- Notice how both LeftBranch and RightBranch have the same version.
-- This would result in a run-time error if we marked either one as
-- the extension of the other.
instance SafeCopy RightBranch where
  version = 2
  kind = extension
  putCopy (RightBranch n) = contain $ safePut str
  getCopy = contain $ RightBranch <$> safeGet

instance Migrate LeftBranch where
  type MigrateFrom LeftBranch = OldType
  migrate (OldType n) = LeftBranch (show n)

instance Migrate RightBranch where
  type MigrateFrom RightBranch = OldType
  migrate (OldType n) = RightBranch (fromIntegral n)

The following coercions are possible:

> safeCoerce (OldType 5) :: LeftBranch
LeftBranch "5"

> safeCoerce (OldType 5) :: RightBranch
RightBranch 5