Skip to content

Commit fa6fbbd

Browse files
committed
feat(parser): Add semantic error handling and command registry
- SemanticParseError with enhanced error context using semantic types - SemanticCommandRegistry for type-safe command management - Fuzzy command suggestions with semantic return types - Command metadata with proper type safety
1 parent e3ae77a commit fa6fbbd

File tree

2 files changed

+219
-0
lines changed

2 files changed

+219
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Semantic parse errors using semantic types for enhanced error handling.
2+
3+
This module provides SemanticParseError, which extends ParseError to include
4+
semantic type information for better error reporting and recovery.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import Any, Optional
10+
11+
from cli_patterns.core.parser_types import CommandId, OptionKey
12+
from cli_patterns.ui.parser.types import ParseError
13+
14+
15+
class SemanticParseError(ParseError):
16+
"""Exception raised during semantic command parsing with semantic type context.
17+
18+
This extends ParseError to include semantic type information that can be
19+
used for better error reporting and recovery mechanisms.
20+
21+
Attributes:
22+
message: Human-readable error message
23+
error_type: Type of parsing error
24+
suggestions: List of suggested corrections (semantic types, shadows base class)
25+
command: Command that caused the error (if applicable)
26+
invalid_option: Invalid option key (if applicable)
27+
valid_options: List of valid option keys (if applicable)
28+
required_role: Required role for command (if applicable)
29+
current_role: Current user role (if applicable)
30+
context_info: Additional context information
31+
"""
32+
33+
def __init__(
34+
self,
35+
error_type: str,
36+
message: str,
37+
suggestions: Optional[list[CommandId]] = None,
38+
command: Optional[CommandId] = None,
39+
invalid_option: Optional[OptionKey] = None,
40+
valid_options: Optional[list[OptionKey]] = None,
41+
required_role: Optional[str] = None,
42+
current_role: Optional[str] = None,
43+
context_info: Optional[dict[str, Any]] = None,
44+
) -> None:
45+
"""Initialize SemanticParseError.
46+
47+
Args:
48+
error_type: Type/category of error
49+
message: Error message
50+
suggestions: Optional list of command suggestions for fixing the error
51+
command: Command that caused the error
52+
invalid_option: Invalid option key that caused the error
53+
valid_options: List of valid option keys
54+
required_role: Required role for command execution
55+
current_role: Current user role
56+
context_info: Additional context information
57+
"""
58+
# Convert semantic suggestions to strings for base class
59+
string_suggestions: Optional[list[str]] = (
60+
[str(cmd) for cmd in suggestions] if suggestions else None
61+
)
62+
super().__init__(error_type, message, string_suggestions)
63+
64+
# Store semantic type information - we shadow the base class suggestions
65+
# This is intentional to provide semantic type access
66+
self.suggestions: list[CommandId] = suggestions or [] # type: ignore[assignment]
67+
self.command = command
68+
self.invalid_option = invalid_option
69+
self.valid_options = valid_options or []
70+
self.required_role = required_role
71+
self.current_role = current_role
72+
self.context_info = context_info or {}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Semantic command registry using semantic types for type-safe command management.
2+
3+
This module provides SemanticCommandRegistry, which manages command metadata
4+
using semantic types for enhanced type safety and better intellisense support.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass, field
10+
from typing import Optional
11+
12+
from cli_patterns.core.parser_types import CommandId, FlagName, OptionKey
13+
14+
15+
@dataclass
16+
class CommandMetadata:
17+
"""Metadata for a registered command using semantic types.
18+
19+
Attributes:
20+
description: Human-readable command description
21+
category: Command category for grouping
22+
aliases: List of command aliases
23+
options: List of valid option keys for this command
24+
flags: List of valid flag names for this command
25+
"""
26+
27+
description: str
28+
category: str = "general"
29+
aliases: list[CommandId] = field(default_factory=list)
30+
options: list[OptionKey] = field(default_factory=list)
31+
flags: list[FlagName] = field(default_factory=list)
32+
33+
34+
class SemanticCommandRegistry:
35+
"""Registry for managing commands with semantic type safety.
36+
37+
This registry stores command metadata using semantic types to provide
38+
type-safe operations for command registration, lookup, and suggestions.
39+
"""
40+
41+
def __init__(self) -> None:
42+
"""Initialize empty command registry."""
43+
self._commands: dict[CommandId, CommandMetadata] = {}
44+
45+
def register_command(
46+
self,
47+
command: CommandId,
48+
description: str,
49+
category: str = "general",
50+
aliases: Optional[list[CommandId]] = None,
51+
options: Optional[list[OptionKey]] = None,
52+
flags: Optional[list[FlagName]] = None,
53+
) -> None:
54+
"""Register a command with its metadata.
55+
56+
Args:
57+
command: Semantic command ID to register
58+
description: Human-readable command description
59+
category: Command category for grouping
60+
aliases: List of command aliases
61+
options: List of valid option keys for this command
62+
flags: List of valid flag names for this command
63+
"""
64+
metadata = CommandMetadata(
65+
description=description,
66+
category=category,
67+
aliases=aliases or [],
68+
options=options or [],
69+
flags=flags or [],
70+
)
71+
self._commands[command] = metadata
72+
73+
def is_registered(self, command: CommandId) -> bool:
74+
"""Check if a command is registered.
75+
76+
Args:
77+
command: Semantic command ID to check
78+
79+
Returns:
80+
True if command is registered, False otherwise
81+
"""
82+
return command in self._commands
83+
84+
def get_command_metadata(self, command: CommandId) -> Optional[CommandMetadata]:
85+
"""Get metadata for a registered command.
86+
87+
Args:
88+
command: Semantic command ID to get metadata for
89+
90+
Returns:
91+
CommandMetadata if command is registered, None otherwise
92+
"""
93+
return self._commands.get(command)
94+
95+
def get_suggestions(
96+
self, partial: str, max_suggestions: int = 5
97+
) -> list[CommandId]:
98+
"""Get command suggestions for a partial input.
99+
100+
Args:
101+
partial: Partial command string to match against
102+
max_suggestions: Maximum number of suggestions to return
103+
104+
Returns:
105+
List of matching command IDs as suggestions
106+
"""
107+
if not partial:
108+
return []
109+
110+
partial_lower = partial.lower()
111+
exact_matches = []
112+
partial_matches = []
113+
114+
# Separate exact prefix matches from partial matches
115+
for command in self._commands:
116+
command_str = str(command).lower()
117+
if command_str.startswith(partial_lower):
118+
exact_matches.append(command)
119+
elif partial_lower in command_str:
120+
partial_matches.append(command)
121+
122+
# Combine exact matches first, then partial matches
123+
suggestions = exact_matches + partial_matches
124+
return suggestions[:max_suggestions]
125+
126+
def get_all_commands(self) -> list[CommandId]:
127+
"""Get all registered commands.
128+
129+
Returns:
130+
List of all registered command IDs
131+
"""
132+
return list(self._commands.keys())
133+
134+
def get_commands_by_category(self, category: str) -> list[CommandId]:
135+
"""Get all commands in a specific category.
136+
137+
Args:
138+
category: Category name to filter by
139+
140+
Returns:
141+
List of command IDs in the specified category
142+
"""
143+
return [
144+
command
145+
for command, metadata in self._commands.items()
146+
if metadata.category == category
147+
]

0 commit comments

Comments
 (0)