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(
22 PathBuf::from("Cargo.toml"),
23 self.generate_cargo_toml(&kebab_name),
24 ));
25
26 files.push(ScaffoldedFile::new(
28 PathBuf::from("src/main.rs"),
29 self.generate_main_rs(),
30 ));
31
32 files.push(ScaffoldedFile::new(PathBuf::from("src/lib.rs"), self.generate_lib_rs()));
34
35 files.push(ScaffoldedFile::new(
37 PathBuf::from("tests/integration_test.rs"),
38 self.generate_integration_test(&crate_name),
39 ));
40
41 files.push(ScaffoldedFile::new(
43 PathBuf::from(".gitignore"),
44 self.generate_gitignore(),
45 ));
46
47 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 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(), ®istry).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}