Repository: canonical/snapcraft (language: python)

Issue:
registries: create pydantic models
### What needs to get done

Formalize pydantic models prototyped in #4917 (types, docstrings, constraints, unit tests).

### Why it needs to get done

To support registries


Discussion / hints:

Thank you for reporting us your feedback!

The internal ticket has been created: https://warthogs.atlassian.net/browse/CRAFT-3332.

> This message was autogenerated


Fix patch:
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 <http://www.gnu.org/licenses/>.
 """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 <http://www.gnu.org/licenses/>.
 
-"""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


Test patch:
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)