packages feed

symbiote 0.0.0 → 0.0.0.1

raw patch · 5 files changed

+260/−140 lines, 5 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Test.Serialization.Symbiote: instance (GHC.Classes.Eq a, GHC.Classes.Eq (Test.Serialization.Symbiote.Operation a)) => GHC.Classes.Eq (Test.Serialization.Symbiote.EitherOp a)
- Test.Serialization.Symbiote: instance (GHC.Show.Show a, GHC.Show.Show (Test.Serialization.Symbiote.Operation a)) => GHC.Show.Show (Test.Serialization.Symbiote.EitherOp a)
- Test.Serialization.Symbiote: instance Data.String.IsString Test.Serialization.Symbiote.Topic
- Test.Serialization.Symbiote: instance GHC.Classes.Eq Test.Serialization.Symbiote.Topic
- Test.Serialization.Symbiote: instance GHC.Classes.Ord Test.Serialization.Symbiote.Topic
- Test.Serialization.Symbiote: instance GHC.Show.Show Test.Serialization.Symbiote.Topic
- Test.Serialization.Symbiote: instance Test.Serialization.Symbiote.SymbioteOperation a => Test.Serialization.Symbiote.Symbiote a (Test.Serialization.Symbiote.EitherOp a)
- Test.Serialization.Symbiote.Aeson: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.ToJSON.ToJSON (Test.Serialization.Symbiote.Operation a), Data.Aeson.Types.FromJSON.FromJSON (Test.Serialization.Symbiote.Operation a), Test.Serialization.Symbiote.SymbioteOperation a) => Test.Serialization.Symbiote.Symbiote a Data.Aeson.Types.Internal.Value
- Test.Serialization.Symbiote.Cereal: instance (Data.Serialize.Serialize a, Data.Serialize.Serialize (Test.Serialization.Symbiote.Operation a), Test.Serialization.Symbiote.SymbioteOperation a) => Test.Serialization.Symbiote.Symbiote a Data.ByteString.Internal.ByteString
- Test.Serialization.Symbiote.Cereal.Lazy: instance (Data.Serialize.Serialize a, Data.Serialize.Serialize (Test.Serialization.Symbiote.Operation a), Test.Serialization.Symbiote.SymbioteOperation a) => Test.Serialization.Symbiote.Symbiote a Data.ByteString.Lazy.Internal.ByteString
+ Test.Serialization.Symbiote: instance (GHC.Classes.Eq a, GHC.Classes.Eq (Test.Serialization.Symbiote.Core.Operation a)) => GHC.Classes.Eq (Test.Serialization.Symbiote.EitherOp a)
+ Test.Serialization.Symbiote: instance (GHC.Show.Show a, GHC.Show.Show (Test.Serialization.Symbiote.Core.Operation a)) => GHC.Show.Show (Test.Serialization.Symbiote.EitherOp a)
+ Test.Serialization.Symbiote: instance Test.Serialization.Symbiote.Core.SymbioteOperation a => Test.Serialization.Symbiote.Core.Symbiote a (Test.Serialization.Symbiote.EitherOp a)
+ Test.Serialization.Symbiote.Aeson: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.ToJSON.ToJSON (Test.Serialization.Symbiote.Core.Operation a), Data.Aeson.Types.FromJSON.FromJSON (Test.Serialization.Symbiote.Core.Operation a), Test.Serialization.Symbiote.Core.SymbioteOperation a) => Test.Serialization.Symbiote.Core.Symbiote a Data.Aeson.Types.Internal.Value
+ Test.Serialization.Symbiote.Cereal: instance (Data.Serialize.Serialize a, Data.Serialize.Serialize (Test.Serialization.Symbiote.Core.Operation a), Test.Serialization.Symbiote.Core.SymbioteOperation a) => Test.Serialization.Symbiote.Core.Symbiote a Data.ByteString.Internal.ByteString
+ Test.Serialization.Symbiote.Cereal.Lazy: instance (Data.Serialize.Serialize a, Data.Serialize.Serialize (Test.Serialization.Symbiote.Core.Operation a), Test.Serialization.Symbiote.Core.SymbioteOperation a) => Test.Serialization.Symbiote.Core.Symbiote a Data.ByteString.Lazy.Internal.ByteString
+ Test.Serialization.Symbiote.Core: DoneGenerating :: GenerateSymbiote s
+ Test.Serialization.Symbiote.Core: Finished :: SymbioteProtocol a s
+ Test.Serialization.Symbiote.Core: GeneratedSymbiote :: s -> s -> GenerateSymbiote s
+ Test.Serialization.Symbiote.Core: MeGenerated :: a -> Operation a -> Maybe s -> SymbioteProtocol a s
+ Test.Serialization.Symbiote.Core: NotStarted :: SymbioteProtocol a s
+ Test.Serialization.Symbiote.Core: SymbioteGeneration :: Int -> SymbioteProtocol a s -> SymbioteGeneration a s
+ Test.Serialization.Symbiote.Core: SymbioteState :: Gen a -> Gen (Operation a) -> (a -> a -> Bool) -> Int -> TVar (SymbioteGeneration a s) -> (a -> s) -> (Operation a -> s) -> (s -> Maybe a) -> (s -> Maybe (Operation a)) -> (Operation a -> a -> a) -> SymbioteState s
+ Test.Serialization.Symbiote.Core: ThemGenerating :: Maybe (s, s) -> SymbioteProtocol a s
+ Test.Serialization.Symbiote.Core: Topic :: Text -> Topic
+ Test.Serialization.Symbiote.Core: [decode'] :: SymbioteState s -> s -> Maybe a
+ Test.Serialization.Symbiote.Core: [decodeOp'] :: SymbioteState s -> s -> Maybe (Operation a)
+ Test.Serialization.Symbiote.Core: [encode'] :: SymbioteState s -> a -> s
+ Test.Serialization.Symbiote.Core: [encodeOp'] :: SymbioteState s -> Operation a -> s
+ Test.Serialization.Symbiote.Core: [equal] :: SymbioteState s -> a -> a -> Bool
+ Test.Serialization.Symbiote.Core: [generateOp] :: SymbioteState s -> Gen (Operation a)
+ Test.Serialization.Symbiote.Core: [generate] :: SymbioteState s -> Gen a
+ Test.Serialization.Symbiote.Core: [generatedOperation] :: GenerateSymbiote s -> s
+ Test.Serialization.Symbiote.Core: [generatedValue] :: GenerateSymbiote s -> s
+ Test.Serialization.Symbiote.Core: [generation] :: SymbioteState s -> TVar (SymbioteGeneration a s)
+ Test.Serialization.Symbiote.Core: [maxSize] :: SymbioteState s -> Int
+ Test.Serialization.Symbiote.Core: [meGenOperation] :: SymbioteProtocol a s -> Operation a
+ Test.Serialization.Symbiote.Core: [meGenReceived] :: SymbioteProtocol a s -> Maybe s
+ Test.Serialization.Symbiote.Core: [meGenValue] :: SymbioteProtocol a s -> a
+ Test.Serialization.Symbiote.Core: [perform'] :: SymbioteState s -> Operation a -> a -> a
+ Test.Serialization.Symbiote.Core: [protocol] :: SymbioteGeneration a s -> SymbioteProtocol a s
+ Test.Serialization.Symbiote.Core: [size] :: SymbioteGeneration a s -> Int
+ Test.Serialization.Symbiote.Core: [themGen] :: SymbioteProtocol a s -> Maybe (s, s)
+ Test.Serialization.Symbiote.Core: class SymbioteOperation a => Symbiote a s
+ Test.Serialization.Symbiote.Core: class SymbioteOperation a where {
+ Test.Serialization.Symbiote.Core: data GenerateSymbiote s
+ Test.Serialization.Symbiote.Core: data SymbioteGeneration a s
+ Test.Serialization.Symbiote.Core: data SymbioteProtocol a s
+ Test.Serialization.Symbiote.Core: data SymbioteState s
+ Test.Serialization.Symbiote.Core: data family Operation a :: *;
+ Test.Serialization.Symbiote.Core: decode :: Symbiote a s => s -> Maybe a
+ Test.Serialization.Symbiote.Core: decodeOp :: Symbiote a s => s -> Maybe (Operation a)
+ Test.Serialization.Symbiote.Core: encode :: Symbiote a s => a -> s
+ Test.Serialization.Symbiote.Core: encodeOp :: Symbiote a s => Operation a -> s
+ Test.Serialization.Symbiote.Core: generateSymbiote :: forall s m. MonadIO m => SymbioteState s -> m (GenerateSymbiote s)
+ Test.Serialization.Symbiote.Core: getProgress :: MonadIO m => SymbioteState s -> m Float
+ Test.Serialization.Symbiote.Core: instance Data.String.IsString Test.Serialization.Symbiote.Core.Topic
+ Test.Serialization.Symbiote.Core: instance GHC.Classes.Eq Test.Serialization.Symbiote.Core.Topic
+ Test.Serialization.Symbiote.Core: instance GHC.Classes.Ord Test.Serialization.Symbiote.Core.Topic
+ Test.Serialization.Symbiote.Core: instance GHC.Show.Show Test.Serialization.Symbiote.Core.Topic
+ Test.Serialization.Symbiote.Core: newGeneration :: SymbioteGeneration a s
+ Test.Serialization.Symbiote.Core: newtype Topic
+ Test.Serialization.Symbiote.Core: perform :: SymbioteOperation a => Operation a -> a -> a
+ Test.Serialization.Symbiote.Core: runSymbioteT :: Monad m => SymbioteT s m () -> Bool -> m (Map Topic (SymbioteState s))
+ Test.Serialization.Symbiote.Core: type SymbioteT s m = ReaderT Bool (StateT (Map Topic (SymbioteState s)) m)
+ Test.Serialization.Symbiote.Core: }

Files

LICENSE view
@@ -1,4 +1,4 @@-Copyright Author name here (c) 2019+Copyright Athen Clark (c) 2019  All rights reserved. 
README.md view
@@ -1,8 +1,55 @@-# symbiote+# Symbiote  This project aims to be a network agnostic and data format agnostic serialization verifier - it's main purpose is to verify that _data_, _operations_ on that data, and _serialization_ of that data is all consistent for multiple platforms. This project defines a trivial protocol that can be re-implemented in any platform, over any network medium. -For an example, check out the tests.+## How the Protocol Works++It's a pretty simple idea:++![](https://github.com/athanclark/symbiote/raw/master/images/drawing-rendered.png)++### Setting++Basically, the test suite envolves two peers - we'll call them Peer A and Peer B, and they're connected+by some network. Likewise, they are both using (possibly different) systems, which we've denoted as Lang A+(like Haskell) and Lang B (like PureScript / JavaScript).++The goal of this suite is to ensure that the _same idea_ defined on these different platforms have the+_same representation_ - both operationally (i.e., QuickCheck laws) and through some encoding (like printing+the data to JSON or a ByteString).++In the diagram above, each system has some idea of what "Type T" looks and feels like, but could be completely+different implementations on each system. Again, though, we care about their _outcome_ being identical; so we+disambiguate the idea from the data.++### Protocol++The first step in the protocol is for the generating party (Peer A) to generate an instance of Type T, and an operation+on that type - this operation, too, needs a serialization implementation (but for most intents and purposes,+the implementation will be only for the scope of use with this test suite).++Peer A then does two things: it serializes both the operation and instance to the network-acceptable format, _and_+it "performs" the operation on that instance to get an _expected result_. Basically, Peer A is using Peer B as a+remote procedure call for that operation, and verifying that the result obtained remotely is identical to the+result obtained locally.++### Remote Procedure Call++As just stated, Peer A then sends the serialized instance and operation to Peer B over that network, where Peer B+decodes the data, and performs the operation. Peer B then encodes the result, and sends it back to Peer A.++### Verification++Peer A now receives the performed data from Peer B, and can now decode it, and test to see if it is equal to the+expected result. If it is, it will tell Peer B that it's "their turn" to generate data.+++All of this relies on QuickCheck to generate the data - with the size of the generated data increasing at each step,+until a maximum is reached (which is clarified in the documentation).++-------------++For an example, check out the tests - they are performed locally in an asynchronous channel, but achieve the right goal.
src/Test/Serialization/Symbiote.hs view
@@ -6,7 +6,6 @@   , ScopedTypeVariables   , NamedFieldPuns   , FlexibleContexts-  , GeneralizedNewtypeDeriving   , StandaloneDeriving   , UndecidableInstances   , FlexibleInstances@@ -14,24 +13,68 @@  {-| -The project operates as follows:+Module: Test.Serialization.Symbiote+Copyright: (c) 2019 Athan Clark+License: BSD-3-Style+Maintainer: athan.clark@gmail.com+Portability: GHC -Given two peers A and B and some communications transport T (utilizing a serialization format S),-and a data type Q with some set of operations on that data type Op_Q,-the following functions / procedures are assumed:+As an example, say you have some data type @TypeA@, and some encoding / decoding instance with Aeson+for that data type. Now, you've also got a few functions that work with that data type - @f :: TypeA -> TypeA@+and @g :: TypeA -> TypeA -> TypeA@, and you've also taken the time to write a proper 'Arbitrary' instance for @TypeA@. -tAB: the function communicates some data in S from peer A to peer B-tBA: the function communicates some data in S from peer B to peer A-encode :: Q -> S-decode :: S -> Q -- disregarding error handling+Your first order of business in making @TypeA@ a symbiote, is to first demonstrate what operations are supported by it: -And the following property should exist, from peer A's perspective:+> {-# LANGUAGE MultiparamTypeClasses, TypeFamilies #-}+>+> instance SymbioteOperation TypeA where+>   data Operation TypeA+>     = F+>     | G TypeA+>   perform op x = case op of+>     F -> f x+>     G y -> g y x -forall f in Op_Q, q in Q.-  f q == decode (tBA (f (tAB (encode q)))+You're also going to need to make sure your new data-family has appropriate serialization instances, as well: -where the left invocation of f occurs in peer A, and the right invocation occurs in peer B.+> instance ToJSON (Operation TypeA) where+>   toJSON op = case op of+>     F -> toJSON "f"+>     G x -> "g" .: x+>+> instance FromJSON (Operation TypeA) where+>   parseJSON json = getF <|> getG+>     where+>       getF = do+>         s <- parseJSON json+>         if s == "f"+>           then pure F+>           else fail "Not F"+>       getG = do+>         x <- json .: "g"+>         pure (G x) +Next, let's make @TypeA@ an instance of 'Symbiote':++> instance Symbiote TypeA Value where+>   encode = Aeson.toJSON+>   decode = Aeson.parseMaybe Aeson.parseJSON+>   encodeOp = Aeson.toJSON+>   decodeOp = Aeson.parseMaybe Aeson.parseJSON++this instance above actually works for any type that implements @ToJSON@ and @FromJSON@ - there's an orphan+definition in "Test.Serialization.Symbiote.Aeson".++Next, you're going to need to actually use this, by registering the type in a test suite:++> myFancyTestSuite :: SymbioteT Value IO ()+> myFancyTestSuite = register "TypeA" 100 (Proxy :: Proxy TypeA)++Lastly, you're going to need to actually run the test suite by attaching it to a network. The best way to+do that, is decide whether this peer will be the first or second peer to start the protocol, then use the+respective 'firstPeer' and 'secondPeer' functions - they take as arguments functions that define "how to send"+and "how to receive" messages, and likewise how to report status.+ -}  module Test.Serialization.Symbiote@@ -40,37 +83,25 @@   , defaultSuccess, defaultFailure, defaultProgress, nullProgress, simpleTest   ) where +import Test.Serialization.Symbiote.Core+  ( Topic (..), newGeneration, SymbioteState (..), SymbioteT, runSymbioteT+  , GenerateSymbiote (..), generateSymbiote, getProgress, Symbiote (..), SymbioteOperation (..))+ import Data.Map (Map) import qualified Data.Map as Map-import Data.Set (Set) import qualified Data.Set as Set-import Data.Text (Text, unpack)-import Data.String (IsString)+import Data.Text (unpack) import Data.Proxy (Proxy (..)) import Text.Printf (printf) import Control.Concurrent.STM-  (TVar, newTVarIO, readTVar, readTVarIO, modifyTVar', writeTVar, atomically, newTChan, readTChan, writeTChan)+  (TVar, newTVarIO, readTVarIO, writeTVar, atomically, newTChan, readTChan, writeTChan) import Control.Concurrent.Async (async, wait)-import Control.Monad (forever, void)+import Control.Monad (void) import Control.Monad.Trans.Control (MonadBaseControl, liftBaseWith)-import Control.Monad.Reader (ReaderT, ask, runReaderT)-import Control.Monad.State (StateT, modify', execStateT)+import Control.Monad.State (modify') import Control.Monad.IO.Class (MonadIO, liftIO) import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary)-import Test.QuickCheck.Gen (Gen, resize)-import qualified Test.QuickCheck.Gen as QC---class SymbioteOperation a where-  data Operation a :: *-  perform :: Operation a -> a -> a---- | A type and operation set over a serialization format-class SymbioteOperation a => Symbiote a s where-  encode   :: a -> s-  decode   :: s -> Maybe a-  encodeOp :: Operation a -> s-  decodeOp :: s -> Maybe (Operation a)+import Test.QuickCheck.Gen (Gen)   -- | The most trivial serialization medium for any @a@.@@ -86,95 +117,7 @@   decodeOp (EitherOp (Left _)) = Nothing   decodeOp (EitherOp (Right x)) = Just x --- | Unique name of a type, for a suite of tests-newtype Topic = Topic Text-  deriving (Eq, Ord, Show, IsString) --- | Protocol state for a particular topic-data SymbioteProtocol a s-  = MeGenerated-    { meGenValue :: a-    , meGenOperation :: Operation a-    , meGenReceived :: Maybe s-    }-  | ThemGenerating-    { themGen :: Maybe (s, s)-    }-  | NotStarted-  | Finished---- | Protocol generation state-data SymbioteGeneration a s = SymbioteGeneration-  { size     :: Int-  , protocol :: SymbioteProtocol a s-  }--newGeneration :: SymbioteGeneration a s-newGeneration = SymbioteGeneration-  { size = 1-  , protocol = NotStarted-  }----- | Internal existential state of a registered topic with type's facilities-data SymbioteState s =-  forall a-  . ( Arbitrary a-    , Arbitrary (Operation a)-    , Symbiote a s-    , Eq a-    ) =>-  SymbioteState-  { generate   :: Gen a-  , generateOp :: Gen (Operation a)-  , equal      :: a -> a -> Bool-  , maxSize    :: Int-  , generation :: TVar (SymbioteGeneration a s)-  , encode'    :: a -> s-  , encodeOp'  :: Operation a -> s-  , decode'    :: s -> Maybe a-  , decodeOp'  :: s -> Maybe (Operation a)-  , perform'   :: Operation a -> a -> a-  }---type SymbioteT s m = ReaderT Bool (StateT (Map Topic (SymbioteState s)) m)--runSymbioteT :: Monad m-             => SymbioteT s m ()-             -> Bool -- ^ Is this the first peer to initiate the protocol?-             -> m (Map Topic (SymbioteState s))-runSymbioteT x isFirst = execStateT (runReaderT x isFirst) Map.empty---data GenerateSymbiote s-  = DoneGenerating-  | GeneratedSymbiote-    { generatedValue :: s-    , generatedOperation :: s-    }---generateSymbiote :: forall s m. MonadIO m => SymbioteState s -> m (GenerateSymbiote s)-generateSymbiote SymbioteState{generate,generateOp,maxSize,generation} = do-  let go g@SymbioteGeneration{size} = g {size = size + 1}-  SymbioteGeneration{size} <- liftIO $ atomically $ modifyTVar' generation go *> readTVar generation-  if size >= maxSize-    then pure DoneGenerating-    else do-      let genResize :: forall q. Gen q -> m q-          genResize = liftIO . QC.generate . resize size-      generatedValue <- encode <$> genResize generate-      generatedOperation <- encodeOp <$> genResize generateOp-      pure GeneratedSymbiote{generatedValue,generatedOperation}---getProgress :: MonadIO m => SymbioteState s -> m Float-getProgress SymbioteState{maxSize,generation} = do-  SymbioteGeneration{size} <- liftIO $ readTVarIO generation-  pure $ fromIntegral size / fromIntegral maxSize-- -- | Register a topic in the test suite register :: forall a s m           . Arbitrary a@@ -248,7 +191,7 @@  -- | Messages sent by the second peer data Second s-  = BadTopics (Map Topic Int) -- ^ Second's available topics with identical gen sizes+  = BadTopics (Map Topic Int)   | Start   | SecondOperating     { secondOperatingTopic :: Topic@@ -309,18 +252,19 @@  -- | Via putStrLn defaultProgress :: Topic -> Float -> IO ()-defaultProgress (Topic t) p = putStrLn $ "Topic " ++ unpack t ++ ": " ++ (printf "%.2f" (p * 100.0)) ++ "%"+defaultProgress (Topic t) p = putStrLn $ "Topic " ++ unpack t ++ ": " ++ printf "%.2f" (p * 100.0) ++ "%"  -- | Do nothing nullProgress :: Topic -> Float -> IO () nullProgress _ _ = pure ()  +-- | Run the test suite as the first peer firstPeer :: forall m s            . MonadIO m           => Show s           => (First s -> m ()) -- ^ Encode and send first messages-          -> (m (Second s)) -- ^ Receive and decode second messages+          -> m (Second s) -- ^ Receive and decode second messages           -> (Topic -> m ()) -- ^ Report when Successful           -> (Failure Second s -> m ()) -- ^ Report when Failed           -> (Topic -> Float -> m ()) -- ^ Report on Progress@@ -360,11 +304,12 @@     _ -> onFailure $ OutOfSyncSecond shouldBeStart  +-- | Run the test suite as the second peer secondPeer :: forall s m             . MonadIO m            => Show s            => (Second s -> m ()) -- ^ Encode and send second messages-           -> (m (First s)) -- ^ Receive and decode first messages+           -> m (First s) -- ^ Receive and decode first messages            -> (Topic -> m ()) -- ^ Report when Successful            -> (Failure First s -> m ()) -- ^ Report when Failed            -> (Topic -> Float -> m ()) -- ^ Report on Progress@@ -420,7 +365,7 @@ generating :: MonadIO m            => Show s            => (me s -> m ()) -- ^ Encode and send first messages-           -> (m (them s)) -- ^ Receive and decode second messages+           -> m (them s) -- ^ Receive and decode second messages            -> (Topic -> Generating s -> me s) -- ^ Build a generating datum, whether first or second            -> (Topic -> Operating s -> me s) -- ^ Build a generating datum, whether first or second            -> (them s -> Maybe (Topic, Generating s)) -- ^ Deconstruct an operating datum, whether first or second@@ -481,7 +426,7 @@                         then do                           encodeAndSend $ makeGen topic YourTurn                           progress <- getProgress symbioteState-                          (onProgress topic progress)+                          onProgress topic progress                           operating                             encodeAndSend receiveAndDecode                             makeGen makeOp@@ -502,11 +447,11 @@       hasReceivedFinished <- liftIO $ readTVarIO hasReceivedFinishedVar       case hasReceivedFinished of         HasReceivedFinished -> do-          (onSuccess topic)+          onSuccess topic           onFinished -- stop cycling - last generation in sequence is from second         HasntReceivedFinished -> do           progress <- getProgress symbioteState-          (onProgress topic progress)+          onProgress topic progress           operating             encodeAndSend receiveAndDecode             makeGen makeOp@@ -521,7 +466,7 @@ operating :: MonadIO m           => Show s           => (me s -> m ()) -- ^ Encode and send first messages-          -> (m (them s)) -- ^ Receive and decode second messages+          -> m (them s) -- ^ Receive and decode second messages           -> (Topic -> Generating s -> me s) -- ^ Build a generating datum, whether first or second           -> (Topic -> Operating s -> me s) -- ^ Build a generating datum, whether first or second           -> (them s -> Maybe (Topic, Generating s)) -- ^ Deconstruct an operating datum, whether first or second@@ -556,7 +501,7 @@             generatingTryFinished           YourTurn -> do             progress <- getProgress symbioteState-            (onProgress topic progress)+            onProgress topic progress             generating               encodeAndSend receiveAndDecode               makeGen makeOp@@ -599,11 +544,11 @@       hasSentFinished <- liftIO $ readTVarIO hasSentFinishedVar       case hasSentFinished of         HasSentFinished -> do-          (onSuccess topic)+          onSuccess topic           onFinished -- stop cycling - last operation in sequence is from first         HasntSentFinished -> do           progress <- getProgress symbioteState-          (onProgress topic progress)+          onProgress topic progress           generating             encodeAndSend receiveAndDecode             makeGen makeOp
+ src/Test/Serialization/Symbiote/Core.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE+    ExistentialQuantification+  , NamedFieldPuns+  , RankNTypes+  , ScopedTypeVariables+  , TypeFamilies+  , MultiParamTypeClasses+  , FlexibleContexts+  , GeneralizedNewtypeDeriving+  #-}++module Test.Serialization.Symbiote.Core where++import Data.Text (Text)+import Data.String (IsString)+import Data.Map (Map)+import qualified Data.Map as Map+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Concurrent.STM+  (TVar, readTVar, readTVarIO, modifyTVar', atomically)+import Control.Monad.Reader (ReaderT, runReaderT)+import Control.Monad.State (StateT, execStateT)+import Test.QuickCheck.Arbitrary (Arbitrary)+import Test.QuickCheck.Gen (Gen, resize)+import qualified Test.QuickCheck.Gen as QC+++-- | A type-level relation between a type and appropriate, testable operations on that type.+class SymbioteOperation a where+  data Operation a :: *+  perform :: Operation a -> a -> a++-- | A serialization format for a particular type, and serialized data type.+class SymbioteOperation a => Symbiote a s where+  encode   :: a -> s+  decode   :: s -> Maybe a+  encodeOp :: Operation a -> s+  decodeOp :: s -> Maybe (Operation a)+++-- | Unique name of a type, for a suite of tests+newtype Topic = Topic Text+  deriving (Eq, Ord, Show, IsString)++-- | Protocol state for a particular topic+data SymbioteProtocol a s+  = MeGenerated+    { meGenValue :: a+    , meGenOperation :: Operation a+    , meGenReceived :: Maybe s+    }+  | ThemGenerating+    { themGen :: Maybe (s, s)+    }+  | NotStarted+  | Finished++-- | Protocol generation state+data SymbioteGeneration a s = SymbioteGeneration+  { size     :: Int+  , protocol :: SymbioteProtocol a s+  }++newGeneration :: SymbioteGeneration a s+newGeneration = SymbioteGeneration+  { size = 1+  , protocol = NotStarted+  }+++-- | Internal existential state of a registered topic with type's facilities+data SymbioteState s =+  forall a+  . ( Arbitrary a+    , Arbitrary (Operation a)+    , Symbiote a s+    , Eq a+    ) =>+  SymbioteState+  { generate   :: Gen a+  , generateOp :: Gen (Operation a)+  , equal      :: a -> a -> Bool+  , maxSize    :: Int+  , generation :: TVar (SymbioteGeneration a s)+  , encode'    :: a -> s+  , encodeOp'  :: Operation a -> s+  , decode'    :: s -> Maybe a+  , decodeOp'  :: s -> Maybe (Operation a)+  , perform'   :: Operation a -> a -> a+  }+++type SymbioteT s m = ReaderT Bool (StateT (Map Topic (SymbioteState s)) m)++runSymbioteT :: Monad m+             => SymbioteT s m ()+             -> Bool -- ^ Is this the first peer to initiate the protocol?+             -> m (Map Topic (SymbioteState s))+runSymbioteT x isFirst = execStateT (runReaderT x isFirst) Map.empty+++data GenerateSymbiote s+  = DoneGenerating+  | GeneratedSymbiote+    { generatedValue :: s+    , generatedOperation :: s+    }+++generateSymbiote :: forall s m. MonadIO m => SymbioteState s -> m (GenerateSymbiote s)+generateSymbiote SymbioteState{generate,generateOp,maxSize,generation} = do+  let go g@SymbioteGeneration{size} = g {size = size + 1}+  SymbioteGeneration{size} <- liftIO $ atomically $ modifyTVar' generation go *> readTVar generation+  if size >= maxSize+    then pure DoneGenerating+    else do+      let genResize :: forall q. Gen q -> m q+          genResize = liftIO . QC.generate . resize size+      generatedValue <- encode <$> genResize generate+      generatedOperation <- encodeOp <$> genResize generateOp+      pure GeneratedSymbiote{generatedValue,generatedOperation}+++getProgress :: MonadIO m => SymbioteState s -> m Float+getProgress SymbioteState{maxSize,generation} = do+  SymbioteGeneration{size} <- liftIO $ readTVarIO generation+  pure $ fromIntegral size / fromIntegral maxSize
symbiote.cabal view
@@ -4,13 +4,13 @@ -- -- see: https://github.com/sol/hpack ----- hash: d552556fe53910bd17c181cb95675e95a8276726b5345eb5f3f9743acae318ff+-- hash: 118f84a8b5a0694dfff43f8d16ecdca761bc22eff9d85afce8dda972d41be76a  name:           symbiote-version:        0.0.0+version:        0.0.0.1 synopsis:       Data serialization, communication, and operation verification implementation description:    Please see the README on GitHub at <https://github.com/athanclark/symbiote#readme>-category:       Data+category:       Data, Testing homepage:       https://github.com/athanclark/symbiote#readme bug-reports:    https://github.com/athanclark/symbiote/issues author:         Athan Clark@@ -33,6 +33,7 @@       Test.Serialization.Symbiote.Aeson       Test.Serialization.Symbiote.Cereal       Test.Serialization.Symbiote.Cereal.Lazy+      Test.Serialization.Symbiote.Core   other-modules:       Paths_symbiote   hs-source-dirs: