From d42bbf220fc400e79d336a4945380cd902806670 Mon Sep 17 00:00:00 2001 From: Wolfgang Silbermayr Date: Sun, 2 Nov 2025 10:55:16 +0100 Subject: [PATCH] Add diagram direction support (#58) --- src/cli.rs | 7 +++++++ src/lib.rs | 10 ++++++++++ src/main.rs | 7 ++++++- src/mermaid_generator.rs | 32 ++++++++++++++++++++++++++------ src/plantuml_generator.rs | 35 +++++++++++++++++++++++++++++++---- 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ad0f4fb..10146b9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -57,5 +57,12 @@ pub fn parse() -> ArgMatches { .action(ArgAction::SetTrue) .default_value("false"), ) + .arg( + Arg::new("direction") + .long("direction") + .value_parser(["tb", "bt", "lr", "rl"]) + .help("Optionally set the direction of the generated diagram") + .action(ArgAction::Set), + ) .get_matches() } diff --git a/src/lib.rs b/src/lib.rs index 35466c4..9535ee9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub struct GeneratorConfigOptions { pub draw_legend: bool, pub inline_puml_lib: bool, pub conceptual_diagram: bool, + pub direction: Option, } pub trait ViewGenerator { @@ -54,3 +55,12 @@ pub fn get_generator(generator_type: GeneratorType) -> Result Ok(Box::new(MermaidGenerator::new()?)), } } + +#[derive(Clone, Debug, Display, EnumString, Eq, PartialEq, PartialOrd, Ord)] +#[strum(serialize_all = "lowercase")] +pub enum Direction { + TB, + BT, + LR, + RL, +} diff --git a/src/main.rs b/src/main.rs index 1ce6216..ee9cfb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use clap::ArgMatches; -use sqlant::{get_generator, lookup_parser, GeneratorConfigOptions, GeneratorType}; +use sqlant::{get_generator, lookup_parser, Direction, GeneratorConfigOptions, GeneratorType}; fn get_arg(args: &ArgMatches, arg_name: &str) -> String { args.get_one::(arg_name).unwrap().to_string() @@ -22,6 +22,10 @@ async fn main() { let generator_type = GeneratorType::from_str(&output_arg).expect("Generator type {output_arg} isn't supported"); let rndr = get_generator(generator_type).unwrap(); + let direction_arg = args.get_one::("direction"); + let direction = direction_arg.map(|dir| { + Direction::from_str(dir).unwrap_or_else(|_| panic!("Direction {dir} isn't supported")) + }); let result = rndr .generate( erd, @@ -31,6 +35,7 @@ async fn main() { draw_legend: args.get_flag("legend"), inline_puml_lib: args.get_flag("inline-puml-lib"), conceptual_diagram: args.get_flag("conceptual"), + direction, }, ) .unwrap(); diff --git a/src/mermaid_generator.rs b/src/mermaid_generator.rs index 1e80468..ea5755f 100644 --- a/src/mermaid_generator.rs +++ b/src/mermaid_generator.rs @@ -1,11 +1,12 @@ use std::sync::Arc; use super::sql_entities::{SqlERData, Table, TableColumn}; -use crate::{GeneratorConfigOptions, ViewGenerator}; +use crate::{Direction, GeneratorConfigOptions, ViewGenerator}; use serde::Serialize; -use tinytemplate::{format_unescaped, TinyTemplate}; +use tinytemplate::{TinyTemplate, format_unescaped}; static MERMAID_TEMPLATE: &str = r#"erDiagram +{{ if direction }}direction {direction}{{ endif }} {{ for ent in entities}}{ent}{{ endfor }} {{ for en in enums}}{en}{{ endfor }} {{ for fk in foreign_keys}}{fk}{{ endfor }} @@ -13,11 +14,9 @@ static MERMAID_TEMPLATE: &str = r#"erDiagram static ENTITY_TEMPLATE: &str = "{name} \\{\n{pks}{fks}{others}}\n"; -static COLUMN_TEMPLATE: &str = - " {col.datatype} {col.name}{{ if is_pk_or_fk }} {{ endif }}{{ if is_pk }}PK,{{ endif }}{{ if is_fk }}FK{{ endif }}"; +static COLUMN_TEMPLATE: &str = " {col.datatype} {col.name}{{ if is_pk_or_fk }} {{ endif }}{{ if is_pk }}PK,{{ endif }}{{ if is_fk }}FK{{ endif }}"; -static REL_TEMPLATE: &str = - "{source_table_name} {{ if is_zero_one_to_one }}|o--||{{else}}}o--||{{ endif }} {target_table_name}: \"\"\n"; +static REL_TEMPLATE: &str = "{source_table_name} {{ if is_zero_one_to_one }}|o--||{{else}}}o--||{{ endif }} {target_table_name}: \"\"\n"; const ENUM_TEMPLATE: &str = "\"{name} (ENUM)\" \\{\n{{ for v in values}} {v} _\n{{ endfor }}}"; @@ -38,8 +37,28 @@ struct SColumn<'a> { is_nn: bool, } +#[derive(Serialize)] +enum SDirection { + TB, + BT, + LR, + RL, +} + +impl From<&Direction> for SDirection { + fn from(value: &Direction) -> Self { + match value { + Direction::TB => Self::TB, + Direction::BT => Self::BT, + Direction::LR => Self::LR, + Direction::RL => Self::RL, + } + } +} + #[derive(Serialize)] struct SMermaid { + direction: Option, entities: Vec, enums: Vec, foreign_keys: Vec, @@ -184,6 +203,7 @@ impl ViewGenerator for MermaidGenerator<'_> { Ok(self.str_templates.render( "mermaid", &SMermaid { + direction: opts.direction.as_ref().map(Into::into), entities, enums, foreign_keys, diff --git a/src/plantuml_generator.rs b/src/plantuml_generator.rs index 1e803e6..8d5ac70 100644 --- a/src/plantuml_generator.rs +++ b/src/plantuml_generator.rs @@ -1,15 +1,16 @@ use std::sync::Arc; use super::sql_entities::{SqlERData, Table, TableColumn}; -use crate::{sql_entities::View, GeneratorConfigOptions, ViewGenerator}; +use crate::{Direction, GeneratorConfigOptions, ViewGenerator, sql_entities::View}; use serde::Serialize; -use tinytemplate::{format_unescaped, TinyTemplate}; +use tinytemplate::{TinyTemplate, format_unescaped}; pub struct PlantUmlDefaultGenerator<'a> { str_templates: TinyTemplate<'a>, } static PUML_TEMPLATE: &str = "@startuml\n\n\ +{{ if direction }}{ direction }\n{{ endif }}\ hide circle\n\ hide empty members\n\ skinparam linetype ortho\n\n\ @@ -27,8 +28,7 @@ static VIEW_TEMPLATE: &str = static COLUMN_TEMPLATE: &str = " column({col.name}, \"{col.datatype}\"{{ if is_pk }}, $pk=true{{ endif }}{{ if is_fk }}, $fk=true{{ endif }}{{if is_nn}}, $nn=true{{ endif }})\n"; -static REL_TEMPLATE: &str = - "{source_table_name} {{ if is_zero_one_to_one }}|o--||{{else}}}o--||{{ endif }} {target_table_name}\n"; +static REL_TEMPLATE: &str = "{source_table_name} {{ if is_zero_one_to_one }}|o--||{{else}}}o--||{{ endif }} {target_table_name}\n"; static ENUM_TEMPLATE: &str = "enum({name}, \"{{ for v in values}}{{if @last}}{v}{{else}}{v}, {{ endif }}{{ endfor }}\")\n"; @@ -69,6 +69,25 @@ struct SEntity { #[derive(Serialize)] struct SLegend(String); +#[derive(Serialize)] +struct SDirection(String); + +impl TryFrom<&Direction> for SDirection { + type Error = crate::SqlantError; + fn try_from(value: &Direction) -> Result { + match value { + Direction::TB => Ok(Self("top to bottom direction".into())), + Direction::BT => Err(crate::SqlantError::Generator( + "bt (bottom-to-top) direction is not available in plantuml.".into(), + )), + Direction::LR => Ok(Self("left to right direction".into())), + Direction::RL => Err(crate::SqlantError::Generator( + "rl (right-to-left) direction is not available in plantuml.".into(), + )), + } + } +} + #[derive(Serialize)] struct SPuml { puml_lib: String, @@ -78,6 +97,7 @@ struct SPuml { foreign_keys: Vec, enums: Vec, legend: Option, + direction: Option, } #[derive(Serialize)] @@ -304,6 +324,12 @@ impl ViewGenerator for PlantUmlDefaultGenerator<'_> { PUML_LIB_INCLUDE.into() }; + let direction = if let Some(direction) = &opts.direction { + Some(SDirection::try_from(direction)?) + } else { + None + }; + Ok(self.str_templates.render( "puml", &SPuml { @@ -313,6 +339,7 @@ impl ViewGenerator for PlantUmlDefaultGenerator<'_> { enums, legend, views, + direction, }, )?) }