configure_me_codegen/
lib.rs

1//! This is the codegen part of `configure_me` crate. Please refer to the documentation of
2//! `configure_me` for details.
3//!
4//! ## Beautiful error messages
5//!
6//! This crate supports emitting beautiful, Rust-like, error messages from build script.
7//! This improves developer experience a lot at the cost of longer compile times.
8//! Thus it is recommended to turn on the feature during development
9//! and keep it off during final release production build.
10//! To emit beautiful messages activate the `spanned` feature.
11//!
12//! ## Unstable metabuild feature
13//!
14//! This crate supports nightly-only `metabuild` feature tracked in https://github.com/rust-lang/rust/issues/49803
15//! Since it is unstable you have to opt-in to instability by activating `unstable-metabuild` Cargo
16//! feature. If you enable it you don't have to write build script anymore, just add
17//! `metabuild = ["configure_me_codegen"]` to `[package]` section of your `Cargo.toml`. Note that
18//! you still have to specify the dependency (with the feature) in `[build-dependencies]`.
19//!
20//! No guarantees about stability are made because of the nature of nightly. Please use this only
21//! to test `metabuild` feature of Cargo and report your experience to the tracking issue. I look
22//! forward into having this stable and the main way of using this crate. Your reports will help.
23
24#[cfg(all(feature = "codespan-reporting", not(feature = "spanned")))]
25compile_error!("use of `codespan-reporting` feature is forbidden, use `spanned` INSTEAD");
26
27extern crate serde;
28#[macro_use]
29extern crate serde_derive;
30extern crate toml;
31extern crate unicode_segmentation;
32extern crate fmt2io;
33extern crate cargo_toml;
34#[cfg(feature = "man")]
35extern crate man;
36#[cfg(feature = "spanned")]
37extern crate codespan_reporting;
38
39pub(crate) mod config;
40pub(crate) mod codegen;
41#[cfg(feature = "man")]
42pub (crate) mod gen_man;
43#[cfg(feature = "debconf")]
44pub (crate) mod debconf;
45
46pub mod manifest;
47
48use std::borrow::Borrow;
49use std::fmt;
50use std::io::{self, Read, Write};
51use std::path::{Path, PathBuf};
52use manifest::LoadManifest;
53
54#[cfg(feature = "spanned")]
55type FileSpec = codespan_reporting::files::SimpleFile<String, String>;
56
57#[cfg(not(feature = "spanned"))]
58type FileSpec = ();
59
60#[derive(Debug)]
61enum ErrorData {
62    Input(InputError),
63    Io(io::Error),
64    Open { file: PathBuf, error: io::Error },
65    Manifest(manifest::Error),
66    MissingManifestDirEnvVar,
67    MissingOutDir,
68    #[cfg(feature = "debconf")]
69    Debconf(debconf::Error),
70}
71
72#[derive(Debug)]
73struct InputError {
74    #[cfg_attr(not(feature = "spanned"), allow(unused))]
75    file: FileSpec,
76    source: InputErrorSource,
77}
78
79#[derive(Debug)]
80enum InputErrorSource {
81    Toml(toml::de::Error),
82    Config(Vec<config::ValidationError>),
83}
84
85impl InputError {
86    #[cfg(feature = "spanned")]
87    fn to_diagnostics(&self) -> impl Iterator<Item=codespan_reporting::diagnostic::Diagnostic<()>> + '_ {
88        use codespan_reporting::diagnostic::Label;
89
90        match &self.source {
91            InputErrorSource::Toml(error) => {
92                let diagnostic = codespan_reporting::diagnostic::Diagnostic::error()
93                    .with_message("failed to parse config specification");
94                let diagnostic = match error.line_col() {
95                    Some((line, col)) => {
96                        let line_sum = self.file.source().split('\n').take(line).map(|line| line.len()).sum::<usize>();
97                        // The code above deosn't account for '\n' characters so we add the count
98                        // here.
99                        let start = line_sum + col + line;
100                        let end = start + 1;
101                        diagnostic.with_labels(vec![
102                            Label::primary((), start..end).with_message(error.to_string()),
103                        ])
104                    },
105                    None => diagnostic.with_notes(vec![error.to_string()]),
106                };
107                Some(diagnostic).into_iter().chain(None.into_iter().flatten())
108            },
109            InputErrorSource::Config(errors) => None.into_iter().chain(Some(errors.iter().map(|error| error.to_diagnostic(()))).into_iter().flatten()),
110        }
111    }
112}
113
114impl fmt::Display for InputError {
115    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
116        match &self.source {
117            InputErrorSource::Toml(err) => write!(f, "failed to parse config specification: {}", err),
118            InputErrorSource::Config(errors) => {
119                for error in errors {
120                    fmt::Display::fmt(&error, f)?;
121                    writeln!(f)?;
122                }
123                Ok(())
124            },
125        }
126    }
127}
128
129
130impl From<toml::de::Error> for InputErrorSource {
131    fn from(value: toml::de::Error) -> Self {
132        InputErrorSource::Toml(value)
133    }
134}
135
136impl From<Vec<config::ValidationError>> for InputErrorSource {
137    fn from(value: Vec<config::ValidationError>) -> Self {
138        InputErrorSource::Config(value)
139    }
140}
141
142/// Error that occured during code generation
143pub struct Error {
144    data: ErrorData,
145}
146
147impl fmt::Display for Error {
148    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
149        match &self.data {
150            ErrorData::Manifest(error) => write!(f, "failed to process manifest: {}", error),
151            ErrorData::Input(error) => fmt::Display::fmt(error, f),
152            ErrorData::Io(err) => write!(f, "I/O error: {}", err),
153            ErrorData::Open { file, error } => write!(f, "failed to open file {}: {}", file.display(), error),
154            ErrorData::MissingManifestDirEnvVar => write!(f, "missing environment variable: CARGO_MANIFEST_DIR"),
155            ErrorData::MissingOutDir => write!(f, "missing environment variable: OUT_DIR"),
156            #[cfg(feature = "debconf")]
157            ErrorData::Debconf(err) => write!(f, "failed to generate debconf: {}", err),
158        }
159    }
160}
161
162/// Implemented using `Display` so that it can be used with `Termination` to display nicer message.
163impl fmt::Debug for Error {
164    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165        #[cfg(feature = "spanned")]
166        {
167            use codespan_reporting::term::termcolor::{NoColor};
168
169            struct WrapIo<W: fmt::Write>(W);
170
171            impl<W: fmt::Write> std::io::Write for WrapIo<W> {
172                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
173                    self.0.write_str(std::str::from_utf8(buf).unwrap()).map_err(|_| std::io::ErrorKind::Other)?;
174                    Ok(buf.len())
175                }
176
177                fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
178                    self.0.write_str(std::str::from_utf8(buf).unwrap()).map_err(|_| std::io::ErrorKind::Other.into())
179                }
180
181                fn flush(&mut self) -> std::io::Result<()> {
182                    Ok(())
183                }
184            }
185
186            if let ErrorData::Input(error) = &self.data {
187                writeln!(f, "invalid config specification:")?;
188                let diagnostics = error.to_diagnostics();
189
190                let mut writer = NoColor::new(WrapIo(&mut *f));
191                let config = codespan_reporting::term::Config::default();
192
193                for diagnostic in diagnostics {
194                    match codespan_reporting::term::emit(&mut writer, &config, &error.file, &diagnostic) {
195                        Ok(()) => (),
196                        Err(codespan_reporting::files::Error::Io(_)) => return Err(fmt::Error),
197                        Err(other) => panic!("unexpected error: {}", other),
198                    }
199                }
200                return Ok(());
201            }
202        }
203        fmt::Display::fmt(self, f)
204    }
205}
206
207#[cfg(not(feature = "spanned"))]
208impl Error {
209    /// Prints a potentially-beautiful error report to stderr.
210    ///
211    /// This prints a beautiful error message to stderr when the `spanned` feature is on
212    /// or a non-beautiful error message otherwise.
213    ///
214    /// Note that this method **always** colors the output.
215    /// The rationale is that this is intended to be used in build scripts only and `cargo`
216    /// captures their output which would make it colorless.
217    /// To turn this off you can set the `NO_COLOR` environment variable.
218    /// You can also use plain `Debug` which is (unusually) user-friendly to support `Termination`.
219    ///
220    /// # Errors
221    ///
222    /// This method returns an error if writing fails.
223    pub fn report(&self) -> std::io::Result<()> {
224        write!(std::io::stderr().lock(), "{}", self)
225    }
226}
227
228#[cfg(feature = "spanned")]
229impl Error {
230    /// Prints a potentially-beautiful error report to stderr.
231    ///
232    /// This prints a beautiful error message to stderr when the `spanned` feature is on
233    /// or a non-beautiful error message otherwise
234    ///
235    /// Note that this method **always** colors the output.
236    /// The rationale is that this is intended to be used in build scripts only and `cargo`
237    /// captures their output which would make it colorless.
238    /// To turn this off you can set the `NO_COLOR` environment variable.
239    /// You can also use plain `Debug` which is (unusually) user-friendly to support `Termination`.
240    ///
241    /// # Errors
242    ///
243    /// This method returns an error if writing fails.
244    pub fn report(&self) -> std::io::Result<()> {
245        use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
246
247        if let ErrorData::Input(error) = &self.data {
248            let diagnostics = error.to_diagnostics();
249
250            let writer = StandardStream::stderr(ColorChoice::Always);
251            let mut writer = writer.lock();
252            let config = codespan_reporting::term::Config::default();
253
254            for diagnostic in diagnostics {
255                match codespan_reporting::term::emit(&mut writer, &config, &error.file, &diagnostic) {
256                    Ok(()) => (),
257                    Err(codespan_reporting::files::Error::Io(error)) => return Err(error),
258                    Err(other) => panic!("unexpected error: {}", other),
259                }
260            }
261            Ok(())
262        } else {
263            write!(std::io::stderr().lock(), "{}", self)
264        }
265    }
266}
267
268impl Error {
269    /// Reports the error and exits the program with non-zero exit code.
270    ///
271    /// # Panics
272    ///
273    /// This method panics if writing to stderr fails.
274    pub fn report_and_exit(&self) -> ! {
275        self.report().expect("failed to write to stderr");
276        std::process::exit(1);
277    }
278}
279
280impl From<ErrorData> for Error {
281    fn from(data: ErrorData) -> Self {
282        Error {
283            data,
284        }
285    }
286}
287
288impl From<io::Error> for Error {
289    fn from(err: io::Error) -> Self {
290        Error {
291            data: ErrorData::Io(err),
292        }
293    }
294}
295
296impl From<manifest::Error> for Error {
297    fn from(err: manifest::Error) -> Self {
298        Error {
299            data: ErrorData::Manifest(err),
300        }
301    }
302}
303
304impl From<manifest::LoadError> for Error {
305    fn from(err: manifest::LoadError) -> Self {
306        Error {
307            data: ErrorData::Manifest(err.into()),
308        }
309    }
310}
311
312impl From<void::Void> for Error {
313    fn from(value: void::Void) -> Self {
314        match value {}
315    }
316}
317
318
319#[cfg(feature = "debconf")]
320impl From<debconf::Error> for Error {
321    fn from(err: debconf::Error) -> Self {
322        Error {
323            data: ErrorData::Debconf(err),
324        }
325    }
326}
327
328fn load<S: Read, N: fmt::Display>(mut source: S, name: N) -> Result<config::Config, Error> {
329    let mut data = String::new();
330    source.read_to_string(&mut data)?;
331    (|| {
332        let cfg = toml::from_str::<config::raw::Config>(&data)?;
333        let cfg = cfg.validate()?;
334
335        Ok(cfg)
336    })().map_err(|source| {
337        #[cfg(feature = "spanned")]
338        {
339            ErrorData::Input(InputError { file: FileSpec::new(name.to_string(), data), source }).into()
340        }
341        #[cfg(not(feature = "spanned"))]
342        {
343            let _ = name;
344            ErrorData::Input(InputError{ file: (), source }).into()
345        }
346    })
347}
348
349fn load_from_file<P: AsRef<Path>>(source: P) -> Result<::config::Config, Error> {
350     let config_spec = std::fs::File::open(&source).map_err(|error| ErrorData::Open { file: source.as_ref().into(), error })?;
351
352     load(config_spec, source.as_ref().display())
353}
354
355fn path_in_out_dir<P: AsRef<Path>>(file_name: P) -> Result<PathBuf, Error> {
356    let mut out: PathBuf = std::env::var_os("OUT_DIR").ok_or(ErrorData::MissingOutDir)?.into();
357    out.push(file_name);
358
359    Ok(out)
360}
361
362fn default_out_file(binary: Option<&str>) -> Result<PathBuf, Error> {
363    const GENERATED_FILE_NAME: &str = "configure_me_config.rs";
364
365    let file_name_owned;
366    let file_name = match binary {
367        Some(binary) => {
368            file_name_owned = format!("{}_{}", binary, GENERATED_FILE_NAME);
369            &file_name_owned
370        },
371        None => GENERATED_FILE_NAME,
372    };
373    path_in_out_dir(file_name)
374}
375
376// Wrapper for error conversions
377fn create_file<P: AsRef<Path> + Into<PathBuf>>(file: P) -> Result<std::fs::File, Error> {
378    std::fs::File::create(&file)
379        .map_err(|error| ErrorData::Open { file: file.into(), error })
380        .map_err(Into::into)
381}
382
383fn generate_to_file<P: AsRef<Path> + Into<PathBuf>>(config_spec: &::config::Config, file: P) -> Result<(), Error> {
384     let config_code = create_file(file)?;
385     ::fmt2io::write(config_code, |config_code| codegen::generate_code(config_spec, config_code)).map_err(Into::into)
386}
387
388fn load_and_generate_default<P: AsRef<Path>>(source: P, binary: Option<&str>) -> Result<::config::Config, Error> {
389    let config_spec = load_from_file(&source)?;
390    generate_to_file(&config_spec, default_out_file(binary)?)?;
391    #[cfg(feature = "debconf")]
392    debconf::generate_if_requested(&config_spec)?;
393    println!("cargo:rerun-if-changed={}", source.as_ref().display());
394    Ok(config_spec)
395}
396
397/// Generates the source code for you from provided `toml` configuration.
398pub fn generate_source<S: Read, O: Write>(source: S, output: O) -> Result<(), Error> {
399    let cfg = load(source, "unknown file")?;
400    
401     ::fmt2io::write(output, |output| codegen::generate_code(&cfg, output)).map_err(Into::into)
402}
403
404/// Generates the source code for you from provided `toml` configuration file.
405///
406/// This function is deprecated because if you use it external tools will be unable to see the path
407/// to specification. It's much better to specify the path in `Cargo.toml` so that your app can be
408/// processed automatically. (E.g. to generate man page in packagers.)
409///
410/// This function should be used from build script as it relies on cargo environment. It handles
411/// generating the name of the file (it's called `config.rs` inside `OUT_DIR`) as well as notifying
412/// cargo of the source file.
413#[deprecated = "Use build_script_auto and put the path into Cargo.toml to expose it to external tools"]
414pub fn build_script<P: AsRef<Path>>(source: P) -> Result<(), Error> {
415    load_and_generate_default(source, None).map(::std::mem::drop)
416}
417
418/// Generates the source code for you
419///
420/// Finds the specification in Cargo.toml `metadata.configure_me`
421///
422/// This function should be used from build script as it relies on cargo environment. It handles
423/// generating the name of the file (it's called `config.rs` inside `OUT_DIR`) as well as notifying
424/// cargo of the source file.
425pub fn build_script_auto() -> Result<(), Error> {
426    use manifest::SpecificationPaths;
427
428    let manifest_dir = manifest::get_dir()?;
429    let manifest_file = manifest_dir.join("Cargo.toml");
430
431    let paths = manifest_file
432        .load_manifest()?
433        .package.ok_or(manifest::Error::MissingPackage)?
434        .metadata.ok_or(manifest::Error::MissingMetadata)?
435        .configure_me.ok_or(manifest::Error::MissingConfigureMeMetadata)?
436        .spec_paths;
437
438    match paths {
439        SpecificationPaths::Single(path) => load_and_generate_default(manifest_dir.join(path), None).map(::std::mem::drop),
440        SpecificationPaths::PerBinary(binaries) => {
441            for (binary, path) in binaries {
442                load_and_generate_default(manifest_dir.join(path), Some(&binary)).map(::std::mem::drop)?;
443            }
444            Ok(())
445        },
446        SpecificationPaths::Other(other) => match other._private {},
447    }
448}
449
450#[cfg(feature = "unstable-metabuild")]
451pub fn metabuild() {
452    build_script_auto().unwrap_or_else(|error| {
453        println!("Could not generate configuration parser: {}", error);
454        std::process::exit(1)
455    })
456}
457
458/// Generates the source code and manual page at default location.
459///
460/// This function is deprecated because generating man page in compilation step is surprising. An
461/// external `cfg_me` tool is provided that can generate the man page and save it to predictable
462/// location. This function uses `OUT_DIR` which is a weird place to put man page (and there's no
463/// better).
464///
465/// This is same as `build_script()`, but additionaly it generates a man page.
466/// The resulting man page will be stored in `$OUT_DIR/app.man`.
467#[cfg(feature = "man")]
468#[deprecated = "use of cfg_me crate to build man pages is cleaner"]
469pub fn build_script_with_man<P: AsRef<Path>>(source: P) -> Result<(), Error> {
470    #[allow(deprecated)]
471    build_script_with_man_written_to(source, path_in_out_dir("app.man")?)
472}
473
474/// Generates the source code and manual page at specified location.
475///
476/// This function is deprecated because generating man page in compilation step is surprising. An
477/// external `cfg_me` tool is provided that can generate the man page and save it to predictable
478/// location. This function needlessly burdens users of the crate to handle location configuration
479/// and makes it hard for toold like packagers to read the man page.
480///
481/// This is same as `build_script_with_man()`, but it allows you to choose where to put the man
482/// page.
483#[cfg(feature = "man")]
484#[deprecated = "use of cfg_me crate to build man pages is cleaner"]
485pub fn build_script_with_man_written_to<P: AsRef<Path>, M: AsRef<Path> + Into<PathBuf>>(source: P, output: M) -> Result<(), Error> {
486    let config_spec = load_and_generate_default(source, None)?;
487    let manifest = manifest::BuildScript.load_manifest()?;
488    let man_page = gen_man::generate_man_page(&config_spec, manifest.borrow())?;
489
490    let mut file = create_file(output)?;
491    file.write_all(man_page.as_bytes())?;
492    #[cfg(feature = "debconf")]
493    debconf::generate_if_requested(&config_spec)?;
494    Ok(())
495}
496
497/// Generates man page **only**.
498///
499/// This is useful outside build scripts.
500#[cfg(feature = "man")]
501pub fn generate_man<M: LoadManifest, W: std::io::Write, S: AsRef<Path>>(source: S, mut dest: W, manifest: M) -> Result<(), Error> where Error: std::convert::From<<M as manifest::LoadManifest>::Error> {
502    let config_spec = load_from_file(&source)?;
503    let manifest = manifest.load_manifest()?;
504    let man_page = gen_man::generate_man_page(&config_spec, manifest.borrow())?;
505    dest.write_all(man_page.as_bytes())?;
506    Ok(())
507}
508
509#[cfg(test)]
510#[deny(warnings)]
511pub(crate) mod tests {
512    use ::generate_source;
513
514    pub const SINGLE_OPTIONAL_PARAM: &str =
515r#"
516[general]
517env_prefix = "TEST_APP"
518
519[[param]]
520name = "foo"
521type = "u32"
522"#;
523
524    pub const SINGLE_MANDATORY_PARAM: &str =
525r#"
526[general]
527env_prefix = "TEST_APP"
528
529[[param]]
530name = "foo"
531type = "u32"
532optional = false
533"#;
534
535    pub const SINGLE_DEFAULT_PARAM: &str =
536r#"
537[general]
538env_prefix = "TEST_APP"
539
540[[param]]
541name = "foo"
542type = "u32"
543default = "42"
544"#;
545
546    pub const SINGLE_SWITCH: &str =
547r#"
548[general]
549env_prefix = "TEST_APP"
550
551[[switch]]
552name = "foo"
553"#;
554
555    pub const MULTIPLE_PARAMS: &str =
556r#"
557[general]
558env_prefix = "TEST_APP"
559
560[[param]]
561name = "foo"
562type = "u32"
563default = "42"
564doc = "A foo"
565
566[[param]]
567name = "bar"
568type = "String"
569optional = true
570doc = "A very, very, very, very, very, very, very, very, very, very, very, very, very, very long documentation..."
571
572[[param]]
573name = "baz"
574type = "String"
575optional = false
576doc = "A much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much, much longer documentation..."
577
578[[switch]]
579name = "verbose"
580# doc intentionally missing, because it's obious...
581
582[[switch]]
583name = "fast"
584default = true
585doc = "Determines whether to mine bitcoins fast or slowly"
586"#;
587
588    pub const NO_ARG: &str =
589r#"
590[general]
591env_prefix = "TEST_APP"
592
593[[param]]
594name = "foo"
595type = "u32"
596argument = false
597"#;
598
599    pub const SHORT_SWITCHES: &str =
600r#"
601[[switch]]
602name = "a"
603abbr = "a"
604doc = "test"
605
606[[switch]]
607name = "b"
608abbr = "b"
609
610[[switch]]
611name = "c"
612abbr = "c"
613count = true
614
615[[param]]
616name = "d"
617type = "String"
618abbr = "d"
619optional = true
620
621[[param]]
622name = "e"
623type = "String"
624abbr = "e"
625optional = true
626
627[[switch]]
628name = "foo_bar"
629abbr = "f"
630"#;
631
632    pub const CONF_FILES: &str =
633r#"
634[general]
635env_prefix = "TEST_APP"
636conf_file_param = "config"
637conf_dir_param = "conf_dir"
638
639[[param]]
640name = "foo"
641type = "u32"
642doc = "A foo"
643"#;
644
645    pub const CUSTOM_MERGE_FN: &str =
646r#"
647[general]
648env_prefix = "TEST_APP"
649
650[[param]]
651name = "foo"
652type = "u32"
653merge_fn = "(|a: &mut u32, b: u32| *a += b)"
654
655[[param]]
656name = "bar"
657type = "String"
658merge_fn = "(|a: &mut String, b: String| a.push_str(&b))"
659"#;
660
661    #[allow(unused)]
662    pub struct ExpectedOutput {
663        pub raw_config: &'static str,
664        pub validate: &'static str,
665        pub merge_in: &'static str,
666        pub merge_args: &'static str,
667        pub config: &'static str,
668        pub arg_parse_error: &'static str,
669    }
670
671    pub const EXPECTED_EMPTY: ExpectedOutput = ExpectedOutput {
672        raw_config: include_str!("../tests/expected_outputs/empty/raw_config.rs"),
673        validate: include_str!("../tests/expected_outputs/empty/validate.rs"),
674        merge_in: include_str!("../tests/expected_outputs/empty/merge_in.rs"),
675        merge_args: include_str!("../tests/expected_outputs/empty/merge_args.rs"),
676        config: include_str!("../tests/expected_outputs/empty/config.rs"),
677        arg_parse_error: include_str!("../tests/expected_outputs/empty/arg_parse_error.rs"),
678    };
679
680    pub const EXPECTED_SINGLE_OPTIONAL_PARAM: ExpectedOutput = ExpectedOutput {
681        raw_config: include_str!("../tests/expected_outputs/single_optional_param/raw_config.rs"),
682        validate: include_str!("../tests/expected_outputs/single_optional_param/validate.rs"),
683        merge_in: include_str!("../tests/expected_outputs/single_optional_param/merge_in.rs"),
684        merge_args: include_str!("../tests/expected_outputs/single_optional_param/merge_args.rs"),
685        config: include_str!("../tests/expected_outputs/single_optional_param/config.rs"),
686        arg_parse_error: include_str!("../tests/expected_outputs/single_optional_param/arg_parse_error.rs"),
687    };
688
689    pub const EXPECTED_SINGLE_MANDATORY_PARAM: ExpectedOutput = ExpectedOutput {
690        raw_config: include_str!("../tests/expected_outputs/single_mandatory_param/raw_config.rs"),
691        validate: include_str!("../tests/expected_outputs/single_mandatory_param/validate.rs"),
692        merge_in: include_str!("../tests/expected_outputs/single_mandatory_param/merge_in.rs"),
693        merge_args: include_str!("../tests/expected_outputs/single_mandatory_param/merge_args.rs"),
694        config: include_str!("../tests/expected_outputs/single_mandatory_param/config.rs"),
695        arg_parse_error: include_str!("../tests/expected_outputs/single_mandatory_param/arg_parse_error.rs"),
696    };
697
698    pub const EXPECTED_SINGLE_DEFAULT_PARAM: ExpectedOutput = ExpectedOutput {
699        raw_config: include_str!("../tests/expected_outputs/single_default_param/raw_config.rs"),
700        validate: include_str!("../tests/expected_outputs/single_default_param/validate.rs"),
701        merge_in: include_str!("../tests/expected_outputs/single_default_param/merge_in.rs"),
702        merge_args: include_str!("../tests/expected_outputs/single_default_param/merge_args.rs"),
703        config: include_str!("../tests/expected_outputs/single_default_param/config.rs"),
704        arg_parse_error: include_str!("../tests/expected_outputs/single_default_param/arg_parse_error.rs"),
705    };
706
707    pub const EXPECTED_SINGLE_SWITCH: ExpectedOutput = ExpectedOutput {
708        raw_config: include_str!("../tests/expected_outputs/single_switch/raw_config.rs"),
709        validate: include_str!("../tests/expected_outputs/single_switch/validate.rs"),
710        merge_in: include_str!("../tests/expected_outputs/single_switch/merge_in.rs"),
711        merge_args: include_str!("../tests/expected_outputs/single_switch/merge_args.rs"),
712        config: include_str!("../tests/expected_outputs/single_switch/config.rs"),
713        arg_parse_error: include_str!("../tests/expected_outputs/single_switch/arg_parse_error.rs"),
714    };
715
716    pub const EXPECTED_SHORT_SWITCHES: ExpectedOutput = ExpectedOutput {
717        raw_config: include_str!("../tests/expected_outputs/short_switches/raw_config.rs"),
718        validate: include_str!("../tests/expected_outputs/short_switches/validate.rs"),
719        merge_in: include_str!("../tests/expected_outputs/short_switches/merge_in.rs"),
720        merge_args: include_str!("../tests/expected_outputs/short_switches/merge_args.rs"),
721        config: include_str!("../tests/expected_outputs/short_switches/config.rs"),
722        arg_parse_error: include_str!("../tests/expected_outputs/short_switches/arg_parse_error.rs"),
723    };
724
725    fn check(src: &str, expected: &str) {
726        use std::io::Write;
727
728        let mut src = src.as_bytes();
729        let mut out = Vec::new();
730        generate_source(&mut src, &mut out).unwrap();
731        if out != expected.as_bytes() {
732            let mut expected_temp = tempfile::Builder::new().prefix("expected").tempfile().unwrap();
733            let mut out_temp = tempfile::Builder::new().prefix("output").tempfile().unwrap();
734
735            expected_temp.write_all(expected.as_bytes()).unwrap();
736            out_temp.write_all(&out).unwrap();
737
738            std::process::Command::new("diff")
739                .arg(expected_temp.path())
740                .arg(out_temp.path())
741                .spawn()
742                .expect("failed to run diff")
743                .wait()
744                .unwrap();
745            panic!("output differs from expected");
746        }
747    }
748
749    #[test]
750    fn empty() {
751        check("", include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/empty-config.rs")));
752    }
753
754    #[test]
755    fn single_optional_param() {
756        check(SINGLE_OPTIONAL_PARAM, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/single_optional_param-config.rs")));
757    }
758
759    #[test]
760    fn single_mandatory_param() {
761        check(SINGLE_MANDATORY_PARAM, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/single_mandatory_param-config.rs")));
762    }
763
764    #[test]
765    fn single_default_param() {
766        check(SINGLE_DEFAULT_PARAM, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/single_default_param-config.rs")));
767    }
768
769    #[test]
770    fn single_switch() {
771        check(SINGLE_SWITCH, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/single_switch-config.rs")));
772    }
773
774    #[test]
775    fn multiple_params() {
776        check(MULTIPLE_PARAMS, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/multiple_params-config.rs")));
777    }
778
779    #[test]
780    fn no_arg() {
781        check(NO_ARG, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/no_arg-config.rs")));
782    }
783
784    #[test]
785    fn short_switches() {
786        check(SHORT_SWITCHES, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/short_switches-config.rs")));
787    }
788
789    #[test]
790    fn conf_files() {
791        check(CONF_FILES, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/conf_files-config.rs")));
792    }
793
794    #[test]
795    fn custom_merge_fn() {
796        check(CUSTOM_MERGE_FN, include_str!(concat!(env!("OUT_DIR"), "/expected_outputs/with_custom_merge-config.rs")));
797    }
798}