NK-WP-0048: {param:env} format spec; check follow-through/rollback templates
Answers ops-warden's observation on the literal KEYCAPE_<CLIENT_ID>_CLIENT_SECRET. The type adopts the spec once ops-warden's planner renders it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 469893@bnt-lap001 Assistant-Session: 49296f15-2171-4137-b972-2443bab95edb
This commit is contained in:
parent
502907a2d3
commit
850907a987
3 changed files with 61 additions and 1 deletions
|
|
@ -79,6 +79,15 @@ def placeholders(text: str) -> set[str]:
|
|||
return {f[1] for f in string.Formatter().parse(text) if f[1]}
|
||||
|
||||
|
||||
#: Format specs a template may apply to a parameter. ``env`` renders an
|
||||
#: environment-variable fragment: upper case, ``-`` as ``_`` ({client_id:env}).
|
||||
FORMAT_SPECS = {"", "env"}
|
||||
|
||||
|
||||
def format_specs(text: str) -> set[str]:
|
||||
return {f[2] or "" for f in string.Formatter().parse(text) if f[1]}
|
||||
|
||||
|
||||
def _walk_strings(value: Any, path: str):
|
||||
if isinstance(value, str):
|
||||
yield path, value
|
||||
|
|
@ -146,6 +155,9 @@ def validate_type(data: dict[str, Any], type_dir: Path | None = None) -> list[Is
|
|||
_validate_materialization(spec["materialization"], params, issues)
|
||||
_validate_verification(spec["verification"], params, issues)
|
||||
_validate_rollback(spec["rollback"], step_ids, spec["materialization"], issues)
|
||||
for section in ("follow_through", "rollback"):
|
||||
if isinstance(spec[section], list):
|
||||
_validate_templates(spec[section], f"type.spec.{section}", params, issues)
|
||||
_validate_acceptance(spec["acceptance"], issues)
|
||||
for section in ("materialization", "follow_through", "verification", "rollback"):
|
||||
for i, item in enumerate(spec[section] if isinstance(spec[section], list) else []):
|
||||
|
|
@ -241,6 +253,8 @@ def _validate_templates(items: Any, path: str, params: dict[str, dict], issues:
|
|||
for name in placeholders(text):
|
||||
if name not in params:
|
||||
issues.append(err(spath, f"template uses unknown parameter {{{name}}}"))
|
||||
for spec in sorted(format_specs(text) - FORMAT_SPECS):
|
||||
issues.append(err(spath, f"template uses unknown format spec {spec!r}"))
|
||||
|
||||
|
||||
def _validate_materialization(items: Any, params: dict[str, dict], issues: list[Issue]) -> None:
|
||||
|
|
@ -446,6 +460,15 @@ def _applicable(items: list[dict], values: dict[str, Any]) -> list[dict]:
|
|||
return [{k: v for k, v in i.items() if k != "when"} for i in items if holds(i.get("when"), values)]
|
||||
|
||||
|
||||
class _Param(str):
|
||||
"""A string parameter value that also renders the ``env`` format spec."""
|
||||
|
||||
def __format__(self, spec: str) -> str:
|
||||
if spec == "env":
|
||||
return self.upper().replace("-", "_")
|
||||
return str.__format__(self, spec)
|
||||
|
||||
|
||||
class _Blank(dict):
|
||||
"""Template values where an optional parameter a case leaves out renders empty."""
|
||||
|
||||
|
|
@ -455,7 +478,8 @@ class _Blank(dict):
|
|||
|
||||
def _fill(value: Any, values: dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
return value.format_map(_Blank({k: (",".join(map(str, v)) if isinstance(v, list) else v)
|
||||
return value.format_map(_Blank({k: _Param(",".join(map(str, v)) if isinstance(v, list) else v)
|
||||
if isinstance(v, (str, list)) else v
|
||||
for k, v in values.items()}))
|
||||
if isinstance(value, dict):
|
||||
return {k: _fill(v, values) for k, v in value.items()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue