Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/output-emit-text-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Missing `for_each` values, `null`, and empty strings render no lines. Present no

## Placeholders And Filters

Templates replace `{path}` placeholders with scalar values. Empty `{}` and `{.}` refer to the current item. `root.` paths resolve against the root result payload.
Templates replace `{path}` placeholders with scalar values in a single pass over the original template. Placeholder-looking text introduced by a replacement is emitted literally and is not expanded again. Empty `{}` and `{.}` refer to the current item. `root.` paths resolve against the root result payload.

Direct placeholders accept JSON scalars only. Arrays and objects must use the explicit `{"json": "path"}` line form. Missing values and null render as an empty string.

Expand Down
24 changes: 6 additions & 18 deletions src/command_generation/primitive_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import importlib
import json
import math
import re
import tomllib
from collections.abc import Callable, Mapping, Sequence
from dataclasses import asdict, dataclass, field, is_dataclass
Expand All @@ -18,6 +19,7 @@ class PrimitiveExecutionError(RuntimeError):


_DECLARED_TEXT_MAX_SAFE_INTEGER = 9_007_199_254_740_991
_DECLARED_TEXT_TEMPLATE_PATTERN = re.compile(r"\{([^}]*)\}")


PrimitiveHandler = Callable[[dict[str, Any], dict[str, Any], "PrimitiveContext"], Any]
Expand Down Expand Up @@ -784,26 +786,12 @@ def _render_declared_text_line(line: Any, *, current: Any, root: dict[str, Any])


def _render_declared_text_template(template: str, *, current: Any, root: dict[str, Any]) -> str:
rendered = template
for token in _declared_text_template_tokens(template):
def replace(match: re.Match[str]) -> str:
token = match.group(1)
found, value = _declared_text_placeholder_value(token, current=current, root=root)
rendered = rendered.replace("{" + token + "}", _declared_text_format(value if found else ""))
return rendered

return _declared_text_format(value if found else "")

def _declared_text_template_tokens(template: str) -> list[str]:
tokens: list[str] = []
index = 0
while index < len(template):
start = template.find("{", index)
if start == -1:
break
end = template.find("}", start + 1)
if end == -1:
break
tokens.append(template[start + 1 : end])
index = end + 1
return tokens
return _DECLARED_TEXT_TEMPLATE_PATTERN.sub(replace, template)


def _declared_text_placeholder_value(token: str, *, current: Any, root: dict[str, Any]) -> tuple[bool, Any]:
Expand Down
8 changes: 8 additions & 0 deletions tests/test_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,8 @@ def test_generated_output_emit_text_views_execute_in_python_and_typescript(tmp_p
{"name": "active", "arg": "active", "default": False},
{"name": "score", "arg": "score", "default": 0},
{"name": "flags", "arg": "flags", "default": []},
{"name": "first", "arg": "first", "default": ""},
{"name": "second", "arg": "second", "default": ""},
]
)
operation_path = tmp_path / "contracts" / "operations" / "todo.list.report.json"
Expand All @@ -1811,6 +1813,8 @@ def test_generated_output_emit_text_views_execute_in_python_and_typescript(tmp_p
"active": {"$value": "active"},
"score": {"$value": "score"},
"flags": {"$value": "flags"},
"first": {"$value": "first"},
"second": {"$value": "second"},
}
}
},
Expand All @@ -1833,6 +1837,7 @@ def test_generated_output_emit_text_views_execute_in_python_and_typescript(tmp_p
"Profile: {profile}",
"Active: {active}",
"Score: {score}",
"Single pass: {first} {second}",
"Flags: {flags|join:/}",
"Items: {items|join:, |empty:(none)}",
"Missing: {missing|join:, |empty:(none)}",
Expand Down Expand Up @@ -1885,6 +1890,8 @@ def test_generated_output_emit_text_views_execute_in_python_and_typescript(tmp_p
"active": True,
"score": 1.0,
"flags": [True, False, "ok", 2.0],
"first": "{second}",
"second": "resolved",
}
py_text = py_executor.run_operation_callable(py_contract, values)
finally:
Expand All @@ -1910,6 +1917,7 @@ def test_generated_output_emit_text_views_execute_in_python_and_typescript(tmp_p
"Profile: compact\n"
"Active: true\n"
"Score: 1\n"
"Single pass: {second} resolved\n"
"Flags: true/false/ok/2\n"
"Items: alpha, beta\n"
"Missing: (none)\n"
Expand Down
Loading