Skip to content

Commit 5077194

Browse files
committed
feat: [#229] add CLI integration for schema generation command
Phase 4: Complete presentation layer integration - Add Schema variant to CreateAction enum with optional output path - Create CreateSchemaCommandController in presentation layer - Add ProgressReporter.result() method for stdout output abstraction - Wire schema command routing in create/router.rs - Add create_schema_controller() factory to Container - Update CommandError.help() to return String for schema errors - Merge identical match arms in CLI tests (clippy compliance) - Update doctest example to include Schema action - Remove unused StdoutWriteFailed error variant - Follow strict output abstraction rules (no direct stdout/stderr) All pre-commit checks pass (linters, formatters, tests).
1 parent d556053 commit 5077194

File tree

14 files changed

+358
-29
lines changed

14 files changed

+358
-29
lines changed

src/bootstrap/container.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::infrastructure::persistence::repository_factory::RepositoryFactory;
1414
use crate::presentation::controllers::configure::ConfigureCommandController;
1515
use crate::presentation::controllers::constants::DEFAULT_LOCK_TIMEOUT;
1616
use crate::presentation::controllers::create::subcommands::environment::CreateEnvironmentCommandController;
17+
use crate::presentation::controllers::create::subcommands::schema::CreateSchemaCommandController;
1718
use crate::presentation::controllers::create::subcommands::template::CreateTemplateCommandController;
1819
use crate::presentation::controllers::destroy::DestroyCommandController;
1920
use crate::presentation::controllers::provision::ProvisionCommandController;
@@ -202,6 +203,12 @@ impl Container {
202203
CreateTemplateCommandController::new(&self.user_output())
203204
}
204205

206+
/// Create a new `CreateSchemaCommandController`
207+
#[must_use]
208+
pub fn create_schema_controller(&self) -> CreateSchemaCommandController {
209+
CreateSchemaCommandController::new(&self.user_output())
210+
}
211+
205212
/// Create a new `ProvisionCommandController`
206213
#[must_use]
207214
pub fn create_provision_controller(&self) -> ProvisionCommandController {
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
//! Unified Create Command Errors
22
//!
33
//! This module defines a unified error type that encompasses all create subcommand errors,
4-
//! providing a single interface for environment and template command errors.
4+
//! providing a single interface for environment, template, and schema command errors.
55
66
use thiserror::Error;
77

88
use super::subcommands::{
9-
environment::CreateEnvironmentCommandError, template::CreateEnvironmentTemplateCommandError,
9+
environment::CreateEnvironmentCommandError, schema::CreateSchemaCommandError,
10+
template::CreateEnvironmentTemplateCommandError,
1011
};
1112

1213
/// Unified error type for all create subcommands
1314
///
1415
/// This error type provides a unified interface for errors that can occur during
15-
/// any create subcommand execution (environment creation or template generation).
16+
/// any create subcommand execution (environment creation, template generation, or schema generation).
1617
/// It wraps the specific command errors while preserving their context and help methods.
1718
#[derive(Debug, Error)]
1819
pub enum CreateCommandError {
@@ -23,6 +24,10 @@ pub enum CreateCommandError {
2324
/// Template generation errors
2425
#[error(transparent)]
2526
Template(#[from] CreateEnvironmentTemplateCommandError),
27+
28+
/// Schema generation errors
29+
#[error(transparent)]
30+
Schema(#[from] CreateSchemaCommandError),
2631
}
2732

2833
impl CreateCommandError {
@@ -31,10 +36,11 @@ impl CreateCommandError {
3136
/// This method delegates to the specific command error's help method,
3237
/// providing context-appropriate troubleshooting guidance.
3338
#[must_use]
34-
pub fn help(&self) -> &'static str {
39+
pub fn help(&self) -> String {
3540
match self {
36-
Self::Environment(err) => err.help(),
37-
Self::Template(err) => err.help(),
41+
Self::Environment(err) => err.help().to_string(),
42+
Self::Template(err) => err.help().to_string(),
43+
Self::Schema(err) => err.help(),
3844
}
3945
}
4046
}

src/presentation/controllers/create/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ mod tests;
5353
pub use errors::CreateCommandError;
5454
pub use router::route_command;
5555
pub use subcommands::environment::{ConfigFormat, ConfigLoader, CreateEnvironmentCommandError};
56+
pub use subcommands::schema::CreateSchemaCommandError;
5657
pub use subcommands::template::CreateEnvironmentTemplateCommandError;

src/presentation/controllers/create/router.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ use super::errors::CreateCommandError;
1212

1313
/// Route the create command to its appropriate subcommand
1414
///
15-
/// This function routes between different create subcommands (environment or template).
15+
/// This function routes between different create subcommands (environment, template, or schema).
1616
///
1717
/// # Arguments
1818
///
19-
/// * `action` - The create action to perform (environment creation or template generation)
19+
/// * `action` - The create action to perform (environment creation, template generation, or schema generation)
2020
/// * `working_dir` - Root directory for environment data storage
2121
/// * `context` - Execution context providing access to application services
2222
///
@@ -53,5 +53,10 @@ pub async fn route_command(
5353
.await
5454
.map_err(CreateCommandError::Template)
5555
}
56+
CreateAction::Schema { output_path } => context
57+
.container()
58+
.create_schema_controller()
59+
.execute(output_path.as_ref())
60+
.map_err(CreateCommandError::Schema),
5661
}
5762
}

src/presentation/controllers/create/subcommands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
//! This module contains the individual subcommands for the create command.
44
55
pub mod environment;
6+
pub mod schema;
67
pub mod template;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Errors for Create Schema Command (Presentation Layer)
2+
3+
use thiserror::Error;
4+
5+
use crate::application::command_handlers::create::schema::CreateSchemaCommandHandlerError;
6+
use crate::presentation::views::progress::ProgressReporterError;
7+
8+
/// Errors that can occur during schema creation command execution
9+
#[derive(Debug, Error)]
10+
pub enum CreateSchemaCommandError {
11+
/// Failed to acquire user output lock
12+
#[error("Failed to acquire user output lock")]
13+
UserOutputLockFailed,
14+
15+
/// Command handler execution failed
16+
#[error("Schema generation command failed")]
17+
CommandFailed {
18+
/// The underlying handler error
19+
#[source]
20+
source: CreateSchemaCommandHandlerError,
21+
},
22+
23+
/// Progress reporter error
24+
#[error("Progress reporter error")]
25+
ProgressReporterFailed {
26+
/// The underlying progress reporter error
27+
#[source]
28+
source: ProgressReporterError,
29+
},
30+
}
31+
32+
impl From<ProgressReporterError> for CreateSchemaCommandError {
33+
fn from(source: ProgressReporterError) -> Self {
34+
Self::ProgressReporterFailed { source }
35+
}
36+
}
37+
38+
impl CreateSchemaCommandError {
39+
/// Returns actionable help text for resolving this error
40+
///
41+
/// Following the project's tiered help system pattern.
42+
#[must_use]
43+
pub fn help(&self) -> String {
44+
match self {
45+
Self::UserOutputLockFailed => "Failed to acquire user output lock.\n\
46+
\n\
47+
This is typically caused by internal concurrency issues.\n\
48+
\n\
49+
What to do:\n\
50+
1. Try running the command again\n\
51+
2. If the problem persists, report it as a bug"
52+
.to_string(),
53+
Self::CommandFailed { source } => {
54+
format!(
55+
"Schema generation command failed.\n\
56+
\n\
57+
{}\n\
58+
\n\
59+
If you need further assistance, check the documentation or report an issue.",
60+
source.help()
61+
)
62+
}
63+
Self::ProgressReporterFailed { .. } => "Progress reporting failed.\n\
64+
\n\
65+
This is an internal error with the progress display system.\n\
66+
\n\
67+
What to do:\n\
68+
1. The command may have still succeeded - check the output\n\
69+
2. If the problem persists, report it as a bug"
70+
.to_string(),
71+
}
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::*;
78+
79+
#[test]
80+
fn it_should_provide_help_text_for_user_output_lock_failed() {
81+
let error = CreateSchemaCommandError::UserOutputLockFailed;
82+
let help = error.help();
83+
assert!(help.contains("What to do:"));
84+
assert!(help.contains("concurrency"));
85+
}
86+
87+
#[test]
88+
fn it_should_provide_help_text_for_progress_reporter_failed() {
89+
let error = CreateSchemaCommandError::ProgressReporterFailed {
90+
source: ProgressReporterError::UserOutputMutexPoisoned,
91+
};
92+
let help = error.help();
93+
assert!(help.contains("What to do:"));
94+
}
95+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! Create Schema Command Controller (Presentation Layer)
2+
//!
3+
//! Handles the presentation layer concerns for JSON Schema generation,
4+
//! including user output and progress reporting.
5+
6+
use std::cell::RefCell;
7+
use std::path::PathBuf;
8+
use std::sync::Arc;
9+
10+
use parking_lot::ReentrantMutex;
11+
12+
use crate::application::command_handlers::create::schema::CreateSchemaCommandHandler;
13+
use crate::presentation::views::progress::ProgressReporter;
14+
use crate::presentation::views::UserOutput;
15+
16+
use super::errors::CreateSchemaCommandError;
17+
18+
/// Steps for schema generation workflow
19+
enum CreateSchemaStep {
20+
GenerateSchema,
21+
}
22+
23+
impl CreateSchemaStep {
24+
fn description(&self) -> &str {
25+
match self {
26+
Self::GenerateSchema => "Generating JSON Schema",
27+
}
28+
}
29+
30+
fn count() -> usize {
31+
1
32+
}
33+
}
34+
35+
/// Controller for create schema command
36+
///
37+
/// Handles the presentation layer for JSON Schema generation,
38+
/// coordinating between the command handler and user output.
39+
pub struct CreateSchemaCommandController {
40+
progress: ProgressReporter,
41+
}
42+
43+
impl CreateSchemaCommandController {
44+
/// Create a new schema generation command controller
45+
pub fn new(user_output: &Arc<ReentrantMutex<RefCell<UserOutput>>>) -> Self {
46+
let progress = ProgressReporter::new(user_output.clone(), CreateSchemaStep::count());
47+
48+
Self { progress }
49+
}
50+
51+
/// Execute the schema generation command
52+
///
53+
/// Generates JSON Schema and either writes to file or outputs to stdout.
54+
///
55+
/// # Arguments
56+
///
57+
/// * `output_path` - Optional path to write schema file. If `None`, outputs to stdout.
58+
///
59+
/// # Returns
60+
///
61+
/// Returns `Ok(())` on success, or error if generation or output fails.
62+
///
63+
/// # Errors
64+
///
65+
/// Returns error if:
66+
/// - Schema generation fails
67+
/// - File write fails (when path provided)
68+
/// - Stdout write fails (when no path provided)
69+
pub fn execute(
70+
&mut self,
71+
output_path: Option<&PathBuf>,
72+
) -> Result<(), CreateSchemaCommandError> {
73+
self.progress
74+
.start_step(CreateSchemaStep::GenerateSchema.description())?;
75+
76+
// Generate schema using application layer handler
77+
let schema = CreateSchemaCommandHandler::execute(output_path.cloned())
78+
.map_err(|source| CreateSchemaCommandError::CommandFailed { source })?;
79+
80+
// Handle output
81+
if output_path.is_some() {
82+
self.progress
83+
.complete_step(Some("Schema written to file successfully"))?;
84+
} else {
85+
// Output to stdout using ProgressReporter abstraction
86+
self.progress.complete_step(Some("Schema generated"))?;
87+
88+
// Write schema to stdout (result data goes to stdout, not stderr)
89+
self.progress.result(&schema)?;
90+
}
91+
92+
self.progress
93+
.complete("Schema generation completed successfully")?;
94+
95+
Ok(())
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
mod tests {
101+
use super::*;
102+
use crate::presentation::views::testing::test_user_output::TestUserOutput;
103+
use crate::presentation::views::VerbosityLevel;
104+
use tempfile::TempDir;
105+
106+
#[test]
107+
fn it_should_generate_schema_to_file_when_path_provided() {
108+
let temp_dir = TempDir::new().unwrap();
109+
let schema_path = temp_dir.path().join("schema.json");
110+
111+
let (user_output, _capture, _capture_stderr) =
112+
TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
113+
let mut controller = CreateSchemaCommandController::new(&user_output);
114+
115+
let result = controller.execute(Some(&schema_path));
116+
assert!(result.is_ok());
117+
118+
// Verify file was created
119+
assert!(schema_path.exists());
120+
121+
// Verify file contains valid JSON schema
122+
let content = std::fs::read_to_string(&schema_path).unwrap();
123+
assert!(content.contains("\"$schema\""));
124+
}
125+
126+
#[test]
127+
fn it_should_complete_progress_when_generating_schema() {
128+
let (user_output, _capture, _capture_stderr) =
129+
TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
130+
let mut controller = CreateSchemaCommandController::new(&user_output);
131+
132+
let temp_dir = TempDir::new().unwrap();
133+
let schema_path = temp_dir.path().join("test.json");
134+
135+
let result = controller.execute(Some(&schema_path));
136+
assert!(result.is_ok());
137+
}
138+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Schema Generation Subcommand
2+
//!
3+
//! This module handles the schema generation subcommand for creating
4+
//! JSON Schema files from configuration types.
5+
6+
pub mod errors;
7+
pub mod handler;
8+
9+
// Re-export the main handler and error types
10+
pub use errors::CreateSchemaCommandError;
11+
pub use handler::CreateSchemaCommandController;

src/presentation/controllers/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@
165165
//! .await
166166
//! .map_err(CreateCommandError::Template)
167167
//! }
168+
//! CreateAction::Schema { output_path } => {
169+
//! context
170+
//! .container()
171+
//! .create_schema_controller()
172+
//! .execute(output_path.as_ref())
173+
//! .map_err(CreateCommandError::Schema)
174+
//! }
168175
//! }
169176
//! }
170177
//! # }

src/presentation/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,5 @@ pub fn handle_error(error: &CommandError, user_output: &Arc<ReentrantMutex<RefCe
9696
let mut output = lock.borrow_mut();
9797
output.error(&format!("{error}"));
9898
output.blank_line();
99-
output.info_block("For detailed troubleshooting:", &[help_text]);
99+
output.info_block("For detailed troubleshooting:", &[&help_text]);
100100
}

0 commit comments

Comments
 (0)