Skip to content

Commit dd6edf2

Browse files
dugshubclaude
andcommitted
CLI-8: Add ParserPipeline and CommandRegistry
- ParserPipeline: Routes input to appropriate parsers - Priority-based parser selection - Condition-based routing support - Comprehensive error aggregation - Fallback handling for unmatched input - CommandRegistry: Manages command metadata - Command registration with metadata - Alias support for commands - Category-based organization - Fuzzy matching for typo suggestions - Thread-safe command management 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b8e990f commit dd6edf2

File tree

2 files changed

+421
-0
lines changed

2 files changed

+421
-0
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Parser pipeline for routing input to appropriate parsers."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import Callable, Optional
7+
8+
from cli_patterns.ui.parser.protocols import Parser
9+
from cli_patterns.ui.parser.types import Context, ParseError, ParseResult
10+
11+
12+
@dataclass
13+
class _ParserEntry:
14+
"""Internal entry for storing parser with metadata."""
15+
16+
parser: Parser
17+
condition: Optional[Callable[[str, Context], bool]]
18+
priority: int
19+
20+
21+
class ParserPipeline:
22+
"""Pipeline for routing input to the appropriate parser.
23+
24+
The pipeline maintains a list of parsers with optional conditions and priorities.
25+
When parsing input, it tries each parser in order until one succeeds.
26+
"""
27+
28+
def __init__(self) -> None:
29+
"""Initialize empty parser pipeline."""
30+
self._parsers: list[_ParserEntry] = []
31+
32+
def add_parser(
33+
self,
34+
parser: Parser,
35+
condition: Optional[Callable[[str, Context], bool]] = None,
36+
priority: int = 0,
37+
) -> None:
38+
"""Add a parser to the pipeline.
39+
40+
Args:
41+
parser: Parser instance to add
42+
condition: Optional condition function that returns True if parser should handle input
43+
priority: Priority for ordering (higher numbers = higher priority, default 0)
44+
"""
45+
entry = _ParserEntry(parser=parser, condition=condition, priority=priority)
46+
self._parsers.append(entry)
47+
48+
# Sort by priority (higher numbers first), maintaining insertion order for same priority
49+
self._parsers.sort(
50+
key=lambda x: (
51+
-x.priority,
52+
(
53+
self._parsers.index(x)
54+
if x in self._parsers[:-1]
55+
else len(self._parsers)
56+
),
57+
)
58+
)
59+
60+
def remove_parser(self, parser: Parser) -> bool:
61+
"""Remove a parser from the pipeline.
62+
63+
Args:
64+
parser: Parser instance to remove
65+
66+
Returns:
67+
True if parser was found and removed, False otherwise
68+
"""
69+
for i, entry in enumerate(self._parsers):
70+
if entry.parser is parser:
71+
self._parsers.pop(i)
72+
return True
73+
return False
74+
75+
def parse(self, input_str: str, context: Context) -> ParseResult:
76+
"""Parse input using the first matching parser in the pipeline.
77+
78+
Args:
79+
input_str: Input string to parse
80+
context: Parsing context
81+
82+
Returns:
83+
ParseResult from the first parser that can handle the input
84+
85+
Raises:
86+
ParseError: If no parser can handle the input or parsing fails
87+
"""
88+
if not self._parsers:
89+
raise ParseError(
90+
error_type="NO_PARSERS",
91+
message="No parsers available in pipeline",
92+
suggestions=["Add parsers to the pipeline"],
93+
)
94+
95+
matching_parsers = []
96+
condition_errors = []
97+
98+
# Find all parsers that can handle the input
99+
for entry in self._parsers:
100+
try:
101+
# Check condition if provided
102+
if entry.condition is not None:
103+
if not entry.condition(input_str, context):
104+
continue
105+
106+
# Check if parser can handle the input
107+
if hasattr(entry.parser, "can_parse"):
108+
if entry.parser.can_parse(input_str, context):
109+
matching_parsers.append(entry)
110+
else:
111+
# If no can_parse method, assume it can handle it
112+
matching_parsers.append(entry)
113+
114+
except Exception as e:
115+
# Condition function failed, skip this parser
116+
condition_errors.append(f"Condition failed for parser: {e}")
117+
continue
118+
119+
if not matching_parsers:
120+
error_msg = "No parser can handle the input"
121+
if condition_errors:
122+
error_msg += f". Condition errors: {'; '.join(condition_errors)}"
123+
124+
raise ParseError(
125+
error_type="NO_MATCHING_PARSER",
126+
message=error_msg,
127+
suggestions=[
128+
"Check input format",
129+
"Add appropriate parser to pipeline",
130+
],
131+
)
132+
133+
# Try the first matching parser (highest priority)
134+
parser_entry = matching_parsers[0]
135+
136+
try:
137+
return parser_entry.parser.parse(input_str, context)
138+
except ParseError:
139+
# Re-raise parse errors from the parser
140+
raise
141+
except Exception as e:
142+
# Convert other exceptions to ParseError
143+
raise ParseError(
144+
error_type="PARSER_ERROR",
145+
message=f"Parser failed: {str(e)}",
146+
suggestions=["Check input format", "Try a different parser"],
147+
) from e
148+
149+
def clear(self) -> None:
150+
"""Clear all parsers from the pipeline."""
151+
self._parsers.clear()
152+
153+
@property
154+
def parser_count(self) -> int:
155+
"""Get the number of parsers in the pipeline."""
156+
return len(self._parsers)

0 commit comments

Comments
 (0)