packages feed

keiro-ops-http-0.22.0.0: src/Keiro/Ops/Http/Protocol.hs

-- | The inspection socket dialect. Frames describe observations; clients recover
-- missed deltas by re-subscribing or reading the paired HTTP page.
module Keiro.Ops.Http.Protocol
  ( FeedName (..),
    feedNameText,
    parseFeedName,
    ClientFrame (..),
    ServerFrame (..),
    Trigger (..),
    SocketErrorCode (..),
    socketErrorText,
  )
where

import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withObject, withText, (.:), (.=))
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Word (Word64)
import GHC.Generics (Generic)

-- | Exactly the three supported first-page views.
data FeedName = Workflows | Timers | ProjectionGroups deriving stock (Eq, Ord, Show, Generic)

feedNameText :: FeedName -> Text
feedNameText = \case Workflows -> "workflows"; Timers -> "timers"; ProjectionGroups -> "projection_groups"

parseFeedName :: Text -> Maybe FeedName
parseFeedName = \case "workflows" -> Just Workflows; "timers" -> Just Timers; "projection_groups" -> Just ProjectionGroups; _ -> Nothing

instance ToJSON FeedName where toJSON = String . feedNameText

instance FromJSON FeedName where parseJSON = withText "feed" (maybe (fail "unknown_feed") pure . parseFeedName)

-- Unknown names remain a typed subscription refusal, independently of malformed
-- JSON or an unknown frame type. The filter object is validated by the view.
data ClientFrame = Subscribe !FeedName !Value | UnknownSubscription !Text | Unsubscribe !FeedName | Ping
  deriving stock (Eq, Show, Generic)

instance FromJSON ClientFrame where
  parseJSON = withObject "client frame" $ \fields -> do
    kind <- fields .: "type"
    case (kind :: Text) of
      "subscribe" -> do
        name <- fields .: "feed"
        pure (maybe (UnknownSubscription name) (\feed -> Subscribe feed (Object (KeyMap.delete "type" (KeyMap.delete "feed" fields)))) (parseFeedName name))
      "unsubscribe" -> do
        name <- fields .: "feed"
        pure (maybe (UnknownSubscription name) Unsubscribe (parseFeedName name))
      "ping" -> pure Ping
      _ -> fail "unknown frame type"

instance ToJSON ClientFrame where
  toJSON = \case
    Subscribe feed filters -> object (["type" .= ("subscribe" :: Text), "feed" .= feed] <> filterFields filters)
    UnknownSubscription feed -> object ["type" .= ("subscribe" :: Text), "feed" .= feed]
    Unsubscribe feed -> object ["type" .= ("unsubscribe" :: Text), "feed" .= feed]
    Ping -> object ["type" .= ("ping" :: Text)]
    where
      filterFields (Object fields) = [(key, value) | (key, value) <- KeyMap.toList fields, key /= "type", key /= "feed"]; filterFields _ = []

data Trigger = Subscribed | Notify | Timeout deriving stock (Eq, Show, Generic)

instance ToJSON Trigger where toJSON = String . \case Subscribed -> "subscribe"; Notify -> "notify"; Timeout -> "timeout"

data SocketErrorCode = InvalidFrame | UnknownFeed | InvalidSubscription | QueueOverflow | ReadFailed | TooManyViews | InspectionBusy | ViewRetiring | InspectionResultTooLarge
  deriving stock (Eq, Show, Generic)

socketErrorText :: SocketErrorCode -> Text
socketErrorText = \case
  InvalidFrame -> "invalid_frame"
  UnknownFeed -> "unknown_feed"
  InvalidSubscription -> "invalid_subscription"
  QueueOverflow -> "queue_overflow"
  ReadFailed -> "read_failed"
  TooManyViews -> "too_many_views"
  InspectionBusy -> "inspection_busy"
  ViewRetiring -> "view_retiring"
  InspectionResultTooLarge -> "inspection_result_too_large"

instance ToJSON SocketErrorCode where toJSON = String . socketErrorText

data ServerFrame
  = Snapshot {feed :: !FeedName, sequence :: !Word64, trigger :: !Trigger, readAt :: !UTCTime, items :: ![Value], extra :: !(Maybe Value)}
  | Update {feed :: !FeedName, sequence :: !Word64, trigger :: !Trigger, readAt :: !UTCTime, changed :: ![Value], removed :: ![Value], extra :: !(Maybe Value)}
  | Unsubscribed !FeedName
  | Pong
  | ErrorFrame {code :: !SocketErrorCode, message :: !Text, errorFeed :: !(Maybe FeedName)}
  | Goodbye
  deriving stock (Eq, Show, Generic)

instance ToJSON ServerFrame where
  toJSON = \case
    Snapshot feed sequence trigger stamp items extra ->
      object
        (["type" .= ("snapshot" :: Text), "feed" .= feed, "sequence" .= sequence, "trigger" .= trigger, "observed_at" .= stamp, "items" .= items] <> extraFields extra)
    Update feed sequence trigger stamp changed removed extra ->
      object
        (["type" .= ("update" :: Text), "feed" .= feed, "sequence" .= sequence, "trigger" .= trigger, "observed_at" .= stamp, "changed" .= changed, "removed" .= removed] <> extraFields extra)
    Unsubscribed feed -> object ["type" .= ("unsubscribed" :: Text), "feed" .= feed]
    Pong -> object ["type" .= ("pong" :: Text)]
    ErrorFrame code message feed -> object (["type" .= ("error" :: Text), "code" .= code, "message" .= message] <> maybe [] (\value -> ["feed" .= value]) feed)
    Goodbye -> object ["type" .= ("goodbye" :: Text)]
    where
      extraFields (Just (Object fields)) = [(key, value) | (key, value) <- KeyMap.toList fields, key `notElem` ["type", "feed", "sequence", "trigger", "observed_at", "items", "changed", "removed"]]
      extraFields _ = []