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
15 changes: 5 additions & 10 deletions sea-orm-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,36 +308,31 @@ pub enum GenerateSubcommands {

#[arg(
long,
value_delimiter = ',',
help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`"
help = "Add extra derive macros to generated model struct, e.g. `--model-extra-derives ts_rs::Ts` or `--model-extra-derives ts_rs::Ts,CustomDerive`"
)]
model_extra_derives: Vec<String>,

#[arg(
long,
value_delimiter = ',',
help = r#"Add extra attributes to generated model struct, no need for `#[]` (comma separated), e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
help = r#"Add extra attributes to generated model struct, no need for `#[]`, e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")'` or pass multiple attributes in one argument: `--model-extra-attributes 'serde(rename_all = "camelCase"),ts(export)'`"#
)]
model_extra_attributes: Vec<String>,

#[arg(
long,
value_delimiter = ',',
help = "Add extra derive macros to generated enums (comma separated), e.g. `--enum-extra-derives 'ts_rs::Ts','CustomDerive'`"
help = "Add extra derive macros to generated enums, e.g. `--enum-extra-derives ts_rs::Ts` or `--enum-extra-derives ts_rs::Ts,CustomDerive`"
)]
enum_extra_derives: Vec<String>,

#[arg(
long,
value_delimiter = ',',
help = r#"Add extra attributes to generated enums, no need for `#[]` (comma separated), e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
help = r#"Add extra attributes to generated enums, no need for `#[]`, e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")'` or pass multiple attributes in one argument: `--enum-extra-attributes 'serde(rename_all = "camelCase"),ts(export)'`"#
)]
enum_extra_attributes: Vec<String>,

#[arg(
long,
value_delimiter = ',',
help = "Add extra derive macros to generated column enum (comma separated), e.g. `--column-extra-derives 'async_graphql::Enum','CustomDerive'`"
help = "Add extra derive macros to generated column enum, e.g. `--column-extra-derives async_graphql::Enum` or `--column-extra-derives async_graphql::Enum,Eq,PartialEq`"
)]
column_extra_derives: Vec<String>,

Expand Down
169 changes: 169 additions & 0 deletions sea-orm-cli/src/commands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,74 @@ use std::{error::Error, fs, path::Path, process::Command, str::FromStr};
use tracing_subscriber::{EnvFilter, prelude::*};
use url::Url;

/// Split a string by comma while respecting parentheses nesting.
/// This allows attributes like `test(a, b)` to be treated as a single value
/// instead of being split into `test(a` and ` b)`.
fn split_by_comma_ignoring_parentheses(s: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current = String::new();
let mut paren_depth = 0;
let mut bracket_depth = 0;
let mut brace_depth = 0;

for c in s.chars() {
match c {
'(' => {
paren_depth += 1;
current.push(c);
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
current.push(c);
}
'[' => {
bracket_depth += 1;
current.push(c);
}
']' => {
bracket_depth = bracket_depth.saturating_sub(1);
current.push(c);
}
'{' => {
brace_depth += 1;
current.push(c);
}
'}' => {
brace_depth = brace_depth.saturating_sub(1);
current.push(c);
}
',' if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 => {
let trimmed = current.trim();
if !trimmed.is_empty() {
result.push(trimmed.to_string());
}
current.clear();
}
_ => {
current.push(c);
}
}
}

// Add the last segment
let trimmed = current.trim();
if !trimmed.is_empty() {
result.push(trimmed.to_string());
}

result
}

/// Process a vector of strings that may contain comma-separated values with nested parentheses.
/// This handles the case where clap no longer splits by comma, so we need to manually split
/// each string while respecting parentheses nesting.
fn process_comma_separated_values(values: Vec<String>) -> Vec<String> {
values
.into_iter()
.flat_map(|s| split_by_comma_ignoring_parentheses(&s))
.collect()
}

