tighterror_build/
coder.rs

1use crate::{
2    errors::{
3        kind::coder::{FAILED_TO_READ_OUTPUT_FILE, FAILED_TO_WRITE_OUTPUT_FILE},
4        TbError,
5    },
6    parser,
7    spec::definitions::STDOUT_PATH,
8};
9use log::error;
10use std::{
11    fs::File,
12    io::{self, Read, Write},
13    path::Path,
14};
15
16mod formatter;
17mod frozen_options;
18pub(crate) use frozen_options::*;
19mod generator;
20use generator::ModuleCode;
21pub(crate) mod idents;
22mod options;
23pub use options::*;
24
25const TMP_FILE_PFX: &str = "tighterror.";
26const TMP_FILE_SFX: &str = ".rs";
27const RUST_FILE_EXTENSION: &str = "rs";
28const ALL_MODULES: &str = "*";
29
30/// Generates Rust source code from a specification file.
31///
32/// See [CodegenOptions] for more information about function parameters.
33///
34/// # Examples
35///
36/// This example shows how the [codegen] function may be called directly.
37/// However, the recommended and a shorter way to invoke the function
38/// is using [CodegenOptions::codegen] method. See [CodegenOptions] for
39/// a full example.
40///
41/// ```no_run
42/// # use tighterror_build::{CodegenOptions, errors::TbError, codegen};
43/// # pub fn foo() -> Result<(), TbError> {
44/// let mut opts = CodegenOptions::new();
45/// opts.spec("tighterror.yaml".to_owned());
46/// codegen(&opts)?;
47/// # Ok(())
48/// # }
49/// # foo().unwrap();
50/// ```
51pub fn codegen(opts: &CodegenOptions) -> Result<(), TbError> {
52    let spec = parser::parse(opts.spec.as_deref())?;
53    debug_assert!(!spec.modules.is_empty());
54
55    let frozen = FrozenOptions::new(opts, &spec)?;
56    let modules = generator::spec_to_rust(&frozen, &spec)?;
57
58    match frozen.output {
59        p if p.as_os_str() == STDOUT_PATH => {
60            debug_assert_eq!(modules.len(), 1);
61            let code = modules[0].code.as_bytes();
62            if let Err(e) = io::stdout().lock().write_all(code) {
63                error!("failed to write to stdout: {e}");
64                FAILED_TO_WRITE_OUTPUT_FILE.into()
65            } else {
66                Ok(())
67            }
68        }
69        _ if frozen.update => update_modules(&frozen, &modules),
70        _ => write_modules(&frozen, &modules),
71    }
72}
73
74fn write_modules(frozen: &FrozenOptions, modules: &[ModuleCode]) -> Result<(), TbError> {
75    if frozen.separate_files {
76        let dir = frozen.output.as_path();
77        for m in modules {
78            let mut path = dir.join(&m.name);
79            path.set_extension(RUST_FILE_EXTENSION);
80            write_code(&m.code, &path)?;
81        }
82    } else {
83        debug_assert_eq!(modules.len(), 1);
84        write_code(&modules[0].code, frozen.output.as_path())?;
85    }
86
87    Ok(())
88}
89
90fn write_code(code: &str, path: &Path) -> Result<(), TbError> {
91    let file = match File::options()
92        .write(true)
93        .create(true)
94        .truncate(true)
95        .open(path)
96    {
97        Ok(f) => f,
98        Err(e) => {
99            error!("failed to open the output file {:?}: {e}", path);
100            return FAILED_TO_WRITE_OUTPUT_FILE.into();
101        }
102    };
103
104    write_and_format(code, path, file)
105}
106
107fn write_and_format(code: &str, path: &Path, mut file: File) -> Result<(), TbError> {
108    if let Err(e) = file.write_all(code.as_bytes()) {
109        error!("failed to write to the output file {:?}: {e}", path);
110        return FAILED_TO_WRITE_OUTPUT_FILE.into();
111    }
112    file.flush().ok();
113    drop(file);
114    formatter::rustfmt(path).ok();
115    Ok(())
116}
117
118fn read_code(path: &Path) -> Result<String, TbError> {
119    let mut file = match File::options().read(true).open(path) {
120        Ok(f) => f,
121        Err(e) => {
122            error!("failed to open the output file {:?}: {e}", path);
123            return FAILED_TO_READ_OUTPUT_FILE.into();
124        }
125    };
126
127    let mut data = String::with_capacity(4096);
128    file.read_to_string(&mut data).map_err(|e| {
129        error!("failed to read the output file {:?}: {e}", path);
130        TbError::from(FAILED_TO_WRITE_OUTPUT_FILE)
131    })?;
132
133    Ok(data)
134}
135
136fn update_modules(frozen: &FrozenOptions, modules: &[ModuleCode]) -> Result<(), TbError> {
137    if frozen.separate_files {
138        let dir = frozen.output.as_path();
139        for m in modules {
140            let mut path = dir.join(&m.name);
141            path.set_extension(RUST_FILE_EXTENSION);
142            update_module(&m.code, &path)?;
143        }
144    } else {
145        debug_assert_eq!(modules.len(), 1);
146        update_module(&modules[0].code, frozen.output.as_path())?;
147    }
148
149    Ok(())
150}
151
152fn update_module(code: &str, path: &Path) -> Result<(), TbError> {
153    if !path.exists() {
154        return write_code(code, path);
155    }
156
157    let existing_data = read_code(path)?;
158
159    let dir = match path.parent() {
160        Some(p) if !p.as_os_str().is_empty() => p,
161        _ => Path::new("."),
162    };
163
164    let tmp_file = tempfile::Builder::new()
165        .prefix(TMP_FILE_PFX)
166        .suffix(TMP_FILE_SFX)
167        .tempfile_in(dir)
168        .map_err(|e| {
169            error!("failed to create a temporary file [dir={:?}]: {e}", dir);
170            TbError::from(FAILED_TO_WRITE_OUTPUT_FILE)
171        })?;
172
173    let (tmp_file, tmp_path) = tmp_file.keep().map_err(|e| {
174        error!("failed to keep the temporary file: {e}");
175        TbError::from(FAILED_TO_WRITE_OUTPUT_FILE)
176    })?;
177
178    write_and_format(code, &tmp_path, tmp_file)?;
179
180    let new_data = read_code(&tmp_path)?;
181
182    if existing_data != new_data {
183        std::fs::rename(&tmp_path, path).map_err(|e| {
184            error!(
185                "failed to rename updated file {:?} to output file path {:?}: {e}",
186                tmp_path, path
187            );
188            TbError::from(FAILED_TO_WRITE_OUTPUT_FILE)
189        })
190    } else {
191        std::fs::remove_file(&tmp_path).map_err(|e| {
192            error!("failed to unlink temporary file {:?}: {e}", tmp_path);
193            TbError::from(FAILED_TO_WRITE_OUTPUT_FILE)
194        })
195    }
196}