Skip to main content

spikard_cli/init/
rust_lang.rs

1//! Rust Project Scaffolder
2//!
3//! Generates a minimal Rust project structure with Spikard integration.
4//! Creates both library and binary targets with integration tests.
5
6use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
7use anyhow::Result;
8use std::path::{Path, PathBuf};
9
10/// Rust project scaffolder
11pub struct RustScaffolder;
12
13impl ProjectScaffolder for RustScaffolder {
14    #[allow(clippy::vec_init_then_push)]
15    fn scaffold(&self, _project_dir: &Path, project_name: &str) -> Result<Vec<ScaffoldedFile>> {
16        let kebab_name = Self::to_kebab_case(project_name);
17        let crate_name = kebab_name.replace('-', "_");
18        let mut files = Vec::new();
19
20        // Create Cargo.toml
21        files.push(ScaffoldedFile::new(
22            PathBuf::from("Cargo.toml"),
23            self.generate_cargo_toml(&kebab_name),
24        ));
25
26        // Create src/main.rs
27        files.push(ScaffoldedFile::new(
28            PathBuf::from("src/main.rs"),
29            self.generate_main_rs(),
30        ));
31
32        // Create src/lib.rs
33        files.push(ScaffoldedFile::new(PathBuf::from("src/lib.rs"), self.generate_lib_rs()));
34
35        // Create tests/integration_test.rs
36        files.push(ScaffoldedFile::new(
37            PathBuf::from("tests/integration_test.rs"),
38            self.generate_integration_test(&crate_name),
39        ));
40
41        // Create .gitignore
42        files.push(ScaffoldedFile::new(
43            PathBuf::from(".gitignore"),
44            self.generate_gitignore(),
45        ));
46
47        // Create README.md
48        files.push(ScaffoldedFile::new(
49            PathBuf::from("README.md"),
50            self.generate_readme(project_name, &kebab_name),
51        ));
52
53        Ok(files)
54    }
55
56    fn next_steps(&self, project_name: &str) -> Vec<String> {
57        vec![
58            format!("cd {}", project_name),
59            "cargo test".to_string(),
60            "cargo run".to_string(),
61        ]
62    }
63}
64
65impl RustScaffolder {
66    /// Convert a project name to kebab-case (for crate names)
67    fn to_kebab_case(name: &str) -> String {
68        name.chars()
69            .map(|c| {
70                if c.is_uppercase() {
71                    format!("-{}", c.to_lowercase())
72                } else if c == '_' {
73                    "-".to_string()
74                } else {
75                    c.to_string()
76                }
77            })
78            .collect::<String>()
79            .trim_start_matches('-')
80            .to_string()
81    }
82
83    fn generate_cargo_toml(&self, kebab_name: &str) -> String {
84        let version = env!("CARGO_PKG_VERSION");
85        format!(
86            r#"[package]
87name = "{kebab_name}"
88version = "0.1.0"
89edition = "2024"
90rust-version = "1.85"
91authors = ["Your Name <you@example.com>"]
92license = "MIT"
93description = "A Spikard-powered HTTP application"
94repository = "https://github.com/yourusername/{kebab_name}"
95
96[dependencies]
97spikard-http = "{version}"
98tokio = {{ version = "1", features = ["full"] }}
99serde = {{ version = "1", features = ["derive"] }}
100serde_json = "1"
101tracing = "0.1"
102tracing-subscriber = "0.3"
103
104[dev-dependencies]
105"#
106        )
107    }
108
109    fn generate_main_rs(&self) -> String {
110        r#"//! Main HTTP server entry point
111//!
112//! This is the binary target for running the Spikard HTTP server.
113
114use serde_json::json;
115use spikard_http::{Handler, Route, RouteMetadata, SchemaRegistry, Server, ServerConfig, StaticResponseHandler};
116use std::sync::Arc;
117
118#[tokio::main]
119async fn main() -> Result<(), Box<dyn std::error::Error>> {
120    // Initialize tracing for logging
121    tracing_subscriber::fmt()
122        .with_max_level(tracing::Level::INFO)
123        .init();
124
125    // Register a health route (GET /) with a static response.
126    let route_metadata: RouteMetadata = serde_json::from_value(json!({
127        "method": "GET",
128        "path": "/",
129        "handler_name": "health",
130        "is_async": false
131    }))?;
132    let registry = SchemaRegistry::new();
133    let route = Route::from_metadata(route_metadata.clone(), &registry).map_err(std::io::Error::other)?;
134    let handler = Arc::new(StaticResponseHandler::from_parts(
135        200,
136        "{\"status\":\"healthy\",\"message\":\"Server is running\"}",
137        Some("application/json"),
138        vec![],
139    )) as Arc<dyn Handler>;
140
141    // Create server configuration
142    let config = ServerConfig::builder()
143        .host("127.0.0.1")
144        .port(8000)
145        .enable_http_trace(true)
146        .build();
147
148    let app = Server::with_handlers_and_metadata(config.clone(), vec![(route, handler)], vec![route_metadata])
149        .map_err(std::io::Error::other)?;
150
151    println!("Server starting on http://127.0.0.1:8000");
152    println!("Press Ctrl+C to stop");
153
154    Server::run_with_config(app, config).await?;
155
156    Ok(())
157}
158"#
159        .to_string()
160    }
161
162    fn generate_lib_rs(&self) -> String {
163        r#"//! Spikard HTTP Application Library
164//!
165//! This library contains the core logic for the HTTP application.
166//! The binary in `main.rs` uses this library to run the server.
167
168/// Health check handler
169///
170/// Returns a simple JSON response indicating the server is healthy.
171pub async fn health_handler() -> Result<serde_json::Value, Box<dyn std::error::Error>> {
172    Ok(serde_json::json!({
173        "status": "healthy",
174        "message": "Server is running"
175    }))
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[tokio::test]
183    async fn test_health_handler() {
184        let result = health_handler().await;
185        assert!(result.is_ok());
186
187        let value = result.unwrap();
188        assert_eq!(value["status"], "healthy");
189    }
190}
191"#
192        .to_string()
193    }
194
195    fn generate_integration_test(&self, crate_name: &str) -> String {
196        format!(
197            r#"//! Integration tests
198//!
199//! Tests that verify the HTTP server and handlers work correctly.
200
201use {crate_name}::health_handler;
202
203#[tokio::test]
204async fn test_health_handler_returns_expected_payload() {{
205    let response = health_handler().await.expect("health handler should succeed");
206    assert_eq!(response["status"], "healthy");
207    assert_eq!(response["message"], "Server is running");
208}}
209"#
210        )
211    }
212
213    fn generate_gitignore(&self) -> String {
214        r"# Rust build artifacts
215/target/
216
217# IDE
218.vscode/
219.idea/
220*.swp
221*.swo
222*~
223*.rs.bk
224
225# Environment
226.env
227.env.local
228
229# OS
230.DS_Store
231Thumbs.db
232
233# Testing
234*.profdata
235"
236        .to_string()
237    }
238
239    fn generate_readme(&self, project_name: &str, kebab_name: &str) -> String {
240        format!(
241            r"# {project_name}
242
243A Rust HTTP server powered by Spikard.
244
245## Requirements
246
247- Rust 1.75+
248
249## Getting Started
250
251### Build
252
253```bash
254cargo build --release
255```
256
257### Run
258
259```bash
260cargo run
261```
262
263The server will start on `http://127.0.0.1:8000`.
264
265### Test
266
267```bash
268cargo test
269```
270
271## Running the Binary
272
273```bash
274cargo run --release
275```
276
277## Project Structure
278
279```
280{kebab_name}/
281├── src/
282│   ├── main.rs      # Binary entry point
283│   └── lib.rs       # Library code
284├── tests/
285│   └── integration_test.rs
286├── Cargo.toml       # Project manifest
287└── README.md
288```
289
290## Development
291
292### Format Code
293
294```bash
295cargo fmt
296```
297
298### Lint
299
300```bash
301cargo clippy -- -D warnings
302```
303
304## Next Steps
305
3061. Update `src/main.rs` to define your HTTP handlers
3072. Implement logic in `src/lib.rs`
3083. Add tests in `tests/integration_test.rs`
3094. Build and run with `cargo run`
310
311## Documentation
312
313- [Spikard Documentation](https://spikard.dev)
314- [Rust Book](https://doc.rust-lang.org/book/)
315- [Tokio Documentation](https://tokio.rs/)
316"
317        )
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_to_kebab_case() {
327        assert_eq!(RustScaffolder::to_kebab_case("MyProject"), "my-project");
328        assert_eq!(RustScaffolder::to_kebab_case("my_project"), "my-project");
329        assert_eq!(RustScaffolder::to_kebab_case("myProject"), "my-project");
330        assert_eq!(RustScaffolder::to_kebab_case("my-project"), "my-project");
331    }
332
333    #[test]
334    fn test_rust_scaffolder_generates_cargo_toml() {
335        let scaffolder = RustScaffolder;
336        let content = scaffolder.generate_cargo_toml("my-project");
337
338        assert!(content.contains("name = \"my-project\""));
339        assert!(content.contains("edition = \"2024\""));
340        assert!(content.contains("spikard-http"));
341        assert!(content.contains("tokio"));
342    }
343
344    #[test]
345    fn test_rust_scaffolder_generates_main_rs() {
346        let scaffolder = RustScaffolder;
347        let content = scaffolder.generate_main_rs();
348
349        assert!(content.contains("#[tokio::main]"));
350        assert!(content.contains("async fn main()"));
351        assert!(content.contains("Server::with_handlers_and_metadata"));
352        assert!(content.contains("127.0.0.1"));
353        assert!(content.contains("8000"));
354    }
355
356    #[test]
357    fn test_rust_scaffolder_generates_lib_rs() {
358        let scaffolder = RustScaffolder;
359        let content = scaffolder.generate_lib_rs();
360
361        assert!(content.contains("health_handler"));
362        assert!(content.contains("async fn health_handler"));
363        assert!(content.contains("#[tokio::test]"));
364    }
365
366    #[test]
367    fn test_rust_scaffolder_next_steps() {
368        let scaffolder = RustScaffolder;
369        let steps = scaffolder.next_steps("my-project");
370
371        assert_eq!(steps.len(), 3);
372        assert!(steps[0].contains("cd my-project"));
373        assert_eq!(steps[1], "cargo test");
374        assert_eq!(steps[2], "cargo run");
375    }
376}