Skip to content

Commit 04ce4d3

Browse files
dugshubclaude
andcommitted
feat(core): add semantic types for wizard system (CLI-4)
Implements CLI-4: Core type system foundation with semantic types. Types Added: - BranchId: Semantic type for branch identifiers - ActionId: Semantic type for action identifiers - OptionKey: Semantic type for option keys - MenuId: Semantic type for menu identifiers - StateValue: JSON-serializable values for state storage Factory Functions: - make_branch_id(), make_action_id(), make_option_key(), make_menu_id() - Optional validation parameter (validate: bool = False) - Zero-overhead by default, opt-in validation for development Type Guards: - is_branch_id(), is_action_id(), is_option_key(), is_menu_id() - Runtime type checking with TypeGuard support Benefits: - Type safety: Prevents ID type confusion at compile time - MyPy strict mode compliance - Zero runtime overhead (NewType pattern) - Clear semantic meaning in function signatures Tests: 28 unit tests covering all factory functions and type guards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 69ebe9c commit 04ce4d3

File tree

2 files changed

+752
-2
lines changed

2 files changed

+752
-2
lines changed

src/cli_patterns/core/types.py

Lines changed: 232 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,233 @@
1-
"""Core type definitions for CLI Patterns."""
1+
"""Core semantic types for the wizard system.
22
3-
# Placeholder for core type definitions
3+
This module defines semantic types that provide type safety for the wizard system
4+
while maintaining MyPy strict mode compliance. These are simple NewType definitions
5+
that prevent type confusion without adding runtime validation complexity.
6+
7+
The semantic types help distinguish between different string contexts in the wizard:
8+
- BranchId: Represents a branch identifier in the wizard tree
9+
- ActionId: Represents an action identifier
10+
- OptionKey: Represents an option key name
11+
- MenuId: Represents a menu identifier for navigation
12+
- StateValue: Represents any JSON-serializable value that can be stored in state
13+
14+
All ID types are backed by strings but provide semantic meaning at the type level.
15+
StateValue is a JSON-compatible type alias for flexible state storage.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from typing import Any, NewType, Optional, Union
21+
22+
from typing_extensions import TypeGuard
23+
24+
# JSON-compatible types for state values
25+
JsonPrimitive = Union[str, int, float, bool, None]
26+
JsonValue = Union[JsonPrimitive, list["JsonValue"], dict[str, "JsonValue"]]
27+
28+
# Core semantic types for wizard system
29+
BranchId = NewType("BranchId", str)
30+
"""Semantic type for branch identifiers in the wizard tree."""
31+
32+
ActionId = NewType("ActionId", str)
33+
"""Semantic type for action identifiers."""
34+
35+
OptionKey = NewType("OptionKey", str)
36+
"""Semantic type for option key names."""
37+
38+
MenuId = NewType("MenuId", str)
39+
"""Semantic type for menu identifiers."""
40+
41+
# State value is any JSON-serializable value
42+
StateValue = JsonValue
43+
"""Type alias for state values - any JSON-serializable data."""
44+
45+
# Type aliases for common collections using semantic types
46+
BranchList = list[BranchId]
47+
"""Type alias for lists of branch IDs."""
48+
49+
BranchSet = set[BranchId]
50+
"""Type alias for sets of branch IDs."""
51+
52+
ActionList = list[ActionId]
53+
"""Type alias for lists of action IDs."""
54+
55+
ActionSet = set[ActionId]
56+
"""Type alias for sets of action IDs."""
57+
58+
OptionDict = dict[OptionKey, StateValue]
59+
"""Type alias for option dictionaries mapping keys to state values."""
60+
61+
MenuList = list[MenuId]
62+
"""Type alias for lists of menu IDs."""
63+
64+
65+
# Factory functions for creating semantic types
66+
def make_branch_id(value: str, validate: Optional[bool] = None) -> BranchId:
67+
"""Create a BranchId from a string value.
68+
69+
Args:
70+
value: String value to convert to BranchId
71+
validate: If True, validate input. If None, use global config. If False, skip.
72+
73+
Returns:
74+
BranchId with semantic type safety
75+
76+
Raises:
77+
ValueError: If validate=True and value is invalid
78+
"""
79+
if validate is None:
80+
# Import here to avoid circular dependency
81+
from cli_patterns.core.config import get_config
82+
83+
validate = get_config()["enable_validation"]
84+
85+
if validate:
86+
if not value or not value.strip():
87+
raise ValueError("BranchId cannot be empty")
88+
if len(value) > 100:
89+
raise ValueError("BranchId is too long (max 100 characters)")
90+
return BranchId(value)
91+
92+
93+
def make_action_id(value: str, validate: Optional[bool] = None) -> ActionId:
94+
"""Create an ActionId from a string value.
95+
96+
Args:
97+
value: String value to convert to ActionId
98+
validate: If True, validate input. If None, use global config. If False, skip.
99+
100+
Returns:
101+
ActionId with semantic type safety
102+
103+
Raises:
104+
ValueError: If validate=True and value is invalid
105+
"""
106+
if validate is None:
107+
from cli_patterns.core.config import get_config
108+
109+
validate = get_config()["enable_validation"]
110+
111+
if validate:
112+
if not value or not value.strip():
113+
raise ValueError("ActionId cannot be empty")
114+
if len(value) > 100:
115+
raise ValueError("ActionId is too long (max 100 characters)")
116+
return ActionId(value)
117+
118+
119+
def make_option_key(value: str, validate: Optional[bool] = None) -> OptionKey:
120+
"""Create an OptionKey from a string value.
121+
122+
Args:
123+
value: String value to convert to OptionKey
124+
validate: If True, validate input. If None, use global config. If False, skip.
125+
126+
Returns:
127+
OptionKey with semantic type safety
128+
129+
Raises:
130+
ValueError: If validate=True and value is invalid
131+
"""
132+
if validate is None:
133+
from cli_patterns.core.config import get_config
134+
135+
validate = get_config()["enable_validation"]
136+
137+
if validate:
138+
if not value or not value.strip():
139+
raise ValueError("OptionKey cannot be empty")
140+
if len(value) > 100:
141+
raise ValueError("OptionKey is too long (max 100 characters)")
142+
return OptionKey(value)
143+
144+
145+
def make_menu_id(value: str, validate: Optional[bool] = None) -> MenuId:
146+
"""Create a MenuId from a string value.
147+
148+
Args:
149+
value: String value to convert to MenuId
150+
validate: If True, validate input. If None, use global config. If False, skip.
151+
152+
Returns:
153+
MenuId with semantic type safety
154+
155+
Raises:
156+
ValueError: If validate=True and value is invalid
157+
"""
158+
if validate is None:
159+
from cli_patterns.core.config import get_config
160+
161+
validate = get_config()["enable_validation"]
162+
163+
if validate:
164+
if not value or not value.strip():
165+
raise ValueError("MenuId cannot be empty")
166+
if len(value) > 100:
167+
raise ValueError("MenuId is too long (max 100 characters)")
168+
return MenuId(value)
169+
170+
171+
# Type guard functions for runtime type checking
172+
def is_branch_id(value: Any) -> TypeGuard[BranchId]:
173+
"""Check if a value is a BranchId at runtime.
174+
175+
Args:
176+
value: Value to check
177+
178+
Returns:
179+
True if value is a BranchId (string type), False otherwise
180+
181+
Note:
182+
This is a type guard function that helps with type narrowing.
183+
At runtime, BranchId is just a string, so this checks for string type.
184+
"""
185+
return isinstance(value, str)
186+
187+
188+
def is_action_id(value: Any) -> TypeGuard[ActionId]:
189+
"""Check if a value is an ActionId at runtime.
190+
191+
Args:
192+
value: Value to check
193+
194+
Returns:
195+
True if value is an ActionId (string type), False otherwise
196+
197+
Note:
198+
This is a type guard function that helps with type narrowing.
199+
At runtime, ActionId is just a string, so this checks for string type.
200+
"""
201+
return isinstance(value, str)
202+
203+
204+
def is_option_key(value: Any) -> TypeGuard[OptionKey]:
205+
"""Check if a value is an OptionKey at runtime.
206+
207+
Args:
208+
value: Value to check
209+
210+
Returns:
211+
True if value is an OptionKey (string type), False otherwise
212+
213+
Note:
214+
This is a type guard function that helps with type narrowing.
215+
At runtime, OptionKey is just a string, so this checks for string type.
216+
"""
217+
return isinstance(value, str)
218+
219+
220+
def is_menu_id(value: Any) -> TypeGuard[MenuId]:
221+
"""Check if a value is a MenuId at runtime.
222+
223+
Args:
224+
value: Value to check
225+
226+
Returns:
227+
True if value is a MenuId (string type), False otherwise
228+
229+
Note:
230+
This is a type guard function that helps with type narrowing.
231+
At runtime, MenuId is just a string, so this checks for string type.
232+
"""
233+
return isinstance(value, str)

0 commit comments

Comments
 (0)