Repository: zimeon/ocfl-py

Issue:
Catch inconsistent id error
See https://github.com/OCFL/fixtures/pull/87 in which the `id` in `v1` does not match the `id` in the root/`v2` inventory.

My validator fails to report this error:
```
(py38) simeon@RottenApple fixtures> ../ocfl-validate.py 1.0/bad-objects/E037_inconsistent_id
INFO:ocfl.object:OCFL object at 1.0/bad-objects/E037_inconsistent_id is VALID
```

Discussion:

Patch:
diff --git a/fixtures b/fixtures
index db7d333..9a9d868 160000
--- a/fixtures
+++ b/fixtures
@@ -1,1 +1,1 @@
-Subproject commit db7d3338fef71be3328510d32d768a2746eb0e1f
+Subproject commit 9a9d86802f853de56a4b7925adcceccabee390ee
diff --git a/ocfl/data/validation-errors.json b/ocfl/data/validation-errors.json
index 210f963..05c85ac 100644
--- a/ocfl/data/validation-errors.json
+++ b/ocfl/data/validation-errors.json
@@ -158,12 +158,18 @@
        "en": "OCFL Object %s inventory missing `head` attribute"
     }
   },
-  "E037": {
+  "E037a": {
     "params": ["where"],
     "description": {
        "en": "OCFL Object %s inventory `id` attribute is empty or badly formed"
     }
   },
+  "E037b": {
+    "params": ["where", "version_id", "root_id"],
+    "description": {
+       "en": "OCFL Object %s inventory id `%s` does not match the value in the root inventory `%s`"
+    }
+  },
   "E038": {
     "params": ["where"],
     "description": {
diff --git a/ocfl/inventory_validator.py b/ocfl/inventory_validator.py
index 4453b22..0eb08ab 100644
--- a/ocfl/inventory_validator.py
+++ b/ocfl/inventory_validator.py
@@ -32,6 +32,7 @@ class InventoryValidator():
         self.where = where
         # Object state
         self.inventory = None
+        self.id = None
         self.digest_algorithm = 'sha512'
         self.content_directory = 'content'
         self.all_versions = []
@@ -56,9 +57,11 @@ class InventoryValidator():
         if 'id' in inventory:
             iid = inventory['id']
             if not isinstance(iid, str) or iid == '':
-                self.error("E037")
-            elif not re.match(r'''(\w+):.+''', iid):
-                self.warning("W005", id=iid)
+                self.error("E037a")
+            else:
+                if not re.match(r'''(\w+):.+''', iid):
+                    self.warning("W005", id=iid)
+                self.id = iid
         else:
             self.error("E036a")
         if 'type' not in inventory:
diff --git a/ocfl/validator.py b/ocfl/validator.py
index 211fcfe..5360e93 100644
--- a/ocfl/validator.py
+++ b/ocfl/validator.py
@@ -40,6 +40,7 @@ class Validator():
             '0005-mutable-head'
         ]
         # The following actually initialized in initialize() method
+        self.id = None
         self.digest_algorithm = None
         self.content_directory = None
         self.inventory_digest_files = None
@@ -52,6 +53,7 @@ class Validator():
 
         Must be called between attempts to validate objects.
         """
+        self.id = None
         self.digest_algorithm = 'sha512'
         self.content_directory = 'content'
         self.inventory_digest_files = {}  # index by version_dir, algorithms may differ
@@ -95,6 +97,7 @@ class Validator():
             inventory_is_valid = self.log.num_errors == 0
             self.root_inv_validator = inv_validator
             all_versions = inv_validator.all_versions
+            self.id = inv_validator.id
             self.content_directory = inv_validator.content_directory
             self.digest_algorithm = inv_validator.digest_algorithm
             self.validate_inventory_digest(inv_file, self.digest_algorithm)
@@ -225,6 +228,9 @@ class Validator():
                 digest_algorithm = inv_validator.digest_algorithm
                 self.validate_inventory_digest(inv_file, digest_algorithm, where=version_dir)
                 self.inventory_digest_files[version_dir] = 'inventory.json.' + digest_algorithm
+                if self.id and 'id' in version_inventory:
+                    if version_inventory['id'] != self.id:
+                        self.log.error('E037b', where=version_dir, root_id=self.id, version_id=version_inventory['id'])
                 if 'manifest' in version_inventory:
                     # Check that all files listed in prior inventories are in manifest
                     not_seen = set(prior_manifest_digests.keys())


Test patch:
diff --git a/tests/test_inventory_validator.py b/tests/test_inventory_validator.py
index af61b9d..1d2a387 100644
--- a/tests/test_inventory_validator.py
+++ b/tests/test_inventory_validator.py
@@ -50,7 +50,7 @@ class TestAll(unittest.TestCase):
         self.assertIn('E041b', log.errors)
         log.clear()
         iv.validate({"id": []})
-        self.assertIn('E037', log.errors)
+        self.assertIn('E037a', log.errors)
         log.clear()
         iv.validate({"id": "not_a_uri", "digestAlgorithm": "sha256"})
         self.assertIn('W005', log.warns)
diff --git a/tests/test_validator.py b/tests/test_validator.py
index 14ac8d4..3ab6ffb 100644
--- a/tests/test_validator.py
+++ b/tests/test_validator.py
@@ -40,6 +40,7 @@ class TestAll(unittest.TestCase):
                            'E033_inventory_bad_json': ['E033'],
                            'E036_no_id': ['E036a'],
                            'E036_no_head': ['E036d'],
+                           'E037_inconsistent_id': ['E037b'],
                            'E040_head_not_most_recent': ['E040'],
                            'E040_wrong_head_doesnt_exist': ['E040'],
                            'E040_wrong_head_format': ['E040'],