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}