1mod operation;
2mod parameter;
3mod property;
4mod templates;
5
6use std::{cell::RefCell, collections::HashSet, ops::Deref};
7
8use operation::Operation;
9use parameter::Parameter;
10use property::Property;
11use templates::*;
12
13use itertools::Itertools;
14use ramhorns::Template;
15use ramhorns_derive::Content;
16
17use log::{debug, trace};
18
19#[derive(Debug)]
21pub struct OpenApiV3 {
22 api: openapiv3::OpenAPI,
23
24 output_path: std::path::PathBuf,
25 package_info: PackageInfo,
26
27 api_template: Vec<ApiTemplateFile>,
28 model_templates: Vec<ModelTemplateFile>,
29 supporting_templates: Vec<SuppTemplateFile>,
30
31 suppress_errors: bool,
32 circ_ref_checker: RefCell<CircularRefChecker>,
33}
34impl OpenApiV3 {
35 pub fn new(
37 api: openapiv3::OpenAPI,
38 tpl_path: Option<std::path::PathBuf>,
39 output_path: Option<std::path::PathBuf>,
40 package_info: PackageInfo,
41 ) -> Result<Self, std::io::Error> {
42 let output_path = output_path.unwrap_or_else(|| std::path::Path::new(".").to_path_buf());
43 let (api_template, model_templates, supporting_templates) =
44 templates::default_templates(&tpl_path)?;
45 Ok(Self {
46 api,
47 output_path,
48 package_info,
49 api_template,
50 model_templates,
51 supporting_templates,
52 suppress_errors: false,
53 circ_ref_checker: RefCell::new(CircularRefChecker::default()),
54 })
55 }
56}
57
58#[derive(Debug)]
59pub struct PackageInfo {
60 pub name: String,
61 pub version: String,
62 pub libname: String,
63 pub edition: String,
64}
65
66#[derive(Clone, Content)]
67struct ApiInfoTpl<'a> {
68 apis: &'a Vec<OperationsApiTpl<'a>>,
69}
70#[derive(Clone, Content)]
71#[ramhorns(rename_all = "camelCase")]
72struct SupportingTpl<'a> {
73 api_info: ApiInfoTpl<'a>,
74 operations: OperationsTpl<'a>,
75 models: ModelTpl<'a>,
76 package_name: &'a str,
77 package_version: &'a str,
78 package_libname: &'a str,
79 package_edition: &'a str,
80 api_doc_path: &'a str,
82 model_doc_path: &'a str,
83}
84#[derive(Clone, Content)]
85#[ramhorns(rename_all = "camelCase")]
86struct ModelsTpl<'a> {
87 models: ModelTpl<'a>,
88}
89#[derive(Clone, Content)]
90struct ModelTpl<'a> {
91 model: &'a Vec<Property>,
92}
93
94#[derive(Content, Debug, Clone)]
95struct OperationsTpl<'a> {
96 operation: &'a Vec<Operation>,
97}
98
99#[derive(Content, Clone, Debug)]
100#[ramhorns(rename_all = "camelCase")]
101pub(super) struct OperationsApiTpl<'a> {
102 classname: &'a str,
103 class_filename: &'a str,
104 has_auth_methods: bool,
105
106 operations: OperationsTpl<'a>,
107}
108pub(super) struct OperationsApi {
109 classname: String,
110 class_filename: String,
111 has_auth_methods: bool,
112
113 operations: Vec<Operation>,
114}
115
116impl OpenApiV3 {
117 pub fn run(&self, models: bool, ops: bool) -> Result<(), std::io::Error> {
119 let models = if models { self.models()? } else { vec![] };
120 let operations = if ops { self.operations()? } else { vec![] };
121 let apis = self.apis(&operations)?;
122 let apis = apis
123 .iter()
124 .map(|o| OperationsApiTpl {
125 classname: o.classname(),
126 class_filename: o.class_filename(),
127 has_auth_methods: o.has_auth_methods,
128 operations: OperationsTpl {
129 operation: &o.operations,
130 },
131 })
132 .collect::<Vec<_>>();
133
134 self.ensure_templates()?;
135
136 self.render_supporting(&models, &operations, &apis)?;
137 self.render_models(&models)?;
138 self.render_apis(&apis)?;
139
140 Ok(())
141 }
142 fn ensure_templates(&self) -> Result<(), std::io::Error> {
143 Self::ensure_path(&self.output_path, true)?;
144 let templates = self
145 .supporting_templates
146 .iter()
147 .map(Deref::deref)
148 .chain(self.api_template.iter().map(Deref::deref))
149 .chain(self.model_templates.iter().map(Deref::deref))
150 .collect::<Vec<_>>();
151 self.ensure_template(&templates)
152 }
153 fn ensure_template_path(
154 &self,
155 path: &std::path::Path,
156 clean: bool,
157 ) -> Result<(), std::io::Error> {
158 let path = self.output_path.join(path);
159 Self::ensure_path(&path, clean)
160 }
161 fn ensure_path(path: &std::path::Path, clean: bool) -> Result<(), std::io::Error> {
162 if clean && path.exists() {
163 if path.is_dir() {
164 std::fs::remove_dir_all(path)?;
165 } else {
166 std::fs::remove_file(path)?;
167 }
168 }
169 std::fs::create_dir_all(path)
170 }
171 fn ensure_template(&self, templates: &[&GenTemplateFile]) -> Result<(), std::io::Error> {
172 templates
173 .iter()
174 .try_for_each(|template| self.ensure_template_path(template.target_prefix(), true))?;
175 templates
176 .iter()
177 .try_for_each(|template| self.ensure_template_path(template.target_prefix(), false))
178 }
179 fn render_supporting(
180 &self,
181 models: &Vec<Property>,
182 operations: &Vec<Operation>,
183 apis: &Vec<OperationsApiTpl>,
184 ) -> Result<(), std::io::Error> {
185 self.supporting_templates
186 .iter()
187 .try_for_each(|e| self.render_supporting_template(e, models, operations, apis))
188 }
189 fn render_apis(&self, apis: &Vec<OperationsApiTpl>) -> Result<(), std::io::Error> {
190 self.api_template
191 .iter()
192 .try_for_each(|e| self.render_template_apis(e, apis))
193 }
194 fn render_models(&self, models: &Vec<Property>) -> Result<(), std::io::Error> {
195 for property in models {
196 let model = &vec![property.clone()];
197 for template in &self.model_templates {
198 let tpl = self.tpl(template)?;
199
200 let path = self.output_path.join(template.model_path(property));
201
202 tpl.render_to_file(
203 path,
204 &ModelsTpl {
205 models: ModelTpl { model },
206 },
207 )?;
208 }
209 }
210
211 Ok(())
212 }
213
214 fn tpl<'a>(&self, template: &'a GenTemplateFile) -> Result<Template<'a>, std::io::Error> {
215 let Some(mustache) = template.input().buffer() else {
216 return Err(std::io::Error::new(
217 std::io::ErrorKind::Unsupported,
218 "Template from path not supported yet",
219 ));
220 };
221 let tpl = Template::new(mustache).map_err(|error| {
222 std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string())
223 })?;
224
225 Ok(tpl)
226 }
227
228 fn render_supporting_template(
229 &self,
230 template: &SuppTemplateFile,
231 models: &Vec<Property>,
232 operations: &Vec<Operation>,
233 apis: &Vec<OperationsApiTpl>,
234 ) -> Result<(), std::io::Error> {
235 let tpl = self.tpl(template)?;
236
237 let path = self
238 .output_path
239 .join(template.target_prefix())
240 .join(template.target_postfix());
241 tpl.render_to_file(
242 path,
243 &SupportingTpl {
244 api_info: ApiInfoTpl { apis },
245 operations: OperationsTpl {
246 operation: operations,
247 },
248 models: ModelTpl { model: models },
249 package_name: self.package_info.name.as_str(),
250 package_version: self.package_info.version.as_str(),
251 package_libname: self.package_info.libname.as_str(),
252 package_edition: self.package_info.edition.as_str(),
253 api_doc_path: "docs/apis/",
254 model_doc_path: "docs/models/",
255 },
256 )?;
257
258 Ok(())
259 }
260
261 #[allow(unused)]
262 fn render_template_models(
263 &self,
264 template: &ModelTemplateFile,
265 models: &Vec<Property>,
266 ) -> Result<(), std::io::Error> {
267 let tpl = self.tpl(template)?;
268
269 for model in models {
270 let path = self.output_path.join(template.model_path(model));
271 let model = &vec![model.clone()];
272 tpl.render_to_file(
273 path,
274 &ModelsTpl {
275 models: ModelTpl { model },
276 },
277 )?;
278 }
279
280 Ok(())
281 }
282
283 fn render_template_apis(
284 &self,
285 template: &ApiTemplateFile,
286 apis: &Vec<OperationsApiTpl>,
287 ) -> Result<(), std::io::Error> {
288 let tpl = self.tpl(template)?;
289
290 for api in apis {
291 let path = self.output_path.join(template.api_path(api));
292 if let Some(parent) = path.parent() {
293 Self::ensure_path(parent, false)?;
296 }
297 tpl.render_to_file(path, api)?;
298 }
299
300 Ok(())
301 }
302
303 fn models(&self) -> Result<Vec<Property>, std::io::Error> {
304 let model = self
305 .api
306 .components
307 .as_ref()
308 .unwrap()
309 .schemas
310 .iter()
311 .map(|(name, ref_or)| {
313 let model = self.resolve_reference_or(ref_or, None, None, Some(name));
314 debug!("Model: {} => {}", name, model);
315 model
316 })
317 .flat_map(|m| m.discovered_models().into_iter().chain(vec![m]))
318 .filter(|m| m.is_model() && !m.data_type().is_empty())
319 .map(Self::post_process)
320 .sorted_by(|a, b| a.schema().cmp(b.schema()))
323 .dedup_by(|a, b| a.schema() == b.schema())
324 .inspect(|model| debug!("Model => {}", model))
325 .collect::<Vec<Property>>();
326 Ok(model)
327 }
328 fn operations(&self) -> Result<Vec<Operation>, std::io::Error> {
329 let operation = self
330 .api
331 .operations()
332 .map(|(path, method, operation)| Operation::new(self, path, method, operation))
333 .sorted_by(Self::sort_op_id)
334 .collect::<Vec<Operation>>();
335
336 Ok(operation)
337 }
338 fn sort_op_id(a: &Operation, b: &Operation) -> std::cmp::Ordering {
339 a.operation_id_original
340 .clone()
341 .unwrap_or_default()
342 .cmp(&b.operation_id_original.clone().unwrap())
343 }
344 fn apis(&self, operations: &Vec<Operation>) -> Result<Vec<OperationsApi>, std::io::Error> {
345 let mut tags = std::collections::HashMap::<String, OperationsApi>::new();
346 for op in operations {
347 for tag in op.tags() {
348 match tags.get_mut(tag) {
349 Some(api) => {
350 api.add_op(op);
351 }
352 None => {
353 tags.insert(tag.clone(), op.into());
354 }
355 }
356 }
357 }
358
359 Ok(tags
367 .into_values()
368 .sorted_by(|l, r| l.classname().cmp(r.classname()))
369 .collect::<Vec<_>>())
370 }
371}
372
373impl OpenApiV3 {
374 fn missing_schema_ref(&self, reference: &str) {
375 if !self.suppress_errors {
376 eprintln!("Schema reference({}) not found", reference);
377 }
378 }
379 fn contains_schema(&self, type_: &str) -> bool {
380 let contains = match &self.api.components {
381 None => false,
382 Some(components) => components.schemas.contains_key(type_),
383 };
384 trace!("Contains {} => {}", type_, contains);
385 contains
386 }
387 fn set_resolving(&self, type_name: &str) {
388 let mut checker = self.circ_ref_checker.borrow_mut();
389 checker.add(type_name);
390 }
391 fn resolving(&self, property: &Property) -> bool {
392 let checker = self.circ_ref_checker.borrow();
393 checker.exists(property.type_ref())
394 }
395 fn clear_resolving(&self, type_name: &str) {
396 let mut checker = self.circ_ref_checker.borrow_mut();
397 checker.remove(type_name);
398 }
399 fn resolve_schema_name(&self, var_name: Option<&str>, reference: &str) -> Property {
400 let type_name = match reference.strip_prefix("#/components/schemas/") {
401 Some(type_name) => type_name,
402 None => todo!("schema not found..."),
403 };
404 trace!("Resolving: {:?}/{}", var_name, type_name);
405 let schemas = self.api.components.as_ref().map(|c| &c.schemas);
406 match schemas.and_then(|s| s.get(type_name)) {
407 None => {
408 panic!("Schema {} Not found!", type_name);
409 }
410 Some(ref_or) => self.resolve_reference_or(ref_or, None, var_name, Some(type_name)),
411 }
412 }
413 fn resolve_schema(
414 &self,
415 schema: &openapiv3::Schema,
416 parent: Option<&Property>,
417 name: Option<&str>,
418 type_: Option<&str>,
419 ) -> Property {
420 trace!("ResolvingSchema: {:?}/{:?}", name, type_);
421 if let Some(type_) = &type_ {
422 self.set_resolving(type_);
423 }
424 let property = Property::from_schema(self, parent, schema, name, type_);
425 if let Some(type_) = &type_ {
426 self.clear_resolving(type_);
427 }
428 property
429 }
430
431 fn resolve_reference_or(
432 &self,
433 reference: &openapiv3::ReferenceOr<openapiv3::Schema>,
434 parent: Option<&Property>,
435 name: Option<&str>, type_: Option<&str>, ) -> Property {
438 match reference {
439 openapiv3::ReferenceOr::Reference { reference } => {
440 self.resolve_schema_name(name, reference)
441 }
442 openapiv3::ReferenceOr::Item(schema) => {
443 self.resolve_schema(schema, parent, name, type_)
444 }
445 }
446 }
447 fn resolve_reference_or_resp(
448 &self,
449 content: &str,
450 reference: &openapiv3::ReferenceOr<openapiv3::Response>,
451 ) -> Property {
452 debug!("Response: {reference:?}");
453 match reference {
454 openapiv3::ReferenceOr::Reference { reference } => {
455 self.resolve_schema_name(None, reference)
456 }
457 openapiv3::ReferenceOr::Item(item) => match item.content.get(content) {
458 Some(media) => match &media.schema {
459 Some(schema) => self.resolve_reference_or(schema, None, None, None),
460 None => Property::default(),
461 },
462 None => Property::default().with_data_property(&property::PropertyDataType::Empty),
463 },
464 }
465 }
466
467 fn post_process(property: Property) -> Property {
468 property.post_process()
469 }
470}
471
472impl OperationsApiTpl<'_> {
473 pub fn classname(&self) -> &str {
475 self.classname
476 }
477 pub fn class_filename(&self) -> &str {
479 self.class_filename
480 }
481}
482impl OperationsApi {
483 pub fn classname(&self) -> &str {
485 &self.classname
486 }
487 pub fn class_filename(&self) -> &str {
489 &self.class_filename
490 }
491 pub(super) fn add_op(&mut self, operation: &Operation) {
493 self.operations.push(operation.clone());
494 }
495}
496
497impl From<&Operation> for OperationsApi {
498 fn from(src: &Operation) -> OperationsApi {
499 OperationsApi {
500 class_filename: src.class_filename().into(),
501 classname: src.classname().into(),
502 operations: vec![src.clone()],
503 has_auth_methods: src.has_auth_methods,
504 }
505 }
506}
507
508#[derive(Clone, Debug, Default)]
515struct CircularRefChecker {
516 type_names: HashSet<String>,
518 current: String,
520}
521impl CircularRefChecker {
522 fn add(&mut self, type_name: &str) {
523 if self.type_names.insert(type_name.to_string()) {
524 self.current = type_name.to_string();
526 }
527 }
528 fn exists(&self, type_name: &str) -> bool {
529 self.current.as_str() != type_name && self.type_names.contains(type_name)
530 }
531 fn remove(&mut self, type_name: &str) {
532 if self.type_names.remove(type_name) {
533 }
535 }
536}