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;
18pub mod package;
19
20use std::path::Path;
21
22pub use crate::config::Config;
23pub use crate::error::Error;
24pub use crate::error::Result;
25use crate::ir::Module;
26use crate::ir::ServerUrls;
27use crate::ir::Service;
28use crate::loader::Spec;
29pub use crate::package::GeneratedFile;
30pub use crate::package::GeneratedPackage;
31pub use crate::package::PackageDrift;
32pub use crate::package::check_package;
33pub use crate::package::write_package;
34
35enum Lowered {
37 Models {
39 module: Module,
41 server_urls: Option<ServerUrls>,
43 },
44 Service {
47 module: Module,
49 service: Service,
51 server_urls: Option<ServerUrls>,
53 targets: emit::Targets,
55 },
56}
57
58fn lower_spec(spec_path: &Path, config: &Config) -> Result<Lowered> {
64 if config.generate.embedded_spec {
65 return Err(Error::Unimplemented("embedded-spec".to_owned()));
66 }
67 let mut spec = Spec::load(spec_path)?;
68 spec.apply_filters(&config.output_options);
69 let want_server = config.generate.std_http_server;
70 let want_client = config.generate.client;
71 let server_urls = if config.generate.server_urls {
72 lower::lower_server_urls(&spec)?
73 } else {
74 None
75 };
76 if !(config.generate.models || want_server || want_client) {
81 return Ok(Lowered::Models {
82 module: Module::default(),
83 server_urls,
84 });
85 }
86 let type_name_suffix = config.output_options.type_name_suffix.as_deref();
90 let names = lower::type_renames(&spec, type_name_suffix)?;
94 let mut module = lower::generate_models(&spec, &names)?;
95 if want_server || want_client {
96 let response_type_suffix = config
97 .output_options
98 .response_type_suffix
99 .as_deref()
100 .filter(|suffix| return !suffix.is_empty())
101 .unwrap_or(crate::config::DEFAULT_RESPONSE_SUFFIX);
102 let mut service = lower::generate_service(&spec, &config.import_mapping, response_type_suffix)?;
103 lower::rewrite_service(&mut service, names.renames());
104 lower::split_by_direction(&mut module, Some(&mut service));
107 if !config.output_options.skip_prune {
108 lower::prune_unused_models(&mut module, &service);
109 }
110 names.check_emitted(&module)?;
114 lower::check_duplicate_models(&module)?;
117 lower::box_recursive_types(&mut module)?;
119 let targets = emit::Targets {
120 server: want_server,
121 client: want_client,
122 };
123 lower::check_type_name_collisions(&service, &module, &emit::reserved_type_names(targets))?;
124 lower::check_prelude_shadowing(&module, targets)?;
125 return Ok(Lowered::Service {
126 module,
127 service,
128 server_urls,
129 targets,
130 });
131 }
132 lower::split_by_direction(&mut module, None);
135 names.check_emitted(&module)?;
136 lower::check_duplicate_models(&module)?;
137 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
138 lower::box_recursive_types(&mut module)?;
139 return Ok(Lowered::Models { module, server_urls });
140}
141
142pub fn generate(spec_path: &Path, config: &Config) -> Result<String> {
154 return match lower_spec(spec_path, config)? {
155 Lowered::Models { module, server_urls } => emit::emit_module(&module, server_urls.as_ref()),
156 Lowered::Service {
157 module,
158 service,
159 server_urls,
160 targets,
161 } => emit::emit_flat(&module, &service, server_urls.as_ref(), targets),
162 };
163}
164
165pub fn generate_package(spec_path: &Path, config: &Config, output_path: &Path) -> Result<GeneratedPackage> {
182 return match lower_spec(spec_path, config)? {
183 Lowered::Models { module, server_urls } => Ok(GeneratedPackage::new(
184 emit::emit_module(&module, server_urls.as_ref())?,
185 Vec::new(),
186 )),
187 Lowered::Service {
188 module,
189 service,
190 server_urls,
191 targets,
192 } => {
193 let stem = package::companion_of(output_path)?
194 .file_name()
195 .map(|stem| return stem.to_string_lossy().into_owned())
196 .ok_or_else(|| {
197 return Error::UnsplittableOutput {
198 path: output_path.display().to_string(),
199 };
200 })?;
201 emit::emit_package(&module, &service, server_urls.as_ref(), targets, &stem)
202 }
203 };
204}
205
206pub fn generate_to_file(spec_path: &Path, config: &Config, output_path: &Path) -> Result<()> {
209 let code = generate(spec_path, config)?;
210 return write_output(output_path, &code);
211}
212
213pub fn generate_models_string(spec_path: &Path) -> Result<String> {
220 let spec = Spec::load(spec_path)?;
221 let names = lower::type_renames(&spec, None)?;
222 let mut module = lower::generate_models(&spec, &names)?;
223 lower::split_by_direction(&mut module, None);
224 names.check_emitted(&module)?;
226 lower::check_duplicate_models(&module)?;
227 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
228 lower::box_recursive_types(&mut module)?;
229 let code = emit::emit_module(&module, None)?;
230 return Ok(code);
231}
232
233pub fn generate_models_to_file(spec_path: &Path, output_path: &Path) -> Result<()> {
236 let code = generate_models_string(spec_path)?;
237 return write_output(output_path, &code);
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum Drift {
243 None,
245 Absent,
248 Differs,
250}
251
252pub fn check_output(output_path: &Path, code: &str) -> Result<Drift> {
270 let existing = match std::fs::read(output_path) {
271 Ok(existing) => existing,
272 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
273 return Ok(Drift::Absent);
274 }
275 Err(source) => {
276 return Err(Error::ReadOutput {
277 path: output_path.display().to_string(),
278 source,
279 });
280 }
281 };
282 if existing == code.as_bytes() {
283 return Ok(Drift::None);
284 }
285 return Ok(Drift::Differs);
286}
287
288pub fn write_output(output_path: &Path, code: &str) -> Result<()> {
290 if let Some(parent) = output_path.parent()
291 && !parent.as_os_str().is_empty()
292 {
293 std::fs::create_dir_all(parent).map_err(|source| {
294 return Error::WriteOutput {
295 path: output_path.display().to_string(),
296 source,
297 };
298 })?;
299 }
300 std::fs::write(output_path, code).map_err(|source| {
301 return Error::WriteOutput {
302 path: output_path.display().to_string(),
303 source,
304 };
305 })?;
306 return Ok(());
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 struct TestDir {
315 path: std::path::PathBuf,
316 }
317
318 impl TestDir {
319 fn new(test_name: &str) -> Self {
320 let unique = format!(
321 "oapi-codegen-check-{test_name}-{}-{}",
322 std::process::id(),
323 std::time::SystemTime::now()
324 .duration_since(std::time::UNIX_EPOCH)
325 .expect("system clock should be after Unix epoch")
326 .as_nanos(),
327 );
328 let path = std::env::temp_dir().join(unique);
329 std::fs::create_dir_all(&path).expect("create test directory");
330 return Self { path };
331 }
332
333 fn join(&self, file: &str) -> std::path::PathBuf {
334 return self.path.join(file);
335 }
336 }
337
338 impl Drop for TestDir {
339 fn drop(&mut self) {
340 let _ = std::fs::remove_dir_all(&self.path);
341 }
342 }
343
344 #[test]
345 fn check_output_reports_an_absent_file_as_drift() {
346 let dir = TestDir::new("absent");
347 let drift = check_output(&dir.join("out.rs"), "pub struct Widget;\n").expect("check an absent file");
349 assert_eq!(drift, Drift::Absent);
350 }
351
352 #[test]
353 fn check_output_reports_equal_content_as_no_drift() {
354 let dir = TestDir::new("equal");
355 let path = dir.join("out.rs");
356 let code = "pub struct Widget;\n";
357 std::fs::write(&path, code).expect("write the output file");
358 let drift = check_output(&path, code).expect("check an equal file");
359 assert_eq!(drift, Drift::None);
360 }
361
362 #[test]
363 fn check_output_reports_different_content_as_drift() {
364 let dir = TestDir::new("differs");
365 let path = dir.join("out.rs");
366 std::fs::write(&path, "pub struct Widget;\n").expect("write the output file");
367 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
368 assert_eq!(drift, Drift::Differs);
369 }
370
371 #[test]
372 fn check_output_compares_exactly() {
373 let dir = TestDir::new("exact");
374 let path = dir.join("out.rs");
375 std::fs::write(&path, "pub struct Widget;").expect("write the output file");
379 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file with no trailing newline");
380 assert_eq!(drift, Drift::Differs);
381 }
382
383 #[test]
384 fn check_output_reports_content_that_is_not_utf8_as_drift() {
385 let dir = TestDir::new("not-utf8");
386 let path = dir.join("out.rs");
387 std::fs::write(&path, [0xFF_u8, 0xFE_u8]).expect("write the output file");
391 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file that is not UTF-8");
392 assert_eq!(drift, Drift::Differs);
393 }
394
395 #[test]
396 fn check_output_writes_nothing() {
397 let dir = TestDir::new("readonly");
398 let path = dir.join("out.rs");
399 let existing = "pub struct Widget;\n";
400 std::fs::write(&path, existing).expect("write the output file");
401 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
402 assert_eq!(drift, Drift::Differs);
403 let after = std::fs::read_to_string(&path).expect("read the output file back");
404 assert_eq!(after, existing, "`check_output` must not change the file");
405 }
406
407 #[test]
408 fn check_output_fails_on_a_path_it_cannot_read() {
409 let dir = TestDir::new("unreadable");
410 let path = dir.join("out.rs");
411 std::fs::create_dir(&path).expect("create a directory where a file belongs");
414 let error = check_output(&path, "pub struct Widget;\n").expect_err("a directory is not readable as a file");
415 assert!(
416 matches!(error, Error::ReadOutput { .. }),
417 "expected `ReadOutput`, got {error:?}"
418 );
419 }
420}