more std, and buffing @node
This commit is contained in:
@@ -18,12 +18,17 @@ from .dsl import (
|
|||||||
from .ops import (
|
from .ops import (
|
||||||
BoolOutput,
|
BoolOutput,
|
||||||
CoalesceInput,
|
CoalesceInput,
|
||||||
|
ConstantInput,
|
||||||
CountOutput,
|
CountOutput,
|
||||||
ItemOutput,
|
ItemOutput,
|
||||||
MaybeItemOutput,
|
MaybeItemOutput,
|
||||||
|
PickKeyInput,
|
||||||
SequenceInput,
|
SequenceInput,
|
||||||
|
TruthyInput,
|
||||||
ValueOutput,
|
ValueOutput,
|
||||||
coalesce,
|
coalesce,
|
||||||
|
constant,
|
||||||
|
default_if_none,
|
||||||
first_item,
|
first_item,
|
||||||
first_item_maybe,
|
first_item_maybe,
|
||||||
first_item_or_none,
|
first_item_or_none,
|
||||||
@@ -31,6 +36,8 @@ from .ops import (
|
|||||||
last_item,
|
last_item,
|
||||||
last_item_or_none,
|
last_item_or_none,
|
||||||
length,
|
length,
|
||||||
|
pick_key,
|
||||||
|
truthy,
|
||||||
)
|
)
|
||||||
from .nodes import (
|
from .nodes import (
|
||||||
AsyncRegistryHandler,
|
AsyncRegistryHandler,
|
||||||
@@ -48,15 +55,18 @@ __all__ = [
|
|||||||
"NodeCatalogEntry",
|
"NodeCatalogEntry",
|
||||||
"BoolOutput",
|
"BoolOutput",
|
||||||
"CoalesceInput",
|
"CoalesceInput",
|
||||||
|
"ConstantInput",
|
||||||
"CountOutput",
|
"CountOutput",
|
||||||
"GraphPath",
|
"GraphPath",
|
||||||
"ItemOutput",
|
"ItemOutput",
|
||||||
"MaybeItemOutput",
|
"MaybeItemOutput",
|
||||||
|
"PickKeyInput",
|
||||||
"NodeReturn",
|
"NodeReturn",
|
||||||
"NodeSpec",
|
"NodeSpec",
|
||||||
"AsyncRegistryHandler",
|
"AsyncRegistryHandler",
|
||||||
"SyncRegistryHandler",
|
"SyncRegistryHandler",
|
||||||
"SequenceInput",
|
"SequenceInput",
|
||||||
|
"TruthyInput",
|
||||||
"ValueOutput",
|
"ValueOutput",
|
||||||
"WorkflowBuilder",
|
"WorkflowBuilder",
|
||||||
"bind_fields",
|
"bind_fields",
|
||||||
@@ -64,6 +74,8 @@ __all__ = [
|
|||||||
"build_registry",
|
"build_registry",
|
||||||
"bind_state",
|
"bind_state",
|
||||||
"coalesce",
|
"coalesce",
|
||||||
|
"constant",
|
||||||
|
"default_if_none",
|
||||||
"merge_maps",
|
"merge_maps",
|
||||||
"context",
|
"context",
|
||||||
"context_path",
|
"context_path",
|
||||||
@@ -79,8 +91,10 @@ __all__ = [
|
|||||||
"last_item",
|
"last_item",
|
||||||
"last_item_or_none",
|
"last_item_or_none",
|
||||||
"length",
|
"length",
|
||||||
|
"pick_key",
|
||||||
"node",
|
"node",
|
||||||
"state",
|
"state",
|
||||||
"state_path",
|
"state_path",
|
||||||
"subgraph_node",
|
"subgraph_node",
|
||||||
|
"truthy",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,17 +13,39 @@ from .spec import NodeSpec
|
|||||||
|
|
||||||
@overload
|
@overload
|
||||||
def node(
|
def node(
|
||||||
fn: NodeCallable[InputT, OutputT] | AsyncNodeCallable[InputT, OutputT],
|
fn: NodeSpec[InputT, OutputT]
|
||||||
|
| NodeCallable[InputT, OutputT]
|
||||||
|
| AsyncNodeCallable[InputT, OutputT],
|
||||||
/,
|
/,
|
||||||
) -> NodeSpec[InputT, OutputT]: ...
|
) -> NodeSpec[InputT, OutputT]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def node(
|
||||||
|
fn: NodeSpec[InputT, OutputT]
|
||||||
|
| NodeCallable[InputT, OutputT]
|
||||||
|
| AsyncNodeCallable[InputT, OutputT],
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
input_model: type[InputT] | None = None,
|
||||||
|
output_model: type[OutputT] | None = None,
|
||||||
|
outcomes: tuple[str, ...] = ("ok",),
|
||||||
|
description: str | None = None,
|
||||||
|
is_async: bool | None = None,
|
||||||
|
) -> NodeSpec[InputT, OutputT]: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def node(
|
def node(
|
||||||
fn: None = None,
|
fn: None = None,
|
||||||
/,
|
/,
|
||||||
) -> Callable[
|
) -> Callable[
|
||||||
[NodeCallable[InputT, OutputT] | AsyncNodeCallable[InputT, OutputT]],
|
[
|
||||||
|
NodeSpec[InputT, OutputT]
|
||||||
|
| NodeCallable[InputT, OutputT]
|
||||||
|
| AsyncNodeCallable[InputT, OutputT]
|
||||||
|
],
|
||||||
NodeSpec[InputT, OutputT],
|
NodeSpec[InputT, OutputT],
|
||||||
]: ...
|
]: ...
|
||||||
|
|
||||||
@@ -40,13 +62,18 @@ def node(
|
|||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
is_async: bool | None = None,
|
is_async: bool | None = None,
|
||||||
) -> Callable[
|
) -> Callable[
|
||||||
[NodeCallable[InputT, OutputT] | AsyncNodeCallable[InputT, OutputT]],
|
[
|
||||||
|
NodeSpec[InputT, OutputT]
|
||||||
|
| NodeCallable[InputT, OutputT]
|
||||||
|
| AsyncNodeCallable[InputT, OutputT]
|
||||||
|
],
|
||||||
NodeSpec[InputT, OutputT],
|
NodeSpec[InputT, OutputT],
|
||||||
]: ...
|
]: ...
|
||||||
|
|
||||||
|
|
||||||
def node(
|
def node(
|
||||||
fn: NodeCallable[InputT, OutputT]
|
fn: NodeSpec[InputT, OutputT]
|
||||||
|
| NodeCallable[InputT, OutputT]
|
||||||
| AsyncNodeCallable[InputT, OutputT]
|
| AsyncNodeCallable[InputT, OutputT]
|
||||||
| None = None,
|
| None = None,
|
||||||
*,
|
*,
|
||||||
@@ -58,9 +85,24 @@ def node(
|
|||||||
is_async: bool | None = None,
|
is_async: bool | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Convert a typed Python function into a reusable workflow node spec."""
|
"""Convert a typed Python function into a reusable workflow node spec."""
|
||||||
|
|
||||||
def decorator(
|
def decorator(
|
||||||
fn: NodeCallable[InputT, OutputT] | AsyncNodeCallable[InputT, OutputT],
|
fn: NodeSpec[InputT, OutputT]
|
||||||
|
| NodeCallable[InputT, OutputT]
|
||||||
|
| AsyncNodeCallable[InputT, OutputT],
|
||||||
) -> NodeSpec[InputT, OutputT]:
|
) -> NodeSpec[InputT, OutputT]:
|
||||||
|
if isinstance(fn, NodeSpec):
|
||||||
|
return NodeSpec(
|
||||||
|
name=name or fn.name,
|
||||||
|
input_model=input_model or fn.input_model,
|
||||||
|
output_model=output_model or fn.output_model,
|
||||||
|
outcomes=outcomes if outcomes != ("ok",) else fn.outcomes,
|
||||||
|
fn=fn.fn,
|
||||||
|
description=description or fn.description,
|
||||||
|
is_async=is_async if is_async is not None else fn.is_async,
|
||||||
|
accepts_context=fn.accepts_context,
|
||||||
|
)
|
||||||
|
|
||||||
inferred_input_model: type[BaseModel] | None = input_model
|
inferred_input_model: type[BaseModel] | None = input_model
|
||||||
inferred_output_model: type[BaseModel] | None = output_model
|
inferred_output_model: type[BaseModel] | None = output_model
|
||||||
if inferred_input_model is None or inferred_output_model is None:
|
if inferred_input_model is None or inferred_output_model is None:
|
||||||
|
|||||||
@@ -12,17 +12,33 @@ from .sequences import (
|
|||||||
last_item_or_none,
|
last_item_or_none,
|
||||||
length,
|
length,
|
||||||
)
|
)
|
||||||
from .values import CoalesceInput, ValueOutput, coalesce
|
from .values import (
|
||||||
|
CoalesceInput,
|
||||||
|
ConstantInput,
|
||||||
|
PickKeyInput,
|
||||||
|
TruthyInput,
|
||||||
|
ValueOutput,
|
||||||
|
coalesce,
|
||||||
|
constant,
|
||||||
|
default_if_none,
|
||||||
|
pick_key,
|
||||||
|
truthy,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BoolOutput",
|
"BoolOutput",
|
||||||
"CoalesceInput",
|
"CoalesceInput",
|
||||||
|
"ConstantInput",
|
||||||
"CountOutput",
|
"CountOutput",
|
||||||
"ItemOutput",
|
"ItemOutput",
|
||||||
"MaybeItemOutput",
|
"MaybeItemOutput",
|
||||||
|
"PickKeyInput",
|
||||||
"SequenceInput",
|
"SequenceInput",
|
||||||
|
"TruthyInput",
|
||||||
"ValueOutput",
|
"ValueOutput",
|
||||||
"coalesce",
|
"coalesce",
|
||||||
|
"constant",
|
||||||
|
"default_if_none",
|
||||||
"first_item",
|
"first_item",
|
||||||
"first_item_maybe",
|
"first_item_maybe",
|
||||||
"first_item_or_none",
|
"first_item_or_none",
|
||||||
@@ -30,4 +46,6 @@ __all__ = [
|
|||||||
"last_item",
|
"last_item",
|
||||||
"last_item_or_none",
|
"last_item_or_none",
|
||||||
"length",
|
"length",
|
||||||
|
"pick_key",
|
||||||
|
"truthy",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,22 +8,32 @@ from wf_authoring.nodes import NodeReturn, node
|
|||||||
|
|
||||||
|
|
||||||
class SequenceInput(BaseModel):
|
class SequenceInput(BaseModel):
|
||||||
|
"""Input model for ops that consume an ordered sequence."""
|
||||||
|
|
||||||
items: list[Any]
|
items: list[Any]
|
||||||
|
|
||||||
|
|
||||||
class ItemOutput(BaseModel):
|
class ItemOutput(BaseModel):
|
||||||
|
"""Output model for ops that return a selected item."""
|
||||||
|
|
||||||
item: Any
|
item: Any
|
||||||
|
|
||||||
|
|
||||||
class MaybeItemOutput(BaseModel):
|
class MaybeItemOutput(BaseModel):
|
||||||
|
"""Output model for ops that may not find an item."""
|
||||||
|
|
||||||
item: Any | None = None
|
item: Any | None = None
|
||||||
|
|
||||||
|
|
||||||
class CountOutput(BaseModel):
|
class CountOutput(BaseModel):
|
||||||
|
"""Output model for ops that return a count."""
|
||||||
|
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
|
|
||||||
class BoolOutput(BaseModel):
|
class BoolOutput(BaseModel):
|
||||||
|
"""Output model for ops that return a boolean value."""
|
||||||
|
|
||||||
value: bool
|
value: bool
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +44,7 @@ class BoolOutput(BaseModel):
|
|||||||
description="Select the first item from a non-empty sequence.",
|
description="Select the first item from a non-empty sequence.",
|
||||||
)
|
)
|
||||||
def first_item(input: SequenceInput) -> ItemOutput:
|
def first_item(input: SequenceInput) -> ItemOutput:
|
||||||
|
"""Select the first item from a non-empty sequence."""
|
||||||
if not input.items:
|
if not input.items:
|
||||||
raise ValueError("first_item requires at least one item")
|
raise ValueError("first_item requires at least one item")
|
||||||
return ItemOutput(item=input.items[0])
|
return ItemOutput(item=input.items[0])
|
||||||
@@ -46,6 +57,7 @@ def first_item(input: SequenceInput) -> ItemOutput:
|
|||||||
description="Select the first item from a sequence, or None when it is empty.",
|
description="Select the first item from a sequence, or None when it is empty.",
|
||||||
)
|
)
|
||||||
def first_item_or_none(input: SequenceInput) -> ItemOutput:
|
def first_item_or_none(input: SequenceInput) -> ItemOutput:
|
||||||
|
"""Select the first item from a sequence, or None if it is empty."""
|
||||||
return ItemOutput(item=input.items[0] if input.items else None)
|
return ItemOutput(item=input.items[0] if input.items else None)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +69,7 @@ def first_item_or_none(input: SequenceInput) -> ItemOutput:
|
|||||||
description="Select the first item from a sequence, routing to found or missing.",
|
description="Select the first item from a sequence, routing to found or missing.",
|
||||||
)
|
)
|
||||||
def first_item_maybe(input: SequenceInput) -> NodeReturn[MaybeItemOutput]:
|
def first_item_maybe(input: SequenceInput) -> NodeReturn[MaybeItemOutput]:
|
||||||
|
"""Select the first item and route by whether one exists."""
|
||||||
if not input.items:
|
if not input.items:
|
||||||
return NodeReturn(outcome="missing", output=MaybeItemOutput())
|
return NodeReturn(outcome="missing", output=MaybeItemOutput())
|
||||||
return NodeReturn(outcome="found", output=MaybeItemOutput(item=input.items[0]))
|
return NodeReturn(outcome="found", output=MaybeItemOutput(item=input.items[0]))
|
||||||
@@ -69,6 +82,7 @@ def first_item_maybe(input: SequenceInput) -> NodeReturn[MaybeItemOutput]:
|
|||||||
description="Select the last item from a non-empty sequence.",
|
description="Select the last item from a non-empty sequence.",
|
||||||
)
|
)
|
||||||
def last_item(input: SequenceInput) -> ItemOutput:
|
def last_item(input: SequenceInput) -> ItemOutput:
|
||||||
|
"""Select the last item from a non-empty sequence."""
|
||||||
if not input.items:
|
if not input.items:
|
||||||
raise ValueError("last_item requires at least one item")
|
raise ValueError("last_item requires at least one item")
|
||||||
return ItemOutput(item=input.items[-1])
|
return ItemOutput(item=input.items[-1])
|
||||||
@@ -81,6 +95,7 @@ def last_item(input: SequenceInput) -> ItemOutput:
|
|||||||
description="Select the last item from a sequence, or None when it is empty.",
|
description="Select the last item from a sequence, or None when it is empty.",
|
||||||
)
|
)
|
||||||
def last_item_or_none(input: SequenceInput) -> ItemOutput:
|
def last_item_or_none(input: SequenceInput) -> ItemOutput:
|
||||||
|
"""Select the last item from a sequence, or None if it is empty."""
|
||||||
return ItemOutput(item=input.items[-1] if input.items else None)
|
return ItemOutput(item=input.items[-1] if input.items else None)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,6 +106,7 @@ def last_item_or_none(input: SequenceInput) -> ItemOutput:
|
|||||||
description="Count the items in a sequence.",
|
description="Count the items in a sequence.",
|
||||||
)
|
)
|
||||||
def length(input: SequenceInput) -> CountOutput:
|
def length(input: SequenceInput) -> CountOutput:
|
||||||
|
"""Count the number of items in a sequence."""
|
||||||
return CountOutput(count=len(input.items))
|
return CountOutput(count=len(input.items))
|
||||||
|
|
||||||
|
|
||||||
@@ -101,4 +117,5 @@ def length(input: SequenceInput) -> CountOutput:
|
|||||||
description="Return whether a sequence is empty.",
|
description="Return whether a sequence is empty.",
|
||||||
)
|
)
|
||||||
def is_empty(input: SequenceInput) -> BoolOutput:
|
def is_empty(input: SequenceInput) -> BoolOutput:
|
||||||
|
"""Return whether a sequence has no items."""
|
||||||
return BoolOutput(value=not input.items)
|
return BoolOutput(value=not input.items)
|
||||||
|
|||||||
@@ -4,15 +4,38 @@ from typing import Any
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from wf_authoring.nodes import node
|
from wf_authoring.nodes import NodeReturn, node
|
||||||
|
|
||||||
|
|
||||||
class CoalesceInput(BaseModel):
|
class CoalesceInput(BaseModel):
|
||||||
|
"""Input model for selecting the first non-None value."""
|
||||||
|
|
||||||
value: Any | None = None
|
value: Any | None = None
|
||||||
fallback: Any
|
fallback: Any
|
||||||
|
|
||||||
|
|
||||||
class ValueOutput(BaseModel):
|
class ValueOutput(BaseModel):
|
||||||
|
"""Output model for ops that emit an arbitrary value."""
|
||||||
|
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
class ConstantInput(BaseModel):
|
||||||
|
"""Input model for passing through a configured value."""
|
||||||
|
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
class PickKeyInput(BaseModel):
|
||||||
|
"""Input model for selecting a value from a mapping by key."""
|
||||||
|
|
||||||
|
mapping: dict[str, Any]
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
|
class TruthyInput(BaseModel):
|
||||||
|
"""Input model for routing by Python truthiness."""
|
||||||
|
|
||||||
value: Any
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
@@ -23,4 +46,48 @@ class ValueOutput(BaseModel):
|
|||||||
description="Return value when it is not None, otherwise return fallback.",
|
description="Return value when it is not None, otherwise return fallback.",
|
||||||
)
|
)
|
||||||
def coalesce(input: CoalesceInput) -> ValueOutput:
|
def coalesce(input: CoalesceInput) -> ValueOutput:
|
||||||
|
"""Return value when it is not None, otherwise return fallback."""
|
||||||
return ValueOutput(value=input.value if input.value is not None else input.fallback)
|
return ValueOutput(value=input.value if input.value is not None else input.fallback)
|
||||||
|
|
||||||
|
|
||||||
|
default_if_none = node(
|
||||||
|
name="authoring.default_if_none",
|
||||||
|
description="Alias for coalesce: return fallback only when value is None.",
|
||||||
|
)(coalesce)
|
||||||
|
"""Alias for coalesce with a more explicit name for None-defaulting workflows."""
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.constant",
|
||||||
|
input_model=ConstantInput,
|
||||||
|
output_model=ValueOutput,
|
||||||
|
description="Return the provided value unchanged.",
|
||||||
|
)
|
||||||
|
def constant(input: ConstantInput) -> ValueOutput:
|
||||||
|
"""Return the provided value unchanged."""
|
||||||
|
return ValueOutput(value=input.value)
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.pick_key",
|
||||||
|
input_model=PickKeyInput,
|
||||||
|
output_model=ValueOutput,
|
||||||
|
description="Select a value from a mapping by key, returning None if missing.",
|
||||||
|
)
|
||||||
|
def pick_key(input: PickKeyInput) -> ValueOutput:
|
||||||
|
"""Select a value from a mapping by key, returning None if missing."""
|
||||||
|
return ValueOutput(value=input.mapping.get(input.key))
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.truthy",
|
||||||
|
input_model=TruthyInput,
|
||||||
|
output_model=ValueOutput,
|
||||||
|
outcomes=("truthy", "falsey"),
|
||||||
|
description="Route based on Python truthiness of a value.",
|
||||||
|
)
|
||||||
|
def truthy(input: TruthyInput) -> NodeReturn[ValueOutput]:
|
||||||
|
"""Route based on Python truthiness of a value."""
|
||||||
|
value = bool(input.value)
|
||||||
|
outcome = "truthy" if value else "falsey"
|
||||||
|
return NodeReturn(outcome=outcome, output=ValueOutput(value=value))
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from wf_authoring import build_registry, node
|
||||||
|
from wf_core import RuntimeContext
|
||||||
|
|
||||||
|
|
||||||
|
class AliasInput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class AliasOutput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_decorator_can_alias_existing_node_spec() -> None:
|
||||||
|
@node(name="test.alias", description="Alias spec.")
|
||||||
|
@node(name="test.original", description="Original spec.")
|
||||||
|
def echo(input: AliasInput) -> AliasOutput:
|
||||||
|
"""Echo input."""
|
||||||
|
return AliasOutput(value=input.value)
|
||||||
|
|
||||||
|
registry = build_registry(echo)
|
||||||
|
result = registry["test.alias"](
|
||||||
|
{"value": "hello"},
|
||||||
|
RuntimeContext(current_node_id="echo"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert echo.name == "test.alias"
|
||||||
|
assert echo.description == "Alias spec."
|
||||||
|
assert echo.input_model is AliasInput
|
||||||
|
assert echo.output_model is AliasOutput
|
||||||
|
assert result == {"outcome": "ok", "output": {"value": "hello"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_can_wrap_function_with_direct_metadata_call() -> None:
|
||||||
|
def echo(input: AliasInput) -> AliasOutput:
|
||||||
|
"""Echo input."""
|
||||||
|
return AliasOutput(value=input.value)
|
||||||
|
|
||||||
|
spec = node(echo, name="test.direct_fn", description="Direct function spec.")
|
||||||
|
|
||||||
|
assert spec.name == "test.direct_fn"
|
||||||
|
assert spec.description == "Direct function spec."
|
||||||
|
assert spec.input_model is AliasInput
|
||||||
|
assert spec.output_model is AliasOutput
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_can_alias_spec_with_direct_metadata_call() -> None:
|
||||||
|
@node(name="test.original")
|
||||||
|
def echo(input: AliasInput) -> AliasOutput:
|
||||||
|
"""Echo input."""
|
||||||
|
return AliasOutput(value=input.value)
|
||||||
|
|
||||||
|
alias = node(echo, name="test.direct_alias", description="Direct alias spec.")
|
||||||
|
|
||||||
|
assert alias.name == "test.direct_alias"
|
||||||
|
assert alias.description == "Direct alias spec."
|
||||||
|
assert alias.fn is echo.fn
|
||||||
|
assert alias.input_model is AliasInput
|
||||||
|
assert alias.output_model is AliasOutput
|
||||||
@@ -8,6 +8,8 @@ from wf_authoring import (
|
|||||||
bind_state,
|
bind_state,
|
||||||
build_registry,
|
build_registry,
|
||||||
coalesce,
|
coalesce,
|
||||||
|
constant,
|
||||||
|
default_if_none,
|
||||||
first_item,
|
first_item,
|
||||||
first_item_maybe,
|
first_item_maybe,
|
||||||
first_item_or_none,
|
first_item_or_none,
|
||||||
@@ -15,7 +17,9 @@ from wf_authoring import (
|
|||||||
last_item,
|
last_item,
|
||||||
last_item_or_none,
|
last_item_or_none,
|
||||||
length,
|
length,
|
||||||
|
pick_key,
|
||||||
state_path,
|
state_path,
|
||||||
|
truthy,
|
||||||
)
|
)
|
||||||
from wf_core import (
|
from wf_core import (
|
||||||
RunStatus,
|
RunStatus,
|
||||||
@@ -197,3 +201,60 @@ def test_coalesce_returns_value_or_fallback() -> None:
|
|||||||
|
|
||||||
assert present == {"outcome": "ok", "output": {"value": "x"}}
|
assert present == {"outcome": "ok", "output": {"value": "x"}}
|
||||||
assert missing == {"outcome": "ok", "output": {"value": "fallback"}}
|
assert missing == {"outcome": "ok", "output": {"value": "fallback"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_if_none_is_coalesce_alias() -> None:
|
||||||
|
registry = build_registry(default_if_none)
|
||||||
|
|
||||||
|
result = registry["authoring.default_if_none"](
|
||||||
|
{"value": None, "fallback": "fallback"},
|
||||||
|
RuntimeContext(current_node_id="default_if_none"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert default_if_none.name == "authoring.default_if_none"
|
||||||
|
assert default_if_none.fn is coalesce.fn
|
||||||
|
assert result == {"outcome": "ok", "output": {"value": "fallback"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_constant_returns_configured_value() -> None:
|
||||||
|
registry = build_registry(constant)
|
||||||
|
|
||||||
|
result = registry["authoring.constant"](
|
||||||
|
{"value": {"source": "fixture"}},
|
||||||
|
RuntimeContext(current_node_id="constant"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {"outcome": "ok", "output": {"value": {"source": "fixture"}}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_key_selects_value_from_mapping() -> None:
|
||||||
|
registry = build_registry(pick_key)
|
||||||
|
|
||||||
|
result = registry["authoring.pick_key"](
|
||||||
|
{"mapping": {"name": "Ada", "age": 36}, "key": "name"},
|
||||||
|
RuntimeContext(current_node_id="pick_key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {"outcome": "ok", "output": {"value": "Ada"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_pick_key_returns_none_when_missing() -> None:
|
||||||
|
registry = build_registry(pick_key)
|
||||||
|
|
||||||
|
result = registry["authoring.pick_key"](
|
||||||
|
{"mapping": {"name": "Ada"}, "key": "missing"},
|
||||||
|
RuntimeContext(current_node_id="pick_key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {"outcome": "ok", "output": {"value": None}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_truthy_routes_truthy_and_falsey_outcomes() -> None:
|
||||||
|
registry = build_registry(truthy)
|
||||||
|
ctx = RuntimeContext(current_node_id="truthy")
|
||||||
|
|
||||||
|
truthy_result = registry["authoring.truthy"]({"value": "yes"}, ctx)
|
||||||
|
falsey_result = registry["authoring.truthy"]({"value": ""}, ctx)
|
||||||
|
|
||||||
|
assert truthy_result == {"outcome": "truthy", "output": {"value": True}}
|
||||||
|
assert falsey_result == {"outcome": "falsey", "output": {"value": False}}
|
||||||
|
|||||||
Reference in New Issue
Block a user