pub async fn run_generate_command(
command: GenerateSubcommands,
verbose: bool,
Expand Down Expand Up @@ -223,6 +291,15 @@ pub async fn run_generate_command(
};
println!("... discovered.");

// Process extra derives and attributes, splitting by comma while respecting parentheses
// This handles cases like `--model-extra-attributes 'cfg_attr(debug_assertions, derive(Debug))'`
// which should be treated as a single attribute, not split into `cfg_attr(debug_assertions` and ` derive(Debug))`
let model_extra_derives = process_comma_separated_values(model_extra_derives);
let model_extra_attributes = process_comma_separated_values(model_extra_attributes);
let enum_extra_derives = process_comma_separated_values(enum_extra_derives);
let enum_extra_attributes = process_comma_separated_values(enum_extra_attributes);
let column_extra_derives = process_comma_separated_values(column_extra_derives);

let writer_context = EntityWriterContext::new(
if expanded_format {
EntityFormat::Expanded
Expand Down Expand Up @@ -472,4 +549,96 @@ mod tests {
_ => unreachable!(),
}
}

#[test]
fn test_split_by_comma_simple() {
// Simple comma-separated values should split normally
let result = super::split_by_comma_ignoring_parentheses("a,b,c");
assert_eq!(result, vec!["a", "b", "c"]);
}

#[test]
fn test_split_by_comma_with_parentheses() {
// Comma inside parentheses should NOT split
let result = super::split_by_comma_ignoring_parentheses("test(a, b)");
assert_eq!(result, vec!["test(a, b)"]);

// Multiple values, one with parentheses containing comma
let result = super::split_by_comma_ignoring_parentheses("attr1,test(a, b)");
assert_eq!(result, vec!["attr1", "test(a, b)"]);
}

#[test]
fn test_split_by_comma_with_nested_parentheses() {
// Nested parentheses with commas
let result = super::split_by_comma_ignoring_parentheses("cfg_attr(debug_assertions, derive(Debug))");
assert_eq!(result, vec!["cfg_attr(debug_assertions, derive(Debug))"]);

// Multiple nested parentheses
let result = super::split_by_comma_ignoring_parentheses("cfg_attr(feature1, attr(a, b)),cfg_attr(feature2, attr(c, d))");
assert_eq!(result, vec!["cfg_attr(feature1, attr(a, b))", "cfg_attr(feature2, attr(c, d))"]);
}

#[test]
fn test_split_by_comma_with_brackets() {
// Brackets should also be respected
let result = super::split_by_comma_ignoring_parentheses("serde(rename_all = \"camelCase\"),ts(export)");
assert_eq!(result, vec!["serde(rename_all = \"camelCase\")", "ts(export)"]);

// Brackets with commas
let result = super::split_by_comma_ignoring_parentheses("attr[key, value],other");
assert_eq!(result, vec!["attr[key, value]", "other"]);
}

#[test]
fn test_split_by_comma_with_braces() {
// Braces should also be respected
let result = super::split_by_comma_ignoring_parentheses("derive{a, b},other");
assert_eq!(result, vec!["derive{a, b}", "other"]);
}

#[test]
fn test_split_by_comma_empty() {
// Empty string should return empty vec
let result = super::split_by_comma_ignoring_parentheses("");
assert!(result.is_empty());

// Only whitespace should return empty vec
let result = super::split_by_comma_ignoring_parentheses(" ");
assert!(result.is_empty());
}

#[test]
fn test_split_by_comma_whitespace_handling() {
// Whitespace around values should be trimmed
let result = super::split_by_comma_ignoring_parentheses(" a , b ");
assert_eq!(result, vec!["a", "b"]);

// Whitespace inside parentheses should be preserved
let result = super::split_by_comma_ignoring_parentheses("test( a , b )");
assert_eq!(result, vec!["test( a , b )"]);
}

#[test]
fn test_process_comma_separated_values() {
// Process multiple strings, each potentially containing comma-separated values
let input = vec![
"attr1,attr2".to_string(),
"test(a, b)".to_string(),
"attr3".to_string(),
];
let result = super::process_comma_separated_values(input);
assert_eq!(result, vec!["attr1", "attr2", "test(a, b)", "attr3"]);
}

#[test]
fn test_split_by_comma_real_world_examples() {
// Real-world example: cfg_attr with derive
let result = super::split_by_comma_ignoring_parentheses("cfg_attr(debug_assertions, derive(Debug)),serde(rename_all = \"camelCase\")");
assert_eq!(result, vec!["cfg_attr(debug_assertions, derive(Debug))", "serde(rename_all = \"camelCase\")"]);

// Real-world example: multiple derives
let result = super::split_by_comma_ignoring_parentheses("derive(Debug, Clone),derive(Serialize, Deserialize)");
assert_eq!(result, vec!["derive(Debug, Clone)", "derive(Serialize, Deserialize)"]);
}
}