add match!
This commit is contained in:
+29
-2
@@ -209,6 +209,33 @@ Creates one boolean decision step. The condition uses the same JSON shape as
|
|||||||
The draft adapter lowers this through `WorkflowBuilder.when()`. The draft step
|
The draft adapter lowers this through `WorkflowBuilder.when()`. The draft step
|
||||||
id becomes the generated condition entry id, so other routes can target it.
|
id becomes the generated condition entry id, so other routes can target it.
|
||||||
|
|
||||||
|
### `match`
|
||||||
|
|
||||||
|
Matches one graph value against ordered equality cases.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"value": "state.status",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"equals": "ready",
|
||||||
|
"then": "run"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"equals": "waiting",
|
||||||
|
"then": "pause"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "__end__"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cases are a list rather than a JSON object so values such as `1`, `"1"`, and
|
||||||
|
`true` are not silently coerced into object keys. The draft adapter lowers this
|
||||||
|
through `WorkflowBuilder.match()`.
|
||||||
|
|
||||||
### `choose`
|
### `choose`
|
||||||
|
|
||||||
Creates an ordered first-true decision chain.
|
Creates an ordered first-true decision chain.
|
||||||
@@ -243,8 +270,8 @@ Creates an ordered first-true decision chain.
|
|||||||
```
|
```
|
||||||
|
|
||||||
`choose` lowers through `WorkflowBuilder.choose()` and expands to generated
|
`choose` lowers through `WorkflowBuilder.choose()` and expands to generated
|
||||||
condition nodes. It replaces the deprecated `route()` concept for draft JSON;
|
condition nodes. `match`, `when`, and `choose` replace the deprecated `route()`
|
||||||
there is intentionally no draft `route` step kind.
|
concept for draft JSON; there is intentionally no draft `route` step kind.
|
||||||
|
|
||||||
## Draft Tools
|
## Draft Tools
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from .models import (
|
|||||||
DraftForeachStep,
|
DraftForeachStep,
|
||||||
DraftInterruptStep,
|
DraftInterruptStep,
|
||||||
DraftJoinStep,
|
DraftJoinStep,
|
||||||
|
DraftMatchCase,
|
||||||
|
DraftMatchStep,
|
||||||
DraftWhenStep,
|
DraftWhenStep,
|
||||||
DraftUseStep,
|
DraftUseStep,
|
||||||
WorkflowDraft,
|
WorkflowDraft,
|
||||||
@@ -23,6 +25,8 @@ __all__ = [
|
|||||||
"DraftForeachStep",
|
"DraftForeachStep",
|
||||||
"DraftInterruptStep",
|
"DraftInterruptStep",
|
||||||
"DraftJoinStep",
|
"DraftJoinStep",
|
||||||
|
"DraftMatchCase",
|
||||||
|
"DraftMatchStep",
|
||||||
"DraftWhenStep",
|
"DraftWhenStep",
|
||||||
"DraftUseStep",
|
"DraftUseStep",
|
||||||
"WorkflowDraft",
|
"WorkflowDraft",
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from wf_authoring import WorkflowBuilder
|
from wf_authoring import WorkflowBuilder
|
||||||
|
from wf_authoring.dsl import PathExpr
|
||||||
from wf_core import JoinNode, Workflow
|
from wf_core import JoinNode, Workflow
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
DraftChooseStep,
|
||||||
DraftForeachStep,
|
DraftForeachStep,
|
||||||
DraftInterruptStep,
|
DraftInterruptStep,
|
||||||
DraftJoinStep,
|
DraftJoinStep,
|
||||||
|
DraftMatchStep,
|
||||||
DraftStep,
|
DraftStep,
|
||||||
DraftUseStep,
|
DraftUseStep,
|
||||||
DraftWhenStep,
|
DraftWhenStep,
|
||||||
DraftChooseStep,
|
|
||||||
WorkflowDraft,
|
WorkflowDraft,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,4 +81,11 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
|||||||
id=step_id,
|
id=step_id,
|
||||||
default=step.choose.default,
|
default=step.choose.default,
|
||||||
).entry
|
).entry
|
||||||
|
if isinstance(step, DraftMatchStep):
|
||||||
|
return builder.match(
|
||||||
|
PathExpr(step.match.value),
|
||||||
|
{case.equals: case.then for case in step.match.cases},
|
||||||
|
id=step_id,
|
||||||
|
default=step.match.default,
|
||||||
|
).entry
|
||||||
raise TypeError(f"unsupported draft step {type(step)!r}")
|
raise TypeError(f"unsupported draft step {type(step)!r}")
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from pydantic import BaseModel, Field, model_validator
|
|||||||
from wf_core.models.conditions import Condition
|
from wf_core.models.conditions import Condition
|
||||||
|
|
||||||
JsonObject = dict[str, Any]
|
JsonObject = dict[str, Any]
|
||||||
STEP_KIND_KEYS = frozenset({"use", "foreach", "interrupt", "join", "when", "choose"})
|
STEP_KIND_KEYS = frozenset(
|
||||||
|
{"use", "foreach", "interrupt", "join", "when", "choose", "match"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DraftUseStep(BaseModel):
|
class DraftUseStep(BaseModel):
|
||||||
@@ -91,6 +93,27 @@ class DraftChooseStep(BaseModel):
|
|||||||
choose: DraftChoosePayload
|
choose: DraftChoosePayload
|
||||||
|
|
||||||
|
|
||||||
|
class DraftMatchCase(BaseModel):
|
||||||
|
"""One ordered equality case in a draft match decision."""
|
||||||
|
|
||||||
|
equals: Any
|
||||||
|
then: str
|
||||||
|
|
||||||
|
|
||||||
|
class DraftMatchPayload(BaseModel):
|
||||||
|
"""Payload for matching one graph value against ordered equality cases."""
|
||||||
|
|
||||||
|
value: str
|
||||||
|
cases: list[DraftMatchCase] = Field(min_length=1)
|
||||||
|
default: str = "__end__"
|
||||||
|
|
||||||
|
|
||||||
|
class DraftMatchStep(BaseModel):
|
||||||
|
"""Draft step that delegates equality decisions to `WorkflowBuilder.match`."""
|
||||||
|
|
||||||
|
match: DraftMatchPayload
|
||||||
|
|
||||||
|
|
||||||
DraftStep = (
|
DraftStep = (
|
||||||
DraftUseStep
|
DraftUseStep
|
||||||
| DraftForeachStep
|
| DraftForeachStep
|
||||||
@@ -98,6 +121,7 @@ DraftStep = (
|
|||||||
| DraftJoinStep
|
| DraftJoinStep
|
||||||
| DraftWhenStep
|
| DraftWhenStep
|
||||||
| DraftChooseStep
|
| DraftChooseStep
|
||||||
|
| DraftMatchStep
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,3 +121,47 @@ def test_adapter_lowers_choose_step_through_builder() -> None:
|
|||||||
("pick_2", "true", "fallback"),
|
("pick_2", "true", "fallback"),
|
||||||
("pick_2", "false", "__end__"),
|
("pick_2", "false", "__end__"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapter_lowers_match_step_through_builder() -> None:
|
||||||
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
|
"name": "match_example",
|
||||||
|
"input_schema": {},
|
||||||
|
"state_schema": {"fields": {}},
|
||||||
|
"output_schema": {},
|
||||||
|
"start": "match_status",
|
||||||
|
"steps": {
|
||||||
|
"match_status": {
|
||||||
|
"match": {
|
||||||
|
"value": "state.status",
|
||||||
|
"cases": [
|
||||||
|
{"equals": "ready", "then": "ready"},
|
||||||
|
{"equals": "waiting", "then": "waiting"},
|
||||||
|
],
|
||||||
|
"default": "__end__",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ready": {"use": "demo.ready"},
|
||||||
|
"waiting": {"use": "demo.waiting"},
|
||||||
|
},
|
||||||
|
"routes": {
|
||||||
|
"ready": {"ok": "__end__"},
|
||||||
|
"waiting": {"ok": "__end__"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow = build_workflow_from_draft(draft)
|
||||||
|
condition_ids = [
|
||||||
|
node.id for node in workflow.nodes if isinstance(node, ConditionNode)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert condition_ids == ["match_status", "match_status_2"]
|
||||||
|
assert workflow.start == "match_status"
|
||||||
|
assert [(edge.from_, edge.outcome, edge.to) for edge in workflow.edges[:4]] == [
|
||||||
|
("match_status", "true", "ready"),
|
||||||
|
("match_status", "false", "match_status_2"),
|
||||||
|
("match_status_2", "true", "waiting"),
|
||||||
|
("match_status_2", "false", "__end__"),
|
||||||
|
]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pydantic import ValidationError
|
|||||||
|
|
||||||
from wf_artifacts.drafts import (
|
from wf_artifacts.drafts import (
|
||||||
DraftChooseStep,
|
DraftChooseStep,
|
||||||
|
DraftMatchStep,
|
||||||
DraftUseStep,
|
DraftUseStep,
|
||||||
DraftWhenStep,
|
DraftWhenStep,
|
||||||
WorkflowDraft,
|
WorkflowDraft,
|
||||||
@@ -87,6 +88,30 @@ def test_workflow_draft_accepts_choose_step() -> None:
|
|||||||
assert isinstance(draft.steps["choose_next"], DraftChooseStep)
|
assert isinstance(draft.steps["choose_next"], DraftChooseStep)
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_draft_accepts_match_step() -> None:
|
||||||
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
|
**_keyed_echo_draft(),
|
||||||
|
"start": "match_status",
|
||||||
|
"steps": {
|
||||||
|
**_keyed_echo_draft()["steps"],
|
||||||
|
"match_status": {
|
||||||
|
"match": {
|
||||||
|
"value": "state.status",
|
||||||
|
"cases": [
|
||||||
|
{"equals": "ready", "then": "echo"},
|
||||||
|
{"equals": "done", "then": "__end__"},
|
||||||
|
],
|
||||||
|
"default": "__end__",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(draft.steps["match_status"], DraftMatchStep)
|
||||||
|
|
||||||
|
|
||||||
def _keyed_echo_draft() -> dict[str, Any]:
|
def _keyed_echo_draft() -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"name": "echo",
|
"name": "echo",
|
||||||
|
|||||||
Reference in New Issue
Block a user