1#![doc = include_str!("../README.md")]
2#![allow(clippy::collapsible_if)]
3pub mod config;
4pub mod doc_parser;
5pub mod dsl;
6pub mod error;
7pub mod generics;
8pub mod index;
9pub mod merger;
10pub mod preprocessor;
11pub mod scanner;
12pub mod type_mapper;
13pub mod visitor;
14
15use config::Config;
16use error::Result;
17use std::path::PathBuf;
18
19#[derive(Default)]
22pub struct Generator {
23 inputs: Vec<PathBuf>,
24 includes: Vec<PathBuf>,
25 outputs: Vec<PathBuf>,
26 schema_outputs: Vec<PathBuf>,
27 path_outputs: Vec<PathBuf>,
28 fragment_outputs: Vec<PathBuf>,
29}
30
31impl Generator {
32 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn with_config(mut self, config: Config) -> Self {
39 if let Some(inputs) = config.input {
40 self.inputs.extend(inputs);
41 }
42 if let Some(includes) = config.include {
43 self.includes.extend(includes);
44 }
45 if let Some(output) = config.output {
46 self.outputs.extend(output);
47 }
48 if let Some(output_schemas) = config.output_schemas {
49 self.schema_outputs.extend(output_schemas);
50 }
51 if let Some(output_paths) = config.output_paths {
52 self.path_outputs.extend(output_paths);
53 }
54 if let Some(output_fragments) = config.output_fragments {
55 self.fragment_outputs.extend(output_fragments);
56 }
57 self
58 }
59
60 pub fn input<P: Into<PathBuf>>(mut self, path: P) -> Self {
62 self.inputs.push(path.into());
63 self
64 }
65
66 pub fn include<P: Into<PathBuf>>(mut self, path: P) -> Self {
68 self.includes.push(path.into());
69 self
70 }
71
72 pub fn output<P: Into<PathBuf>>(mut self, path: P) -> Self {
74 self.outputs.push(path.into());
75 self
76 }
77
78 pub fn output_schemas<P: Into<PathBuf>>(mut self, path: P) -> Self {
80 self.schema_outputs.push(path.into());
81 self
82 }
83
84 pub fn output_paths<P: Into<PathBuf>>(mut self, path: P) -> Self {
86 self.path_outputs.push(path.into());
87 self
88 }
89
90 pub fn output_fragments<P: Into<PathBuf>>(mut self, path: P) -> Self {
92 self.fragment_outputs.push(path.into());
93 self
94 }
95
96 pub fn generate(self) -> Result<()> {
98 if self.outputs.is_empty()
99 && self.schema_outputs.is_empty()
100 && self.path_outputs.is_empty()
101 && self.fragment_outputs.is_empty()
102 {
103 return Err(std::io::Error::new(
104 std::io::ErrorKind::InvalidInput,
105 "At least one output path (output, output_schemas, output_paths, or output_fragments) is required",
106 )
107 .into());
108 }
109
110 log::info!(
112 "Scanning directories: {:?} and includes: {:?}",
113 self.inputs,
114 self.includes
115 );
116 let snippets = scanner::scan_directories(&self.inputs, &self.includes)?;
117
118 log::info!("Merging {} snippets", snippets.len());
120 let merged_value = merger::merge_openapi(snippets)?;
121
122 if !self.outputs.is_empty() {
124 if let serde_yaml::Value::Mapping(map) = &merged_value {
125 let openapi_key = serde_yaml::Value::String("openapi".to_string());
126 let info_key = serde_yaml::Value::String("info".to_string());
127
128 if !map.contains_key(&openapi_key) || !map.contains_key(&info_key) {
129 return Err(error::Error::NoRootFound);
130 }
131 } else {
132 return Err(error::Error::NoRootFound);
133 }
134
135 for output in &self.outputs {
136 self.write_file(output, &merged_value)?;
137 log::info!("Written full spec to {:?}", output);
138 }
139 }
140
141 if !self.schema_outputs.is_empty() {
143 let schemas = merged_value
144 .get("components")
145 .and_then(|c| c.get("schemas"))
146 .cloned()
147 .unwrap_or_else(|| serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
148
149 if let serde_yaml::Value::Mapping(m) = &schemas {
150 if m.is_empty() {
151 log::warn!("Generating empty schemas file.");
152 }
153 }
154
155 for output in &self.schema_outputs {
156 self.write_file(output, &schemas)?;
157 log::info!("Written schemas to {:?}", output);
158 }
159 }
160
161 if !self.path_outputs.is_empty() {
163 let paths = merged_value
164 .get("paths")
165 .cloned()
166 .unwrap_or_else(|| serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
167
168 if let serde_yaml::Value::Mapping(m) = &paths {
169 if m.is_empty() {
170 log::warn!("Generating empty paths file.");
171 }
172 }
173
174 for output in &self.path_outputs {
175 self.write_file(output, &paths)?;
176 log::info!("Written paths to {:?}", output);
177 }
178 }
179
180 if !self.fragment_outputs.is_empty() {
184 let mut fragment = merged_value.clone();
185 if let serde_yaml::Value::Mapping(ref mut map) = fragment {
186 map.remove(serde_yaml::Value::String("openapi".to_string()));
187 map.remove(serde_yaml::Value::String("info".to_string()));
188 map.remove(serde_yaml::Value::String("servers".to_string()));
189 }
190
191 for output in &self.fragment_outputs {
192 self.write_file(output, &fragment)?;
193 log::info!("Written fragment to {:?}", output);
194 }
195 }
196
197 Ok(())
198 }
199
200 fn write_file<T: serde::Serialize>(&self, path: &PathBuf, content: &T) -> Result<()> {
201 if let Some(parent) = path.parent() {
203 std::fs::create_dir_all(parent)?;
204 }
205
206 let file = std::fs::File::create(path)?;
207 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("yaml");
208
209 match extension {
210 "json" => {
211 serde_json::to_writer_pretty(file, content)?;
212 }
213 "yaml" | "yml" => {
214 serde_yaml::to_writer(file, content)?;
215 }
216 _ => {
217 serde_yaml::to_writer(file, content)?;
218 }
219 }
220 Ok(())
221 }
222}