mod code;
mod error;
mod file;
mod parser;
use error::IOErrorToError;
pub use error::{Error, Result};
use file::MarkedFile;
use parser::ParsedTableMacro;
pub use parser::FILE_SIGNATURE;
use std::collections::HashMap;
use std::fmt::Display;
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct TableOptions<'a> {
ignore: Option<bool>,
autogenerated_columns: Option<Vec<&'a str>>,
#[cfg(feature = "tsync")]
tsync: Option<bool>,
#[cfg(feature = "async")]
use_async: Option<bool>,
use_serde: Option<bool>,
only_necessary_derives: Option<bool>,
}
impl<'a> TableOptions<'a> {
pub fn get_ignore(&self) -> bool {
self.ignore.unwrap_or_default()
}
#[cfg(feature = "tsync")]
pub fn get_tsync(&self) -> bool {
self.tsync.unwrap_or_default()
}
#[cfg(feature = "async")]
pub fn get_async(&self) -> bool {
self.use_async.unwrap_or_default()
}
pub fn get_serde(&self) -> bool {
self.use_serde.unwrap_or(true)
}
pub fn get_only_necessary_derives(&self) -> bool {
self.only_necessary_derives.unwrap_or(false)
}
pub fn get_autogenerated_columns(&self) -> &[&'_ str] {
self.autogenerated_columns.as_deref().unwrap_or_default()
}
pub fn ignore(self) -> Self {
Self {
ignore: Some(true),
..self
}
}
#[cfg(feature = "tsync")]
pub fn tsync(self) -> Self {
Self {
tsync: Some(true),
..self
}
}
#[cfg(feature = "async")]
pub fn use_async(self) -> Self {
Self {
use_async: Some(true),
..self
}
}
pub fn disable_serde(self) -> Self {
Self {
use_serde: Some(false),
..self
}
}
pub fn only_necessary_derives(self) -> Self {
Self {
only_necessary_derives: Some(true),
..self
}
}
pub fn autogenerated_columns(self, cols: Vec<&'a str>) -> Self {
Self {
autogenerated_columns: Some(cols),
..self
}
}
pub fn apply_defaults(&self, other: &TableOptions<'a>) -> Self {
Self {
ignore: self.ignore.or(other.ignore),
#[cfg(feature = "tsync")]
tsync: self.tsync.or(other.tsync),
#[cfg(feature = "async")]
use_async: self.use_async.or(other.use_async),
autogenerated_columns: self
.autogenerated_columns
.clone()
.or_else(|| other.autogenerated_columns.clone()),
use_serde: self.use_serde.or(other.use_serde),
only_necessary_derives: self.only_necessary_derives.or(other.only_necessary_derives),
}
}
}
#[derive(Debug, Clone)]
pub struct GenerationConfig<'a> {
pub table_options: HashMap<&'a str, TableOptions<'a>>,
pub default_table_options: TableOptions<'a>,
pub connection_type: String,
pub schema_path: String,
pub model_path: String,
pub once_common_structs: bool,
pub single_model_file: bool,
pub file_mode: FileMode,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileMode {
Overwrite,
NewFile,
None,
}
impl GenerationConfig<'_> {
pub fn table(&self, name: &str) -> TableOptions<'_> {
let t = self
.table_options
.get(name)
.unwrap_or(&self.default_table_options);
t.apply_defaults(&self.default_table_options)
}
}
pub fn generate_code(
diesel_schema_file_contents: String,
config: &GenerationConfig,
) -> Result<Vec<ParsedTableMacro>> {
parser::parse_and_generate_code(diesel_schema_file_contents, config)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileChangesStatus {
Unchanged,
UnchangedIgnored,
Overwritten,
NewFile(PathBuf),
Modified,
Deleted,
DeletedIgnored,
}
impl Display for FileChangesStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
FileChangesStatus::Unchanged => "Unchanged",
FileChangesStatus::UnchangedIgnored => "Unchanged(Ignored)",
FileChangesStatus::Overwritten => "Overwritten",
FileChangesStatus::Modified => "Modified",
FileChangesStatus::Deleted => "Deleted",
FileChangesStatus::DeletedIgnored => "Deleted(Ignored)",
FileChangesStatus::NewFile(_) => "NewFile",
}
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileChanges {
pub file: PathBuf,
pub status: FileChangesStatus,
}
impl FileChanges {
pub fn new<P: AsRef<std::path::Path>>(path: P, status: FileChangesStatus) -> Self {
Self {
file: path.as_ref().to_owned(),
status,
}
}
pub fn from_markedfile_custom(
marked_file: &MarkedFile,
status_modified: FileChangesStatus,
status_unmodified: FileChangesStatus,
) -> Self {
if marked_file.is_modified() {
Self::new(marked_file, status_modified)
} else {
Self::new(marked_file, status_unmodified)
}
}
}
impl From<&MarkedFile> for FileChanges {
fn from(value: &MarkedFile) -> Self {
Self::from_markedfile_custom(
value,
FileChangesStatus::Modified,
FileChangesStatus::Unchanged,
)
}
}
const DSYNCNEW: &str = ".dsyncnew.rs";
fn write_file(
config: &GenerationConfig,
mut file: MarkedFile,
file_status: &mut Vec<FileChanges>,
) -> Result<()> {
let (write, file_change_status) = match config.file_mode {
FileMode::Overwrite => (true, FileChangesStatus::Modified),
FileMode::NewFile => {
let old_path = file.path;
let mut file_name = old_path
.file_name()
.ok_or(Error::other("Expected file to have a file_name"))?
.to_os_string();
file_name.push(DSYNCNEW);
file.path = old_path.clone();
file.path.set_file_name(file_name);
(true, FileChangesStatus::NewFile(old_path))
}
FileMode::None => (false, FileChangesStatus::UnchangedIgnored),
};
if write && file.is_modified() {
file.write()?;
}
file_status.push(FileChanges::from_markedfile_custom(
&file,
file_change_status,
FileChangesStatus::Unchanged,
));
Ok(())
}
pub fn generate_files(
input_diesel_schema_file: PathBuf,
output_models_dir: PathBuf,
config: GenerationConfig,
) -> Result<Vec<FileChanges>> {
let input = input_diesel_schema_file;
let output_dir = output_models_dir;
let generated = generate_code(
std::fs::read_to_string(&input).attach_path_err(&input)?,
&config,
)?;
if !output_dir.exists() {
std::fs::create_dir(&output_dir).attach_path_err(&output_dir)?;
} else if !output_dir.is_dir() {
return Err(Error::not_a_directory(
"Expected output argument to be a directory or non-existent.",
output_dir,
));
}
let mut file_status = Vec::new();
let mut mod_rs = MarkedFile::new(output_dir.join("mod.rs"))?;
if config.once_common_structs {
let mut common_file = MarkedFile::new(output_dir.join("common.rs"))?;
if config.file_mode != FileMode::NewFile {
common_file.ensure_file_signature()?;
}
common_file.change_file_contents({
let mut tmp = String::from(FILE_SIGNATURE);
tmp.push('\n');
tmp.push_str(&code::generate_common_structs(
&config.default_table_options,
));
tmp
});
write_file(&config, common_file, &mut file_status)?;
mod_rs.ensure_mod_stmt("common");
}
for table in generated.iter() {
if config.once_common_structs && table.name == "common" {
return Err(Error::other("Cannot have a table named \"common\" while having option \"once_common_structs\" enabled"));
}
let table_name = table.name.to_string();
let table_dir = if config.single_model_file {
output_dir.clone()
} else {
output_dir.join(&table_name)
};
if !table_dir.exists() {
std::fs::create_dir(&table_dir).attach_path_err(&table_dir)?;
}
if !table_dir.is_dir() {
return Err(Error::not_a_directory("Expected a directory", table_dir));
}
let table_file_name = if config.single_model_file {
let mut table_name = table_name; table_name.push_str(".rs");
table_name
} else {
"generated.rs".into()
};
let mut table_generated_rs = MarkedFile::new(table_dir.join(table_file_name))?;
if config.file_mode != FileMode::NewFile {
table_generated_rs.ensure_file_signature()?;
}
table_generated_rs.change_file_contents(
table
.generated_code
.as_ref()
.ok_or(Error::other(format!(
"Expected code for table \"{}\" to be generated",
table.struct_name
)))?
.clone(),
);
write_file(&config, table_generated_rs, &mut file_status)?;
if !config.single_model_file {
let mut table_mod_rs = MarkedFile::new(table_dir.join("mod.rs"))?;
table_mod_rs.ensure_mod_stmt("generated");
table_mod_rs.ensure_use_stmt("generated::*");
table_mod_rs.write()?;
file_status.push(FileChanges::from(&table_mod_rs));
}
mod_rs.ensure_mod_stmt(table.name.to_string().as_str());
}
for item in std::fs::read_dir(&output_dir).attach_path_err(&output_dir)? {
let item = item.attach_path_err(&output_dir)?;
let file_type = item
.file_type()
.attach_path_msg(item.path(), "Could not determine type of file")?;
if !file_type.is_dir() {
continue;
}
let generated_rs_path = item.path().join("generated.rs");
if !generated_rs_path.exists()
|| !generated_rs_path.is_file()
|| !MarkedFile::new(generated_rs_path.clone())?.has_file_signature()
{
continue;
}
let file_name = item.file_name();
let associated_table_name = file_name.to_str().ok_or(Error::other(format!(
"Could not determine name of file '{:#?}'",
item.path()
)))?;
let found = generated.iter().find(|g| {
g.name
.to_string()
.eq_ignore_ascii_case(associated_table_name)
});
if found.is_some() {
continue;
}
match config.file_mode {
FileMode::Overwrite => {
std::fs::remove_file(&generated_rs_path).attach_path_err(&generated_rs_path)?;
file_status.push(FileChanges::new(
&generated_rs_path,
FileChangesStatus::Deleted,
));
}
FileMode::NewFile | FileMode::None => {
file_status.push(FileChanges::new(
&generated_rs_path,
FileChangesStatus::DeletedIgnored,
));
}
}
let table_mod_rs_path = item.path().join("mod.rs");
if table_mod_rs_path.exists() {
let mut table_mod_rs = MarkedFile::new(table_mod_rs_path)?;
table_mod_rs.remove_mod_stmt("generated");
table_mod_rs.remove_use_stmt("generated::*");
if table_mod_rs.get_file_contents().trim().is_empty() {
if config.file_mode == FileMode::Overwrite {
let table_mod_rs = table_mod_rs.delete()?;
file_status.push(FileChanges::new(&table_mod_rs, FileChangesStatus::Deleted));
} else {
file_status.push(FileChanges::new(
&table_mod_rs,
FileChangesStatus::DeletedIgnored,
));
}
} else {
let (write, file_change_status) = match config.file_mode {
FileMode::Overwrite => (true, FileChangesStatus::Modified),
FileMode::NewFile | FileMode::None => {
(false, FileChangesStatus::UnchangedIgnored)
}
};
if write && table_mod_rs.is_modified() {
table_mod_rs.write()?;
}
file_status.push(FileChanges::from_markedfile_custom(
&table_mod_rs,
file_change_status,
FileChangesStatus::Unchanged,
));
}
}
let is_empty = item
.path()
.read_dir()
.attach_path_err(item.path())?
.next()
.is_none();
if is_empty {
std::fs::remove_dir(item.path()).attach_path_err(item.path())?;
}
if config.file_mode == FileMode::Overwrite {
mod_rs.remove_mod_stmt(associated_table_name);
}
}
mod_rs.write()?;
file_status.push(FileChanges::from(&mod_rs));
Ok(file_status)
}