miyabi_cli/
service.rs

1//! Command service trait layer
2//!
3//! This module defines the core trait for all CLI commands, enabling:
4//! - Consistent JSON output across all commands
5//! - Reusable command logic for agents and other entry points
6//! - Testable command execution with structured outputs
7
8use crate::error::Result;
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use std::fmt::Debug;
12
13/// Command service trait for all CLI commands
14///
15/// This trait provides a unified interface for command execution with support for:
16/// - Type-safe structured outputs
17/// - JSON serialization
18/// - Async execution
19/// - Error handling
20///
21/// # Example
22///
23/// ```rust
24/// use miyabi_cli::service::CommandService;
25/// use serde::Serialize;
26///
27/// #[derive(Debug, Serialize)]
28/// pub struct StatusOutput {
29///     pub is_installed: bool,
30///     pub git_branch: Option<String>,
31/// }
32///
33/// pub struct StatusCommand {
34///     pub watch: bool,
35/// }
36///
37/// #[async_trait]
38/// impl CommandService for StatusCommand {
39///     type Output = StatusOutput;
40///
41///     async fn execute(&self) -> Result<Self::Output> {
42///         // Command implementation
43///         Ok(StatusOutput {
44///             is_installed: true,
45///             git_branch: Some("main".to_string()),
46///         })
47///     }
48/// }
49/// ```
50#[async_trait]
51#[allow(dead_code)]
52pub trait CommandService: Send + Sync {
53    /// Output type for this command
54    ///
55    /// Must implement Serialize for JSON output and Debug for error reporting
56    type Output: Serialize + Debug + Send;
57
58    /// Execute the command and return structured output
59    ///
60    /// This method performs the core command logic and returns a structured result
61    /// that can be serialized to JSON or displayed to the user.
62    async fn execute(&self) -> Result<Self::Output>;
63
64    /// Convert output to JSON string
65    ///
66    /// Default implementation uses serde_json::to_string_pretty
67    fn to_json(&self, output: &Self::Output) -> Result<String> {
68        serde_json::to_string_pretty(output).map_err(|e| {
69            crate::error::CliError::Serialization(format!("Failed to serialize output: {}", e))
70        })
71    }
72
73    /// Execute command and optionally output as JSON
74    ///
75    /// This is a convenience method that combines execute() and to_json()
76    async fn execute_with_json(&self, json: bool) -> Result<String> {
77        let output = self.execute().await?;
78
79        if json {
80            self.to_json(&output)
81        } else {
82            // For non-JSON output, return debug representation
83            Ok(format!("{:?}", output))
84        }
85    }
86}
87
88/// Command output metadata
89///
90/// Common metadata fields that can be included in any command output
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[allow(dead_code)]
93pub struct CommandMetadata {
94    /// Command name
95    pub command: String,
96    /// Execution timestamp (ISO 8601)
97    pub timestamp: String,
98    /// Success status
99    pub success: bool,
100    /// Optional error message
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub error: Option<String>,
103}
104
105#[allow(dead_code)]
106impl CommandMetadata {
107    /// Create new command metadata
108    pub fn new(command: impl Into<String>, success: bool) -> Self {
109        Self {
110            command: command.into(),
111            timestamp: chrono::Utc::now().to_rfc3339(),
112            success,
113            error: None,
114        }
115    }
116
117    /// Create metadata with error
118    pub fn with_error(command: impl Into<String>, error: impl Into<String>) -> Self {
119        Self {
120            command: command.into(),
121            timestamp: chrono::Utc::now().to_rfc3339(),
122            success: false,
123            error: Some(error.into()),
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[derive(Debug, Serialize)]
133    struct TestOutput {
134        message: String,
135    }
136
137    struct TestCommand;
138
139    #[async_trait]
140    impl CommandService for TestCommand {
141        type Output = TestOutput;
142
143        async fn execute(&self) -> Result<Self::Output> {
144            Ok(TestOutput {
145                message: "test".to_string(),
146            })
147        }
148    }
149
150    #[tokio::test]
151    async fn test_command_service_execute() {
152        let cmd = TestCommand;
153        let output = cmd.execute().await.unwrap();
154        assert_eq!(output.message, "test");
155    }
156
157    #[tokio::test]
158    async fn test_command_service_to_json() {
159        let cmd = TestCommand;
160        let output = cmd.execute().await.unwrap();
161        let json = cmd.to_json(&output).unwrap();
162        assert!(json.contains("\"message\""));
163        assert!(json.contains("\"test\""));
164    }
165
166    #[tokio::test]
167    async fn test_command_service_execute_with_json() {
168        let cmd = TestCommand;
169        let json_output = cmd.execute_with_json(true).await.unwrap();
170        assert!(json_output.contains("\"message\""));
171    }
172
173    #[test]
174    fn test_command_metadata_creation() {
175        let metadata = CommandMetadata::new("test", true);
176        assert_eq!(metadata.command, "test");
177        assert!(metadata.success);
178        assert!(metadata.error.is_none());
179    }
180
181    #[test]
182    fn test_command_metadata_with_error() {
183        let metadata = CommandMetadata::with_error("test", "test error");
184        assert_eq!(metadata.command, "test");
185        assert!(!metadata.success);
186        assert_eq!(metadata.error, Some("test error".to_string()));
187    }
188}