diff --git a/snapcraft/models/__init__.py b/snapcraft/models/__init__.py
index b61840fa6d..c12016e948 100644
--- a/snapcraft/models/__init__.py
+++ b/snapcraft/models/__init__.py
@@ -15,7 +15,13 @@
# along with this program. If not, see .
"""Data models for snapcraft."""
-from .assertions import Assertion, RegistryAssertion
+from .assertions import (
+ Assertion,
+ EditableAssertion,
+ EditableRegistryAssertion,
+ Registry,
+ RegistryAssertion,
+)
from .manifest import Manifest
from .project import (
MANDATORY_ADOPTABLE_FIELDS,
@@ -43,12 +49,15 @@
"Component",
"ComponentProject",
"ContentPlug",
+ "EditableAssertion",
+ "EditableRegistryAssertion",
"GrammarAwareProject",
"Hook",
"Lint",
"Manifest",
"Platform",
"Project",
+ "Registry",
"RegistryAssertion",
"SnapcraftBuildPlanner",
"Socket",
diff --git a/snapcraft/models/assertions.py b/snapcraft/models/assertions.py
index 8f1364173c..d557ff9092 100644
--- a/snapcraft/models/assertions.py
+++ b/snapcraft/models/assertions.py
@@ -14,29 +14,82 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
-"""Models for assertion sets."""
+"""Assertion models."""
-from typing import Any, Literal
+from typing import Literal
+import pydantic
from craft_application import models
+from typing_extensions import Self
-class RegistryAssertion(models.CraftBaseModel):
- """Data model for a registry assertion."""
+class Registry(models.CraftBaseModel):
+ """Access and data definitions for a specific facet of a snap or system."""
+
+ request: str | None = None
+ """Optional dot-separated path to access the field."""
+
+ storage: str
+ """Dot-separated storage path."""
+
+ access: Literal["read", "write", "read-write"] | None = None
+ """Access permissions for the field."""
+
+ content: list[Self] | None = None
+ """Optional nested rules."""
+
+
+class Rules(models.CraftBaseModel):
+ """A list of registries for a particular view."""
+
+ rules: list[Registry]
+
+
+class EditableRegistryAssertion(models.CraftBaseModel):
+ """Subset of a registries assertion that can be edited by the user."""
account_id: str
- authority_id: str
- body: dict[str, Any] | str | None = None
- body_length: str | None = None
+ """Issuer of the registry assertion and owner of the signing key."""
+
name: str
- revision: int = 0
- sign_key_sha3_384: str | None = None
summary: str | None = None
- timestamp: str
+ revision: int | None = 0
+
+ views: dict[str, Rules]
+ """A map of logical views of how the storage is accessed."""
+
+ body: str | None = None
+ """A JSON schema that defines the storage structure."""
+
+
+class RegistryAssertion(EditableRegistryAssertion):
+ """A full registries assertion containing editable and non-editable fields."""
+
type: Literal["registry"]
- views: dict[str, Any]
+
+ authority_id: str
+ """Issuer of the registry assertion and owner of the signing key."""
+
+ timestamp: str
+ """Timestamp of when the assertion was issued."""
+
+ body_length: str | None = None
+ """Length of the body field."""
+
+ sign_key_sha3_384: str | None = None
+ """Signing key ID."""
+
+
+class RegistriesList(models.CraftBaseModel):
+ """A list of registry assertions."""
+
+ registry_list: list[RegistryAssertion] = pydantic.Field(default_factory=list)
# this will be a union for validation sets and registries once
# validation sets are migrated from the legacy codebase
Assertion = RegistryAssertion
+
+# this will be a union for editable validation sets and editable registries once
+# validation sets are migrated from the legacy codebase
+EditableAssertion = EditableRegistryAssertion
diff --git a/tests/unit/models/test_assertions.py b/tests/unit/models/test_assertions.py
index d2653dcd71..a6fed23d95 100644
--- a/tests/unit/models/test_assertions.py
+++ b/tests/unit/models/test_assertions.py
@@ -17,11 +17,92 @@
"""Tests for Assertion models."""
+from snapcraft.models import EditableRegistryAssertion, Registry, RegistryAssertion
-def test_assertion_defaults(fake_registry_assertion_data, check):
+
+def test_registry_defaults(check):
+ """Test default values of the Registry model."""
+ registry = Registry.unmarshal({"storage": "test-storage"})
+
+ check.is_none(registry.request)
+ check.is_none(registry.access)
+ check.is_none(registry.content)
+
+
+def test_registry_nested(check):
+ """Test that nested registries are supported."""
+ registry = Registry.unmarshal(
+ {
+ "request": "test-request",
+ "storage": "test-storage",
+ "access": "read",
+ "content": [
+ {
+ "request": "nested-request",
+ "storage": "nested-storage",
+ "access": "write",
+ }
+ ],
+ }
+ )
+
+ check.equal(registry.request, "test-request")
+ check.equal(registry.storage, "test-storage")
+ check.equal(registry.access, "read")
+ check.equal(
+ registry.content,
+ [Registry(request="nested-request", storage="nested-storage", access="write")],
+ )
+
+
+def test_editable_registry_assertion_defaults(check):
+ """Test default values of the EditableRegistryAssertion model."""
+ assertion = EditableRegistryAssertion.unmarshal(
+ {
+ "account_id": "test-account-id",
+ "name": "test-registry",
+ "views": {
+ "wifi-setup": {
+ "rules": [
+ {
+ "storage": "wifi.ssids",
+ }
+ ]
+ }
+ },
+ }
+ )
+
+ check.is_none(assertion.summary)
+ check.equal(assertion.revision, 0)
+ check.is_none(assertion.body)
+
+
+def test_registry_assertion_defaults(check):
"""Test default values of the RegistryAssertion model."""
- check.equal(fake_registry_assertion_data.body, None)
- check.equal(fake_registry_assertion_data.body_length, None)
- check.equal(fake_registry_assertion_data.sign_key_sha3_384, None)
- check.equal(fake_registry_assertion_data.summary, None)
- check.equal(fake_registry_assertion_data.revision, 0)
+ assertion = RegistryAssertion.unmarshal(
+ {
+ "account_id": "test-account-id",
+ "authority_id": "test-authority-id",
+ "name": "test-registry",
+ "timestamp": "2024-01-01T10:20:30Z",
+ "type": "registry",
+ "views": {
+ "wifi-setup": {
+ "rules": [
+ {
+ "access": "read-write",
+ "request": "ssids",
+ "storage": "wifi.ssids",
+ }
+ ]
+ }
+ },
+ }
+ )
+
+ check.is_none(assertion.body)
+ check.is_none(assertion.body_length)
+ check.is_none(assertion.sign_key_sha3_384)
+ check.is_none(assertion.summary)
+ check.equal(assertion.revision, 0)