Skip to content

Commit bc2e490

Browse files
dugshubclaude
andcommitted
feat(core): add Pydantic models for wizard configuration
Implement comprehensive Pydantic v2 models for wizard system including actions, options, branches, and complete wizard configuration. Models: - BaseConfig: Common fields (metadata, tags) for all configs - Action types: BashActionConfig, PythonActionConfig (discriminated unions) - Option types: String, Select, Path, Number, Boolean (discriminated unions) - MenuConfig: Navigation menu configuration - BranchConfig: Complete branch with actions, options, menus - WizardConfig: Top-level wizard configuration - SessionState: Unified wizard + parser state - Result types: ActionResult, CollectionResult, NavigationResult Features: - Discriminated unions for type-safe extensibility - Pydantic v2 with ConfigDict - Strict validation enabled - Field descriptions for all attributes - 100% test coverage with 42 comprehensive tests - MyPy strict mode compliance Part of CLI-4: Minimal Core Type Definitions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 95c5541 commit bc2e490

File tree

2 files changed

+1103
-2
lines changed

2 files changed

+1103
-2
lines changed

src/cli_patterns/core/models.py

Lines changed: 342 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,343 @@
1-
"""Core data models for CLI Patterns."""
1+
"""Core data models for CLI Patterns.
22
3-
# Placeholder for core data models
3+
This module defines Pydantic models for the wizard configuration structure.
4+
All models use MyPy strict mode and Pydantic v2 features including:
5+
- Discriminated unions for extensibility
6+
- Field validation
7+
- JSON serialization/deserialization
8+
- StrictModel base class for type safety
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import Any, Literal, Optional, Union
14+
15+
from pydantic import BaseModel, ConfigDict, Field
16+
17+
from cli_patterns.core.types import (
18+
ActionId,
19+
BranchId,
20+
MenuId,
21+
OptionKey,
22+
)
23+
24+
# StateValue is defined as Any for Pydantic compatibility
25+
# The actual type constraint (JSON-serializable) is enforced at serialization time
26+
StateValue = Any
27+
28+
29+
class StrictModel(BaseModel):
30+
"""Base model with strict validation enabled.
31+
32+
This ensures type safety and proper validation throughout the system.
33+
"""
34+
35+
model_config = ConfigDict(
36+
# Strict mode for type safety
37+
strict=True,
38+
# Allow arbitrary types (for semantic types)
39+
arbitrary_types_allowed=True,
40+
# Extra fields are forbidden
41+
extra="forbid",
42+
)
43+
44+
45+
class BaseConfig(StrictModel):
46+
"""Base configuration providing common fields for all config types.
47+
48+
This class provides metadata and tagging infrastructure that all
49+
configuration types can use.
50+
"""
51+
52+
metadata: dict[str, Any] = Field(default_factory=dict)
53+
"""Arbitrary metadata for extensions and tooling."""
54+
55+
tags: list[str] = Field(default_factory=list)
56+
"""Tags for categorization and filtering."""
57+
58+
59+
# ============================================================================
60+
# Action Configuration Models
61+
# ============================================================================
62+
63+
64+
class BashActionConfig(BaseConfig):
65+
"""Configuration for bash command actions.
66+
67+
Executes a bash command with optional environment variables.
68+
"""
69+
70+
type: Literal["bash"] = Field(
71+
default="bash", description="Action type discriminator"
72+
)
73+
id: ActionId = Field(description="Unique action identifier")
74+
name: str = Field(description="Human-readable action name")
75+
description: Optional[str] = Field(default=None, description="Action description")
76+
command: str = Field(description="Bash command to execute")
77+
env: dict[str, str] = Field(
78+
default_factory=dict, description="Environment variables for command"
79+
)
80+
81+
82+
class PythonActionConfig(BaseConfig):
83+
"""Configuration for Python function actions.
84+
85+
Calls a Python function from a specified module.
86+
"""
87+
88+
type: Literal["python"] = Field(
89+
default="python", description="Action type discriminator"
90+
)
91+
id: ActionId = Field(description="Unique action identifier")
92+
name: str = Field(description="Human-readable action name")
93+
description: Optional[str] = Field(default=None, description="Action description")
94+
module: str = Field(description="Python module path")
95+
function: str = Field(description="Function name to call")
96+
97+
98+
# Discriminated union of all action types
99+
# TODO: Future extension point - add new action types here
100+
ActionConfigUnion = Union[BashActionConfig, PythonActionConfig]
101+
102+
103+
# ============================================================================
104+
# Option Configuration Models
105+
# ============================================================================
106+
107+
108+
class StringOptionConfig(BaseConfig):
109+
"""Configuration for string input options."""
110+
111+
type: Literal["string"] = Field(
112+
default="string", description="Option type discriminator"
113+
)
114+
id: OptionKey = Field(description="Unique option identifier")
115+
name: str = Field(description="Human-readable option name")
116+
description: str = Field(description="Option description/prompt")
117+
default: Optional[str] = Field(default=None, description="Default value")
118+
required: bool = Field(default=False, description="Whether option is required")
119+
120+
121+
class SelectOptionConfig(BaseConfig):
122+
"""Configuration for selection options (dropdown/menu)."""
123+
124+
type: Literal["select"] = Field(
125+
default="select", description="Option type discriminator"
126+
)
127+
id: OptionKey = Field(description="Unique option identifier")
128+
name: str = Field(description="Human-readable option name")
129+
description: str = Field(description="Option description/prompt")
130+
choices: list[str] = Field(description="Available choices")
131+
default: Optional[str] = Field(default=None, description="Default value")
132+
required: bool = Field(default=False, description="Whether option is required")
133+
134+
135+
class PathOptionConfig(BaseConfig):
136+
"""Configuration for file/directory path options."""
137+
138+
type: Literal["path"] = Field(
139+
default="path", description="Option type discriminator"
140+
)
141+
id: OptionKey = Field(description="Unique option identifier")
142+
name: str = Field(description="Human-readable option name")
143+
description: str = Field(description="Option description/prompt")
144+
must_exist: bool = Field(
145+
default=False, description="Whether path must exist for validation"
146+
)
147+
default: Optional[str] = Field(default=None, description="Default value")
148+
required: bool = Field(default=False, description="Whether option is required")
149+
150+
151+
class NumberOptionConfig(BaseConfig):
152+
"""Configuration for numeric input options."""
153+
154+
type: Literal["number"] = Field(
155+
default="number", description="Option type discriminator"
156+
)
157+
id: OptionKey = Field(description="Unique option identifier")
158+
name: str = Field(description="Human-readable option name")
159+
description: str = Field(description="Option description/prompt")
160+
min_value: Optional[float] = Field(
161+
default=None, description="Minimum allowed value"
162+
)
163+
max_value: Optional[float] = Field(
164+
default=None, description="Maximum allowed value"
165+
)
166+
default: Optional[float] = Field(default=None, description="Default value")
167+
required: bool = Field(default=False, description="Whether option is required")
168+
169+
170+
class BooleanOptionConfig(BaseConfig):
171+
"""Configuration for boolean (yes/no) options."""
172+
173+
type: Literal["boolean"] = Field(
174+
default="boolean", description="Option type discriminator"
175+
)
176+
id: OptionKey = Field(description="Unique option identifier")
177+
name: str = Field(description="Human-readable option name")
178+
description: str = Field(description="Option description/prompt")
179+
default: Optional[bool] = Field(default=None, description="Default value")
180+
required: bool = Field(default=False, description="Whether option is required")
181+
182+
183+
# Discriminated union of all option types
184+
# TODO: Future extension point - add new option types here (e.g., multi-select, date, etc.)
185+
OptionConfigUnion = Union[
186+
StringOptionConfig,
187+
SelectOptionConfig,
188+
PathOptionConfig,
189+
NumberOptionConfig,
190+
BooleanOptionConfig,
191+
]
192+
193+
194+
# ============================================================================
195+
# Menu and Navigation Configuration
196+
# ============================================================================
197+
198+
199+
class MenuConfig(StrictModel):
200+
"""Configuration for navigation menu items.
201+
202+
Menus allow tree-based navigation between branches.
203+
"""
204+
205+
id: MenuId = Field(description="Unique menu identifier")
206+
label: str = Field(description="Menu item label displayed to user")
207+
target: BranchId = Field(description="Target branch to navigate to")
208+
description: Optional[str] = Field(
209+
default=None, description="Optional menu description"
210+
)
211+
212+
213+
# ============================================================================
214+
# Branch Configuration
215+
# ============================================================================
216+
217+
218+
class BranchConfig(BaseConfig):
219+
"""Configuration for a wizard branch.
220+
221+
A branch represents a screen/step in the wizard with actions, options,
222+
and navigation menus.
223+
"""
224+
225+
id: BranchId = Field(description="Unique branch identifier")
226+
title: str = Field(description="Branch title displayed to user")
227+
description: Optional[str] = Field(default=None, description="Branch description")
228+
actions: list[ActionConfigUnion] = Field(
229+
default_factory=list, description="Actions available in this branch"
230+
)
231+
options: list[OptionConfigUnion] = Field(
232+
default_factory=list, description="Options to collect in this branch"
233+
)
234+
menus: list[MenuConfig] = Field(
235+
default_factory=list, description="Navigation menus in this branch"
236+
)
237+
238+
239+
# ============================================================================
240+
# Wizard Configuration
241+
# ============================================================================
242+
243+
244+
class WizardConfig(BaseConfig):
245+
"""Complete wizard configuration.
246+
247+
This is the top-level configuration that defines an entire wizard,
248+
including all branches and the entry point.
249+
"""
250+
251+
name: str = Field(description="Wizard name (identifier)")
252+
version: str = Field(description="Wizard version (semver recommended)")
253+
description: Optional[str] = Field(default=None, description="Wizard description")
254+
entry_branch: BranchId = Field(
255+
description="Initial branch to display when wizard starts"
256+
)
257+
branches: list[BranchConfig] = Field(description="All branches in the wizard tree")
258+
259+
# TODO: Add validator to ensure entry_branch exists in branches
260+
# This would be done with @model_validator in Pydantic v2
261+
262+
263+
# ============================================================================
264+
# Session State
265+
# ============================================================================
266+
267+
268+
class SessionState(StrictModel):
269+
"""Unified session state for wizard and parser.
270+
271+
This model combines both wizard state (navigation, options) and
272+
parser state (mode, history) into a single unified state.
273+
"""
274+
275+
# Wizard state
276+
current_branch: Optional[BranchId] = Field(
277+
default=None, description="Currently active branch"
278+
)
279+
navigation_history: list[BranchId] = Field(
280+
default_factory=list, description="Branch navigation history for 'back' command"
281+
)
282+
option_values: dict[OptionKey, StateValue] = Field(
283+
default_factory=dict, description="Collected option values"
284+
)
285+
286+
# Shared state
287+
variables: dict[str, StateValue] = Field(
288+
default_factory=dict,
289+
description="Variables for interpolation (e.g., ${var} in commands)",
290+
)
291+
292+
# Parser state
293+
parse_mode: str = Field(default="interactive", description="Current parsing mode")
294+
command_history: list[str] = Field(
295+
default_factory=list, description="Command history for readline/recall"
296+
)
297+
298+
299+
# ============================================================================
300+
# Result Types
301+
# ============================================================================
302+
303+
304+
class ActionResult(StrictModel):
305+
"""Result from executing an action.
306+
307+
Contains success status, output, and error information.
308+
"""
309+
310+
action_id: ActionId = Field(description="ID of executed action")
311+
success: bool = Field(description="Whether action succeeded")
312+
output: str = Field(default="", description="Action output (stdout)")
313+
exit_code: int = Field(default=0, description="Exit code (for bash actions)")
314+
error: Optional[str] = Field(default=None, description="Error message if failed")
315+
316+
317+
class CollectionResult(StrictModel):
318+
"""Result from collecting an option value.
319+
320+
Contains the collected value or error information.
321+
"""
322+
323+
option_key: OptionKey = Field(description="Key of option being collected")
324+
success: bool = Field(description="Whether collection succeeded")
325+
value: Optional[StateValue] = Field(
326+
default=None, description="Collected value if successful"
327+
)
328+
error: Optional[str] = Field(
329+
default=None, description="Error message if collection failed"
330+
)
331+
332+
333+
class NavigationResult(StrictModel):
334+
"""Result from a navigation operation.
335+
336+
Contains target branch and success/error information.
337+
"""
338+
339+
success: bool = Field(description="Whether navigation succeeded")
340+
target: BranchId = Field(description="Target branch")
341+
error: Optional[str] = Field(
342+
default=None, description="Error message if navigation failed"
343+
)

0 commit comments

Comments
 (0)