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 if !config.output_options.skip_prune {
105 lower::prune_unused_models(&mut module, &service);
106 }
107 names.check_emitted(&module)?;
111 lower::check_duplicate_models(&module)?;
114 lower::box_recursive_types(&mut module)?;
116 let targets = emit::Targets {
117 server: want_server,
118 client: want_client,
119 };
120 lower::check_type_name_collisions(&service, &module, &emit::reserved_type_names(targets))?;
121 lower::check_prelude_shadowing(&module, targets)?;
122 return Ok(Lowered::Service {
123 module,
124 service,
125 server_urls,
126 targets,
127 });
128 }
129 names.check_emitted(&module)?;
132 lower::check_duplicate_models(&module)?;
133 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
134 lower::box_recursive_types(&mut module)?;
135 return Ok(Lowered::Models { module, server_urls });
136}
137
138pub fn generate(spec_path: &Path, config: &Config) -> Result<String> {
150 return match lower_spec(spec_path, config)? {
151 Lowered::Models { module, server_urls } => emit::emit_module(&module, server_urls.as_ref()),
152 Lowered::Service {
153 module,
154 service,
155 server_urls,
156 targets,
157 } => emit::emit_flat(&module, &service, server_urls.as_ref(), targets),
158 };
159}
160
161pub fn generate_package(spec_path: &Path, config: &Config, output_path: &Path) -> Result<GeneratedPackage> {
178 return match lower_spec(spec_path, config)? {
179 Lowered::Models { module, server_urls } => Ok(GeneratedPackage::new(
180 emit::emit_module(&module, server_urls.as_ref())?,
181 Vec::new(),
182 )),
183 Lowered::Service {
184 module,
185 service,
186 server_urls,
187 targets,
188 } => {
189 let stem = package::companion_of(output_path)?
190 .file_name()
191 .map(|stem| return stem.to_string_lossy().into_owned())
192 .ok_or_else(|| {
193 return Error::UnsplittableOutput {
194 path: output_path.display().to_string(),
195 };
196 })?;
197 emit::emit_package(&module, &service, server_urls.as_ref(), targets, &stem)
198 }
199 };
200}
201
202pub fn generate_to_file(spec_path: &Path, config: &Config, output_path: &Path) -> Result<()> {
205 let code = generate(spec_path, config)?;
206 return write_output(output_path, &code);
207}
208
209pub fn generate_models_string(spec_path: &Path) -> Result<String> {
216 let spec = Spec::load(spec_path)?;
217 let names = lower::type_renames(&spec, None)?;
218 let mut module = lower::generate_models(&spec, &names)?;
219 names.check_emitted(&module)?;
221 lower::check_duplicate_models(&module)?;
222 lower::check_prelude_shadowing(&module, emit::Targets::default())?;
223 lower::box_recursive_types(&mut module)?;
224 let code = emit::emit_module(&module, None)?;
225 return Ok(code);
226}
227
228pub fn generate_models_to_file(spec_path: &Path, output_path: &Path) -> Result<()> {
231 let code = generate_models_string(spec_path)?;
232 return write_output(output_path, &code);
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum Drift {
238 None,
240 Absent,
243 Differs,
245}
246
247pub fn check_output(output_path: &Path, code: &str) -> Result<Drift> {
265 let existing = match std::fs::read(output_path) {
266 Ok(existing) => existing,
267 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
268 return Ok(Drift::Absent);
269 }
270 Err(source) => {
271 return Err(Error::ReadOutput {
272 path: output_path.display().to_string(),
273 source,
274 });
275 }
276 };
277 if existing == code.as_bytes() {
278 return Ok(Drift::None);
279 }
280 return Ok(Drift::Differs);
281}
282
283pub fn write_output(output_path: &Path, code: &str) -> Result<()> {
285 if let Some(parent) = output_path.parent()
286 && !parent.as_os_str().is_empty()
287 {
288 std::fs::create_dir_all(parent).map_err(|source| {
289 return Error::WriteOutput {
290 path: output_path.display().to_string(),
291 source,
292 };
293 })?;
294 }
295 std::fs::write(output_path, code).map_err(|source| {
296 return Error::WriteOutput {
297 path: output_path.display().to_string(),
298 source,
299 };
300 })?;
301 return Ok(());
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 struct TestDir {
310 path: std::path::PathBuf,
311 }
312
313 impl TestDir {
314 fn new(test_name: &str) -> Self {
315 let unique = format!(
316 "oapi-codegen-check-{test_name}-{}-{}",
317 std::process::id(),
318 std::time::SystemTime::now()
319 .duration_since(std::time::UNIX_EPOCH)
320 .expect("system clock should be after Unix epoch")
321 .as_nanos(),
322 );
323 let path = std::env::temp_dir().join(unique);
324 std::fs::create_dir_all(&path).expect("create test directory");
325 return Self { path };
326 }
327
328 fn join(&self, file: &str) -> std::path::PathBuf {
329 return self.path.join(file);
330 }
331 }
332
333 impl Drop for TestDir {
334 fn drop(&mut self) {
335 let _ = std::fs::remove_dir_all(&self.path);
336 }
337 }
338
339 #[test]
340 fn check_output_reports_an_absent_file_as_drift() {
341 let dir = TestDir::new("absent");
342 let drift = check_output(&dir.join("out.rs"), "pub struct Widget;\n").expect("check an absent file");
344 assert_eq!(drift, Drift::Absent);
345 }
346
347 #[test]
348 fn check_output_reports_equal_content_as_no_drift() {
349 let dir = TestDir::new("equal");
350 let path = dir.join("out.rs");
351 let code = "pub struct Widget;\n";
352 std::fs::write(&path, code).expect("write the output file");
353 let drift = check_output(&path, code).expect("check an equal file");
354 assert_eq!(drift, Drift::None);
355 }
356
357 #[test]
358 fn check_output_reports_different_content_as_drift() {
359 let dir = TestDir::new("differs");
360 let path = dir.join("out.rs");
361 std::fs::write(&path, "pub struct Widget;\n").expect("write the output file");
362 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
363 assert_eq!(drift, Drift::Differs);
364 }
365
366 #[test]
367 fn check_output_compares_exactly() {
368 let dir = TestDir::new("exact");
369 let path = dir.join("out.rs");
370 std::fs::write(&path, "pub struct Widget;").expect("write the output file");
374 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file with no trailing newline");
375 assert_eq!(drift, Drift::Differs);
376 }
377
378 #[test]
379 fn check_output_reports_content_that_is_not_utf8_as_drift() {
380 let dir = TestDir::new("not-utf8");
381 let path = dir.join("out.rs");
382 std::fs::write(&path, [0xFF_u8, 0xFE_u8]).expect("write the output file");
386 let drift = check_output(&path, "pub struct Widget;\n").expect("check a file that is not UTF-8");
387 assert_eq!(drift, Drift::Differs);
388 }
389
390 #[test]
391 fn check_output_writes_nothing() {
392 let dir = TestDir::new("readonly");
393 let path = dir.join("out.rs");
394 let existing = "pub struct Widget;\n";
395 std::fs::write(&path, existing).expect("write the output file");
396 let drift = check_output(&path, "pub struct Gadget;\n").expect("check a stale file");
397 assert_eq!(drift, Drift::Differs);
398 let after = std::fs::read_to_string(&path).expect("read the output file back");
399 assert_eq!(after, existing, "`check_output` must not change the file");
400 }
401
402 #[test]
403 fn check_output_fails_on_a_path_it_cannot_read() {
404 let dir = TestDir::new("unreadable");
405 let path = dir.join("out.rs");
406 std::fs::create_dir(&path).expect("create a directory where a file belongs");
409 let error = check_output(&path, "pub struct Widget;\n").expect_err("a directory is not readable as a file");
410 assert!(
411 matches!(error, Error::ReadOutput { .. }),
412 "expected `ReadOutput`, got {error:?}"
413 );
414 }
415}