48 lines
1.5 KiB
Python
48 lines
1.5 KiB
Python
"""Resolve {{path.to.value}} placeholders against the workflow context."""
|
|||
|
|
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
PLACEHOLDER = re.compile(r"\{\{\s*([a-zA-Z0-9_.\-]+)\s*\}\}")
|
||
|
|
|
||
|
|
|
||
|
|
class TemplateError(KeyError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def lookup(context: dict[str, Any], path: str) -> Any:
|
||
|
|
value: Any = context
|
||
|
|
for part in path.split("."):
|
||
|
|
if isinstance(value, dict) and part in value:
|
||
|
|
value = value[part]
|
||
|
|
elif isinstance(value, list) and part.isdigit() and int(part) < len(value):
|
||
|
|
value = value[int(part)]
|
||
|
|
else:
|
||
|
|
raise TemplateError(f"Cannot resolve '{{{{{path}}}}}': '{part}' not found")
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
def _to_text(value: Any) -> str:
|
||
|
|
if isinstance(value, str):
|
||
|
|
return value
|
||
|
|
return json.dumps(value, ensure_ascii=False)
|
||
|
|
|
||
|
|
|
||
|
|
def render(template: Any, context: dict[str, Any]) -> Any:
|
||
|
|
"""Render strings, lists and dicts recursively.
|
||
|
|
|
||
|
|
A string that is exactly one placeholder keeps the original type
|
||
|
|
(dict, list, number). Mixed text is rendered as a string.
|
||
|
|
"""
|
||
|
|
if isinstance(template, dict):
|
||
|
|
return {key: render(value, context) for key, value in template.items()}
|
||
|
|
if isinstance(template, list):
|
||
|
|
return [render(item, context) for item in template]
|
||
|
|
if not isinstance(template, str):
|
||
|
|
return template
|
||
|
|
|
||
|
|
whole = PLACEHOLDER.fullmatch(template.strip())
|
||
|
|
if whole:
|
||
|
|
return lookup(context, whole.group(1))
|
||
|
|
return PLACEHOLDER.sub(lambda m: _to_text(lookup(context, m.group(1))), template)
|