Skip to content

Commit 17e653b

Browse files
dgarrosclaude
andcommitted
feat(schema): generate a typed ComputedAttribute model instead of dict[str, Any]
The computed_attribute block is now a dedicated model (ComputedAttributeWrite/Read) with a Literal kind and extra=forbid, so the write contract for a computed attribute is explicit and its kind/unknown-field errors are caught with a field-level location. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 576a3cf commit 17e653b

3 files changed

Lines changed: 64 additions & 2 deletions

File tree

infrahub_sdk/schema/generated/read.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@
77
from pydantic import BaseModel, ConfigDict, Field
88

99

10+
class ComputedAttributeRead(BaseModel):
11+
model_config = ConfigDict()
12+
kind: Literal["User", "Jinja2", "TransformPython"] = Field(
13+
...,
14+
description="Defines how the value of the attribute is computed.",
15+
)
16+
jinja2_template: str | None = Field(
17+
default=None,
18+
description="Jinja2 template used to compute the value, required when kind is Jinja2.",
19+
)
20+
transform: str | None = Field(
21+
default=None,
22+
description="Python transform name or ID, required when kind is TransformPython.",
23+
)
24+
25+
1026
class AttributeSchemaRead(BaseModel):
1127
model_config = ConfigDict()
1228
id: str | None = Field(
@@ -51,7 +67,7 @@ class AttributeSchemaRead(BaseModel):
5167
default=None,
5268
description="Define a list of valid values for the attribute.",
5369
)
54-
computed_attribute: dict[str, Any] | None = Field(
70+
computed_attribute: ComputedAttributeRead | None = Field(
5571
default=None,
5672
description="Defines how the value of this attribute will be populated.",
5773
)

infrahub_sdk/schema/generated/write.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@
77
from pydantic import BaseModel, ConfigDict, Field
88

99

10+
class ComputedAttributeWrite(BaseModel):
11+
model_config = ConfigDict(extra="forbid")
12+
kind: Literal["User", "Jinja2", "TransformPython"] = Field(
13+
...,
14+
description="Defines how the value of the attribute is computed.",
15+
)
16+
jinja2_template: str | None = Field(
17+
default=None,
18+
description="Jinja2 template used to compute the value, required when kind is Jinja2.",
19+
)
20+
transform: str | None = Field(
21+
default=None,
22+
description="Python transform name or ID, required when kind is TransformPython.",
23+
)
24+
25+
1026
class AttributeSchemaWrite(BaseModel):
1127
model_config = ConfigDict(extra="forbid")
1228
id: str | None = Field(
@@ -51,7 +67,7 @@ class AttributeSchemaWrite(BaseModel):
5167
default=None,
5268
description="Define a list of valid values for the attribute.",
5369
)
54-
computed_attribute: dict[str, Any] | None = Field(
70+
computed_attribute: ComputedAttributeWrite | None = Field(
5571
default=None,
5672
description="Defines how the value of this attribute will be populated.",
5773
)

tests/unit/test_schema_offline_validation.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,33 @@ def test_node_read_level_field_hierarchy_is_rejected() -> None:
227227

228228
assert result.valid is False
229229
assert "nodes[0].hierarchy" in _fields_named(result), result.messages
230+
231+
232+
def test_valid_computed_attribute_block_passes() -> None:
233+
schema = _valid_schema()
234+
schema["nodes"][0]["attributes"][0]["computed_attribute"] = {"kind": "Jinja2", "jinja2_template": "{{ name }}"}
235+
236+
result = validate_schema(schema=schema)
237+
238+
assert result.valid is True, result.messages
239+
240+
241+
def test_computed_attribute_out_of_enum_kind_is_rejected_naming_field_and_value() -> None:
242+
schema = _valid_schema()
243+
schema["nodes"][0]["attributes"][0]["computed_attribute"] = {"kind": "NotARealKind"}
244+
245+
result = validate_schema(schema=schema)
246+
247+
assert result.valid is False
248+
assert "nodes[0].attributes[0].computed_attribute.kind" in _fields_named(result), result.messages
249+
assert any("NotARealKind" in message for message in result.messages), result.messages
250+
251+
252+
def test_computed_attribute_unknown_field_is_rejected_with_dotted_location() -> None:
253+
schema = _valid_schema()
254+
schema["nodes"][0]["attributes"][0]["computed_attribute"] = {"kind": "Jinja2", "not_a_real_field": "x"}
255+
256+
result = validate_schema(schema=schema)
257+
258+
assert result.valid is False
259+
assert "nodes[0].attributes[0].computed_attribute.not_a_real_field" in _fields_named(result), result.messages

0 commit comments

Comments
 (0)