Skip to content

Commit e50b3fe

Browse files
committed
Add method to validate format of object file
1 parent a91d1db commit e50b3fe

File tree

5 files changed

+204
-74
lines changed

5 files changed

+204
-74
lines changed

infrahub_sdk/ctl/object.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,28 @@ async def load(
4040
client = initialize_client()
4141

4242
for file in files:
43-
file.validate_content()
44-
schema = await client.schema.get(kind=file.spec.kind, branch=branch)
43+
await file.validate_format(client=client, branch=branch)
4544

46-
for item in file.spec.data:
47-
await file.spec.create_node(client=client, schema=schema, data=item, branch=branch)
45+
for file in files:
46+
await file.process(client=client, branch=branch)
47+
48+
49+
@app.command()
50+
@catch_exception(console=console)
51+
async def validate(
52+
paths: list[Path],
53+
debug: bool = False,
54+
branch: str = typer.Option(None, help="Branch on which to validate the objects."),
55+
_: str = CONFIG_PARAM,
56+
) -> None:
57+
"""Validate one or multiple objects files."""
58+
59+
init_logging(debug=debug)
60+
61+
logging.getLogger("infrahub_sdk").setLevel(logging.INFO)
62+
63+
files = load_yamlfile_from_disk_and_exit(paths=paths, file_type=ObjectFile, console=console)
64+
client = initialize_client()
65+
66+
for file in files:
67+
await file.validate_format(client=client, branch=branch)

infrahub_sdk/schema/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ class RelationshipSchema(BaseModel):
143143
class RelationshipSchemaAPI(RelationshipSchema):
144144
model_config = ConfigDict(use_enum_values=True)
145145

146-
identifier: str
147146
inherited: bool = False
148147
read_only: bool = False
149148
hierarchical: str | None = None

infrahub_sdk/spec/object.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pydantic import BaseModel, Field
66

7+
from ..exceptions import ValidationError
78
from ..schema import RelationshipSchema
89
from ..yaml import InfrahubFile, InfrahubFileKind
910

@@ -56,7 +57,9 @@ async def get_relationship_info(
5657
peer_schema = await client.schema.get(kind=info.peer_kind, branch=branch)
5758

5859
try:
59-
info.peer_rel = peer_schema.get_matching_relationship(id=rel_schema.identifier, direction=rel_schema.direction)
60+
info.peer_rel = peer_schema.get_matching_relationship(
61+
id=rel_schema.identifier or "", direction=rel_schema.direction
62+
)
6063
except ValueError:
6164
pass
6265

@@ -72,6 +75,64 @@ class InfrahubObjectFileData(BaseModel):
7275
kind: str
7376
data: list[dict[str, Any]] = Field(default_factory=list)
7477

78+
async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ValidationError]:
79+
errors: list[ValidationError] = []
80+
schema = await client.schema.get(kind=self.kind, branch=branch)
81+
for item in self.data:
82+
errors.extend(await self.validate_object(client=client, schema=schema, data=item, branch=branch))
83+
return errors
84+
85+
async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
86+
schema = await client.schema.get(kind=self.kind, branch=branch)
87+
for item in self.data:
88+
await self.create_node(client=client, schema=schema, data=item, branch=branch)
89+
90+
@classmethod
91+
async def validate_object(
92+
cls,
93+
client: InfrahubClient,
94+
schema: MainSchemaTypesAPI,
95+
data: dict,
96+
context: dict | None = None,
97+
branch: str | None = None,
98+
) -> list[ValidationError]:
99+
errors: list[ValidationError] = []
100+
context = context or {}
101+
102+
# First validate if all mandatory fields are present
103+
for element in schema.mandatory_attribute_names + schema.mandatory_relationship_names:
104+
if not any([element in data.keys(), element in context.keys()]):
105+
errors.append(ValidationError(identifier=element, message=f"{element} is mandatory"))
106+
107+
# Validate if all attributes are valid
108+
for key, value in data.items():
109+
if key not in schema.attribute_names and key not in schema.relationship_names:
110+
errors.append(
111+
ValidationError(identifier=key, message=f"{key} is not a valid attribute or relationship")
112+
)
113+
114+
if key in schema.attribute_names:
115+
if not isinstance(value, (str, int, float, bool, list, dict)):
116+
errors.append(
117+
ValidationError(
118+
identifier=key, message=f"{key} must be a string, int, float, bool, list, or dict"
119+
)
120+
)
121+
122+
if key in schema.relationship_names:
123+
rel_info = await get_relationship_info(
124+
client=client, schema=schema, key=key, value=value, branch=branch
125+
)
126+
if not rel_info.is_valid:
127+
errors.append(
128+
ValidationError(
129+
identifier=key, message=rel_info.reason_relationship_not_valid or "Invalid relationship"
130+
)
131+
)
132+
# TODO: Validate the sub-object too
133+
134+
return errors
135+
75136
@classmethod
76137
def enrich_node(cls, data: dict, context: dict) -> dict: # noqa: ARG003
77138
return data
@@ -88,10 +149,9 @@ async def create_node(
88149
) -> InfrahubNode:
89150
context = context or {}
90151

91-
# First validate if all mandatory fields are present
92-
for element in schema.mandatory_attribute_names + schema.mandatory_relationship_names:
93-
if not any([element in data.keys(), element in context.keys()]):
94-
raise ValueError(f"{element} is mandatory")
152+
errors = await cls.validate_object(client=client, schema=schema, data=data, context=context, branch=branch)
153+
if errors:
154+
raise ValidationError(identifier=schema.kind, message="Object is not valid")
95155

96156
clean_data: dict[str, Any] = {}
97157

@@ -240,3 +300,13 @@ def validate_content(self) -> None:
240300
if self.kind != InfrahubFileKind.OBJECT:
241301
raise ValueError("File is not an Infrahub Object file")
242302
self._spec = InfrahubObjectFileData(**self.data.spec)
303+
304+
async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> None:
305+
self.validate_content()
306+
errors = await self.spec.validate_format(client=client, branch=branch)
307+
if errors:
308+
error_message = "\n".join([f"{error.identifier} : {error.message}" for error in errors])
309+
raise ValidationError(identifier=str(self.location), message=error_message)
310+
311+
async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
312+
await self.spec.process(client=client, branch=branch)

tests/integration/test_spec_object.py

Lines changed: 42 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@
1010
from infrahub_sdk.utils import get_fixtures_dir
1111

1212

13+
def load_object_file(name: str) -> ObjectFile:
14+
files = ObjectFile.load_from_disk(paths=[get_fixtures_dir() / "spec_objects" / name])
15+
assert len(files) == 1
16+
return files[0]
17+
18+
1319
class TestSpecObject(TestInfrahubDockerClient, SchemaAnimal):
20+
@pytest.fixture(scope="class")
21+
def branch_name(self) -> str:
22+
return "branch2"
23+
1424
@pytest.fixture(scope="class")
1525
def spec_objects_fixtures_dir(self) -> Path:
1626
return get_fixtures_dir() / "spec_objects"
@@ -24,98 +34,66 @@ async def initial_schema(self, default_branch: str, client: InfrahubClient, sche
2434
)
2535
assert resp.errors == {}
2636

27-
async def test_load_tags(
28-
self, client: InfrahubClient, default_branch: str, initial_schema: None, spec_objects_fixtures_dir: Path
29-
):
30-
files = ObjectFile.load_from_disk(paths=[spec_objects_fixtures_dir / "animal_tags01.yml"])
31-
assert len(files) == 1
32-
obj_file = files[0]
37+
async def test_create_branch(self, client: InfrahubClient, initial_schema: None, branch_name: str):
38+
await client.branch.create(branch_name=branch_name, sync_with_git=False)
3339

34-
obj_file.validate_content()
40+
async def test_load_tags(self, client: InfrahubClient, branch_name: str, initial_schema: None):
41+
obj_file = load_object_file("animal_tags01.yml")
42+
await obj_file.validate_format(client=client, branch=branch_name)
3543

3644
# Check that the nodes are not present in the database before loading the file
37-
assert len(await client.all(kind=obj_file.spec.kind)) == 0
45+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 0
3846

39-
schema = await client.schema.get(kind=obj_file.spec.kind, branch=default_branch)
40-
for item in obj_file.spec.data:
41-
await obj_file.spec.create_node(client=client, schema=schema, data=item, branch=default_branch)
47+
await obj_file.process(client=client, branch=branch_name)
4248

43-
assert len(await client.all(kind=obj_file.spec.kind)) == 3
49+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 3
4450

45-
async def test_update_tags(
46-
self, client: InfrahubClient, default_branch: str, initial_schema: None, spec_objects_fixtures_dir: Path
47-
):
48-
files = ObjectFile.load_from_disk(paths=[spec_objects_fixtures_dir / "animal_tags02.yml"])
49-
assert len(files) == 1
50-
obj_file = files[0]
51-
52-
obj_file.validate_content()
51+
async def test_update_tags(self, client: InfrahubClient, branch_name: str, initial_schema: None):
52+
obj_file = load_object_file("animal_tags02.yml")
53+
await obj_file.validate_format(client=client, branch=branch_name)
5354

5455
# Check that the nodes are not present in the database before loading the file
55-
assert len(await client.all(kind=obj_file.spec.kind)) == 3
56+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 3
5657

57-
schema = await client.schema.get(kind=obj_file.spec.kind, branch=default_branch)
58-
for item in obj_file.spec.data:
59-
await obj_file.spec.create_node(client=client, schema=schema, data=item, branch=default_branch)
58+
await obj_file.process(client=client, branch=branch_name)
6059

61-
tags = await client.all(kind=obj_file.spec.kind)
60+
tags = await client.all(kind=obj_file.spec.kind, branch=branch_name)
6261
tags_by_name = {"__".join(tag.get_human_friendly_id()): tag for tag in tags}
6362
assert len(tags_by_name) == 4
6463
assert tags_by_name["Veterinarian"].description.value == "Licensed animal healthcare professional"
6564

66-
async def test_load_persons(
67-
self, client: InfrahubClient, default_branch: str, initial_schema: None, spec_objects_fixtures_dir: Path
68-
):
69-
files = ObjectFile.load_from_disk(paths=[spec_objects_fixtures_dir / "animal_person01.yml"])
70-
assert len(files) == 1
71-
obj_file = files[0]
72-
73-
obj_file.validate_content()
65+
async def test_load_persons(self, client: InfrahubClient, branch_name: str, initial_schema: None):
66+
obj_file = load_object_file("animal_person01.yml")
67+
await obj_file.validate_format(client=client, branch=branch_name)
7468

7569
# Check that the nodes are not present in the database before loading the file
76-
assert await client.all(kind=obj_file.spec.kind) == []
70+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 0
7771

78-
schema = await client.schema.get(kind=obj_file.spec.kind, branch=default_branch)
79-
for item in obj_file.spec.data:
80-
await obj_file.spec.create_node(client=client, schema=schema, data=item, branch=default_branch)
72+
await obj_file.process(client=client, branch=branch_name)
8173

82-
assert len(await client.all(kind=obj_file.spec.kind)) == 3
74+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 3
8375

84-
async def test_load_dogs(
85-
self, client: InfrahubClient, default_branch: str, initial_schema: None, spec_objects_fixtures_dir: Path
86-
):
87-
files = ObjectFile.load_from_disk(paths=[spec_objects_fixtures_dir / "animal_dog01.yml"])
88-
assert len(files) == 1
89-
obj_file = files[0]
90-
91-
obj_file.validate_content()
76+
async def test_load_dogs(self, client: InfrahubClient, branch_name: str, initial_schema: None):
77+
obj_file = load_object_file("animal_dog01.yml")
78+
await obj_file.validate_format(client=client, branch=branch_name)
9279

9380
# Check that the nodes are not present in the database before loading the file
94-
assert await client.all(kind=obj_file.spec.kind) == []
95-
96-
schema = await client.schema.get(kind=obj_file.spec.kind, branch=default_branch)
97-
for item in obj_file.spec.data:
98-
await obj_file.spec.create_node(client=client, schema=schema, data=item, branch=default_branch)
81+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 0
9982

100-
assert len(await client.all(kind=obj_file.spec.kind)) == 4
83+
await obj_file.process(client=client, branch=branch_name)
10184

102-
async def test_load_persons02(
103-
self, client: InfrahubClient, default_branch: str, initial_schema: None, spec_objects_fixtures_dir: Path
104-
):
105-
files = ObjectFile.load_from_disk(paths=[spec_objects_fixtures_dir / "animal_person02.yml"])
106-
assert len(files) == 1
107-
obj_file = files[0]
85+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 4
10886

109-
obj_file.validate_content()
87+
async def test_load_persons02(self, client: InfrahubClient, branch_name: str, initial_schema: None):
88+
obj_file = load_object_file("animal_person02.yml")
89+
await obj_file.validate_format(client=client, branch=branch_name)
11090

11191
# Check that the nodes are not present in the database before loading the file
112-
assert len(await client.all(kind=obj_file.spec.kind)) == 4
92+
assert len(await client.all(kind=obj_file.spec.kind, branch=branch_name)) == 4
11393

114-
schema = await client.schema.get(kind=obj_file.spec.kind, branch=default_branch)
115-
for item in obj_file.spec.data:
116-
await obj_file.spec.create_node(client=client, schema=schema, data=item, branch=default_branch)
94+
await obj_file.process(client=client, branch=branch_name)
11795

118-
persons = await client.all(kind=obj_file.spec.kind)
96+
persons = await client.all(kind=obj_file.spec.kind, branch=branch_name)
11997
person_by_name = {"__".join(person.get_human_friendly_id()): person for person in persons}
12098
assert len(persons) == 5
12199

tests/unit/sdk/spec/test_object.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pytest
2+
from pytest_httpx import HTTPXMock
3+
4+
from infrahub_sdk.client import InfrahubClient
5+
from infrahub_sdk.exceptions import ValidationError
6+
from infrahub_sdk.spec.object import ObjectFile
7+
8+
9+
@pytest.fixture
10+
def root_location() -> dict:
11+
return {"apiVersion": "infrahub.app/v1", "kind": "Object", "spec": {"kind": "BuiltinLocation", "data": []}}
12+
13+
14+
@pytest.fixture
15+
def location_mexico_01(root_location: dict) -> dict:
16+
data = [{"name": "Mexico", "type": "Country"}]
17+
18+
location = root_location.copy()
19+
location["spec"]["data"] = data
20+
return location
21+
22+
23+
@pytest.fixture
24+
def location_bad_syntax01(root_location: dict) -> dict:
25+
data = [{"notthename": "Mexico", "type": "Country"}]
26+
location = root_location.copy()
27+
location["spec"]["data"] = data
28+
return location
29+
30+
31+
@pytest.fixture
32+
def location_bad_syntax02(root_location: dict) -> dict:
33+
data = [{"name": "Mexico", "notvalidattribute": "notvalidattribute", "type": "Country"}]
34+
location = root_location.copy()
35+
location["spec"]["data"] = data
36+
return location
37+
38+
39+
async def test_validate_object(client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_mexico_01):
40+
obj = ObjectFile(location="some/path", content=location_mexico_01)
41+
await obj.validate_format(client=client)
42+
43+
assert obj.spec.kind == "BuiltinLocation"
44+
45+
46+
async def test_validate_object_bad_syntax01(
47+
client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_bad_syntax01
48+
):
49+
obj = ObjectFile(location="some/path", content=location_bad_syntax01)
50+
with pytest.raises(ValidationError) as exc:
51+
await obj.validate_format(client=client)
52+
53+
assert "name" in str(exc.value)
54+
55+
56+
async def test_validate_object_bad_syntax02(
57+
client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_bad_syntax02
58+
):
59+
obj = ObjectFile(location="some/path", content=location_bad_syntax02)
60+
with pytest.raises(ValidationError) as exc:
61+
await obj.validate_format(client=client)
62+
63+
assert "notvalidattribute" in str(exc.value)

0 commit comments

Comments
 (0)