nested path support for in/out maps, merge strat refactor

validation + runtime for write targets
This commit is contained in:
lda
2026-05-17 15:27:17 +07:00 Verified
parent 37914449a1
commit c6360d3892
10 changed files with 413 additions and 88 deletions
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
from typing import Any
from wf_core.errors import WorkflowExecutionError
def apply_builtin_merge(
*,
strategy: str,
current_value: Any,
incoming_value: Any,
destination_path: str,
) -> Any:
"""Apply one built-in merge rule.
This is the future seam for source-owned reducer libraries. The current core
still supports only built-in rules and keeps them pure over current and
incoming values.
"""
if strategy == "replace":
return incoming_value
if strategy == "append":
if current_value is None:
return [incoming_value] if not isinstance(incoming_value, list) else incoming_value
if not isinstance(current_value, list):
raise WorkflowExecutionError(
f"cannot append into non-list state path {destination_path!r}"
)
return [
*current_value,
*incoming_value,
] if isinstance(incoming_value, list) else [*current_value, incoming_value]
if strategy == "merge_object":
if current_value is None:
if not isinstance(incoming_value, dict):
raise WorkflowExecutionError(
f"cannot merge non-object value into {destination_path!r}"
)
return dict(incoming_value)
if not isinstance(current_value, dict) or not isinstance(incoming_value, dict):
raise WorkflowExecutionError(
f"merge_object requires dict values at {destination_path!r}"
)
return current_value | incoming_value
raise WorkflowExecutionError(f"unknown merge strategy {strategy!r}")
+8 -4
View File
@@ -5,6 +5,7 @@ from typing import Any, cast
from wf_core.conditions import safe_resolve_path
from wf_core.errors import WorkflowExecutionError
from wf_core.local_paths import LocalPathError, set_local_value
from wf_core.models.results import NodeResult
from wf_core.models.schemas import NodeDef
from wf_core.models.steps import NodeUse
@@ -30,15 +31,18 @@ def _resolve_node_execution(
) -> tuple[dict[str, Any], RuntimeContext]:
frame = run.current_frame()
context_values = frame_context_values(frame)
resolved_input = {
destination_field: safe_resolve_path(
resolved_input: dict[str, Any] = {}
for source_path, destination_field in node.in_map.items():
value = safe_resolve_path(
source_path,
state=run.state,
workflow_input=run.workflow_input,
context=context_values,
)
for source_path, destination_field in node.in_map.items()
}
try:
set_local_value(resolved_input, destination_field, value)
except LocalPathError as exc:
raise WorkflowExecutionError(str(exc)) from exc
validate_payload_against_schema(
node_def.input_schema, resolved_input, f"node input for {node.id}"
)
+21 -43
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Any
from wf_core.errors import WorkflowExecutionError
from wf_core.local_paths import LocalPathError, get_local_value, has_overlapping_paths
from wf_core.models.steps import NodeUse
from wf_core.models.workflow import Workflow
from wf_core.paths import (
@@ -11,6 +12,7 @@ from wf_core.paths import (
set_nested_value,
split_graph_path,
)
from wf_core.runtime.ops.merges import apply_builtin_merge
def apply_output_map(
@@ -36,16 +38,22 @@ def apply_mapped_state(
*,
missing_field_message: str,
) -> dict[str, Any]:
state_changes: dict[str, Any] = {}
if has_overlapping_paths(mapping.values()):
raise WorkflowExecutionError("mapped state patch has overlapping destination paths")
patch: dict[str, Any] = {}
for source_field, destination_path in mapping.items():
if source_field not in source_data:
try:
value = get_local_value(source_data, source_field)
except LocalPathError:
raise WorkflowExecutionError(
missing_field_message.format(field=repr(source_field))
)
value = source_data[source_field]
) from None
patch[destination_path] = value
for destination_path, value in patch.items():
write_state_value(workflow, state, destination_path, value)
state_changes[destination_path] = value
return state_changes
return dict(patch)
def write_state_value(
@@ -65,44 +73,14 @@ def write_state_value(
declared_field = workflow.state_schema.fields.get(field_name)
merge_strategy = declared_field.merge_strategy if declared_field else "replace"
key_path = parts
if merge_strategy == "replace":
safe_set_nested_value(state, key_path, value)
return
current_value = get_nested_value(state, key_path)
if merge_strategy == "append":
if current_value is None:
safe_set_nested_value(
state, key_path, [value] if not isinstance(value, list) else value
)
return
if not isinstance(current_value, list):
raise WorkflowExecutionError(
f"cannot append into non-list state path {destination_path!r}"
)
if isinstance(value, list):
current_value.extend(value)
else:
current_value.append(value)
return
if merge_strategy == "merge_object":
if current_value is None:
if not isinstance(value, dict):
raise WorkflowExecutionError(
f"cannot merge non-object value into {destination_path!r}"
)
safe_set_nested_value(state, key_path, dict(value))
return
if not isinstance(current_value, dict) or not isinstance(value, dict):
raise WorkflowExecutionError(
f"merge_object requires dict values at {destination_path!r}"
)
current_value.update(value)
return
raise WorkflowExecutionError(f"unknown merge strategy {merge_strategy!r}")
merged_value = apply_builtin_merge(
strategy=merge_strategy,
current_value=current_value,
incoming_value=value,
destination_path=destination_path,
)
safe_set_nested_value(state, key_path, merged_value)
def project_output(workflow: Workflow, state: dict[str, Any]) -> dict[str, Any]: