dsync_hasezoey/
lib.rs

1mod code;
2mod error;
3mod file;
4mod parser;
5
6use code::get_connection_type_name;
7use error::IOErrorToError;
8pub use error::{Error, Result};
9
10use file::MarkedFile;
11use parser::ParsedTableMacro;
12pub use parser::FILE_SIGNATURE;
13use std::collections::HashMap;
14use std::fmt::Display;
15use std::path::PathBuf;
16
17/// Individual Options for a given table
18#[derive(Debug, Clone, Default)]
19pub struct TableOptions<'a> {
20    /// Ignore a specific table
21    ignore: Option<bool>,
22
23    /// Names used for autogenerated columns which are NOT primary keys (for example: `created_at`, `updated_at`, etc.).
24    autogenerated_columns: Option<Vec<&'a str>>,
25
26    #[cfg(feature = "tsync")]
27    /// Adds #[tsync] attribute to structs (see https://github.com/Wulf/tsync)
28    tsync: Option<bool>,
29
30    #[cfg(feature = "async")]
31    /// Uses diesel_async for generated functions (see https://github.com/weiznich/diesel_async)
32    use_async: Option<bool>,
33
34    /// Generates serde::Serialize and serde::Deserialize derive implementations
35    use_serde: Option<bool>,
36
37    /// Only Generate the necessary derives for a struct
38    only_necessary_derives: Option<bool>,
39
40    /// Indicate that this table is read-only
41    read_only: Option<bool>,
42
43    /// Indicated whether or not to generate "impls"
44    impls: Option<bool>,
45
46    /// Use "str" over "String" for "Create*" structs
47    create_str_over_string: Option<bool>,
48}
49
50impl<'a> TableOptions<'a> {
51    pub fn get_ignore(&self) -> bool {
52        self.ignore.unwrap_or_default()
53    }
54
55    #[cfg(feature = "tsync")]
56    pub fn get_tsync(&self) -> bool {
57        self.tsync.unwrap_or_default()
58    }
59
60    #[cfg(feature = "async")]
61    pub fn get_async(&self) -> bool {
62        self.use_async.unwrap_or_default()
63    }
64
65    pub fn get_serde(&self) -> bool {
66        self.use_serde.unwrap_or(true)
67    }
68
69    pub fn get_only_necessary_derives(&self) -> bool {
70        self.only_necessary_derives.unwrap_or(false)
71    }
72
73    pub fn get_autogenerated_columns(&self) -> &[&'_ str] {
74        self.autogenerated_columns.as_deref().unwrap_or_default()
75    }
76
77    pub fn get_read_only(&self) -> bool {
78        self.read_only.unwrap_or_default()
79    }
80
81    pub fn get_generate_impls(&self) -> bool {
82        self.impls.unwrap_or(true)
83    }
84
85    pub fn get_create_str(&self) -> bool {
86        self.create_str_over_string.unwrap_or(false)
87    }
88
89    pub fn ignore(self) -> Self {
90        Self {
91            ignore: Some(true),
92            ..self
93        }
94    }
95
96    #[cfg(feature = "tsync")]
97    pub fn tsync(self) -> Self {
98        Self {
99            tsync: Some(true),
100            ..self
101        }
102    }
103
104    #[cfg(feature = "async")]
105    pub fn use_async(self) -> Self {
106        Self {
107            use_async: Some(true),
108            ..self
109        }
110    }
111
112    pub fn disable_serde(self) -> Self {
113        Self {
114            use_serde: Some(false),
115            ..self
116        }
117    }
118
119    pub fn only_necessary_derives(self) -> Self {
120        Self {
121            only_necessary_derives: Some(true),
122            ..self
123        }
124    }
125
126    pub fn autogenerated_columns(self, cols: Vec<&'a str>) -> Self {
127        Self {
128            autogenerated_columns: Some(cols),
129            ..self
130        }
131    }
132
133    pub fn disable_impls(self) -> Self {
134        Self {
135            impls: Some(false),
136            ..self
137        }
138    }
139
140    pub fn create_str_over_string(self) -> Self {
141        Self {
142            create_str_over_string: Some(true),
143            ..self
144        }
145    }
146
147    pub fn set_read_only(&mut self, bool: bool) {
148        self.read_only = Some(bool);
149    }
150
151    /// Fills any `None` properties with values from another TableConfig
152    pub fn apply_defaults(&self, other: &TableOptions<'a>) -> Self {
153        Self {
154            ignore: self.ignore.or(other.ignore),
155            #[cfg(feature = "tsync")]
156            tsync: self.tsync.or(other.tsync),
157            #[cfg(feature = "async")]
158            use_async: self.use_async.or(other.use_async),
159            autogenerated_columns: self
160                .autogenerated_columns
161                .clone()
162                .or_else(|| other.autogenerated_columns.clone()),
163
164            use_serde: self.use_serde.or(other.use_serde),
165            only_necessary_derives: self.only_necessary_derives.or(other.only_necessary_derives),
166            read_only: self.read_only.or(other.read_only),
167            impls: self.impls.or(other.impls),
168            create_str_over_string: self.create_str_over_string.or(other.create_str_over_string),
169        }
170    }
171}
172
173#[derive(Debug, Clone)]
174pub struct GenerationConfig<'a> {
175    /// Specific Table options for a given table
176    pub table_options: HashMap<&'a str, TableOptions<'a>>,
177    /// Default table options, used when not in `table_options`
178    pub default_table_options: TableOptions<'a>,
179    /// Connection type to insert
180    /// Example: "diesel::SqliteConnection"
181    pub connection_type: String,
182    /// diesel schema path to use
183    /// Example: "crate::schema::"
184    pub schema_path: String,
185    /// model path to use
186    /// Example: "crate::models::"
187    pub model_path: String,
188    /// Only generate common structs once and put them in a common file
189    pub once_common_structs: bool,
190    /// Only generate a single model file instead of a folder with a "mod.rs" and a "generated.rs"
191    pub single_model_file: bool,
192    /// Set which filemode to use
193    pub file_mode: FileMode,
194    /// A prefix of read-only tables
195    pub read_only_prefix: Option<Vec<String>>,
196    /// Only generate the "Connection" type once
197    pub once_connection: bool,
198    /// Lessen conflicts with diesel types
199    pub lessen_conflicts: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum FileMode {
204    /// Overwrite the file path, as long as a dsync signature is present
205    Overwrite,
206    /// Create a ".dsyncnew" file if changed
207    NewFile,
208    /// Do nothing for the file
209    None,
210}
211
212impl GenerationConfig<'_> {
213    pub fn table(&self, name: &str) -> TableOptions<'_> {
214        let res = self
215            .table_options
216            .get(name)
217            .unwrap_or(&self.default_table_options);
218
219        let mut res = res.apply_defaults(&self.default_table_options);
220
221        if let Some(ref read_only_prefix) = self.read_only_prefix {
222            if read_only_prefix.iter().any(|v| name.starts_with(v)) {
223                res.set_read_only(true);
224            }
225        }
226
227        res
228    }
229}
230
231/// Generate a model for a given schema
232/// Model is returned and not saved to disk
233pub fn generate_code(
234    diesel_schema_file_contents: String,
235    config: &GenerationConfig,
236) -> Result<Vec<ParsedTableMacro>> {
237    parser::parse_and_generate_code(diesel_schema_file_contents, config)
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum FileChangesStatus {
242    /// Status to mark unchanged file contents
243    Unchanged,
244    /// Status to mark unchanged files, because of some ignore rule (like [FileMode::None])
245    UnchangedIgnored,
246    /// Status to mark overwritten file contents
247    Overwritten,
248    /// Status to mark a ".dsyncnew" file being generated
249    /// and the path to the original file
250    NewFile(PathBuf),
251    /// Status to mark file contents to be modified
252    Modified,
253    /// Status if the file has been deleted
254    Deleted,
255    /// Status if the file should be deleted, but is not because of some ignore rule (like [FileMode::None])
256    DeletedIgnored,
257}
258
259impl Display for FileChangesStatus {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        write!(
262            f,
263            "{}",
264            match self {
265                FileChangesStatus::Unchanged => "Unchanged",
266                FileChangesStatus::UnchangedIgnored => "Unchanged(Ignored)",
267                FileChangesStatus::Overwritten => "Overwritten",
268                FileChangesStatus::Modified => "Modified",
269                FileChangesStatus::Deleted => "Deleted",
270                FileChangesStatus::DeletedIgnored => "Deleted(Ignored)",
271                FileChangesStatus::NewFile(_) => "NewFile",
272            }
273        )
274    }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct FileChanges {
279    /// File in question
280    pub file: PathBuf,
281    /// Status of the file
282    pub status: FileChangesStatus,
283}
284
285impl FileChanges {
286    pub fn new<P: AsRef<std::path::Path>>(path: P, status: FileChangesStatus) -> Self {
287        Self {
288            file: path.as_ref().to_owned(),
289            status,
290        }
291    }
292
293    /// Create a new instance based on if the input file is modified or not
294    /// using either `status_modified` or `status_unmodified`
295    pub fn from_markedfile_custom(
296        marked_file: &MarkedFile,
297        status_modified: FileChangesStatus,
298        status_unmodified: FileChangesStatus,
299    ) -> Self {
300        if marked_file.is_modified() {
301            Self::new(marked_file, status_modified)
302        } else {
303            Self::new(marked_file, status_unmodified)
304        }
305    }
306}
307
308impl From<&MarkedFile> for FileChanges {
309    fn from(value: &MarkedFile) -> Self {
310        Self::from_markedfile_custom(
311            value,
312            FileChangesStatus::Modified,
313            FileChangesStatus::Unchanged,
314        )
315    }
316}
317
318/// The file extension to use for [FileMode::NewFile]
319/// also adds another ".rs", so that IDE's can syntax highlight correctly
320const DSYNCNEW: &str = ".dsyncnew.rs";
321
322/// Write a [MarkedFile] depending on what [FileMode] is used and add it to [Vec<FileChanges>]
323fn write_file(
324    config: &GenerationConfig,
325    mut file: MarkedFile,
326    file_status: &mut Vec<FileChanges>,
327) -> Result<()> {
328    let (write, file_change_status) = match config.file_mode {
329        FileMode::Overwrite => (true, FileChangesStatus::Modified),
330        FileMode::NewFile => {
331            let old_path = file.path;
332            let mut file_name = old_path
333                .file_name()
334                .ok_or(Error::other("Expected file to have a file_name"))?
335                .to_os_string();
336            file_name.push(DSYNCNEW);
337
338            file.path = old_path.clone();
339            file.path.set_file_name(file_name);
340
341            (true, FileChangesStatus::NewFile(old_path))
342        }
343        FileMode::None => (false, FileChangesStatus::UnchangedIgnored),
344    };
345
346    // additional "is_modified" check, because "newfile" changed the path and write would generate a file even if unchanged
347    if write && file.is_modified() {
348        file.write()?;
349    }
350
351    // set status to "Unchanged" if no change happened and to "UnchangedIgnored" if a change happened, but not written
352    file_status.push(FileChanges::from_markedfile_custom(
353        &file,
354        file_change_status,
355        FileChangesStatus::Unchanged,
356    ));
357
358    Ok(())
359}
360
361/// Generate all models for a given diesel schema input file
362/// Models are saved to disk
363pub fn generate_files(
364    input_diesel_schema_file: PathBuf,
365    output_models_dir: PathBuf,
366    config: GenerationConfig,
367) -> Result<Vec<FileChanges>> {
368    let input = input_diesel_schema_file;
369    let output_dir = output_models_dir;
370
371    let generated = generate_code(
372        std::fs::read_to_string(&input).attach_path_err(&input)?,
373        &config,
374    )?;
375
376    if !output_dir.exists() {
377        std::fs::create_dir(&output_dir).attach_path_err(&output_dir)?;
378    } else if !output_dir.is_dir() {
379        return Err(Error::not_a_directory(
380            "Expected output argument to be a directory or non-existent.",
381            output_dir,
382        ));
383    }
384
385    let mut file_status = Vec::new();
386
387    // check that the mod.rs file exists
388    let mut mod_rs = MarkedFile::new(output_dir.join("mod.rs"))?;
389
390    let mut common_file = MarkedFile::new(output_dir.join("common.rs"))?;
391
392    // dont check file signature if a ".dsyncnew" file will be generated
393    if config.file_mode != FileMode::NewFile {
394        common_file.ensure_file_signature()?;
395    }
396
397    common_file.change_file_contents_no_modify(format!("{}\n", FILE_SIGNATURE));
398
399    if config.once_common_structs {
400        common_file.change_file_contents({
401            let mut tmp = String::from(common_file.get_file_contents());
402            tmp.push_str(&code::generate_common_structs(
403                &config.default_table_options,
404            ));
405            tmp
406        });
407    }
408
409    if config.once_connection {
410        common_file.change_file_contents({
411            let mut tmp = String::from(common_file.get_file_contents());
412            if !common_file.is_empty() {
413                tmp.push('\n');
414            }
415            tmp.push_str(&format!(
416                "/// Connection Type as set in dsync\npub type {} = {};\n",
417                get_connection_type_name(&config),
418                config.connection_type
419            ));
420            tmp
421        })
422    }
423
424    if !common_file.is_empty() {
425        // always write the "mod" statement, even if "write_file" is not writing
426        mod_rs.ensure_mod_stmt("common");
427    }
428
429    write_file(&config, common_file, &mut file_status)?;
430
431    // pass 1: add code for new tables
432    for table in generated.iter() {
433        if config.once_common_structs && table.name == "common" {
434            return Err(Error::other("Cannot have a table named \"common\" while having option \"once_common_structs\" enabled"));
435        }
436
437        let table_name = table.name.to_string();
438        let table_dir = if config.single_model_file {
439            output_dir.clone()
440        } else {
441            output_dir.join(&table_name)
442        };
443
444        if !table_dir.exists() {
445            std::fs::create_dir(&table_dir).attach_path_err(&table_dir)?;
446        }
447
448        if !table_dir.is_dir() {
449            return Err(Error::not_a_directory("Expected a directory", table_dir));
450        }
451
452        let table_file_name = if config.single_model_file {
453            let mut table_name = table_name; // avoid a clone, because its the last usage of "table_name"
454            table_name.push_str(".rs");
455            table_name
456        } else {
457            "generated.rs".into()
458        };
459
460        let mut table_generated_rs = MarkedFile::new(table_dir.join(table_file_name))?;
461
462        // dont check file signature if a ".dsyncnew" file will be generated
463        if config.file_mode != FileMode::NewFile {
464            table_generated_rs.ensure_file_signature()?;
465        }
466
467        table_generated_rs.change_file_contents(
468            table
469                .generated_code
470                .as_ref()
471                .ok_or(Error::other(format!(
472                    "Expected code for table \"{}\" to be generated",
473                    table.struct_name
474                )))?
475                .clone(),
476        );
477
478        write_file(&config, table_generated_rs, &mut file_status)?;
479
480        if !config.single_model_file {
481            let mut table_mod_rs = MarkedFile::new(table_dir.join("mod.rs"))?;
482
483            table_mod_rs.ensure_mod_stmt("generated");
484            table_mod_rs.ensure_use_stmt("generated::*");
485            // always write the "mod" statement, even if "write_file" is not writing
486            table_mod_rs.write()?;
487
488            file_status.push(FileChanges::from(&table_mod_rs));
489        }
490
491        mod_rs.ensure_mod_stmt(table.name.to_string().as_str());
492    }
493
494    // pass 2: delete code for removed tables
495    for item in std::fs::read_dir(&output_dir).attach_path_err(&output_dir)? {
496        let item = item.attach_path_err(&output_dir)?;
497
498        // check if item is a directory
499        let file_type = item
500            .file_type()
501            .attach_path_msg(item.path(), "Could not determine type of file")?;
502        if !file_type.is_dir() {
503            continue;
504        }
505
506        // check if it's a generated file
507        let generated_rs_path = item.path().join("generated.rs");
508        if !generated_rs_path.exists()
509            || !generated_rs_path.is_file()
510            || !MarkedFile::new(generated_rs_path.clone())?.has_file_signature()
511        {
512            continue;
513        }
514
515        // okay, it's generated, but we need to check if it's for a deleted table
516        let file_name = item.file_name();
517        let associated_table_name = file_name.to_str().ok_or(Error::other(format!(
518            "Could not determine name of file '{:#?}'",
519            item.path()
520        )))?;
521        let found = generated.iter().find(|g| {
522            g.name
523                .to_string()
524                .eq_ignore_ascii_case(associated_table_name)
525        });
526        if found.is_some() {
527            continue;
528        }
529
530        match config.file_mode {
531            FileMode::Overwrite => {
532                // this table was deleted, let's delete the generated code
533                std::fs::remove_file(&generated_rs_path).attach_path_err(&generated_rs_path)?;
534                file_status.push(FileChanges::new(
535                    &generated_rs_path,
536                    FileChangesStatus::Deleted,
537                ));
538            }
539            FileMode::NewFile | FileMode::None => {
540                file_status.push(FileChanges::new(
541                    &generated_rs_path,
542                    FileChangesStatus::DeletedIgnored,
543                ));
544            }
545        }
546
547        // remove the mod.rs file if there isn't anything left in there except the use stmt
548        let table_mod_rs_path = item.path().join("mod.rs");
549        if table_mod_rs_path.exists() {
550            let mut table_mod_rs = MarkedFile::new(table_mod_rs_path)?;
551
552            table_mod_rs.remove_mod_stmt("generated");
553            table_mod_rs.remove_use_stmt("generated::*");
554
555            if table_mod_rs.get_file_contents().trim().is_empty() {
556                if config.file_mode == FileMode::Overwrite {
557                    let table_mod_rs = table_mod_rs.delete()?;
558                    file_status.push(FileChanges::new(&table_mod_rs, FileChangesStatus::Deleted));
559                } else {
560                    file_status.push(FileChanges::new(
561                        &table_mod_rs,
562                        FileChangesStatus::DeletedIgnored,
563                    ));
564                }
565            } else {
566                // not using "write_file" because of custom "NewFile" handling
567                let (write, file_change_status) = match config.file_mode {
568                    FileMode::Overwrite => (true, FileChangesStatus::Modified),
569                    FileMode::NewFile | FileMode::None => {
570                        (false, FileChangesStatus::UnchangedIgnored)
571                    }
572                };
573
574                if write && table_mod_rs.is_modified() {
575                    table_mod_rs.write()?;
576                }
577
578                // set status to "Unchanged" if no change happened and to "UnchangedIgnored" if a change happened, but not written
579                file_status.push(FileChanges::from_markedfile_custom(
580                    &table_mod_rs,
581                    file_change_status,
582                    FileChangesStatus::Unchanged,
583                ));
584            }
585        }
586
587        // delete the table dir if there's nothing else in there
588        let is_empty = item
589            .path()
590            .read_dir()
591            .attach_path_err(item.path())?
592            .next()
593            .is_none();
594        if is_empty {
595            std::fs::remove_dir(item.path()).attach_path_err(item.path())?;
596        }
597
598        // dont remove "mod" statement on delete for anything other than ::Overwrite
599        if config.file_mode == FileMode::Overwrite {
600            // remove the module from the main mod_rs file
601            mod_rs.remove_mod_stmt(associated_table_name);
602        }
603    }
604
605    // always write the "mod" statement, even if "write_file" is not writing
606    mod_rs.write()?;
607
608    file_status.push(FileChanges::from(&mod_rs));
609
610    Ok(file_status)
611}