1pub fn validate_crate_name(name: &str) -> Result<(), String> {
9 if name.is_empty() {
10 return Err("name must not be empty".into());
11 }
12 if name.contains(['/', '\\', '"', '\'', '\n', '\r']) {
13 return Err("name must not contain path separators, quotes or newlines".into());
14 }
15 let valid = name.char_indices().all(|(i, c)| {
16 (i == 0 && c.is_ascii_lowercase())
17 || (i > 0 && (c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-'))
18 });
19 if !valid {
20 return Err("name must match ^[a-z][a-z0-9_-]*$ (start with a lowercase letter)".into());
21 }
22 Ok(())
23}
24
25pub fn generate_cargo_toml(name: &str) -> String {
26 let ecat_version = env!("CARGO_PKG_VERSION");
27 format!(
28 r#"[package]
29name = "{name}"
30version = "0.1.0"
31edition = "2024"
32
33[dependencies]
34ecat = "{ecat_version}"
35ecat-transport-http = "{ecat_version}"
36ecat-middleware = "{ecat_version}"
37ecat-logging = "{ecat_version}"
38tokio = {{ version = "1", features = ["full"] }}
39tracing = "0.1"
40axum = "0.8"
41serde = {{ version = "1", features = ["derive"] }}
42serde_json = "1"
43"#
44 )
45}
46
47pub fn generate_main_rs() -> &'static str {
48 r#"use axum::{routing::get, Json, Router};
49use ecat::App;
50use ecat_middleware::{LoggingLayer, TracingLayer};
51use ecat_transport_http::HttpServer;
52use serde::Serialize;
53use tower::ServiceBuilder;
54
55#[derive(Serialize)]
56struct HealthResponse {
57 status: &'static str,
58}
59
60async fn health() -> Json<HealthResponse> {
61 Json(HealthResponse { status: "ok" })
62}
63
64#[tokio::main]
65async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
66 let middleware = ServiceBuilder::new()
67 .layer(TracingLayer::new(env!("CARGO_PKG_NAME")))
68 .layer(LoggingLayer);
69
70 let router = Router::new()
71 .route("/health", get(health))
72 .layer(middleware);
73
74 let http_srv = HttpServer::new("0.0.0.0:8000").router(router);
75
76 let mut app = App::builder()
77 .name(env!("CARGO_PKG_NAME"))
78 .version(env!("CARGO_PKG_VERSION"))
79 .server(http_srv)
80 .build()?;
81
82 app.run().await?;
83 Ok(())
84}
85"#
86}
87
88pub fn generate_proto_file() -> &'static str {
89 r#"syntax = "proto3";
90package service;
91
92service Service {
93 rpc Health(HealthRequest) returns (HealthResponse);
94}
95
96message HealthRequest {}
97message HealthResponse {
98 string status = 1;
99}
100"#
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn cargo_toml_contains_package_name() {
109 let toml = generate_cargo_toml("myapp");
110 assert!(toml.contains("name = \"myapp\""));
111 assert!(toml.contains("edition = \"2024\""));
112 }
113
114 #[test]
115 fn cargo_toml_includes_dependencies() {
116 let toml = generate_cargo_toml("test-app");
117 let ecat_version = env!("CARGO_PKG_VERSION");
118 assert!(toml.contains(&format!("ecat = \"{ecat_version}\"")));
119 assert!(toml.contains(&format!("ecat-transport-http = \"{ecat_version}\"")));
120 assert!(toml.contains("axum = \"0.8\""));
121 assert!(toml.contains("tokio"));
122 }
123
124 #[test]
125 fn cargo_toml_uses_current_ecat_version() {
126 let toml = generate_cargo_toml("test-app");
127 let ecat_version = env!("CARGO_PKG_VERSION");
128 assert!(!toml.contains("ecat = \"1.0\""));
129 assert!(toml.contains(&format!("ecat-logging = \"{ecat_version}\"")));
130 }
131
132 #[test]
133 fn main_rs_contains_health_endpoint() {
134 let src = generate_main_rs();
135 assert!(src.contains("/health"));
136 assert!(src.contains("HealthResponse"));
137 assert!(src.contains("HttpServer::new"));
138 }
139
140 #[test]
141 fn main_rs_uses_middleware() {
142 let src = generate_main_rs();
143 assert!(src.contains("TracingLayer"));
144 assert!(src.contains("LoggingLayer"));
145 }
146
147 #[test]
148 fn proto_file_has_service_definition() {
149 let proto = generate_proto_file();
150 assert!(proto.contains("service Service"));
151 assert!(proto.contains("HealthRequest"));
152 assert!(proto.contains("HealthResponse"));
153 assert!(proto.contains("syntax = \"proto3\""));
154 }
155
156 #[test]
157 fn proto_file_valid_syntax() {
158 let proto = generate_proto_file();
159 assert!(proto.starts_with("syntax = \"proto3\";"));
160 assert!(proto.contains("message HealthRequest {}"));
161 }
162
163 #[test]
164 fn cargo_toml_special_chars_in_name() {
165 let toml = generate_cargo_toml("my_app-123");
166 assert!(toml.contains("name = \"my_app-123\""));
167 }
168
169 #[test]
170 fn validate_crate_name_accepts_valid_names() {
171 assert!(validate_crate_name("myapp").is_ok());
172 assert!(validate_crate_name("my-app-123").is_ok());
173 assert!(validate_crate_name("my_app").is_ok());
174 }
175
176 #[test]
177 fn validate_crate_name_rejects_path_traversal() {
178 assert!(validate_crate_name("../../x").is_err());
179 assert!(validate_crate_name("a/b").is_err());
180 assert!(validate_crate_name("a\\b").is_err());
181 }
182
183 #[test]
184 fn validate_crate_name_rejects_injection_chars() {
185 assert!(validate_crate_name("a\"b").is_err());
186 assert!(validate_crate_name("a'b").is_err());
187 assert!(validate_crate_name("a\nb").is_err());
188 assert!(validate_crate_name("a\rb").is_err());
189 }
190
191 #[test]
192 fn validate_crate_name_rejects_bad_patterns() {
193 assert!(validate_crate_name("").is_err());
194 assert!(validate_crate_name("1app").is_err());
195 assert!(validate_crate_name("App").is_err());
196 assert!(validate_crate_name("app name").is_err());
197 assert!(validate_crate_name("app.name").is_err());
198 }
199}