1pub mod cli;
9pub mod config;
10mod coverage;
11pub mod deps;
12mod diagnostic;
13pub mod emit;
14pub mod error;
15pub mod filter;
16pub mod ir;
17pub mod loader;
18pub mod lower;
19pub mod naming;
20pub mod package;
21
22use std::path::Path;
23
24pub use crate::config::Config;
25pub use crate::error::Error;
26pub use crate::error::Result;
27use crate::ir::Module;
28use crate::ir::ServerUrls;
29use crate::ir::Service;
30use crate::loader::Spec;
31pub use crate::package::GeneratedFile;
32pub use crate::package::GeneratedPackage;
33pub use crate::package::PackageDrift;
34pub use crate::package::check_package;
35pub use crate::package::write_package;
36
37enum Lowered {
39 Models {
41 module: Module,
43 server_urls: Option<ServerUrls>,
45 },
46 Service {
49 module: Module,
51 service: Service,
53 server_urls: Option<ServerUrls>,
55 targets: emit::Targets,
57 },
58}
59
60fn lower_spec(spec_path: &Path, config: &Config) -> Result<Lowered> {
66 if config.generate.embedded_spec {
67 return Err(Error::Unimplemented("embedded-spec".to_owned()));
68 }
69 let mut spec = Spec::load(spec_path)?;
70 spec.apply_filters(&config.output_options);
71 let want_server = config.generate.std_http_server;
72 let want_client = config.generate.client;
73 let server_urls = if config.generate.server_urls {
74 lower::lower_server_urls(&spec)?
75 } else {
76 None
77 };
78 if !(config.generate.models || want_server || want_client) {
83 return Ok(Lowered::Models {
84 module: Module::default(),
85 server_urls,
86 });
87 }
88 let type_name_suffix = config.output_options.type_name_suffix.as_deref();
92 let names = lower::type_renames(&spec, type_name_suffix)?;
96 let mut module = lower::generate_models(&spec, &names)?;
97 if want_server || want_client {
98 let response_type_suffix = config
99 .output_options
100 .response_type_suffix
101 .as_deref()
102 .filter(|suffix| return !suffix.is_empty())
103 .unwrap_or(crate::config::DEFAULT_RESPONSE_SUFFIX);
104 let mut service = lower::generate_service(&spec, &config.import_mapping, response_type_suffix)?;
105 lower::rewrite_service(&mut service, names.renames());
106 lower::split_by_direction(&mut module, Some(&mut service));
109 if !config.output_options.skip_prune {
110 lower::prune_unused_models(&mut module, &service);
111 }
112 names.check_emitted(&module)?;
116 lower::check_duplicate_models(&module)?;
119 lower::box_recursive_types(&mut module)?;
121 let targets = emit::Targets {
122 server: want_server,
123 client: want_client,
124 };
125 let mut reserved = emit::reserved_type_names(targets);
126 if !emit::uses_nullable(&module, Some(&service)) {
127 reserved.retain(|name| return name.name != "Nullable");
128 }
129 lower::check_type_name_collisions(&service, &module, &reserved)?;
130 lower::check_prelude_shadowing(&module, targets)?;
131 return Ok(Lowered::Service {
132 module,
133 service,
134 server_urls,
135 targets,
136 });
137 }
138 lower::split_by_direction(&mut module, None);
141 names.check_emitted(&module)?;
142 lower::check_duplicate_models(&module)?;
143 if emit::uses_nullable(&module, None) {
144 lower::check_type_name_collisions(
145 &Service::default(),
146 &module,
147 &emit::reserved_type_names(emit::Targets::default()),
148 )?;
149 }
150 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
151 lower::box_recursive_types(&mut module)?;
152 return Ok(Lowered::Models { module, server_urls });
153}
154
155pub fn generate(spec_path: &Path, config: &Config) -> Result<String> {
167 return match lower_spec(spec_path, config)? {
168 Lowered::Models { module, server_urls } => emit::emit_module(&module, server_urls.as_ref()),
169 Lowered::Service {
170 module,
171 service,
172 server_urls,
173 targets,
174 } => emit::emit_flat(&module, &service, server_urls.as_ref(), targets),
175 };
176}
177
178pub fn generate_package(spec_path: &Path, config: &Config, output_path: &Path) -> Result<GeneratedPackage> {
195 return match lower_spec(spec_path, config)? {
196 Lowered::Models { module, server_urls } => Ok(GeneratedPackage::new(
197 emit::emit_module(&module, server_urls.as_ref())?,
198 Vec::new(),
199 )),
200 Lowered::Service {
201 module,
202 service,
203 server_urls,
204 targets,
205 } => {
206 let stem = package::companion_of(output_path)?
207 .file_name()
208 .map(|stem| return stem.to_string_lossy().into_owned())
209 .ok_or_else(|| {
210 return Error::UnsplittableOutput {
211 path: output_path.display().to_string(),
212 };
213 })?;
214 emit::emit_package(&module, &service, server_urls.as_ref(), targets, &stem)
215 }
216 };
217}
218
219pub fn generate_to_file(spec_path: &Path, config: &Config, output_path: &Path) -> Result<()> {
222 let code = generate(spec_path, config)?;
223 return write_output(output_path, &code);
224}
225
226pub fn generate_models_string(spec_path: &Path) -> Result<String> {
233 let spec = Spec::load(spec_path)?;
234 let names = lower::type_renames(&spec, None)?;
235 let mut module = lower::generate_models(&spec, &names)?;
236 lower::split_by_direction(&mut module, None);
237 names.check_emitted(&module)?;
239 lower::check_duplicate_models(&module)?;
240 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
241 lower::box_recursive_types(&mut module)?;
242 let code = emit::emit_module(&module, None)?;
243 return Ok(code);
244}
245
246pub fn generate_models_to_file(spec_path: &Path, output_path: &Path) -> Result<()> {
249 let code = generate_models_string(spec_path)?;
250 return write_output(output_path, &code);
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum Drift {
256 None,
258 Absent,
261 Differs,
263}
264
265pub fn check_output(output_path: &Path, code: &str) -> Result<Drift> {
283 let existing = match std::fs::read(output_path) {
284 Ok(existing) => existing,
285 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
286 return Ok(Drift::Absent);
287 }
288 Err(source) => {
289 return Err(Error::ReadOutput {
290 path: output_path.display().to_string(),
291 source,
292 });
293 }
294 };
295 if existing == code.as_bytes() {
296 return Ok(Drift::None);
297 }
298 return Ok(Drift::Differs);
299}
300
301pub fn write_output(output_path: &Path, code: &str) -> Result<()> {
303 if let Some(parent) = output_path.parent()
304 && !parent.as_os_str().is_empty()
305 {
306 std::fs::create_dir_all(parent).map_err(|source| {
307 return Error::WriteOutput {
308 path: output_path.display().to_string(),
309 source,
310 };
311 })?;
312 }
313 std::fs::write(output_path, code).map_err(|source| {
314 return Error::WriteOutput {
315 path: output_path.display().to_string(),
316 source,
317 };
318 })?;
319 return Ok(());
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 struct TestDir {
328 path: std::path::PathBuf,
329 }
330
331 impl TestDir {
332 fn new(test_name: &str) -> Self {
333 let unique = format!(
334 "oapi-codegen-check-{test_name}-{}-{}",
335 std::process::id(),
336 std::time::SystemTime::now()
337 .duration_since(std::time::UNIX_EPOCH)
338 .expect("system clock should be after Unix epoch")
339 .as_nanos(),
340 );
341 let path = std::env::temp_dir().join(unique);
342 std::fs::create_dir_all(&path).expect("create test directory");
343 return Self { path };
344 }
345
346 fn join(&self, file: &str) -> std::path::PathBuf {
347 return self.path.join(file);
348 }
349 }
350
351 impl Drop for TestDir {
352 fn drop(&mut self) {
353 let _ = std::fs::remove_dir_all(&self.path);
354 }
355 }
356
357 #[test]
358 fn check_output_reports_an_absent_file_as_drift() {
359 let dir = TestDir::new("absent");
360 let drift = check_output(&dir.join("out.rs"), "pub struct Widget;\n").expect("check an absent file");
362 assert_eq!(drift, Drift::Absent);
363 }
364
365 #[test]
366 fn check_output_reports_equal_content_as_no_drift() {
367 let dir = TestDir::new("equal");
368 let path = dir.join("out.rs");
369 let code = "pub struct Widget;\n";
370 std::fs::write(&path, code).expect("write the output file");
371 let drift = check_output(&path, code).expect("check an equal file");
372 assert_eq!(drift, Drift::None);
373 }
374
375 #[test]
376 fn check_output_reports_different_content_as_drift() {
377 let dir = TestDir::new("differs");
378 let path = dir.join("out.rs");
379 std::fs::write(&path, "pub struct Widget;\n").expect("write the output file");
380 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
381 assert_eq!(drift, Drift::Differs);
382 }
383
384 #[test]
385 fn check_output_compares_exactly() {
386 let dir = TestDir::new("exact");
387 let path = dir.join("out.rs");
388 std::fs::write(&path, "pub struct Widget;").expect("write the output file");
392 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file with no trailing newline");
393 assert_eq!(drift, Drift::Differs);
394 }
395
396 #[test]
397 fn check_output_reports_content_that_is_not_utf8_as_drift() {
398 let dir = TestDir::new("not-utf8");
399 let path = dir.join("out.rs");
400 std::fs::write(&path, [0xFF_u8, 0xFE_u8]).expect("write the output file");
404 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file that is not UTF-8");
405 assert_eq!(drift, Drift::Differs);
406 }
407
408 #[test]
409 fn check_output_writes_nothing() {
410 let dir = TestDir::new("readonly");
411 let path = dir.join("out.rs");
412 let existing = "pub struct Widget;\n";
413 std::fs::write(&path, existing).expect("write the output file");
414 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
415 assert_eq!(drift, Drift::Differs);
416 let after = std::fs::read_to_string(&path).expect("read the output file back");
417 assert_eq!(after, existing, "`check_output` must not change the file");
418 }
419
420 #[test]
421 fn check_output_fails_on_a_path_it_cannot_read() {
422 let dir = TestDir::new("unreadable");
423 let path = dir.join("out.rs");
424 std::fs::create_dir(&path).expect("create a directory where a file belongs");
427 let error = check_output(&path, "pub struct Widget;\n").expect_err("a directory is not readable as a file");
428 assert!(
429 matches!(error, Error::ReadOutput { .. }),
430 "expected `ReadOutput`, got {error:?}"
431 );
432 }
433}