Skip to main content

spikard_cli/init/
scaffolder.rs

1//! Project scaffolding traits and structures for language-specific setup.
2//!
3//! This module defines the contract for scaffolding new Spikard projects
4//! in a language-agnostic way, allowing different implementations for
5//! Python, TypeScript, Rust, Ruby, and PHP.
6
7use std::path::PathBuf;
8
9/// A file that will be created as part of project scaffolding.
10///
11/// # Fields
12///
13/// - `path`: Relative or absolute path where the file should be written
14/// - `content`: The complete text content of the file
15#[derive(Debug, Clone)]
16pub struct ScaffoldedFile {
17    /// Path where the file should be written
18    pub path: PathBuf,
19    /// Complete content of the file
20    pub content: String,
21}
22
23impl ScaffoldedFile {
24    /// Create a new scaffolded file.
25    ///
26    /// # Arguments
27    ///
28    /// - `path`: The target path for this file
29    /// - `content`: The file content as a string
30    ///
31    /// # Example
32    ///
33    /// ```
34    /// use spikard_cli::init::ScaffoldedFile;
35    /// use std::path::PathBuf;
36    ///
37    /// let file = ScaffoldedFile::new(
38    ///     PathBuf::from("src/main.py"),
39    ///     "print('Hello, world!')".to_string(),
40    /// );
41    ///
42    /// assert_eq!(file.path, PathBuf::from("src/main.py"));
43    /// assert!(file.content.contains("Hello"));
44    /// ```
45    #[must_use]
46    pub const fn new(path: PathBuf, content: String) -> Self {
47        Self { path, content }
48    }
49}
50
51/// Language-agnostic trait for scaffolding new Spikard projects.
52///
53/// Implementations define how to create the initial project structure,
54/// configuration files, and example handlers for a specific language.
55///
56/// # Design Philosophy
57///
58/// - **Zero-cost abstraction**: Trait implementations are compiled inline
59/// - **Composability**: Each method is independently callable for flexibility
60/// - **Clarity**: Method names and signatures are self-documenting
61/// - **Extensibility**: New methods can be added without breaking existing implementations
62///
63/// # Example
64///
65/// ```ignore
66/// use spikard_cli::init::{ProjectScaffolder, ScaffoldedFile};
67/// use std::path::PathBuf;
68///
69/// struct MyScaffolder;
70///
71/// impl ProjectScaffolder for MyScaffolder {
72///     fn scaffold(
73///         &self,
74///         project_dir: &std::path::Path,
75///         project_name: &str,
76///     ) -> anyhow::Result<Vec<ScaffoldedFile>> {
77///         let mut files = vec![];
78///         files.push(ScaffoldedFile::new(
79///             PathBuf::from("pyproject.toml"),
80///             format!("[project]\nname = \"{}\"", project_name),
81///         ));
82///         Ok(files)
83///     }
84///
85///     fn next_steps(&self, _project_name: &str) -> Vec<String> {
86///         vec!["cd my_api".to_string()]
87///     }
88/// }
89/// ```
90pub trait ProjectScaffolder {
91    /// Scaffold a new project with language-idiomatic structure.
92    ///
93    /// This method is responsible for generating all files needed for a new
94    /// Spikard project in the target language. The returned files will be
95    /// written to disk by the caller.
96    ///
97    /// # Arguments
98    ///
99    /// - `project_dir`: The root directory where the project will be created
100    /// - `project_name`: The name of the project (used for package names, module names, etc.)
101    ///
102    /// # Returns
103    ///
104    /// A vector of `ScaffoldedFile` instances representing all files to be created.
105    /// The order of files is not guaranteed to be preserved on disk.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if scaffolding fails for any reason (e.g., invalid project name,
110    /// I/O errors, or validation failures).
111    ///
112    /// # Example
113    ///
114    /// ```ignore
115    /// let files = scaffolder.scaffold(Path::new("."), "my_api")?;
116    /// // files might contain:
117    /// // - pyproject.toml
118    /// // - src/main.py
119    /// // - examples/basic_handler.py
120    /// // - tests/test_handlers.py
121    /// // - README.md
122    /// # Ok::<(), anyhow::Error>(())
123    /// ```
124    fn scaffold(&self, project_dir: &std::path::Path, project_name: &str) -> anyhow::Result<Vec<ScaffoldedFile>>;
125
126    /// Return next steps messages for the user after scaffolding completes.
127    ///
128    /// These messages guide the user through initial setup steps like
129    /// installing dependencies, running tests, or starting the server.
130    ///
131    /// # Arguments
132    ///
133    /// - `project_name`: The name of the project that was scaffolded
134    ///
135    /// # Returns
136    ///
137    /// A vector of human-readable instruction strings that should be
138    /// displayed to the user after successful project creation.
139    ///
140    /// # Example
141    ///
142    /// ```ignore
143    /// let steps = scaffolder.next_steps("my_api");
144    /// // steps might return:
145    /// // [
146    /// //   "cd my_api",
147    /// //   "python -m venv venv",
148    /// //   ". venv/bin/activate",
149    /// //   "pip install -e .",
150    /// //   "python -m pytest",
151    /// // ]
152    /// ```
153    fn next_steps(&self, project_name: &str) -> Vec<String>;
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_scaffolded_file_creation() {
162        let file = ScaffoldedFile::new(PathBuf::from("src/main.py"), "print('hello')".to_string());
163        assert_eq!(file.path, PathBuf::from("src/main.py"));
164        assert_eq!(file.content, "print('hello')");
165    }
166
167    #[test]
168    fn test_scaffolded_file_clone() {
169        let file1 = ScaffoldedFile::new(PathBuf::from("test.txt"), "content".to_string());
170        let file2 = file1.clone();
171        assert_eq!(file1.path, file2.path);
172        assert_eq!(file1.content, file2.content);
173    }
174}