Skip to content

Commit 3624054

Browse files
committed
refactor namespace and version specifications for modularity
1 parent 3b73b3d commit 3624054

File tree

3 files changed

+52
-23
lines changed

3 files changed

+52
-23
lines changed

src/ssvc/_mixins.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@
2727
from semver import Version
2828

2929
from ssvc import _schemaVersion
30-
from ssvc.namespaces import NS_PATTERN, NameSpace
30+
from ssvc.namespaces import NameSpace, NamespaceString
31+
32+
VERSION_PATTERN = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
33+
3134

3235
VersionField = Annotated[
3336
str,
3437
Field(
3538
description="The version of the SSVC object. This should be a valid semantic version string.",
3639
examples=["1.0.0", "2.1.3"],
37-
pattern=r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
40+
pattern=VERSION_PATTERN,
3841
min_length=5,
3942
),
4043
]
@@ -80,7 +83,7 @@ class _Namespaced(BaseModel):
8083

8184
# the field definition enforces the pattern for namespaces
8285
# additional validation is performed in the field_validator immediately after the pattern check
83-
namespace: str = Field(pattern=NS_PATTERN, min_length=3, max_length=100)
86+
namespace: NamespaceString
8487

8588
@field_validator("namespace", mode="before")
8689
@classmethod

src/ssvc/namespaces.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,67 @@
2525

2626
import re
2727
from enum import StrEnum, auto
28+
from typing import Annotated
29+
30+
from pydantic import Field
2831

2932
X_PFX = "x_"
3033
"""The prefix for extension namespaces. Extension namespaces must start with this prefix."""
3134

35+
MIN_NS_LENGTH = 3
36+
MAX_NS_LENGTH = 1000
37+
NS_LENGTH_INTERVAL = MAX_NS_LENGTH - MIN_NS_LENGTH
38+
39+
LENGTH_CHECK_PATTERN = rf"(?=.{{{MIN_NS_LENGTH},{MAX_NS_LENGTH}}}$)"
40+
"""Ensures the string is between MIN_NS_LENGTH and MAX_NS_LENGTH characters long."""
41+
42+
PREFIX_CHECK_PATTERN = rf"(x_)?[a-z0-9]{{{MIN_NS_LENGTH}}}"
43+
"""Ensures the string starts with an optional prefix followed by at least 3 alphanumeric characters."""
44+
45+
REMAINDER_CHECK_PATTERN = rf"([/.-]?[a-z0-9]+){{0,{NS_LENGTH_INTERVAL}}}$"
46+
"""Ensures that the string contains only lowercase alphanumeric characters and limited punctuation characters (`/`, `.`, `-`),"""
47+
48+
3249
# pattern to match
33-
# `(?=.{3,100}$)`: 3-25 characters long
34-
# `^(x_)`: `x_` prefix is optional
35-
# `[a-z0-9]{3,4}`: must start with 3-4 alphanumeric characters
36-
# `[/.-]?`: only one punctuation character is allowed between alphanumeric characters
37-
# `[a-z0-9]+`: at least one alphanumeric character is required after the punctuation character
38-
# `([/.-]?[a-z0-9]+){0,22}`: zero to 22 occurrences of the punctuation character followed by at least one alphanumeric character
39-
# (note that the total limit will kick in at or before this point)
40-
# `$`: end of the string
41-
NS_PATTERN = re.compile(r"^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$")
42-
"""The regular expression pattern for validating namespaces.
43-
44-
Note:
50+
# NOTE: be careful with this regex. We're using f-strings to insert the min and max lengths, so we need to ensure that
51+
# literal { and } characters are escaped properly (doubled up) so they appear in as single braces in the final regex.
52+
NS_PATTERN = re.compile(
53+
rf"^{LENGTH_CHECK_PATTERN}{PREFIX_CHECK_PATTERN}{REMAINDER_CHECK_PATTERN}$"
54+
)
55+
f"""The regular expression pattern for validating namespaces.
56+
57+
!!! note "Namespace Validation Rules"
58+
4559
Namespace values must
4660
47-
- be 3-25 characters long
61+
- be {MIN_NS_LENGTH}-{MAX_NS_LENGTH} characters long
4862
- contain only lowercase alphanumeric characters and limited punctuation characters (`/`,`.` and `-`)
4963
- have only one punctuation character in a row
50-
- start with 3-4 alphanumeric characters after the optional extension prefix
64+
- start with 3 alphanumeric characters after the optional extension prefix
5165
- end with an alphanumeric character
5266
53-
See examples in the `NameSpace` enum.
5467
"""
5568

69+
NamespaceString = Annotated[
70+
str,
71+
Field(
72+
description="The namespace of the SSVC object.",
73+
examples=["ssvc", "cisa", "x_private-test", "ssvc/de-DE/reference-arch-1"],
74+
pattern=NS_PATTERN,
75+
min_length=MIN_NS_LENGTH,
76+
max_length=MAX_NS_LENGTH,
77+
),
78+
]
79+
"""A string datatype for namespace values, for use in Pydantic models."""
80+
5681

5782
class NameSpace(StrEnum):
58-
"""
83+
f"""
5984
Defines the official namespaces for SSVC.
6085
6186
The namespace value must be one of the members of this enum or start with the prefix specified in X_PFX.
62-
Namespaces must be 3-25 lowercase characters long and must start with 3-4 alphanumeric characters after the optional prefix.
87+
Namespaces must be {MIN_NS_LENGTH}-{MAX_NS_LENGTH} lowercase characters long and must start with 3-4
88+
alphanumeric characters after the optional prefix.
6389
Limited punctuation characters (/.-) are allowed between alphanumeric characters, but only one at a time.
6490
6591
Example:

src/test/test_mixins.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pydantic import BaseModel, ValidationError
2424

2525
from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned
26-
from ssvc.namespaces import NameSpace
26+
from ssvc.namespaces import MAX_NS_LENGTH, NameSpace
2727

2828

2929
class TestMixins(unittest.TestCase):
@@ -92,12 +92,12 @@ def test_namespaced_create_errors(self):
9292
_Namespaced(namespace="x_")
9393

9494
# error if namespace starts with x_ but is too long
95-
for i in range(150):
95+
for i in range(MAX_NS_LENGTH + 50):
9696
shortest = "x_aaa"
9797
ns = shortest + "a" * i
9898
with self.subTest(ns=ns):
9999
# length limit set in the NS_PATTERN regex
100-
if len(ns) <= 100:
100+
if len(ns) <= MAX_NS_LENGTH:
101101
# expect success on shorter than limit
102102
_Namespaced(namespace=ns)
103103
else:

0 commit comments

Comments
 (0)