bytepatch-0.2.1: src/BytePatch/Pretty.hs
{-# LANGUAGE DataKinds, TypeFamilies, UndecidableInstances #-}
{-|
Convenience interface to enable defining edits at offsets with some optional
safety checks.
Redefines some types to enable us to easily leverage Aeson's generic JSON schema
deriving. That sadly means we can't use some of the interesting offset plumbing.
TODO I should definitely bite the bullet and use the plumbing now that it's so
cool.
-}
module BytePatch.Pretty
(
-- * Core types
CommonMultiEdits(..)
, MultiEdit(..)
, EditOffset(..)
-- * Convenience functions
, normalizeSimple
-- * Low-level interface
, applyBaseOffset
, listAlgebraConcatEtc
, normalize
) where
import BytePatch.Core
import BytePatch.Pretty.PatchRep
import qualified Data.ByteString as BS
import Data.Maybe ( fromMaybe )
import GHC.Generics ( Generic )
import GHC.Natural
type Bytes = BS.ByteString
-- | A list of 'MultiEdit's with some common configuration.
data CommonMultiEdits a = CommonMultiEdits
{ cmesBaseOffset :: Maybe (SeekRep 'CursorSeek)
-- ^
-- The base offset from which all offsets are located. An actual offset is
-- calculated by adding the base offset to an offset. Actual offsets below 0
-- are invalid, meaning for an offset @o@ with base offset @bo@, the actual
-- offset is only valid when @o >= bo@. Negative base offsets are allowed.
, cmesEdits :: [MultiEdit 'CursorSeek a]
} deriving (Eq, Show, Generic, Functor, Foldable, Traversable)
-- | A single edit to be applied at a list of offsets.
data MultiEdit (s :: SeekKind) a = MultiEdit
{ meData :: a -- ^ The value (e.g. bytes, text) to add.
, meAt :: [EditOffset s a] -- ^ Offsets to apply edit at.
} deriving (Generic, Functor, Foldable, Traversable)
deriving instance (Eq (SeekRep s), Eq a) => Eq (MultiEdit s a)
deriving instance (Show (SeekRep s), Show a) => Show (MultiEdit s a)
-- | An edit offset, with metadata to use for preparing and applying the edit.
data EditOffset (s :: SeekKind) a = EditOffset
{ eoOffset :: SeekRep s
-- ^ Stream offset for edit.
, eoAbsOffset :: Maybe (SeekRep 'AbsSeek)
-- ^ Absolute stream offset for edit. Used for checking against actual offset.
, eoMaxLength :: Maybe Natural
-- ^ Maximum number of bytes allowed to write at this offset.
, eoEditMeta :: Maybe (EditMeta a)
-- ^ Optional apply time metadata for the edit at this offset.
} deriving (Generic, Functor, Foldable, Traversable)
deriving instance (Eq (SeekRep s), Eq a) => Eq (EditOffset s a)
deriving instance (Show (SeekRep s), Show a) => Show (EditOffset s a)
-- | Normalize a list of 'CommonMultiEdits's, discarding everything on error.
normalizeSimple :: PatchRep a => [CommonMultiEdits a] -> Maybe [Patch 'AbsSeek Bytes]
normalizeSimple cmess =
let (p, errs) = listAlgebraConcatEtc . map applyBaseOffset $ cmess
in case errs of
_:_ -> Nothing
[] -> normalize p
-- Drops no info, not easy to consume.
applyBaseOffset
:: CommonMultiEdits a
-> (Integer, [(MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])])
applyBaseOffset cmes =
(baseOffset, recalculateMultiPatchOffsets baseOffset (cmesEdits cmes))
where baseOffset = fromMaybe 0 (cmesBaseOffset cmes)
-- lmao this sucks. generalisation bad
listAlgebraConcatEtc :: [(a, [(b, [c])])] -> ([b], [(c, a)])
listAlgebraConcatEtc = mconcat . map go
where
go (baseOffset, inps) = tuplemconcat (map (go' baseOffset) inps)
go' x (mp, offs) = (mp, map (\o -> (o, x)) offs)
tuplemconcat = foldr (\(a, bs) (as, bs') -> (a:as, bs <> bs')) ([], mempty)
recalculateMultiPatchOffsets
:: Integer
-> [MultiEdit 'CursorSeek a]
-> [(MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])]
recalculateMultiPatchOffsets baseOffset = map go
where
go :: MultiEdit 'CursorSeek a -> (MultiEdit 'AbsSeek a, [EditOffset 'CursorSeek a])
go me =
let (osRecalculated, osInvalid) = recalculateOffsets baseOffset (meAt me)
in (me { meAt = osRecalculated }, osInvalid)
recalculateOffsets
:: Integer
-> [EditOffset 'CursorSeek a]
-> ([EditOffset 'AbsSeek a], [EditOffset 'CursorSeek a])
recalculateOffsets baseOffset = partitionMaybe go
where
go o = let actualOffset = baseOffset + eoOffset o
in case tryIntegerToNatural actualOffset of
Nothing -> Nothing
Just actualOffset' -> Just $ o { eoOffset = actualOffset' }
tryIntegerToNatural :: Integer -> Maybe Natural
tryIntegerToNatural n | n < 0 = Nothing
| otherwise = Just $ naturalFromInteger n
normalize :: PatchRep a => [MultiEdit 'AbsSeek a] -> Maybe [Patch 'AbsSeek Bytes]
normalize xs = concat <$> mapM go xs
where go (MultiEdit contents os) = mapM (tryMakeSingleReplace contents) os
-- TODO now can error with "[expected] content has no valid patch rep"
tryMakeSingleReplace :: PatchRep a => a -> EditOffset 'AbsSeek a -> Maybe (Patch 'AbsSeek Bytes)
tryMakeSingleReplace contents (EditOffset os maos mMaxLen mMeta) =
case toPatchRep contents of
Left errStr -> error errStr -- TODO
Right bs ->
if offsetIsCorrect
then case mMaxLen of
Just maxLen -> if BS.length bs > fromIntegral maxLen then Nothing else overwrite bs
Nothing -> overwrite bs
else Nothing
where
overwrite bs = case traverse toPatchRep meta of
Left errStr -> error errStr -- TODO
Right meta' ->
Just $ Patch os $ Edit { editData = bs
, editMeta = meta' }
meta = fromMaybe (EditMeta Nothing Nothing) mMeta
offsetIsCorrect = case maos of Nothing -> True
Just aos -> os == aos
--------------------------------------------------------------------------------
-- | Map a failable function over a list, retaining "failed" 'Nothing' results.
partitionMaybe :: (a -> Maybe b) -> [a] -> ([b], [a])
partitionMaybe f =
foldr (\x -> maybe (mapSnd (x:)) (\y -> mapFst (y:)) (f x)) ([], [])
mapFst :: (a -> c) -> (a, b) -> (c, b)
mapFst f (a, b) = (f a, b)
mapSnd :: (b -> c) -> (a, b) -> (a, c)
mapSnd f (a, b) = (a, f b)