spikard_cli/init/
rust_lang.rs1use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
7use anyhow::Result;
8use std::path::{Path, PathBuf};
9
10pub 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 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(), ®istry).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}