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#[derive(Debug, Clone, Default)]
19pub struct TableOptions<'a> {
20 ignore: Option<bool>,
22
23 autogenerated_columns: Option<Vec<&'a str>>,
25
26 #[cfg(feature = "tsync")]
27 tsync: Option<bool>,
29
30 #[cfg(feature = "async")]
31 use_async: Option<bool>,
33
34 use_serde: Option<bool>,
36
37 only_necessary_derives: Option<bool>,
39
40 read_only: Option<bool>,
42
43 impls: Option<bool>,
45
46 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 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 pub table_options: HashMap<&'a str, TableOptions<'a>>,
177 pub default_table_options: TableOptions<'a>,
179 pub connection_type: String,
182 pub schema_path: String,
185 pub model_path: String,
188 pub once_common_structs: bool,
190 pub single_model_file: bool,
192 pub file_mode: FileMode,
194 pub read_only_prefix: Option<Vec<String>>,
196 pub once_connection: bool,
198 pub lessen_conflicts: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum FileMode {
204 Overwrite,
206 NewFile,
208 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
231pub 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 Unchanged,
244 UnchangedIgnored,
246 Overwritten,
248 NewFile(PathBuf),
251 Modified,
253 Deleted,
255 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 pub file: PathBuf,
281 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 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
318const DSYNCNEW: &str = ".dsyncnew.rs";
321
322fn 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 if write && file.is_modified() {
348 file.write()?;
349 }
350
351 file_status.push(FileChanges::from_markedfile_custom(
353 &file,
354 file_change_status,
355 FileChangesStatus::Unchanged,
356 ));
357
358 Ok(())
359}
360
361pub 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 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 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 mod_rs.ensure_mod_stmt("common");
427 }
428
429 write_file(&config, common_file, &mut file_status)?;
430
431 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; 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 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 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 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 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 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 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 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 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 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 file_status.push(FileChanges::from_markedfile_custom(
580 &table_mod_rs,
581 file_change_status,
582 FileChangesStatus::Unchanged,
583 ));
584 }
585 }
586
587 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 if config.file_mode == FileMode::Overwrite {
600 mod_rs.remove_mod_stmt(associated_table_name);
602 }
603 }
604
605 mod_rs.write()?;
607
608 file_status.push(FileChanges::from(&mod_rs));
609
610 Ok(file_status)
611}