mirror of
https://github.com/diesel-rs/diesel.git
synced 2024-10-04 09:39:24 +03:00
Add a config file for Diesel CLI, autorun print-schema
As discussed in #1621 and elsewhere, we'd like to replace `infer_schema!` with a config file that re-writes your schema file automatically when it changes. This file will eventually be expanded to support most of the CLI args to `print-schema`, and gain additional options to further customize the output. However, this is the minimal first step. I would like to change the default file generated by `diesel setup` to include `file = "src/schema.rs"`, but before we do that I think we should change the behavior on tables with no PK to print a warning that it's being skipped instead of panicking.
This commit is contained in:
parent
76978b5bc9
commit
ae22270600
@ -17,11 +17,13 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
clap = "2.27"
|
||||
clippy = { optional = true, version = "=0.0.185" }
|
||||
diesel = { version = "~1.2.0", default-features = false }
|
||||
dotenv = ">=0.8, <0.11"
|
||||
infer_schema_internals = "~1.2.0"
|
||||
clippy = { optional = true, version = "=0.0.185" }
|
||||
migrations_internals = "~1.2.0"
|
||||
serde = { version = "1.0.0", features = ["derive"] }
|
||||
toml = "0.4.6"
|
||||
url = { version = "1.4.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
10
diesel_cli/build.rs
Normal file
10
diesel_cli/build.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
if env::var("CARGO_PKG_VERSION").unwrap() == "1.3.0" {
|
||||
panic!(
|
||||
"Did you remember to publish documentation on the config file? \
|
||||
If not go do it. And then delete this build script."
|
||||
);
|
||||
}
|
||||
}
|
@ -122,6 +122,17 @@ pub fn build_cli() -> App<'static, 'static> {
|
||||
.help("Render documentation comments for tables and columns"),
|
||||
);
|
||||
|
||||
let config_arg = Arg::with_name("CONFIG_FILE")
|
||||
.long("config-file")
|
||||
.help(
|
||||
"The location of the configuration file to use. Falls back to the \
|
||||
`DIESEL_CONFIG_FILE` environment variable if unspecified. Defaults \
|
||||
to `diesel.toml` in your project root. See \
|
||||
diesel.rs/guides/configuring-diesel-cli for documentation on this file.",
|
||||
)
|
||||
.global(true)
|
||||
.takes_value(true);
|
||||
|
||||
App::new("diesel")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.setting(AppSettings::VersionlessSubcommands)
|
||||
@ -129,6 +140,7 @@ pub fn build_cli() -> App<'static, 'static> {
|
||||
"You can also run `diesel SUBCOMMAND -h` to get more information about that subcommand.",
|
||||
)
|
||||
.arg(database_arg)
|
||||
.arg(config_arg)
|
||||
.subcommand(migration_subcommand)
|
||||
.subcommand(setup_subcommand)
|
||||
.subcommand(database_subcommand)
|
||||
|
41
diesel_cli/src/config.rs
Normal file
41
diesel_cli/src/config.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use clap::ArgMatches;
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use super::{find_project_root, handle_error};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub print_schema: PrintSchema,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn file_path(matches: &ArgMatches) -> PathBuf {
|
||||
matches
|
||||
.value_of("CONFIG_FILE")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| env::var_os("DIESEL_CONFIG_FILE").map(PathBuf::from))
|
||||
.unwrap_or_else(|| {
|
||||
find_project_root()
|
||||
.unwrap_or_else(handle_error)
|
||||
.join("diesel.toml")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read(matches: &ArgMatches) -> Result<Self, Box<Error>> {
|
||||
let path = Self::file_path(matches);
|
||||
let mut bytes = Vec::new();
|
||||
fs::File::open(path)?.read_to_end(&mut bytes)?;
|
||||
toml::from_slice(&bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct PrintSchema {
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
2
diesel_cli/src/default_files/diesel.toml
Normal file
2
diesel_cli/src/default_files/diesel.toml
Normal file
@ -0,0 +1,2 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
@ -17,9 +17,13 @@ extern crate diesel;
|
||||
extern crate dotenv;
|
||||
extern crate infer_schema_internals;
|
||||
extern crate migrations_internals;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
extern crate toml;
|
||||
#[cfg(feature = "url")]
|
||||
extern crate url;
|
||||
|
||||
mod config;
|
||||
mod database_error;
|
||||
#[macro_use]
|
||||
mod database;
|
||||
@ -32,10 +36,12 @@ use chrono::*;
|
||||
use clap::{ArgMatches, Shell};
|
||||
use migrations_internals::{self as migrations, MigrationConnection};
|
||||
use std::any::Any;
|
||||
use std::error::Error;
|
||||
use std::io::stdout;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs};
|
||||
|
||||
use self::config::Config;
|
||||
use self::database_error::{DatabaseError, DatabaseResult};
|
||||
use migrations_internals::TIMESTAMP_FORMAT;
|
||||
|
||||
@ -46,7 +52,7 @@ fn main() {
|
||||
let matches = cli::build_cli().get_matches();
|
||||
|
||||
match matches.subcommand() {
|
||||
("migration", Some(matches)) => run_migration_command(matches),
|
||||
("migration", Some(matches)) => run_migration_command(matches).unwrap_or_else(handle_error),
|
||||
("setup", Some(matches)) => run_setup_command(matches),
|
||||
("database", Some(matches)) => run_database_command(matches),
|
||||
("bash-completion", Some(matches)) => generate_bash_completion_command(matches),
|
||||
@ -55,7 +61,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_migration_command(matches: &ArgMatches) {
|
||||
fn run_migration_command(matches: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
match matches.subcommand() {
|
||||
("run", Some(_)) => {
|
||||
let database_url = database::database_url(matches);
|
||||
@ -63,7 +69,8 @@ fn run_migration_command(matches: &ArgMatches) {
|
||||
call_with_conn!(
|
||||
database_url,
|
||||
migrations::run_pending_migrations_in_directory(&dir, &mut stdout())
|
||||
).unwrap_or_else(handle_error);
|
||||
)?;
|
||||
regenerate_schema_if_file_specified(matches)?;
|
||||
}
|
||||
("revert", Some(_)) => {
|
||||
let database_url = database::database_url(matches);
|
||||
@ -71,19 +78,20 @@ fn run_migration_command(matches: &ArgMatches) {
|
||||
call_with_conn!(
|
||||
database_url,
|
||||
migrations::revert_latest_migration_in_directory(&dir)
|
||||
).unwrap_or_else(handle_error);
|
||||
)?;
|
||||
regenerate_schema_if_file_specified(matches)?;
|
||||
}
|
||||
("redo", Some(_)) => {
|
||||
let database_url = database::database_url(matches);
|
||||
let dir = migrations_dir(matches);
|
||||
call_with_conn!(database_url, redo_latest_migration(&dir));
|
||||
regenerate_schema_if_file_specified(matches)?;
|
||||
}
|
||||
("list", Some(_)) => {
|
||||
let database_url = database::database_url(matches);
|
||||
let dir = migrations_dir(matches);
|
||||
let mut migrations =
|
||||
call_with_conn!(database_url, migrations::mark_migrations_in_directory(&dir))
|
||||
.unwrap_or_else(handle_error);
|
||||
call_with_conn!(database_url, migrations::mark_migrations_in_directory(&dir))?;
|
||||
|
||||
migrations.sort_by_key(|&(ref m, _)| m.version().to_string());
|
||||
|
||||
@ -101,8 +109,8 @@ fn run_migration_command(matches: &ArgMatches) {
|
||||
}
|
||||
("pending", Some(_)) => {
|
||||
let database_url = database::database_url(matches);
|
||||
let result = call_with_conn!(database_url, migrations::any_pending_migrations);
|
||||
println!("{:?}", result.unwrap());
|
||||
let result = call_with_conn!(database_url, migrations::any_pending_migrations)?;
|
||||
println!("{:?}", result);
|
||||
}
|
||||
("generate", Some(args)) => {
|
||||
use std::io::Write;
|
||||
@ -111,10 +119,10 @@ fn run_migration_command(matches: &ArgMatches) {
|
||||
let version = migration_version(args);
|
||||
let versioned_name = format!("{}_{}", version, migration_name);
|
||||
let migration_dir = migrations_dir(matches).join(versioned_name);
|
||||
fs::create_dir(&migration_dir).unwrap();
|
||||
fs::create_dir(&migration_dir)?;
|
||||
|
||||
let migration_dir_relative =
|
||||
convert_absolute_path_to_relative(&migration_dir, &env::current_dir().unwrap());
|
||||
convert_absolute_path_to_relative(&migration_dir, &env::current_dir()?);
|
||||
|
||||
let up_path = migration_dir.join("up.sql");
|
||||
println!(
|
||||
@ -129,12 +137,13 @@ fn run_migration_command(matches: &ArgMatches) {
|
||||
"Creating {}",
|
||||
migration_dir_relative.join("down.sql").display()
|
||||
);
|
||||
let mut down = fs::File::create(down_path).unwrap();
|
||||
down.write_all(b"-- This file should undo anything in `up.sql`")
|
||||
.unwrap();
|
||||
let mut down = fs::File::create(down_path)?;
|
||||
down.write_all(b"-- This file should undo anything in `up.sql`")?;
|
||||
}
|
||||
_ => unreachable!("The cli parser should prevent reaching here"),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use std::fmt::Display;
|
||||
@ -165,6 +174,7 @@ fn migrations_dir(matches: &ArgMatches) -> PathBuf {
|
||||
|
||||
fn run_setup_command(matches: &ArgMatches) {
|
||||
let migrations_dir = create_migrations_dir(matches).unwrap_or_else(handle_error);
|
||||
create_config_file(matches).unwrap_or_else(handle_error);
|
||||
|
||||
database::setup_database(matches, &migrations_dir).unwrap_or_else(handle_error);
|
||||
}
|
||||
@ -187,6 +197,17 @@ fn create_migrations_dir(matches: &ArgMatches) -> DatabaseResult<PathBuf> {
|
||||
Ok(dir.to_owned())
|
||||
}
|
||||
|
||||
fn create_config_file(matches: &ArgMatches) -> DatabaseResult<()> {
|
||||
use std::io::Write;
|
||||
let path = Config::file_path(matches);
|
||||
if !path.exists() {
|
||||
let mut file = fs::File::create(path)?;
|
||||
file.write_all(include_bytes!("default_files/diesel.toml"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_database_command(matches: &ArgMatches) {
|
||||
match matches.subcommand() {
|
||||
("setup", Some(args)) => {
|
||||
@ -327,6 +348,22 @@ fn run_infer_schema(matches: &ArgMatches) {
|
||||
).map_err(handle_error::<_, ()>);
|
||||
}
|
||||
|
||||
fn regenerate_schema_if_file_specified(matches: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
use print_schema::*;
|
||||
|
||||
let config = Config::read(matches)?;
|
||||
if let Some(path) = config.print_schema.file {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let database_url = database::database_url(matches);
|
||||
let mut file = fs::File::create(path)?;
|
||||
print_schema::output_schema(&database_url, None, &Filtering::None, false, &mut file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempdir;
|
||||
|
@ -1,6 +1,7 @@
|
||||
use infer_schema_internals::*;
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::io::{self, stdout};
|
||||
|
||||
pub enum Filtering {
|
||||
Whitelist(Vec<TableName>),
|
||||
@ -25,6 +26,22 @@ pub fn run_print_schema(
|
||||
schema_name: Option<&str>,
|
||||
filtering: &Filtering,
|
||||
include_docs: bool,
|
||||
) -> Result<(), Box<Error>> {
|
||||
output_schema(
|
||||
database_url,
|
||||
schema_name,
|
||||
filtering,
|
||||
include_docs,
|
||||
&mut stdout(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn output_schema<W: io::Write>(
|
||||
database_url: &str,
|
||||
schema_name: Option<&str>,
|
||||
filtering: &Filtering,
|
||||
include_docs: bool,
|
||||
out: &mut W,
|
||||
) -> Result<(), Box<Error>> {
|
||||
let table_names = load_table_names(database_url, schema_name)?
|
||||
.into_iter()
|
||||
@ -44,9 +61,9 @@ pub fn run_print_schema(
|
||||
};
|
||||
|
||||
if let Some(schema_name) = schema_name {
|
||||
print!("{}", ModuleDefinition(schema_name, definitions));
|
||||
write!(out, "{}", ModuleDefinition(schema_name, definitions))?;
|
||||
} else {
|
||||
print!("{}", definitions);
|
||||
write!(out, "{}", definitions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -311,3 +311,38 @@ fn migration_run_runs_pending_migrations_custom_migration_dir_2() {
|
||||
);
|
||||
assert!(db.table_exists("users"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_run_updates_schema_if_config_present() {
|
||||
let p = project("migration_run_updates_schema_if_config_present")
|
||||
.folder("migrations")
|
||||
.file(
|
||||
"diesel.toml",
|
||||
r#"
|
||||
[print_schema]
|
||||
file = "src/my_schema.rs"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Make sure the project is setup
|
||||
p.command("setup").run();
|
||||
|
||||
p.create_migration(
|
||||
"12345_create_users_table",
|
||||
"CREATE TABLE users (id INTEGER PRIMARY KEY)",
|
||||
"DROP TABLE users",
|
||||
);
|
||||
|
||||
assert!(!p.has_file("src/my_schema.rs"));
|
||||
|
||||
let result = p.command("migration").arg("run").run();
|
||||
|
||||
assert!(result.is_success(), "Result was unsuccessful {:?}", result);
|
||||
assert!(
|
||||
result.stdout().contains("Running migration 12345"),
|
||||
"Unexpected stdout {}",
|
||||
result.stdout()
|
||||
);
|
||||
assert!(p.has_file("src/my_schema.rs"));
|
||||
}
|
||||
|
@ -182,3 +182,63 @@ fn setup_works_with_migration_dir_by_env() {
|
||||
assert!(!p.has_file("migrations"));
|
||||
assert!(p.has_file("bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_creates_config_file() {
|
||||
let p = project("setup_creates_config_file").build();
|
||||
|
||||
// Make sure the project builder didn't create the file
|
||||
assert!(!p.has_file("diesel.toml"));
|
||||
|
||||
let result = p.command("setup").run();
|
||||
|
||||
assert!(result.is_success(), "Result was unsuccessful {:?}", result);
|
||||
assert!(p.has_file("diesel.toml"));
|
||||
assert!(
|
||||
p.file_contents("diesel.toml")
|
||||
.contains("diesel.rs/guides/configuring-diesel-cli")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_can_take_config_file_by_env() {
|
||||
let p = project("setup_can_take_config_file_by_env").build();
|
||||
|
||||
// Make sure the project builder didn't create the file
|
||||
assert!(!p.has_file("diesel.toml"));
|
||||
assert!(!p.has_file("foo"));
|
||||
|
||||
let result = p.command("setup").env("DIESEL_CONFIG_FILE", "foo").run();
|
||||
|
||||
assert!(result.is_success(), "Result was unsuccessful {:?}", result);
|
||||
assert!(!p.has_file("diesel.toml"));
|
||||
assert!(p.has_file("foo"));
|
||||
assert!(
|
||||
p.file_contents("foo")
|
||||
.contains("diesel.rs/guides/configuring-diesel-cli")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_can_take_config_file_by_param() {
|
||||
let p = project("setup_can_take_config_file_by_param").build();
|
||||
|
||||
// Make sure the project builder didn't create the file
|
||||
assert!(!p.has_file("diesel.toml"));
|
||||
assert!(!p.has_file("foo"));
|
||||
assert!(!p.has_file("bar"));
|
||||
|
||||
let result = p.command("setup")
|
||||
.env("DIESEL_CONFIG_FILE", "foo")
|
||||
.arg("--config-file=bar")
|
||||
.run();
|
||||
|
||||
assert!(result.is_success(), "Result was unsuccessful {:?}", result);
|
||||
assert!(!p.has_file("diesel.toml"));
|
||||
assert!(!p.has_file("foo"));
|
||||
assert!(p.has_file("bar"));
|
||||
assert!(
|
||||
p.file_contents("bar")
|
||||
.contains("diesel.rs/guides/configuring-diesel-cli")
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ extern crate dotenv;
|
||||
extern crate url;
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempdir::TempDir;
|
||||
|
||||
@ -16,6 +17,7 @@ pub fn project(name: &str) -> ProjectBuilder {
|
||||
pub struct ProjectBuilder {
|
||||
name: String,
|
||||
folders: Vec<String>,
|
||||
files: Vec<(PathBuf, String)>,
|
||||
}
|
||||
|
||||
impl ProjectBuilder {
|
||||
@ -23,6 +25,7 @@ impl ProjectBuilder {
|
||||
ProjectBuilder {
|
||||
name: name.into(),
|
||||
folders: Vec::new(),
|
||||
files: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +34,11 @@ impl ProjectBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn file(mut self, name: &str, contents: &str) -> Self {
|
||||
self.files.push((name.into(), contents.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Project {
|
||||
let tempdir = TempDir::new(&self.name).unwrap();
|
||||
|
||||
@ -40,6 +48,13 @@ impl ProjectBuilder {
|
||||
fs::create_dir(tempdir.path().join(folder)).unwrap();
|
||||
}
|
||||
|
||||
for (file, contents) in self.files {
|
||||
fs::File::create(tempdir.path().join(file))
|
||||
.unwrap()
|
||||
.write_all(contents.as_bytes())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
Project {
|
||||
directory: tempdir,
|
||||
name: self.name,
|
||||
@ -118,6 +133,13 @@ impl Project {
|
||||
self.directory.path().join(path).exists()
|
||||
}
|
||||
|
||||
pub fn file_contents<P: AsRef<Path>>(&self, path: P) -> String {
|
||||
let mut f = File::open(self.directory.path().join(path)).expect("Could not open file");
|
||||
let mut result = String::new();
|
||||
f.read_to_string(&mut result).expect("Could not read file");
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
pub fn delete_file<P: AsRef<Path>>(&self, path: P) {
|
||||
let file = self.directory.path().join(path);
|
||||
|
Loading…
Reference in New Issue
Block a user