dsync/
lib.rs

1//! dsync library
2//!
3//! The dsync library allows creating a custom binary for dsync
4//!
5//! ## Features
6//!
7//! - `async`: enable support for [diesel_async](https://github.com/weiznich/diesel_async)
8//! - `tsync`: enable support for [tsync](https://github.com/Wulf/tsync)
9//! - `backtrace`: enable attaching backtraces to dsync errors
10//! - `derive-queryablebyname`: enable `diesel::QueryableByName` derives on READ structs
11//! - `advanced-queries`: enable experimental pagination and filter functions ([examples](https://github.com/Wulf/dsync/tree/a44afdd08f4447e367aa47ecb91fae88b57f8944/test/advanced_queries))
12//!
13//! default features: `tsync`, `backtrace`, `derive-queryablebyname`
14
15mod code;
16pub mod error;
17mod file;
18mod global;
19mod parser;
20
21pub use global::{
22    BytesType, GenerationConfig, GenerationConfigOpts, StringType, TableOptions,
23    DEFAULT_MODEL_PATH, DEFAULT_SCHEMA_PATH,
24};
25
26use error::IOErrorToError;
27pub use error::{Error, Result};
28use file::MarkedFile;
29use heck::ToSnakeCase;
30use parser::ParsedTableMacro;
31pub use parser::FILE_SIGNATURE;
32use std::fmt::Display;
33use std::path::{Path, PathBuf};
34
35/// Generate a model for the given schema contents
36///
37/// Model is returned and not saved to disk yet
38pub fn generate_code(
39    diesel_schema_file_contents: &str,
40    config: &GenerationConfig,
41) -> Result<Vec<ParsedTableMacro>> {
42    parser::parse_and_generate_code(diesel_schema_file_contents, config)
43}
44
45/// Status indicating what happened to a file
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum FileChangeStatus {
48    /// Status for unchanged file contents
49    Unchanged,
50    /// Status for modified file contents
51    Modified,
52    /// Status if the file has been deleted
53    Deleted,
54}
55
56impl Display for FileChangeStatus {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(
59            f,
60            "{}",
61            match self {
62                FileChangeStatus::Unchanged => "Unchanged",
63                FileChangeStatus::Modified => "Modified",
64                FileChangeStatus::Deleted => "Deleted",
65            }
66        )
67    }
68}
69
70/// Status indicating what happened to a specific file
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct FileChange {
73    /// File in question
74    pub file: PathBuf,
75    /// Status of the file
76    pub status: FileChangeStatus,
77}
78
79impl FileChange {
80    pub fn new<P: AsRef<std::path::Path>>(path: P, status: FileChangeStatus) -> Self {
81        Self {
82            file: path.as_ref().to_owned(),
83            status,
84        }
85    }
86}
87
88// easily create a [FileChange] from a [MarkedFile]
89impl From<&MarkedFile> for FileChange {
90    fn from(value: &MarkedFile) -> Self {
91        if value.is_modified() {
92            Self::new(&value.path, FileChangeStatus::Modified)
93        } else {
94            Self::new(&value.path, FileChangeStatus::Unchanged)
95        }
96    }
97}
98
99/// Helper function for consistent table module name generation
100/// this is used for the rust module path name and for the filename
101///
102/// input: "tableA", output -> "table_a"
103fn get_table_module_name(table_name: &str) -> String {
104    table_name.to_snake_case().to_lowercase()
105}
106
107/// Generate all Models for a given diesel schema file
108///
109/// Models are saved to disk
110pub fn generate_files(
111    input_diesel_schema_file: &Path,
112    output_models_dir: &Path,
113    config: GenerationConfig,
114) -> Result<Vec<FileChange>> {
115    global::validate_config(&config)?;
116
117    let generated = generate_code(
118        &std::fs::read_to_string(input_diesel_schema_file)
119            .attach_path_err(input_diesel_schema_file)?,
120        &config,
121    )?;
122
123    if !output_models_dir.exists() {
124        std::fs::create_dir(output_models_dir).attach_path_err(output_models_dir)?;
125    } else if !output_models_dir.is_dir() {
126        return Err(Error::not_a_directory(
127            "Expected output argument to be a directory or non-existent.",
128            output_models_dir,
129        ));
130    }
131
132    // using generated len, because that is very likely the amount (at least) for files
133    let mut file_changes = Vec::with_capacity(generated.len());
134
135    // check that the mod.rs file exists
136    let mut mod_rs = MarkedFile::new(output_models_dir.join("mod.rs"))?;
137
138    if config.any_once_option() {
139        let mut common_file = MarkedFile::new(output_models_dir.join("common.rs"))?;
140        common_file.ensure_file_signature()?;
141        common_file.change_file_contents({
142            let mut tmp = format!("{FILE_SIGNATURE}\n");
143            if config.get_once_common_structs() {
144                tmp.push_str(&code::generate_common_structs(
145                    config.get_default_table_options(),
146                ));
147            }
148            if config.get_once_connection_type() {
149                tmp.push('\n');
150                tmp.push_str(&code::generate_connection_type(&config));
151
152                // add ending new-line, this should not cause duplicate new-lines because this only gets run if any of the options is set
153                // this will need to be refactored if there should ever be more options using common_file
154                tmp.push('\n');
155            }
156
157            tmp
158        });
159        common_file.write()?;
160        file_changes.push(FileChange::from(&common_file));
161
162        mod_rs.ensure_mod_stmt("common");
163    }
164
165    // pass 1: add code for new tables
166    for table in generated.iter() {
167        if config.get_once_common_structs() && table.name == "common" {
168            return Err(Error::other("Cannot have a table named \"common\" while having option \"once_common_structs\" enabled"));
169        }
170        let table_name = table.name.to_string();
171        let table_filename = get_table_module_name(&table_name);
172        let table_config = config.table(&table_name);
173        let table_dir = if table_config.get_single_model_file() {
174            output_models_dir.to_owned()
175        } else {
176            output_models_dir.join(&table_filename)
177        };
178
179        if !table_dir.exists() {
180            std::fs::create_dir(&table_dir).attach_path_err(&table_dir)?;
181        }
182
183        if !table_dir.is_dir() {
184            return Err(Error::not_a_directory("Expected a directory", table_dir));
185        }
186
187        let table_file_name = if table_config.get_single_model_file() {
188            let mut table_name = table_name;
189            table_name.push_str(".rs");
190            table_name
191        } else {
192            "generated.rs".into()
193        };
194
195        let mut table_generated_rs = MarkedFile::new(table_dir.join(table_file_name))?;
196        let mut table_mod_rs = MarkedFile::new(table_dir.join("mod.rs"))?;
197
198        table_generated_rs.ensure_file_signature()?;
199        table_generated_rs.change_file_contents(table.generated_code.clone());
200        table_generated_rs.write()?;
201
202        file_changes.push(FileChange::from(&table_generated_rs));
203
204        if !table_config.get_single_model_file() {
205            table_mod_rs.ensure_mod_stmt("generated");
206            table_mod_rs.ensure_use_stmt("generated::*");
207            table_mod_rs.write()?;
208            file_changes.push(FileChange::from(&table_mod_rs));
209        }
210
211        mod_rs.ensure_mod_stmt(&table_filename);
212    }
213
214    // pass 2: delete code for removed tables
215    for item in std::fs::read_dir(output_models_dir).attach_path_err(output_models_dir)? {
216        // TODO: this does not work with "single-model-file"
217        let item = item.attach_path_err(output_models_dir)?;
218
219        // check if item is a directory
220        let file_type = item
221            .file_type()
222            .attach_path_msg(item.path(), "Could not determine type of file")?;
223        if !file_type.is_dir() {
224            continue;
225        }
226
227        // check if it's a generated file
228        let generated_rs_path = item.path().join("generated.rs");
229        if !generated_rs_path.exists()
230            || !generated_rs_path.is_file()
231            || !MarkedFile::new(generated_rs_path.clone())?.has_file_signature()
232        {
233            continue;
234        }
235
236        // okay, it's generated, but we need to check if it's for a deleted table
237        let file_name = item.file_name();
238        let associated_table_name = file_name.to_str().ok_or(Error::other(format!(
239            "Could not determine name of file '{:#?}'",
240            item.path()
241        )))?;
242        let found = generated.iter().find(|g| {
243            get_table_module_name(&g.name.to_string()).eq_ignore_ascii_case(associated_table_name)
244        });
245        if found.is_some() {
246            continue;
247        }
248
249        // this table was deleted, let's delete the generated code
250        std::fs::remove_file(&generated_rs_path).attach_path_err(&generated_rs_path)?;
251        file_changes.push(FileChange::new(
252            &generated_rs_path,
253            FileChangeStatus::Deleted,
254        ));
255
256        // remove the mod.rs file if there isn't anything left in there except the use stmt
257        let table_mod_rs_path = item.path().join("mod.rs");
258        if table_mod_rs_path.exists() {
259            let mut table_mod_rs = MarkedFile::new(table_mod_rs_path)?;
260
261            table_mod_rs.remove_mod_stmt("generated");
262            table_mod_rs.remove_use_stmt("generated::*");
263            table_mod_rs.write()?;
264
265            if table_mod_rs.get_file_contents().trim().is_empty() {
266                let table_mod_rs = table_mod_rs.delete()?;
267                file_changes.push(FileChange::new(table_mod_rs, FileChangeStatus::Deleted));
268            } else {
269                table_mod_rs.write()?; // write the changes we made above
270                file_changes.push(FileChange::from(&table_mod_rs));
271            }
272        }
273
274        // delete the table dir if there's nothing else in there
275        let is_empty = item
276            .path()
277            .read_dir()
278            .attach_path_err(item.path())?
279            .next()
280            .is_none();
281        if is_empty {
282            std::fs::remove_dir(item.path()).attach_path_err(item.path())?;
283        }
284
285        // remove the module from the main mod_rs file
286        mod_rs.remove_mod_stmt(associated_table_name);
287    }
288
289    mod_rs.write()?;
290    file_changes.push(FileChange::from(&mod_rs));
291
292    Ok(file_changes)
293}