Skip to content

Commit a13e71a

Browse files
authored
Merge pull request #4 from dbluhm/feature/optional-validation
feat: add optional validation of input doc
2 parents 1e89dce + f12a190 commit a13e71a

File tree

7 files changed

+283
-10
lines changed

7 files changed

+283
-10
lines changed

did_peer_4/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from base58 import b58decode, b58encode
55
from hashlib import sha256
66

7+
from .valid import validate_input_document
78

89
# Regex patterns
910
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
@@ -55,8 +56,11 @@ def _hash_encoded_doc(encoded_doc: str) -> str:
5556

5657
def encode(
5758
document: Dict[str, Any],
59+
validate: bool = True,
5860
) -> str:
5961
"""Encode an input document into a did:peer:4."""
62+
if validate:
63+
document = dict(validate_input_document(document))
6064
encoded_doc = _encode_doc(document)
6165
hashed = _hash_encoded_doc(encoded_doc)
6266
return f"did:peer:4{hashed}:{encoded_doc}"
@@ -195,3 +199,14 @@ def resolve_short_from_doc(
195199
raise ValueError("Document does not match DID")
196200

197201
return resolve_short(long)
202+
203+
204+
__all__ = [
205+
"encode",
206+
"encode_short",
207+
"decode",
208+
"resolve",
209+
"resolve_short",
210+
"resolve_short_from_doc",
211+
"validate_input_document",
212+
]

did_peer_4/valid.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Validate input documents."""
2+
from typing import Any, Mapping
3+
4+
5+
def resources(document: Mapping[str, Any]):
6+
"""Yield all resources in a document, skipping references."""
7+
keys = (
8+
"verificationMethod",
9+
"authentication",
10+
"assertionMethod",
11+
"keyAgreement",
12+
"capabilityDelegation",
13+
"capabilityInvocation",
14+
"service",
15+
)
16+
for key in keys:
17+
if key in document:
18+
if not isinstance(document[key], list):
19+
raise ValueError(f"{key} must be a list")
20+
21+
for index, resource in enumerate(document[key]):
22+
if isinstance(resource, dict):
23+
yield key, index, resource
24+
25+
26+
def validate_input_document(document: Mapping[str, Any]) -> Mapping[str, Any]:
27+
"""Validate did:peer:4 input document.
28+
29+
This validation is deliberately superficial. It is intended to catch mistakes
30+
in the input document that would cause the peer DID to be invalid. It is not
31+
intended to validate the contents of the document, which is left to the caller
32+
after resolution.
33+
34+
The following checks are performed:
35+
36+
- The document must be a Mapping.
37+
- The document must not be empty.
38+
- The document must not contain an id.
39+
- If present, alsoKnownAs must be a list.
40+
- verificationMethod, authentication, assertionMethod, keyAgreement,
41+
capabilityDelegation, capabilityInvocation, and service must be lists, if
42+
present.
43+
- All resources (verification methods, embedded verification methods,
44+
services) must have an id.
45+
- All resource ids must be strings.
46+
- All resource ids must be relative.
47+
- All resources must have a type.
48+
"""
49+
if not isinstance(document, Mapping):
50+
raise ValueError("document must be a Mapping")
51+
52+
if not document:
53+
raise ValueError("document must not be empty")
54+
55+
if "id" in document:
56+
raise ValueError("id must not be present in input document")
57+
58+
if "alsoKnownAs" in document:
59+
if not isinstance(document["alsoKnownAs"], list):
60+
raise ValueError("alsoKnownAs must be a list")
61+
62+
for key, index, resource in resources(document):
63+
if "id" not in resource:
64+
raise ValueError(f"{key}[{index}]: resource must have an id")
65+
66+
ident = resource["id"]
67+
if not isinstance(ident, str):
68+
raise ValueError(f"{key}[{index}]: resource id must be a string")
69+
70+
if not ident.startswith("#"):
71+
raise ValueError(f"{key}[{index}]: resource id must be relative")
72+
73+
if "type" not in resource:
74+
raise ValueError(f"{key}[{index}]: resource must have a type")
75+
76+
return document

pdm.lock

Lines changed: 120 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,14 @@ dev = [
2222
"black>=23.7.0",
2323
"ruff>=0.0.285",
2424
"pre-commit>=3.3.3",
25+
"pytest-cov>=4.1.0",
2526
]
27+
28+
[tool.coverage.report]
29+
exclude_lines = [
30+
"pragma: no cover",
31+
"@abstract"
32+
]
33+
precision = 2
34+
skip_covered = true
35+
show_missing = true

tests/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Tests for did:peer:4."""
2+
from pathlib import Path
3+
4+
5+
def examples():
6+
"""Load json from examples directory and return generator over examples."""
7+
examples_dir = Path(__file__).parent / "examples"
8+
yield from examples_dir.glob("*.json")
9+
10+
11+
EXAMPLES = list(examples())

tests/test_readme_examples.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
import json
22
import pytest
3-
from pathlib import Path
43

54
from did_peer_4 import encode, long_to_short, resolve, resolve_short
65

7-
8-
def examples():
9-
"""Load json from examples directory and return generator over examples."""
10-
examples_dir = Path(__file__).parent / "examples"
11-
yield from examples_dir.glob("*.json")
12-
13-
14-
EXAMPLES = list(examples())
6+
from . import EXAMPLES
157

168

179
def print_example(

0 commit comments

Comments
 (0)