packages feed

mergeless-0.4.0.0: src/Data/Mergeless/Item.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.Mergeless.Item where

import Autodocodec
import Control.DeepSeq
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import Data.Validity
import Data.Validity.Containers ()
import GHC.Generics (Generic)

{-# ANN module ("HLint: ignore Use lambda-case" :: String) #-}

data ClientItem a
  = ClientEmpty
  | ClientAdded !a
  | ClientSynced !a
  | ClientDeleted
  deriving stock (Show, Eq, Ord, Generic)
  deriving (FromJSON, ToJSON) via (Autodocodec (ClientItem a))

instance Validity a => Validity (ClientItem a)

instance NFData a => NFData (ClientItem a)

instance HasCodec a => HasCodec (ClientItem a) where
  codec =
    object "ClientItem" $
      dimapCodec f g $
        disjointEitherCodec
          ( disjointEitherCodec
              (typeField "empty" <*> pure ())
              (typeField "added" <*> requiredField "value" "item that was added, client-side")
          )
          ( disjointEitherCodec
              (typeField "synced" <*> requiredField "value" "the item that is known, client-side")
              (typeField "deleted" <*> pure ())
          )
    where
      f :: Either (Either () a) (Either a ()) -> ClientItem a
      f = \case
        Left (Left ()) -> ClientEmpty
        Left (Right v) -> ClientAdded v
        Right (Left v) -> ClientSynced v
        Right (Right ()) -> ClientDeleted
      g :: ClientItem a -> Either (Either () a) (Either a ())
      g = \case
        ClientEmpty -> Left (Left ())
        ClientAdded v -> Left (Right v)
        ClientSynced v -> Right (Left v)
        ClientDeleted -> Right (Right ())

      typeField :: Text -> ObjectCodec b (x -> x)
      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName

-- | A synchronisation request for items with identifiers of type @i@ and values of type @a@
data ItemSyncRequest a
  = ItemSyncRequestPoll
  | ItemSyncRequestNew !a
  | ItemSyncRequestKnown
  | ItemSyncRequestDeleted
  deriving stock (Show, Eq, Ord, Generic)
  deriving (FromJSON, ToJSON) via (Autodocodec (ItemSyncRequest a))

instance Validity a => Validity (ItemSyncRequest a)

instance NFData a => NFData (ItemSyncRequest a)

instance HasCodec a => HasCodec (ItemSyncRequest a) where
  codec =
    object "ItemSyncRequest" $
      dimapCodec f g $
        disjointEitherCodec
          ( disjointEitherCodec
              (typeField "empty" <*> pure ())
              (typeField "added" <*> requiredField "value" "item that was added, client-side")
          )
          ( disjointEitherCodec
              (typeField "synced" <*> pure ())
              (typeField "deleted" <*> pure ())
          )
    where
      f = \case
        Left (Left ()) -> ItemSyncRequestPoll
        Left (Right v) -> ItemSyncRequestNew v
        Right (Left ()) -> ItemSyncRequestKnown
        Right (Right ()) -> ItemSyncRequestDeleted

      g = \case
        ItemSyncRequestPoll -> Left (Left ())
        ItemSyncRequestNew v -> Left (Right v)
        ItemSyncRequestKnown -> Right (Left ())
        ItemSyncRequestDeleted -> Right (Right ())

      typeField :: Text -> ObjectCodec b (x -> x)
      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName

makeItemSyncRequest :: ClientItem a -> ItemSyncRequest a
makeItemSyncRequest ci =
  case ci of
    ClientEmpty -> ItemSyncRequestPoll
    ClientAdded a -> ItemSyncRequestNew a
    ClientSynced _ -> ItemSyncRequestKnown
    ClientDeleted -> ItemSyncRequestDeleted

-- | A synchronisation response for items with identifiers of type @i@ and values of type @a@
data ItemSyncResponse a
  = ItemSyncResponseInSyncEmpty
  | ItemSyncResponseInSyncFull
  | ItemSyncResponseClientAdded
  | ItemSyncResponseClientDeleted
  | ItemSyncResponseServerAdded !a
  | ItemSyncResponseServerDeleted
  deriving stock (Show, Eq, Ord, Generic)
  deriving (FromJSON, ToJSON) via (Autodocodec (ItemSyncResponse a))

instance Validity a => Validity (ItemSyncResponse a)

instance NFData a => NFData (ItemSyncResponse a)

instance HasCodec a => HasCodec (ItemSyncResponse a) where
  codec =
    object "ItemSyncResponse" $
      dimapCodec f g $
        disjointEitherCodec
          ( disjointEitherCodec
              (typeField "in-sync-empty" <*> pure ())
              (typeField "in-sync-full" <*> pure ())
          )
          ( disjointEitherCodec
              ( disjointEitherCodec
                  (typeField "client-added" <*> pure ())
                  (typeField "client-deleted" <*> pure ())
              )
              ( disjointEitherCodec
                  (typeField "server-added" <*> requiredField "value" "the value that was added, server-side")
                  (typeField "server-deleted" <*> pure ())
              )
          )
    where
      f = \case
        Left (Left ()) -> ItemSyncResponseInSyncEmpty
        Left (Right ()) -> ItemSyncResponseInSyncFull
        Right (Left (Left ())) -> ItemSyncResponseClientAdded
        Right (Left (Right ())) -> ItemSyncResponseClientDeleted
        Right (Right (Left v)) -> ItemSyncResponseServerAdded v
        Right (Right (Right ())) -> ItemSyncResponseServerDeleted

      g = \case
        ItemSyncResponseInSyncEmpty -> Left (Left ())
        ItemSyncResponseInSyncFull -> Left (Right ())
        ItemSyncResponseClientAdded -> Right (Left (Left ()))
        ItemSyncResponseClientDeleted -> Right (Left (Right ()))
        ItemSyncResponseServerAdded v -> Right (Right (Left v))
        ItemSyncResponseServerDeleted -> Right (Right (Right ()))

      typeField :: Text -> ObjectCodec b (x -> x)
      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName

-- | Merge a synchronisation response back into a client-side store.
mergeItemSyncResponse :: ClientItem a -> ItemSyncResponse a -> ClientItem a
mergeItemSyncResponse ci sr =
  let mismatch = ci
   in case ci of
        ClientEmpty ->
          case sr of
            ItemSyncResponseInSyncEmpty -> ClientEmpty
            ItemSyncResponseServerAdded s -> ClientSynced s
            _ -> mismatch
        ClientAdded a ->
          case sr of
            ItemSyncResponseClientAdded -> ClientSynced a
            ItemSyncResponseServerAdded s -> ClientSynced s
            -- For completeness sake.
            -- This can only happen if two clients make the item at the same time.
            -- In practice, with named items in a collection, this will never happen.
            _ -> mismatch
        ClientSynced _ ->
          case sr of
            ItemSyncResponseInSyncFull -> ci -- No change
            ItemSyncResponseServerDeleted -> ClientEmpty
            _ -> mismatch
        ClientDeleted ->
          case sr of
            ItemSyncResponseClientDeleted -> ClientEmpty
            _ -> mismatch

-- | An item in a central store with a value of type @a@
data ServerItem a
  = ServerItemEmpty
  | ServerItemFull !a
  deriving stock (Show, Eq, Ord, Generic)
  deriving (FromJSON, ToJSON) via (Autodocodec (ServerItem a))

instance Validity a => Validity (ServerItem a)

instance NFData a => NFData (ServerItem a)

instance HasCodec a => HasCodec (ServerItem a) where
  codec =
    object "ServerItem" $
      dimapCodec f g $
        possiblyJointEitherCodec
          (requiredField "value" "the item on the server side")
          (pure ())
    where
      f = \case
        Left v -> ServerItemFull v
        Right () -> ServerItemEmpty
      g = \case
        ServerItemFull v -> Left v
        ServerItemEmpty -> Right ()

processServerItemSync :: ServerItem a -> ItemSyncRequest a -> (ItemSyncResponse a, ServerItem a)
processServerItemSync si sr =
  case si of
    ServerItemEmpty ->
      case sr of
        ItemSyncRequestPoll ->
          -- Both the client and the server think the item is empty, fine.
          (ItemSyncResponseInSyncEmpty, si)
        ItemSyncRequestNew a ->
          -- The client has a new item and the server has space for it, add it.
          (ItemSyncResponseClientAdded, ServerItemFull a)
        ItemSyncRequestKnown ->
          -- The client has an item that the server doesn't, so the server must have
          -- deleted it when another client asked to do that.
          -- Leave it deleted.
          (ItemSyncResponseServerDeleted, si)
        ItemSyncRequestDeleted ->
          -- The server has deleted an item but the current client hasn't been made aware of that
          -- AND this server also deleted that item in the meantime.
          -- Just leave it deleted.
          (ItemSyncResponseClientDeleted, si)
    ServerItemFull s ->
      case sr of
        ItemSyncRequestPoll ->
          -- The server has an item that the client doesn't, send it to the client.
          (ItemSyncResponseServerAdded s, si)
        ItemSyncRequestNew _ ->
          -- The client wants to add an item that the server already has.
          -- That means that another client has added that same item in the meantime.
          -- This wouldn't happen if the items were named.
          -- In this case, for completeness sake,
          (ItemSyncResponseServerAdded s, si)
        ItemSyncRequestKnown -> (ItemSyncResponseInSyncFull, si)
        ItemSyncRequestDeleted -> (ItemSyncResponseClientDeleted, ServerItemEmpty)