Skip to content

Commit c4f96f4

Browse files
authored
feat: add validators (#5)
1 parent 3c2aa68 commit c4f96f4

File tree

8 files changed

+1469
-69
lines changed

8 files changed

+1469
-69
lines changed

openproficiency/ProficiencyScore.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
from enum import Enum
55
from typing import Union
6+
from .validators import validate_kebab_case
67

78

89
class ProficiencyScoreName(Enum):
@@ -29,7 +30,18 @@ def __init__(
2930
self.topic_id = topic_id
3031
self.score = score
3132

32-
# Properties - Score
33+
# Properties
34+
@property
35+
def topic_id(self) -> str:
36+
"""Get the topic ID."""
37+
return self._topic_id
38+
39+
@topic_id.setter
40+
def topic_id(self, value: str) -> None:
41+
"""Set the topic ID. kebab-case"""
42+
validate_kebab_case(value)
43+
self._topic_id = value
44+
3345
@property
3446
def score(self) -> float:
3547
"""Get the score as a numeric value between 0.0 and 1.0."""

openproficiency/Topic.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
from typing import List, Union
5+
from .validators import validate_kebab_case
56

67

78
class Topic:
@@ -28,6 +29,18 @@ def __init__(
2829
self.add_subtopics(subtopics)
2930
self.add_pretopics(pretopics)
3031

32+
# Properties
33+
@property
34+
def id(self) -> str:
35+
"""Get the topic ID."""
36+
return self._id
37+
38+
@id.setter
39+
def id(self, value: str) -> None:
40+
"""Set the topic ID. kebab-case"""
41+
validate_kebab_case(value)
42+
self._id = value
43+
3144
# Methods
3245
def add_subtopic(self, subtopic: Union[str, "Topic"]) -> None:
3346
"""

openproficiency/TopicList.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from datetime import datetime, timezone
66
from typing import Dict, Any, Union, List, cast
77
from .Topic import Topic
8+
from .validators import validate_kebab_case, validate_hostname
89

910

1011
class TopicList:
@@ -61,6 +62,28 @@ def get_topic(self, topic_id: str) -> Union[Topic, None]:
6162
return self.topics.get(topic_id, None)
6263

6364
# Properties
65+
@property
66+
def owner(self) -> str:
67+
"""Get the owner name."""
68+
return self._owner
69+
70+
@owner.setter
71+
def owner(self, value: str) -> None:
72+
"""Set the owner with hostname validation. Format: hostname, Ex: `example.com`"""
73+
validate_hostname(value)
74+
self._owner = value
75+
76+
@property
77+
def name(self) -> str:
78+
"""Get the TopicList name. Format: kebab-case"""
79+
return self._name
80+
81+
@name.setter
82+
def name(self, value: str) -> None:
83+
"""Set the TopicList name with kebab-case validation."""
84+
validate_kebab_case(value)
85+
self._name = value
86+
6487
@property
6588
def version(self) -> Union[str, None]:
6689
"""Get the semantic version of the TopicList."""

openproficiency/validators.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Validation utilities for OpenProficiency library."""
2+
3+
import re
4+
5+
6+
def validate_kebab_case(value: str) -> None:
7+
"""
8+
Validate that a string follows kebab-case format.
9+
10+
Pattern: lowercase alphanumeric characters with hyphens as separators.
11+
Examples: "topic", "topic-id", "math-level-1"
12+
13+
Args:
14+
value: The string to validate
15+
16+
Raises:
17+
ValueError: If the value is not in valid kebab-case format
18+
"""
19+
if not value:
20+
raise ValueError("Value cannot be empty")
21+
22+
# Pattern: starts and ends with alphanumeric, can have hyphens between
23+
pattern = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
24+
25+
if not re.match(pattern, value):
26+
raise ValueError(
27+
f"Value must be in kebab-case format (lowercase alphanumeric with hyphens). "
28+
f"Got: '{value}'. Examples: 'topic', 'topic-id', 'math-level-1'"
29+
)
30+
31+
32+
def validate_hostname(value: str) -> None:
33+
"""
34+
Validate that a string is a valid hostname or domain name.
35+
36+
Pattern: lowercase alphanumeric characters with hyphens and dots as separators.
37+
Examples: "example.com", "sub.example.com"
38+
39+
Args:
40+
value: The string to validate
41+
42+
Raises:
43+
ValueError: If the value is not in valid hostname format
44+
"""
45+
if not value:
46+
raise ValueError("Value cannot be empty")
47+
48+
# Pattern: hostname components separated by dots (requires at least 2 components)
49+
# Each component: starts and ends with alphanumeric, can have hyphens between
50+
component_pattern = r"[a-z0-9]+(?:-[a-z0-9]+)*"
51+
pattern = f"^{component_pattern}(?:\\.{component_pattern})+$"
52+
53+
if not re.match(pattern, value):
54+
raise ValueError(
55+
f"Value must be a valid hostname (lowercase alphanumeric with hyphens and dots). "
56+
f"Got: '{value}'. Examples: 'example.com','sub.example.com'"
57+
)

tests/ProficiencyScore_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ def test_init_invalid_score_type(self):
9494
assert "ProficiencyScoreName" in str(result)
9595

9696
# Properties
97+
def test_topic_id(self):
98+
"""Update the topic id and read it back."""
99+
100+
# Arrange
101+
ps = ProficiencyScore(
102+
topic_id="git-commit",
103+
score=0.1,
104+
)
105+
updated_topic_id = "git-merge"
106+
107+
# Act
108+
ps.topic_id = updated_topic_id
109+
110+
# Assert
111+
assert ps.topic_id == updated_topic_id
112+
97113
def test_score_numeric(self):
98114
"""Test setting score with numeric value."""
99115

0 commit comments

Comments
 (0)