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, registry) = scanner::scan_directories(&self.inputs, &self.includes)?;
117
118 log::info!("Merging {} snippets", snippets.len());
120 let mut merged_value = merger::merge_openapi(snippets)?;
121
122 if let serde_yaml_ng::Value::Mapping(ref mut root_map) = merged_value {
124 if let Some(serde_yaml_ng::Value::Mapping(comp_map)) =
125 root_map.get_mut(serde_yaml_ng::Value::String("components".to_string()))
126 {
127 comp_map.remove(serde_yaml_ng::Value::String(
128 "x-oas-forge-templates".to_string(),
129 ));
130 comp_map.remove(serde_yaml_ng::Value::String(
131 "x-oas-forge-fragments".to_string(),
132 ));
133
134 if comp_map.is_empty() {
136 root_map.remove(serde_yaml_ng::Value::String("components".to_string()));
137 }
138 }
139 }
140
141 if !self.outputs.is_empty() {
143 if let serde_yaml_ng::Value::Mapping(map) = &merged_value {
144 let openapi_key = serde_yaml_ng::Value::String("openapi".to_string());
145 let info_key = serde_yaml_ng::Value::String("info".to_string());
146
147 if !map.contains_key(&openapi_key) || !map.contains_key(&info_key) {
148 return Err(error::Error::NoRootFound);
149 }
150 } else {
151 return Err(error::Error::NoRootFound);
152 }
153
154 for output in &self.outputs {
155 self.write_file(output, &merged_value)?;
156 log::info!("Written full spec to {:?}", output);
157 }
158 }
159
160 if !self.schema_outputs.is_empty() {
162 let schemas = merged_value
163 .get("components")
164 .and_then(|c| c.get("schemas"))
165 .cloned()
166 .unwrap_or_else(|| serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new()));
167
168 if let serde_yaml_ng::Value::Mapping(m) = &schemas {
169 if m.is_empty() {
170 log::warn!("Generating empty schemas file.");
171 }
172 }
173
174 for output in &self.schema_outputs {
175 self.write_file(output, &schemas)?;
176 log::info!("Written schemas to {:?}", output);
177 }
178 }
179
180 if !self.path_outputs.is_empty() {
182 let paths = merged_value
183 .get("paths")
184 .cloned()
185 .unwrap_or_else(|| serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new()));
186
187 if let serde_yaml_ng::Value::Mapping(m) = &paths {
188 if m.is_empty() {
189 log::warn!("Generating empty paths file.");
190 }
191 }
192
193 for output in &self.path_outputs {
194 self.write_file(output, &paths)?;
195 log::info!("Written paths to {:?}", output);
196 }
197 }
198
199 if !self.fragment_outputs.is_empty() {
204 let mut fragment = merged_value.clone();
205 if let serde_yaml_ng::Value::Mapping(ref mut map) = fragment {
206 map.remove(serde_yaml_ng::Value::String("openapi".to_string()));
207 map.remove(serde_yaml_ng::Value::String("info".to_string()));
208 map.remove(serde_yaml_ng::Value::String("servers".to_string()));
209 }
210
211 if !registry.blueprints.is_empty() || !registry.fragments.is_empty() {
213 if let serde_yaml_ng::Value::Mapping(ref mut root_map) = fragment {
214 let components_key = serde_yaml_ng::Value::String("components".to_string());
215 let components = root_map.entry(components_key).or_insert_with(|| {
216 serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new())
217 });
218 if let serde_yaml_ng::Value::Mapping(comp_map) = components {
219 if !registry.blueprints.is_empty() {
220 if let Ok(val) = serde_json::to_value(®istry.blueprints) {
221 if let Ok(yaml_val) = serde_yaml_ng::to_value(&val) {
222 comp_map.insert(
223 serde_yaml_ng::Value::String(
224 "x-oas-forge-templates".to_string(),
225 ),
226 yaml_val,
227 );
228 }
229 }
230 }
231 if !registry.fragments.is_empty() {
232 if let Ok(val) = serde_json::to_value(®istry.fragments) {
233 if let Ok(yaml_val) = serde_yaml_ng::to_value(&val) {
234 comp_map.insert(
235 serde_yaml_ng::Value::String(
236 "x-oas-forge-fragments".to_string(),
237 ),
238 yaml_val,
239 );
240 }
241 }
242 }
243 }
244 }
245 }
246
247 for output in &self.fragment_outputs {
248 self.write_file(output, &fragment)?;
249 log::info!("Written fragment to {:?}", output);
250 }
251 }
252
253 Ok(())
254 }
255
256 fn write_file<T: serde::Serialize>(&self, path: &PathBuf, content: &T) -> Result<()> {
257 if let Some(parent) = path.parent() {
259 std::fs::create_dir_all(parent)?;
260 }
261
262 let file = std::fs::File::create(path)?;
263 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("yaml");
264
265 match extension {
266 "json" => {
267 serde_json::to_writer_pretty(file, content)?;
268 }
269 "yaml" | "yml" => {
270 serde_yaml_ng::to_writer(file, content)?;
271 }
272 _ => {
273 serde_yaml_ng::to_writer(file, content)?;
274 }
275 }
276 Ok(())
277 }
278}