packages feed

hq-json-0.1.1: src/HQ/Runner/Rewrite.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HQ.Runner.Rewrite where

import Data.Aeson (Value (..))
import HQ.Early (Early, leave, orLeave)
import HQ.Error (HQError (..))
import HQ.JSON.Encoder (BSStream, EmitState, EncoderConfig, emitChunk, flushEmit)
import HQ.JSON.Event
import HQ.Optic (Optic (..), focusesWhole, prismPredicate)
import HQ.Runner.Cursor
import HQ.Runner.Error (RunnerError (..))
import HQ.Runner.Fold
import HQ.Runner.Rewrite.Emit
import HQ.Transformation (Transformation (Const), runTransformation)
import HQ.Transformation.Error (TransformationError (..))
import Relude hiding (Compose, Const)

--------------------------------------------------------------------------------
-- Over and delete: document rewriting
--------------------------------------------------------------------------------

-- | What happens to a focused value: removed (@delete@) or rewritten
-- (@over@, including @set@ as @over@ with a constant).
data Rewriter = RewriteDelete | RewriteTransform Transformation
  deriving (Show, Eq)

data KeyAction = DropKey | KeepKey | RenameKey Text
  deriving (Show, Eq)

runDelete :: Early HQError -> Optic -> EncoderConfig -> RewriteContinuation
runDelete early = runRewrite early RewriteDelete

runOver ::
  Early HQError -> Optic -> Transformation -> EncoderConfig -> RewriteContinuation
runOver early optic transformation =
  runRewrite early (RewriteTransform transformation) optic

-- | Rewrite the document at the cursor, replacing or omitting focused
-- values in place; everything else passes through unchanged.
--
-- Each optic case below is a top-level worker taking its context
-- explicitly (rather than one @where@-closure over the whole query):
-- GHC gives every helper its own worker/wrapper and strictness, and
-- small leaves inline into their callers, instead of fusing all of
-- @over@/@delete@ into a single giant worker.
runRewrite ::
  Early HQError -> Rewriter -> Optic -> EncoderConfig -> RewriteContinuation
runRewrite early rewriter optic config cur0 st0 = do
  RewriteResult st' after <- rewriteRun early rewriter config optic cur0 st0
  st'' <- flushEmit st'
  pure (RewriteResult st'' after)

rewriteRun ::
  Early HQError -> Rewriter -> EncoderConfig -> Optic -> RewriteContinuation
rewriteRun early rewriter config step input st = case step of
  Id -> rewriteValue early rewriter config input st
  _ -> rewriteNavigate early rewriter config step Id input st

rewriteNavigate ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Optic ->
  Optic ->
  RewriteContinuation
rewriteNavigate early rewriter config step suffix input st = case step of
  Id -> rewriteRun early rewriter config suffix input st
  Compose left right ->
    rewriteNavigate early rewriter config left (right <> suffix) input st
  Field name -> rewriteField early rewriter config name suffix input st
  Each -> rewriteEach early rewriter config suffix input st
  Keys -> rewriteKeys early rewriter config suffix input st
  Values -> rewriteValues early rewriter config suffix input st
  Prism kind ->
    rewritePrism early rewriter config (prismPredicate kind) suffix input st
  PrismJust -> rewriteJust early rewriter config suffix input st
  Ix i -> rewriteIndex early rewriter config i suffix input st
  Filter o t -> rewriteFilter early rewriter config o t suffix input st

-- | Split a member-value suffix with a leading @filter@ step into
-- its gate and remainder, so a kept value deleted as a whole drops
-- its key too.
stripFilterGate :: Optic -> Maybe (Optic, Transformation, Optic)
stripFilterGate (Compose (Filter o t) rest') = Just (o, t, rest')
stripFilterGate (Filter o t) = Just (o, t, Id)
stripFilterGate _ = Nothing

rewriteValue ::
  Early HQError -> Rewriter -> EncoderConfig -> RewriteContinuation
rewriteValue early rewriter config input st = case rewriter of
  RewriteDelete -> RewriteResult st <$> lift (skipValueE early input)
  -- A constant replacement never reads the focused value: skip it
  -- at the text level instead of decoding it.
  RewriteTransform (Const value) -> do
    emitValueChunks config value st =<< lift (skipValueE early input)
  RewriteTransform t -> do
    Materialized v _ rest <- lift (materializeValue early input)
    value <- lift (orLeave early (applyTransformation t v))
    emitValueChunks config value st rest

-- | Emit a transformed value's events as chunks.
emitValueChunks ::
  EncoderConfig -> Value -> EmitState -> Cursor -> BSStream IO RewriteResult
emitValueChunks config value st cursor = go st (valueToEvents value)
  where
    go s [] = pure (RewriteResult s cursor)
    go s (event : events) = do
      s' <- emitChunk config event s
      go s' events

