keiro-dsl-0.17.0.0: src/Keiro/Dsl/Parser/Coordination.hs
-- | Process-manager, router, timer, dispatch, and correlation syntax.
module Keiro.Dsl.Parser.Coordination
( pProcess,
pRouter,
)
where
import Data.List (intersperse)
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.Frontend.Internal (FrontendContext)
import Keiro.Dsl.Grammar
import Keiro.Dsl.LanguageVersion (LanguageFeature (DeclarativeRouterSelectionSyntax, ProcessReactionSyntax))
import Keiro.Dsl.Parser.Core
import Keiro.Dsl.Parser.Expression (pExpr)
import Keiro.Dsl.Parser.Mapped (pMappedTypeExpr)
import Keiro.Dsl.Source (Located (..), SourceSpan)
import Keiro.Dsl.Syntax (SurfaceElement (..))
import Text.Megaparsec
import Text.Megaparsec.Char (char)
-- Process manager + durable timer (EP-3)
--------------------------------------------------------------------------------
pProcess :: FrontendContext -> P (ProcessNode, [Located SurfaceElement])
pProcess context = do
loc <- getLoc
keyword "process"
pid <- ident
keyword "name"
nm <- stringLit
(processBody, corr, saga, tgt, projs, rejected, poison, reactionSpans) <- try (pReactionProcessBody context) <|> pLegacyProcessBody
let processNode =
ProcessNode
{ id = pid,
name = nm,
correlate = corr,
saga = saga,
target = tgt,
projections = projs,
body = processBody,
rejected = rejected,
poison = poison,
loc = loc
}
elements =
[ Located {span = sourceSpan, value = SurfaceProcessReaction pid inputName ordinal}
| (inputName, ordinal, sourceSpan) <- reactionSpans
]
pure (processNode, elements)
pLegacyProcessBody :: P (ProcessBody, CorrelateDecl, SagaRef, Name, [Name], PolicyChoice, PolicyChoice, [(Name, Int, SourceSpan)])
pLegacyProcessBody = do
inp <- pInputDecl
corr <- pCorrelate
saga <- pSaga
keyword "target"
tgt <- ident
projs <- keyword "projections" *> brackets (many ident)
handle <- pHandle
_ <- optional pDispatchIdLine
rejected <- pPolicyLine "rejected"
poison <- pPolicyLine "poison"
timer <- pTimerNode
pure (LegacyProcessBody inp handle timer, corr, saga, tgt, projs, rejected, poison, [])
pReactionProcessBody :: FrontendContext -> P (ProcessBody, CorrelateDecl, SagaRef, Name, [Name], PolicyChoice, PolicyChoice, [(Name, Int, SourceSpan)])
pReactionProcessBody context = do
marker <- withOwnedSpan (keyword "reactions")
requireLanguageFeatureAt context ProcessReactionSyntax (spanOf marker)
versionLoc <- getLoc
keyword "version"
reactionVersion <- fromIntegral <$> boundedDecimal
inputs <- NE.fromList <$> some pInputDecl
corr <- pCorrelate
saga <- pSaga
keyword "target"
tgt <- ident
projs <- keyword "projections" *> brackets (many ident)
parsedReactions <- some (pReactionNode context)
let reactions = NE.fromList (map fst parsedReactions)
reactionSpans = numberReactionSpans (concatMap snd parsedReactions)
pFixedDispatchIdLine ["name", "correlationId", "sourceEventId", "targetStreamName", "occurrence"]
rejected <- pPolicyLine "rejected"
poison <- pPolicyLine "poison"
policy <- optional pTimerPolicy
timers <- many pReactionTimerNode
pure
( ReactionProcessBody
ReactionBody
{ version = reactionVersion,
versionLoc = versionLoc,
inputs = inputs,
reactions = reactions,
timerPolicy = policy,
timers = timers
},
corr,
saga,
tgt,
projs,
rejected,
poison,
reactionSpans
)
where
numberReactionSpans raw =
[ (inputName, length [() | (priorName, _, _) <- take index raw, priorName == inputName], sourceSpan)
| (index, (inputName, _, sourceSpan)) <- zip [0 ..] raw
]
pReactionNode :: FrontendContext -> P (ReactionNode, [(Name, Int, SourceSpan)])
pReactionNode context = do
loc <- getLoc
keyword "on"
inputName <- ident
firstIsGuarded <- optional (lookAhead (keyword "when" <|> keyword "otherwise"))
locatedArms <- case firstIsGuarded of
Just _ -> some (withOwnedSpan (pGuardedReactionArm context))
Nothing -> (: []) <$> withOwnedSpan pUnconditionalReactionArm
let arms = NE.fromList [arm | Located {value = arm} <- locatedArms]
spans = [(inputName, ordinal, spanOf arm) | (ordinal, arm) <- zip [0 ..] locatedArms]
pure (ReactionNode {on = inputName, arms = arms, loc = loc}, spans)
pGuardedReactionArm :: FrontendContext -> P ReactionArm
pGuardedReactionArm context = do
loc <- getLoc
armGuard <-
(WhenArm <$> (keyword "when" *> pExpr context))
<|> (OtherwiseArm <$ keyword "otherwise")
armBody <- pReactionArmBody
pure ReactionArm {guard = armGuard, body = armBody, loc = loc}
pUnconditionalReactionArm :: P ReactionArm
pUnconditionalReactionArm = do
loc <- getLoc
armBody <- pReactionArmBody
pure ReactionArm {guard = UnconditionalArm, body = armBody, loc = loc}
pReactionArmBody :: P ArmBody
pReactionArmBody =
(NoAction <$ (keyword "no-action"))
<|> do
advance <- optional pAdvanceReaction
followUps <- many pFollowUp
case (advance, followUps) of
(Nothing, []) -> fail "reaction arm must declare advance, a follow-up, or no-action"
_ -> pure ArmActions {advance = advance, followUps = followUps}
pAdvanceReaction :: P AdvanceReaction
pAdvanceReaction = do
loc <- getLoc
keyword "advance"
command <- ident
fields <- braces (many pFieldBinding)
acceptedBlock <- optional $ do
keyword "accepted"
acceptedFollowUps <- many pFollowUp
silent <- optional (keyword "silent" *> keyword "no-action")
pure (acceptedFollowUps, maybe False (const True) silent)
pure
AdvanceReaction
{ command = command,
fields = fields,
accepted = fst <$> acceptedBlock,
silentNoAction = maybe False snd acceptedBlock,
loc = loc
}
pFollowUp :: P FollowUp
pFollowUp =
choice
[ FollowDispatch <$> try pDispatch,
FollowSchedule <$> try pSchedule,
do
loc <- getLoc
keyword "cancel"
FollowCancel <$> ident <*> pure loc
]
pSchedule :: P ScheduleNode
pSchedule = do
loc <- getLoc
keyword "schedule"
timer <- ident
mode <- maybe ScheduleRearm (const ScheduleOnce) <$> optional (keyword "once")
fireAt <- keyword "fireAt" *> pFireAt
bindings <- braces (many pFieldBinding)
pure ScheduleNode {timer = timer, mode = mode, fireAt = fireAt, bindings = bindings, loc = loc}
pTimerPolicy :: P TimerPolicy
pTimerPolicy = do
loc <- getLoc
keyword "timers"
keyword "max-attempts"
maxAttempts <- boundedDecimal
keyword "dead-letter"
deadLetter <- stringLit
pure TimerPolicy {maxAttempts = maxAttempts, deadLetter = deadLetter, loc = loc}
pReactionTimerNode :: P ReactionTimerNode
pReactionTimerNode = do
loc <- getLoc
keyword "timer"
name <- ident
timerId <- keyword "id" *> pIdExpr
payload <- keyword "payload" *> braces (many pPayloadField)
fire <- pFire
_ <- keyword "decode" *> keyword "unknown-status" *> symbol "=>"
decodeUnknown <- ident
pure ReactionTimerNode {name = name, id = timerId, payload = payload, fire = fire, decodeUnknown = decodeUnknown, loc = loc}
pPayloadField :: P PayloadField
pPayloadField = do
name <- ident
choice
[ PayloadConstant name <$> (symbol "=" *> stringLit),
PayloadTyped name <$> optional (symbol ":" *> ident)
]
pRouter :: FrontendContext -> P RouterNode
pRouter context = do
loc <- getLoc
keyword "router"
rid <- ident
keyword "name"
nm <- stringLit
(inp, typedInputSpan) <- pRouterInputDecl context
key <- pRouterKey (maybe False (const True) typedInputSpan)
resolved <- pResolveDecl context
case (typedInputSpan, (.source) resolved) of
(Just _, ResolveDeclarative {}) -> pure ()
(Nothing, ResolveReadModel {}) -> pure ()
(Nothing, ResolveHole) -> pure ()
(Just _, _) -> fail "typed router input requires declarative selection"
(Nothing, ResolveDeclarative {}) -> fail "declarative selection requires a typed router input"
keyword "target"
target <- ident
projections <- keyword "projections" *> brackets (many ident)
dispatch <- pRouterDispatch
pRouterDispatchIdLine
rejected <- pPolicyLine "rejected"
poison <- pPolicyLine "poison"
pure
RouterNode
{ id = rid,
name = nm,
input = inp,
key = key,
resolve = resolved,
target = target,
projections = projections,
dispatch = dispatch,
rejected = rejected,
poison = poison,
loc = loc
}
pRouterKey :: Bool -> P CorrelateDecl
pRouterKey declarative = do
keyword "key"
_ <- keyword "input" *> symbol "."
field <- ident
via <- if declarative then pure "idText" else keyword "via" *> ident
pure CorrelateDecl {field = field, via = via}
pResolveDecl :: FrontendContext -> P ResolveDecl
pResolveDecl context = do
loc <- getLoc
keyword "resolve"
choice [pDeclarativeResolve context loc, pCustomResolve loc]
pCustomResolve :: Loc -> P ResolveDecl
pCustomResolve loc = do
keyword "stable"
keyword "via"
source <- choice [ResolveReadModel <$> (keyword "read-model" *> ident), ResolveHole <$ keyword "hole"]
keyword "row"
row <- braces (many ident)
pure ResolveDecl {source = source, row = row, loc = loc}
pDeclarativeResolve :: FrontendContext -> Loc -> P ResolveDecl
pDeclarativeResolve context loc = do
marker <- withOwnedSpan (keyword "declarative")
requireLanguageFeatureAt context DeclarativeRouterSelectionSyntax (spanOf marker)
selection <- braces (pRouterSelection context loc)
pure ResolveDecl {source = ResolveDeclarative selection, row = [], loc = loc}
pRouterSelection :: FrontendContext -> Loc -> P RouterSelectionDecl
pRouterSelection context loc = do
(identity, identityLoc) <- locatedClause "identity" stringLit
(version, versionLoc) <- locatedClause "version" (fromIntegral <$> boundedDecimal)
queryLoc <- getLoc
keyword "query"
_ <- symbol "=" *> keyword "read-model"
query <- ident
keyword "with"
queryInputLoc <- getLoc
queryInput <- ident
keyword "where"
_ <- symbol "="
predicate <- pExpr context
keyword "recipient"
_ <- symbol "="
recipient <- pExpr context
(order, orderLoc) <- locatedClause "order" pSelectionPolicyName
(dedupe, dedupeLoc) <- locatedClause "dedupe" pSelectionPolicyName
recipientLimit <- optional $ try $ do
(limit, limitLoc) <- locatedClause "max-recipients" (fromIntegral <$> boundedDecimal)
pure (limit, limitLoc)
emptyPolicyLoc <- getLoc
keyword "empty"
_ <- symbol "=>"
emptyPolicy <- pSelectionDisposition
failurePolicyLoc <- getLoc
keyword "failure"
_ <- symbol "=>"
failurePolicy <- pSelectionDisposition
(redelivery, redeliveryLoc) <- locatedClause "redelivery" pSelectionPolicyName
(partial, partialLoc) <- locatedClause "partial" pSelectionPolicyName
pure
RouterSelectionDecl
{ identity = identity,
identityLoc = identityLoc,
version = version,
versionLoc = versionLoc,
query = query,
queryLoc = queryLoc,
queryInput = queryInput,
queryInputLoc = queryInputLoc,
predicate = predicate,
recipient = recipient,
limit = recipientLimit,
order = order,
orderLoc = orderLoc,
dedupe = dedupe,
dedupeLoc = dedupeLoc,
emptyPolicy = emptyPolicy,
emptyPolicyLoc = emptyPolicyLoc,
failurePolicy = failurePolicy,
failurePolicyLoc = failurePolicyLoc,
redelivery = redelivery,
redeliveryLoc = redeliveryLoc,
partial = partial,
partialLoc = partialLoc,
loc = loc
}
locatedClause :: Text -> P a -> P (a, Loc)
locatedClause clause parser = do
clauseLoc <- getLoc
keyword clause
_ <- symbol "="
value <- parser
pure (value, clauseLoc)
pSelectionDisposition :: P SelectionDispositionSyntax
pSelectionDisposition =
choice
[ SelectionAck <$ keyword "ack",
SelectionRetry <$ keyword "retry",
SelectionDeadLetter <$ keyword "deadLetter",
SelectionHalt <$ keyword "halt"
]
pSelectionPolicyName :: P Name
pSelectionPolicyName = T.intercalate "-" <$> ((:) <$> ident <*> many (symbol "-" *> ident))
pRouterDispatch :: P RouterDispatchNode
pRouterDispatch = do
loc <- getLoc
keyword "dispatch-each"
command <- ident
fields <- braces (many pFieldBinding)
disposition <-
DispatchDisposition
<$> (keyword "on-appended" *> pDisp)
<*> (symbol ";" *> keyword "on-duplicate" *> pDisp)
<*> (symbol ";" *> keyword "on-failed" *> pDisp)
pure RouterDispatchNode {command = command, fields = fields, disposition = disposition, loc = loc}
-- | @dispatch-id strategy=uuidv5 from=(…)@ where the tuple is fixed by the
-- runtime that derives the id. The line documents a derivation the spec cannot
-- change, so the only sound thing to accept is the exact spelling that is true.
pFixedDispatchIdLine :: [Text] -> P ()
pFixedDispatchIdLine inputs = do
keyword "dispatch-id"
_ <- symbol "strategy" *> symbol "=" *> keyword "uuidv5"
_ <- symbol "from" *> symbol "=" *> parens (sequence_ (intersperse (() <$ symbol ",") (map keyword inputs)))
pure ()
-- | @Keiro.Router.deterministicRouterCommandId@ keys on the router name, the
-- correlation key, the source event, the resolved target stream, and the
-- same-stream occurrence.
pRouterDispatchIdLine :: P ()
pRouterDispatchIdLine =
pFixedDispatchIdLine ["name", "key", "sourceEventId", "targetStreamName", "occurrence"]
pPolicyLine :: Text -> P PolicyChoice
pPolicyLine clause = keyword clause *> symbol "=>" *> pPolicyChoice
pPolicyChoice :: P PolicyChoice
pPolicyChoice =
choice
[ PolHalt <$ keyword "halt",
PolDeadLetter <$ keyword "deadLetter",
PolSkip <$ keyword "skip"
]
pInputDecl :: P InputDecl
pInputDecl = do
loc <- getLoc
keyword "input"
nm <- ident
fs <- braces (many pField)
pure InputDecl {name = nm, fields = fs, valueType = Nothing, loc = loc}
pRouterInputDecl :: FrontendContext -> P (InputDecl, Maybe SourceSpan)
pRouterInputDecl context = do
loc <- getLoc
keyword "input"
name <- ident
choice
[ do
marker <- withOwnedSpan (symbol ":")
inputType <- pMappedTypeExpr context
pure (InputDecl {name = name, fields = [], valueType = Just inputType, loc = loc}, Just (spanOf marker)),
do
fields <- braces (many pField)
pure (InputDecl {name = name, fields = fields, valueType = Nothing, loc = loc}, Nothing)
]
pCorrelate :: P CorrelateDecl
pCorrelate = do
keyword "correlate"
_ <- keyword "input" *> symbol "."
f <- ident
keyword "via"
v <- ident
pure CorrelateDecl {field = f, via = v}
pSaga :: P SagaRef
pSaga = do
keyword "saga"
agg <- ident
keyword "category"
categoryName <- stringLit
pure SagaRef {agg = agg, category = categoryName}
pHandle :: P HandleNode
pHandle = do
keyword "on"
onName <- ident
adv <- pAdvance
disps <- many pDispatch
keyword "schedule"
sched <- ident
pure HandleNode {on = onName, advance = adv, dispatch = disps, schedule = sched}
pAdvance :: P AdvanceNode
pAdvance = do
keyword "advance"
cmd <- ident
fs <- braces (many pFieldBinding)
pure AdvanceNode {advCommand = cmd, advFields = fs}
pDispatch :: P DispatchNode
pDispatch = do
loc <- getLoc
keyword "dispatch"
tgt <- ident
_ <- symbol "@"
key <- dottedRef
cmd <- ident
fs <- braces (many pFieldBinding)
disp <-
DispatchDisposition
<$> (keyword "on-appended" *> pDisp)
<*> (symbol ";" *> keyword "on-duplicate" *> pDisp)
<*> (symbol ";" *> keyword "on-failed" *> pDisp)
pure DispatchNode {target = tgt, key = key, command = cmd, fields = fs, disposition = disp, loc = loc}
pDisp :: P Disp
pDisp =
choice
[ DAckOk <$ keyword "AckOk",
DRetry <$ keyword "Retry",
DDeadLetter <$> (keyword "DeadLetter" *> stringLit)
]
-- | A process manager's twin of 'pRouterDispatchIdLine'.
-- @Keiro.ProcessManager.deterministicCommandId@ keys on the manager name, the
-- correlation id, the source event, and the positional emit index — a different
-- fixed tuple from the router's, so the two lines are checked separately but
-- equally strictly. Before ExecPlan 199 a process accepted any strategy
-- identifier and any tuple, so `dispatch-id strategy=md5 from=(banana)` checked
-- clean here while the same line was a parse error on a router.
pDispatchIdLine :: P ()
pDispatchIdLine =
pFixedDispatchIdLine ["name", "correlationId", "sourceEventId", "emitIndex"]
pTimerNode :: P TimerNode
pTimerNode = do
loc <- getLoc
keyword "timer"
nm <- ident
tid <- keyword "id" *> pIdExpr
fat <- keyword "fireAt" *> pFireAt
pay <- keyword "payload" *> braces (many pFieldBinding)
fire <- pFire
_ <- keyword "decode" *> keyword "unknown-status" *> symbol "=>"
unk <- ident
keyword "max-attempts"
ma <- boundedDecimal
keyword "dead-letter"
dl <- stringLit
pure
TimerNode
{ name = nm,
id = tid,
fireAt = fat,
payload = pay,
fire = fire,
decodeUnknown = unk,
maxAttempts = ma,
deadLetter = dl,
loc = loc
}
pIdExpr :: P IdExpr
pIdExpr = do
keyword "uuidv5"
pfx <- stringLit
_ <- symbol "<>"
field <- ident
pure IdExpr {strategy = UuidV5Id, prefix = pfx, field = field}
pFireAt :: P FireAtExpr
pFireAt = do
_ <- keyword "input" *> symbol "."
f <- ident
_ <- symbol "+"
w <- pWindow
pure FireAtExpr {field = f, window = w}
pFire :: P FireNode
pFire = do
keyword "fire"
keyword "dispatch"
tgt <- ident
_ <- symbol "@"
key <- dottedRef
cmd <- ident
fs <- braces (many pFieldBinding)
fid <- keyword "fired-event-id" *> pIdExpr
disp <-
FireDisposition
<$> (keyword "on-ok" *> pFireOutcome)
<*> (symbol ";" *> keyword "on-reject" *> pFireOutcome)
<*> (symbol ";" *> keyword "on-ambiguous" *> pFireOutcome)
<*> (symbol ";" *> keyword "on-error" *> pFireOutcome)
<*> (symbol ";" *> keyword "not-mine" *> pFireOutcome)
pure FireNode {target = tgt, key = key, command = cmd, fields = fs, firedEventId = fid, disposition = disp}
pFireOutcome :: P FireOutcome
pFireOutcome = choice [OFired <$ keyword "Fired", ORetry <$ keyword "Retry"]
pFieldBinding :: P FieldBinding
pFieldBinding = do
n <- ident
v <- optional (symbol "=" *> pBindingValue)
pure FieldBinding {name = n, value = v}
-- | A binding value: a quoted string (kept quoted) or a dotted reference.
pBindingValue :: P Text
pBindingValue = choice [quoted, dottedRef]
where
quoted = do
s <- stringLit
pure ("\"" <> s <> "\"")
-- | A dotted/plain reference token like @input.hospitalId@, @timer.id@,
-- @correlationId@.
dottedRef :: P Text
dottedRef = lexeme $ do
c <- asciiLetter
cs <- many (asciiAlphaNum <|> char '_' <|> char '.')
pure (T.pack (c : cs))
-- | A double-quoted string literal, returning raw (unescaped) inner text.
-- The surface syntax supports a closed escape set so unknown escapes remain
-- available for backward-compatible extensions.