From b6fc02696da82f39b89cfa69da1b5183ed0e4351 Mon Sep 17 00:00:00 2001 From: xingzihai <1315258019@qq.com> Date: Wed, 1 Apr 2026 17:59:31 +0000 Subject: [PATCH] fix: entity generation fails with comma in parentheses (#2063) When model-extra-attributes contains a comma inside parentheses, entity generation fails because clap splits by comma without respecting parentheses nesting. For example: --model-extra-attributes 'test(a, b)' gets incorrectly split into ['test(a', ' b)']. The fix: - Remove value_delimiter from CLI arguments for extra derives/attributes - Add split_by_comma_ignoring_parentheses() helper function - Process values in run_generate_command with parentheses-aware splitting This allows users to pass attributes like: --model-extra-attributes 'cfg_attr(debug_assertions, derive(Debug))' Signed-off-by: OpenClaw Agent --- sea-orm-cli/src/cli.rs | 15 +-- sea-orm-cli/src/commands/generate.rs | 169 +++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 10 deletions(-) diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index 810a81ba66..4f0baf8323 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -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, #[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, #[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, #[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, #[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, diff --git a/sea-orm-cli/src/commands/generate.rs b/sea-orm-cli/src/commands/generate.rs index 7e9c5916e2..e80ad9babd 100644 --- a/sea-orm-cli/src/commands/generate.rs +++ b/sea-orm-cli/src/commands/generate.rs @@ -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 { + 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) -> Vec { + values + .into_iter() + .flat_map(|s| split_by_comma_ignoring_parentheses(&s)) + .collect() +} + pub async fn run_generate_command( command: GenerateSubcommands, verbose: bool, @@ -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 @@ -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)"]); + } }