1pub mod cli;
9pub mod config;
10pub mod deps;
11pub mod emit;
12pub mod error;
13pub mod filter;
14pub mod ir;
15pub mod loader;
16pub mod lower;
17pub mod naming;
18
19use std::path::Path;
20
21pub use crate::config::Config;
22pub use crate::error::Error;
23pub use crate::error::Result;
24use crate::ir::Module;
25use crate::loader::Spec;
26
27pub fn generate(spec_path: &Path, config: &Config) -> Result<String> {
36 if config.generate.embedded_spec {
37 return Err(Error::Unimplemented("embedded-spec".to_owned()));
38 }
39 let mut spec = Spec::load(spec_path)?;
40 spec.apply_filters(&config.output_options);
41 let want_server = config.generate.std_http_server;
42 let want_client = config.generate.client;
43 let server_urls = if config.generate.server_urls {
44 lower::lower_server_urls(&spec)?
45 } else {
46 None
47 };
48 if !(config.generate.models || want_server || want_client) {
53 return emit::emit_module(&Module::default(), server_urls.as_ref());
54 }
55 let type_name_suffix = config.output_options.type_name_suffix.as_deref();
59 let names = lower::type_renames(&spec, type_name_suffix)?;
63 let mut module = lower::generate_models(&spec, &names)?;
64 if want_server || want_client {
65 let response_type_suffix = config
66 .output_options
67 .response_type_suffix
68 .as_deref()
69 .filter(|suffix| return !suffix.is_empty())
70 .unwrap_or(crate::config::DEFAULT_RESPONSE_SUFFIX);
71 let mut service = lower::generate_service(&spec, &config.import_mapping, response_type_suffix)?;
72 lower::rewrite_service(&mut service, names.renames());
73 if !config.output_options.skip_prune {
74 lower::prune_unused_models(&mut module, &service);
75 }
76 names.check_emitted(&module)?;
80 lower::check_duplicate_models(&module)?;
83 lower::box_recursive_types(&mut module)?;
85 let targets = emit::Targets {
86 server: want_server,
87 client: want_client,
88 };
89 lower::check_type_name_collisions(&service, &module, &emit::reserved_type_names(targets))?;
90 lower::check_prelude_shadowing(&module, targets)?;
91 return emit::emit_flat(&module, &service, server_urls.as_ref(), targets);
92 }
93 names.check_emitted(&module)?;
96 lower::check_duplicate_models(&module)?;
97 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
98 lower::box_recursive_types(&mut module)?;
99 return emit::emit_module(&module, server_urls.as_ref());
100}
101
102pub fn generate_to_file(spec_path: &Path, config: &Config, output_path: &Path) -> Result<()> {
105 let code = generate(spec_path, config)?;
106 return write_output(output_path, &code);
107}
108
109pub fn generate_models_string(spec_path: &Path) -> Result<String> {
116 let spec = Spec::load(spec_path)?;
117 let names = lower::type_renames(&spec, None)?;
118 let mut module = lower::generate_models(&spec, &names)?;
119 names.check_emitted(&module)?;
121 lower::check_duplicate_models(&module)?;
122 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
123 lower::box_recursive_types(&mut module)?;
124 let code = emit::emit_module(&module, None)?;
125 return Ok(code);
126}
127
128pub fn generate_models_to_file(spec_path: &Path, output_path: &Path) -> Result<()> {
131 let code = generate_models_string(spec_path)?;
132 return write_output(output_path, &code);
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum Drift {
138 None,
140 Absent,
143 Differs,
145}
146
147pub fn check_output(output_path: &Path, code: &str) -> Result<Drift> {
165 let existing = match std::fs::read(output_path) {
166 Ok(existing) => existing,
167 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
168 return Ok(Drift::Absent);
169 }
170 Err(source) => {
171 return Err(Error::ReadOutput {
172 path: output_path.display().to_string(),
173 source,
174 });
175 }
176 };
177 if existing == code.as_bytes() {
178 return Ok(Drift::None);
179 }
180 return Ok(Drift::Differs);
181}
182
183pub fn write_output(output_path: &Path, code: &str) -> Result<()> {
185 if let Some(parent) = output_path.parent()
186 && !parent.as_os_str().is_empty()
187 {
188 std::fs::create_dir_all(parent).map_err(|source| {
189 return Error::WriteOutput {
190 path: output_path.display().to_string(),
191 source,
192 };
193 })?;
194 }
195 std::fs::write(output_path, code).map_err(|source| {
196 return Error::WriteOutput {
197 path: output_path.display().to_string(),
198 source,
199 };
200 })?;
201 return Ok(());
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 struct TestDir {
210 path: std::path::PathBuf,
211 }
212
213 impl TestDir {
214 fn new(test_name: &str) -> Self {
215 let unique = format!(
216 "oapi-codegen-check-{test_name}-{}-{}",
217 std::process::id(),
218 std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .expect("system clock should be after Unix epoch")
221 .as_nanos(),
222 );
223 let path = std::env::temp_dir().join(unique);
224 std::fs::create_dir_all(&path).expect("create test directory");
225 return Self { path };
226 }
227
228 fn join(&self, file: &str) -> std::path::PathBuf {
229 return self.path.join(file);
230 }
231 }
232
233 impl Drop for TestDir {
234 fn drop(&mut self) {
235 let _ = std::fs::remove_dir_all(&self.path);
236 }
237 }
238
239 #[test]
240 fn check_output_reports_an_absent_file_as_drift() {
241 let dir = TestDir::new("absent");
242 let drift = check_output(&dir.join("out.rs"), "pub struct Widget;\n").expect("check an absent file");
244 assert_eq!(drift, Drift::Absent);
245 }
246
247 #[test]
248 fn check_output_reports_equal_content_as_no_drift() {
249 let dir = TestDir::new("equal");
250 let path = dir.join("out.rs");
251 let code = "pub struct Widget;\n";
252 std::fs::write(&path, code).expect("write the output file");
253 let drift = check_output(&path, code).expect("check an equal file");
254 assert_eq!(drift, Drift::None);
255 }
256
257 #[test]
258 fn check_output_reports_different_content_as_drift() {
259 let dir = TestDir::new("differs");
260 let path = dir.join("out.rs");
261 std::fs::write(&path, "pub struct Widget;\n").expect("write the output file");
262 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
263 assert_eq!(drift, Drift::Differs);
264 }
265
266 #[test]
267 fn check_output_compares_exactly() {
268 let dir = TestDir::new("exact");
269 let path = dir.join("out.rs");
270 std::fs::write(&path, "pub struct Widget;").expect("write the output file");
274 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file with no trailing newline");
275 assert_eq!(drift, Drift::Differs);
276 }
277
278 #[test]
279 fn check_output_reports_content_that_is_not_utf8_as_drift() {
280 let dir = TestDir::new("not-utf8");
281 let path = dir.join("out.rs");
282 std::fs::write(&path, [0xFF_u8, 0xFE_u8]).expect("write the output file");
286 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file that is not UTF-8");
287 assert_eq!(drift, Drift::Differs);
288 }
289
290 #[test]
291 fn check_output_writes_nothing() {
292 let dir = TestDir::new("readonly");
293 let path = dir.join("out.rs");
294 let existing = "pub struct Widget;\n";
295 std::fs::write(&path, existing).expect("write the output file");
296 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
297 assert_eq!(drift, Drift::Differs);
298 let after = std::fs::read_to_string(&path).expect("read the output file back");
299 assert_eq!(after, existing, "`check_output` must not change the file");
300 }
301
302 #[test]
303 fn check_output_fails_on_a_path_it_cannot_read() {
304 let dir = TestDir::new("unreadable");
305 let path = dir.join("out.rs");
306 std::fs::create_dir(&path).expect("create a directory where a file belongs");
309 let error = check_output(&path, "pub struct Widget;\n").expect_err("a directory is not readable as a file");
310 assert!(
311 matches!(error, Error::ReadOutput { .. }),
312 "expected `ReadOutput`, got {error:?}"
313 );
314 }
315}