Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions cot-core/src/error/error_impl.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::error::Error as StdError;
use std::fmt::Display;
use std::io;
use std::ops::Deref;

use derive_more::with_trait::Debug;
Expand Down Expand Up @@ -291,6 +292,8 @@ impl From<tower_sessions::session::Error> for Error {
}
}

impl_into_cot_error!(io::Error);

#[cfg(test)]
mod tests {
use derive_more::with_trait::Debug;
Expand All @@ -301,11 +304,11 @@ mod tests {

#[derive(Debug, thiserror::Error)]
#[error("outer error")]
struct OuterError(#[source] std::io::Error);
struct OuterError(#[source] io::Error);

#[test]
fn error_new() {
let inner = std::io::Error::other("server error");
let inner = io::Error::other("server error");
let error = Error::wrap(inner);

assert!(StdError::source(&error).is_none());
Expand All @@ -314,7 +317,7 @@ mod tests {

#[test]
fn error_display() {
let inner = std::io::Error::other("server error");
let inner = io::Error::other("server error");
let error = Error::internal(inner);

let display = format!("{error}");
Expand All @@ -324,7 +327,7 @@ mod tests {

#[test]
fn error_wrap_and_is_wrapper() {
let inner = std::io::Error::other("wrapped");
let inner = io::Error::other("wrapped");
let error = Error::wrap(inner);

assert!(error.is_wrapper());
Expand Down Expand Up @@ -375,7 +378,7 @@ mod tests {

#[test]
fn error_from_template_render() {
let askama_err = askama::Error::Custom(Box::new(std::io::Error::other("fail")));
let askama_err = askama::Error::Custom(Box::new(io::Error::other("fail")));
let error: Error = askama_err.into();

assert!(error.to_string().contains("failed to render template"));
Expand Down Expand Up @@ -407,10 +410,10 @@ mod tests {
let err = Error::with_status("root error", StatusCode::BAD_REQUEST);
assert_snapshot!(format!("{err:?}"), @"root error");

let err = Error::wrap(std::io::Error::other("io error"));
let err = Error::wrap(io::Error::other("io error"));
assert_snapshot!(format!("{err:?}"), @"io error");

let io_err = std::io::Error::other("inner io error");
let io_err = io::Error::other("inner io error");
let err = Error::wrap(OuterError(io_err));
assert_snapshot!(format!("{err:?}"), @r###"
outer error
Expand All @@ -419,7 +422,7 @@ mod tests {
0: inner io error
"###);

let err = Error::internal(OuterError(std::io::Error::other("inner io error")));
let err = Error::internal(OuterError(io::Error::other("inner io error")));
assert_snapshot!(format!("{err:?}"), @r###"
outer error

Expand All @@ -438,9 +441,7 @@ mod tests {
#[error("wrapper error")]
struct WrapperError(#[source] OuterError);

let err = Error::internal(WrapperError(OuterError(std::io::Error::other(
"inner io error",
))));
let err = Error::internal(WrapperError(OuterError(io::Error::other("inner io error"))));

assert_snapshot!(format!("{err:?}"), @"
wrapper error
Expand All @@ -458,7 +459,7 @@ mod tests {
)]
fn error_debug_printing_alternate() {
let err = Error::with_status(
OuterError(std::io::Error::other("inner io error")),
OuterError(io::Error::other("inner io error")),
StatusCode::INTERNAL_SERVER_ERROR,
);
assert_snapshot!(format!("{err:#?}"), @r#"
Expand Down
73 changes: 72 additions & 1 deletion cot/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use async_trait::async_trait;
pub use clap;
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
#[cfg(feature = "db")]
use cot::db::migrations::{MigrationEngine, SyncDynMigration};
use cot::db::migrations::{GraphExporter, GraphFormat, MigrationEngine, SyncDynMigration};
use cot::project::BootstrappedProject;
use derive_more::Debug;

Expand All @@ -21,6 +21,7 @@ const LISTEN_PARAM: &str = "listen";
const COLLECT_STATIC_DIR_PARAM: &str = "dir";
const MIGRATION_GROUP_SUBCOMMAND: &str = "migration";
const MIGRATION_ROLLBACK_SUBCOMMAND: &str = "rollback";
const MIGRATION_GRAPH_SUBCOMMAND: &str = "graph";

/// A central point for configuring the default Command Line Interface (CLI) for
/// Cot-powered projects.
Expand Down Expand Up @@ -101,6 +102,7 @@ impl Cli {
let mut migration_group =
CliTaskGroup::new(MIGRATION_GROUP_SUBCOMMAND).about("Database migration commands");
migration_group.add_task(MigrationRollback);
migration_group.add_task(MigrationGraph);

cli.add_task(migration_group);
}
Expand Down Expand Up @@ -655,6 +657,75 @@ impl CliTask for MigrationRollback {
}
}

#[cfg(feature = "db")]
struct MigrationGraph;

#[cfg(feature = "db")]
#[async_trait(?Send)]
impl CliTask for MigrationGraph {
fn subcommand(&self) -> Command {
Command::new(MIGRATION_GRAPH_SUBCOMMAND)
.about("Export the migration dependency graph for visualization")
.arg(
Arg::new("format")
.long("format")
.value_name("FORMAT")
.value_parser(["dot", "mermaid"])
.default_value("dot")
.help("Output format: dot (Graphviz) or mermaid"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to also include long_help that would point to GraphViz and Mermaid links?

also, nit: I think it's Mermaid, not mermaid.

)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.value_parser(value_parser!(PathBuf))
.required(false)
.help("Write to a file instead of stdout"),
)
}

async fn execute(
&mut self,
matches: &ArgMatches,
bootstrapper: Bootstrapper<WithConfig>,
) -> Result<()> {
let format = match matches.get_one::<String>("format").map(String::as_str) {
Some("mermaid") => GraphFormat::Mermaid,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move dot and mermaid strings into constants to make sure we use the same values in parsing and in the clap builder?

_ => GraphFormat::Dot,
};

let bootstrapper = bootstrapper
.with_apps()
.with_database()
.await?
.boot()
.await?;

let BootstrappedProject {
context,
handler: _,
error_handler: _,
} = bootstrapper.finish();

let mut migrations: Vec<Box<SyncDynMigration>> = Vec::new();
for app in context.apps() {
migrations.extend(app.migrations());
}

let engine = MigrationEngine::new(migrations)?;
let exporter = GraphExporter::new(engine.migrations());
let rendered = exporter.export(format)?;

match matches.get_one::<PathBuf>("output") {
Some(path) => std::fs::write(path, rendered)?,
None => println!("{rendered}"),
}

Ok(())
}
}

/// A macro to generate a [`CliMetadata`] struct from the Cargo manifest.
#[macro_export]
macro_rules! metadata {
Expand Down
6 changes: 6 additions & 0 deletions cot/src/db/migrations.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Database migrations.

mod graph_export;
mod sorter;

use std::collections::{HashSet, VecDeque};
Expand All @@ -9,6 +10,7 @@ use std::io::Write;
use std::{fmt, io};

pub use cot_macros::migration_op;
pub(crate) use graph_export::{GraphExporter, GraphFormat};
use sea_query::{ColumnDef, StringLen};
use thiserror::Error;
use tracing::{Level, info};
Expand Down Expand Up @@ -486,6 +488,10 @@ impl MigrationEngine {
.await?;
Ok(())
}

pub(crate) fn migrations(&self) -> &[MigrationWrapper] {
&self.migrations
}
}

/// Resolves the possible migration names that can be used to refer to a
Expand Down
Loading
Loading