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