-- | Prism: value whose first event satisfies the predicate goes
-- through the suffix; everything else passes through unchanged.
rewritePrism ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  (JSONEvent -> Bool) ->
  Optic ->
  RewriteContinuation
rewritePrism early rewriter config predicate suffix input st =
  onEventOrEndChunks early input st $ \(Step event rest) ->
    if predicate event
      then rewriteRun early rewriter config suffix (pushCursor event rest) st
      else takeValueChunks early config (pushCursor event rest) st

-- | Prism on non-null: null passes through, anything else goes
-- through the suffix.
rewriteJust ::
  Early HQError -> Rewriter -> EncoderConfig -> Optic -> RewriteContinuation
rewriteJust early rewriter config suffix input st =
  onEventOrEndChunks early input st $ \case
    (Step JSONNull rest) -> do
      st' <- emitChunk config JSONNull st
      pure (RewriteResult st' rest)
    (Step event rest) ->
      rewriteRun early rewriter config suffix (pushCursor event rest) st

-- | Array index: rewrite the element at @index@, pass through the
-- rest of the array. Non-arrays pass through unchanged.
rewriteIndex ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Int ->
  Optic ->
  RewriteContinuation
rewriteIndex early rewriter config index suffix input st =
  onOpenOrSkipChunks early config input st eventsWithCont
  where
    eventsWithCont =
      [ ( JSONBeginArray,
          \rest s -> go index rest =<< emitChunk config JSONBeginArray s
        )
      ]

    go n stream s = do
      step <- lift (expectArrayStep early stream)
      case step of
        Left rest -> do
          s' <- emitChunk config JSONEndArray s
          pure (RewriteResult s' rest)
        Right (Step event rest)
          | n == 0 -> do
              RewriteResult s' after <- rewriteRun early rewriter config suffix (pushCursor event rest) s
              go (-1) after s'
          | otherwise -> do
              RewriteResult s' after <-
                takeValueChunks early config (pushCursor event rest) s
              go (n - 1) after s'

-- | Object field: rewrite the member named @name@ (dropping the key
-- too under @delete@ when the suffix lands on the value), pass
-- every other member through unchanged.
rewriteField ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Text ->
  Optic ->
  RewriteContinuation
rewriteField early rewriter config name suffix input st =
  onOpenOrSkipChunks early config input st eventsWithCont
  where
    eventsWithCont =
      [ ( JSONBeginObject,
          \rest s -> do
            st' <- emitChunk config JSONBeginObject s
            traverseObjectChunks early config rest st'
              $ \(ObjectStep key rest') s' ->
                if key == name
                  then rewriteOneMember early rewriter config suffix key rest' s'
                  else emitKeyAndTake early config key rest' s'
        )
      ]

-- | The @each@ traversal: rewrite every array element, or every
-- object member value. Non-containers pass through.
rewriteEach ::
  Early HQError -> Rewriter -> EncoderConfig -> Optic -> RewriteContinuation
rewriteEach early rewriter config suffix input st =
  onOpenOrSkipChunks early config input st eventsWithCont
  where
    eventsWithCont =
      [ ( JSONBeginArray,
          \rest s -> do
            st' <- emitChunk config JSONBeginArray s
            traverseArrayChunks early config rest st' $ \(Step event rest') s' ->
              rewriteRun early rewriter config suffix (pushCursor event rest') s'
        ),
        ( JSONBeginObject,
          \rest s -> do
            st' <- emitChunk config JSONBeginObject s
            traverseObjectChunks early config rest st' $ \(ObjectStep key rest') s' ->
              rewriteOneMember early rewriter config suffix key rest' s'
        )
      ]

-- | The @values@ traversal: rewrite every object member value.
-- Arrays and scalars pass through unchanged (unlike 'rewriteEach').
rewriteValues ::
  Early HQError -> Rewriter -> EncoderConfig -> Optic -> RewriteContinuation
rewriteValues early rewriter config suffix input st =
  onOpenOrSkipChunks early config input st eventsWithCont
  where
    eventsWithCont =
      [ ( JSONBeginObject,
          \rest s -> do
            st' <- emitChunk config JSONBeginObject s
            traverseObjectChunks early config rest st'
              $ \(ObjectStep key rest') s' ->
                rewriteOneMember early rewriter config suffix key rest' s'
        )
      ]

-- | The @filter@ optic: gate the focused value as a whole, running
-- the suffix on kept values and passing dropped values through
-- unchanged.
rewriteFilter ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Optic ->
  Transformation ->
  Optic ->
  RewriteContinuation
rewriteFilter early rewriter config o t suffix input st =
  onEventOrEndChunks early input st $ \(Step event rest) -> do
    GateResult keep _ valCursor _ <-
      lift $ gateTake early o t $ pushCursor event rest
    if keep
      then rewriteRun early rewriter config suffix valCursor st
      else takeValueChunks early config valCursor st

-- | The @keys@ traversal: rewrite object keys, leaving values alone.
-- Arrays and scalars pass through unchanged (@keys@ is objects-only).
rewriteKeys ::
  Early HQError -> Rewriter -> EncoderConfig -> Optic -> RewriteContinuation
rewriteKeys early rewriter config suffix input st =
  onOpenOrSkipChunks early config input st eventsWithCont
  where
    eventsWithCont =
      [ ( JSONBeginObject,
          \rest s -> do
            st' <- emitChunk config JSONBeginObject s
            traverseObjectChunks early config rest st'
              $ \(ObjectStep key rest') s' -> do
                action <- lift (rewriteKeyText early suffix rewriter key)
                applyKeyAction early config action key rest' s'
        )
      ]

applyKeyAction ::
  Early HQError ->
  EncoderConfig ->
  KeyAction ->
  Text ->
  Cursor ->
  EmitState ->
  BSStream IO RewriteResult
applyKeyAction early config action key rest s = case action of
  DropKey -> RewriteResult s <$> lift (skipValueE early rest)
  KeepKey -> emitKeyAndTake early config key rest s
  RenameKey newKey -> emitKeyAndTake early config newKey rest s

-- | Apply the remainder of a @keys@ optic plus the enclosing
-- 'Rewriter' to one object key. Keys are scalar strings, so the
-- suffix either focuses the key as a whole (checked with
-- 'focusesWhole', e.g. @Id@ or @_String@) or it matches nothing and
-- the key is kept.
rewriteKeyText :: Early HQError -> Optic -> Rewriter -> Text -> IO KeyAction
rewriteKeyText early suffix rw key = case rw of
  RewriteDelete
    | focusesWhole suffix (JSONString key) -> pure DropKey
    | otherwise -> pure KeepKey
  RewriteTransform t
    | focusesWhole suffix (JSONString key) ->
        case runTransformation t (String key) of
          Left err -> leave early $ HQTransformationError err
          Right (String next) ->
            pure (if next == key then KeepKey else RenameKey next)
          Right _ -> leave early $ HQTransformationError KeyNotString
    | otherwise -> pure KeepKey

-- | Rewrite one object member, returning the cursor after it.
-- The key survives unless a @delete@ lands on the member value as
-- a whole. A leading @filter@ step gates the value first: dropped
-- members pass through, and kept members deleted as a whole lose
-- their key too. The object walk itself is shared via
-- 'traverseObjectChunks'.
rewriteOneMember ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Optic ->
  Text ->
  Cursor ->
  EmitState ->
  BSStream IO RewriteResult
rewriteOneMember early rewriter config suffix key stream st = do
  result <- lift $ pullCursor early stream
  case result of
    Nothing -> lift $ leave early $ HQRunnerError ExpectedMemberValue
    Just (Step event rest) -> case stripFilterGate suffix of
      Just (o, t, rest') ->
        rewriteFilteredMember early rewriter config o t rest' key event rest st
      Nothing ->
        rewritePlainMember early rewriter config suffix key event rest st

rewriteFilteredMember ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Optic ->
  Transformation ->
  Optic ->
  Text ->
  JSONEvent ->
  Cursor ->
  EmitState ->
  BSStream IO RewriteResult
rewriteFilteredMember early rewriter config o t rest' key event rest st = do
  GateResult keep firstEv valCursor afterValue <-
    lift (gateTake early o t (pushCursor event rest))
  if not keep
    then emitKeyAndTake early config key valCursor st
    else case (rewriter, focusesWhole rest' firstEv) of
      (RewriteDelete, True) -> pure (RewriteResult st afterValue)
      _ -> do
        st' <- emitChunk config (JSONObjectKey key) st
        rewriteRun early rewriter config rest' valCursor st'

rewritePlainMember ::
  Early HQError ->
  Rewriter ->
  EncoderConfig ->
  Optic ->
  Text ->
  JSONEvent ->
  Cursor ->
  EmitState ->
  BSStream IO RewriteResult
rewritePlainMember early rewriter config suffix key event rest st =
  case rewriter of
    RewriteDelete
      | focusesWhole suffix event -> do
          after <- lift (skipValueE early (pushCursor event rest))
          pure (RewriteResult st after)
      | otherwise -> do
          st' <- emitChunk config (JSONObjectKey key) st
          rewriteRun early rewriter config suffix (pushCursor event rest) st'
    RewriteTransform _ -> do
      st' <- emitChunk config (JSONObjectKey key) st
      rewriteRun early rewriter config suffix (pushCursor event rest) st'