camel_cli/commands/
openapi.rs1use std::path::Path;
5
6use clap::{Args, Subcommand};
7use serde_json::Value;
8
9use camel_dsl::openapi::generate_openapi;
10use camel_dsl::yaml::extract_rest_blocks;
11
12#[derive(Subcommand, Debug)]
13pub enum OpenapiAction {
14 Generate(OpenapiGenerateArgs),
16}
17
18#[derive(Args, Debug)]
19pub struct OpenapiGenerateArgs {
20 pub file: String,
22
23 #[arg(long, default_value = "Generated API")]
25 pub title: String,
26
27 #[arg(long, default_value = "1.0.0")]
29 pub version: String,
30}
31
32pub fn run_generate(args: &OpenapiGenerateArgs) -> Result<Value, String> {
38 let content = std::fs::read_to_string(&args.file)
39 .map_err(|e| format!("failed to read '{}': {e}", args.file))?;
40
41 let extension = Path::new(&args.file)
42 .extension()
43 .and_then(|ext| ext.to_str())
44 .unwrap_or("");
45
46 let rest_blocks = match extension {
47 "yaml" | "yml" => {
48 extract_rest_blocks(&content).map_err(|e| format!("YAML parse error: {e}"))?
49 }
50 "json" => {
51 let dsl: camel_dsl::route_ast::RouteDslRoutes =
52 serde_json::from_str(&content).map_err(|e| format!("JSON parse error: {e}"))?;
53 dsl.rest
54 }
55 _ => extract_rest_blocks(&content).map_err(|e| format!("parse error (tried YAML): {e}"))?,
56 };
57
58 if rest_blocks.is_empty() {
59 return Err("no 'rest:' blocks found in file".to_string());
60 }
61
62 let lowered = camel_dsl::rest::lower_all_rest_to_routes(&rest_blocks)
64 .map_err(|e| format!("validation error: {e}"))?;
65 camel_dsl::rest::check_duplicate_route_ids(&lowered)
66 .map_err(|e| format!("validation error: {e}"))?;
67
68 let result = generate_openapi(&rest_blocks, &args.title, &args.version);
69
70 for warning in &result.warnings {
71 eprintln!("warning: {warning}");
72 }
73
74 Ok(result.document)
75}
76
77pub fn run(action: OpenapiAction) {
79 match action {
80 OpenapiAction::Generate(args) => match run_generate(&args) {
81 Ok(doc) => {
82 let pretty =
83 serde_json::to_string_pretty(&doc).unwrap_or_else(|_| "{}".to_string()); println!("{pretty}");
85 }
86 Err(e) => {
87 eprintln!("error: {e}");
88 std::process::exit(1);
89 }
90 },
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::io::Write;
98
99 #[test]
100 fn generate_from_yaml_file() {
101 let yaml = r#"
102rest:
103 - host: 0.0.0.0
104 port: 9090
105 path: /api/users
106 operations:
107 - method: GET
108 operation_id: listUsers
109 to: direct:listUsers
110 - method: POST
111 operation_id: createUser
112 consumes: application/json
113 produces: application/json
114 success_status: 201
115 to: direct:createUser
116 request_schema:
117 type: object
118 properties:
119 name:
120 type: string
121 required: [name]
122"#;
123 let mut tmp = tempfile::NamedTempFile::new().unwrap(); write!(tmp, "{yaml}").unwrap(); let args = OpenapiGenerateArgs {
127 file: tmp.path().to_string_lossy().to_string(),
128 title: "Test API".to_string(),
129 version: "0.1.0".to_string(),
130 };
131
132 let doc = run_generate(&args).expect("generation should succeed");
133 assert_eq!(doc["openapi"], "3.0.3");
134 assert_eq!(doc["info"]["title"], "Test API");
135 assert_eq!(doc["info"]["version"], "0.1.0");
136
137 let paths = &doc["paths"];
138 assert!(paths["/api/users"].get("get").is_some());
139 assert!(paths["/api/users"].get("post").is_some());
140
141 let post_schema =
143 &paths["/api/users"]["post"]["requestBody"]["content"]["application/json"]["schema"];
144 assert_eq!(post_schema["properties"]["name"]["type"], "string");
145 }
146
147 #[test]
148 fn generate_from_json_file() {
149 let json = r#"{"rest":[{"host":"0.0.0.0","port":9090,"path":"/api/users","operations":[{"method":"GET","operation_id":"listUsers","to":"direct:listUsers"}]}]}"#;
150 let mut tmp = tempfile::NamedTempFile::with_suffix(".json").unwrap(); write!(tmp, "{json}").unwrap(); let args = OpenapiGenerateArgs {
154 file: tmp.path().to_string_lossy().to_string(),
155 title: "Test API".to_string(),
156 version: "1.0.0".to_string(),
157 };
158
159 let doc = run_generate(&args).expect("generation should succeed");
160 assert_eq!(doc["openapi"], "3.0.3");
161 assert_eq!(
162 doc["paths"]["/api/users"]["get"]["operationId"],
163 "listUsers"
164 );
165 }
166
167 #[test]
168 fn error_when_no_rest_blocks() {
169 let yaml = r#"
170routes:
171 - id: myRoute
172 from: timer:tick
173 steps:
174 - to: log:info
175"#;
176 let mut tmp = tempfile::NamedTempFile::new().unwrap(); write!(tmp, "{yaml}").unwrap(); let args = OpenapiGenerateArgs {
180 file: tmp.path().to_string_lossy().to_string(),
181 title: "API".to_string(),
182 version: "1.0.0".to_string(),
183 };
184
185 let result = run_generate(&args);
186 assert!(result.is_err());
187 assert!(result.unwrap_err().contains("no 'rest:' blocks")); }
189
190 #[test]
191 fn validation_error_on_duplicate_path_verb() {
192 let yaml = r#"
193rest:
194 - host: 0.0.0.0
195 port: 9090
196 path: /api/users
197 operations:
198 - method: GET
199 operation_id: listUsers
200 to: direct:listUsers
201 - host: 0.0.0.0
202 port: 9090
203 path: /api/users
204 operations:
205 - method: GET
206 operation_id: listUsers2
207 to: direct:listUsers
208"#;
209 let mut tmp = tempfile::NamedTempFile::new().unwrap(); write!(tmp, "{yaml}").unwrap(); let args = OpenapiGenerateArgs {
213 file: tmp.path().to_string_lossy().to_string(),
214 title: "API".to_string(),
215 version: "1.0.0".to_string(),
216 };
217
218 let result = run_generate(&args);
219 assert!(result.is_err());
220 assert!(result.unwrap_err().contains("validation error"));
221 }
222
223 #[test]
224 fn error_when_file_not_found() {
225 let args = OpenapiGenerateArgs {
226 file: "/nonexistent/file.yaml".to_string(),
227 title: "API".to_string(),
228 version: "1.0.0".to_string(),
229 };
230 let result = run_generate(&args);
231 assert!(result.is_err());
232 }
233}