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