Skip to content

Commit eed1458

Browse files
authored
Merge pull request #137 from dev-five-git/fix-enum-upper
Enum upper
2 parents 28e9555 + 32dd495 commit eed1458

File tree

6 files changed

+132
-40
lines changed

6 files changed

+132
-40
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch"},"note":"Fix enum SCREAMING_SNAKE_CASE","date":"2026-04-17T07:02:13.191508900Z"}

Cargo.lock

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ documentation = "https://docs.rs/vespertide"
1313
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
1414

1515
[workspace.dependencies]
16-
vespertide-core = { path = "crates/vespertide-core", version = "0.1.58", default-features = false }
17-
vespertide-config = { path = "crates/vespertide-config", version = "0.1.58", default-features = false }
18-
vespertide-loader = { path = "crates/vespertide-loader", version = "0.1.58", default-features = false }
19-
vespertide-macro = { path = "crates/vespertide-macro", version = "0.1.58" }
20-
vespertide-naming = { path = "crates/vespertide-naming", version = "0.1.58" }
21-
vespertide-planner = { path = "crates/vespertide-planner", version = "0.1.58" }
22-
vespertide-query = { path = "crates/vespertide-query", version = "0.1.58" }
23-
vespertide-exporter = { path = "crates/vespertide-exporter", version = "0.1.58" }
16+
vespertide-core = { path = "crates/vespertide-core", default-features = false }
17+
vespertide-config = { path = "crates/vespertide-config", default-features = false }
18+
vespertide-loader = { path = "crates/vespertide-loader", default-features = false }
19+
vespertide-macro = { path = "crates/vespertide-macro" }
20+
vespertide-naming = { path = "crates/vespertide-naming" }
21+
vespertide-planner = { path = "crates/vespertide-planner" }
22+
vespertide-query = { path = "crates/vespertide-query" }
23+
vespertide-exporter = { path = "crates/vespertide-exporter" }
2424

2525
[profile.dev]
2626
debug = 1 # Line tables only — faster DWARF generation for large codegen output

crates/vespertide-exporter/src/seaorm/mod.rs

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,7 +1584,7 @@ fn render_enum(
15841584
lines.push(format!("#[derive({})]", derives.join(", ")));
15851585
lines.push(format!(
15861586
"#[serde(rename_all = \"{}\")]",
1587-
config.enum_naming_case().serde_rename_all()
1587+
enum_serde_rename_all(values, config)
15881588
));
15891589

15901590
match values {
@@ -1605,8 +1605,9 @@ fn render_enum(
16051605

16061606
match values {
16071607
EnumValues::String(string_values) => {
1608+
let use_screaming_snake_variants = uses_screaming_snake_variants(string_values);
16081609
for s in string_values {
1609-
let variant_name = enum_variant_name(s);
1610+
let variant_name = enum_string_variant_name(s, use_screaming_snake_variants);
16101611
lines.push(format!(" #[sea_orm(string_value = \"{}\")]", s));
16111612
lines.push(format!(" {},", variant_name));
16121613
}
@@ -1631,6 +1632,20 @@ fn render_enum(
16311632
fn enum_variant_name(s: &str) -> String {
16321633
let pascal = to_pascal_case(s);
16331634

1635+
finalize_enum_variant_name(pascal)
1636+
}
1637+
1638+
fn enum_string_variant_name(s: &str, use_screaming_snake_variants: bool) -> String {
1639+
let pascal = if use_screaming_snake_variants {
1640+
screaming_snake_to_pascal_case(s)
1641+
} else {
1642+
to_pascal_case(s)
1643+
};
1644+
1645+
finalize_enum_variant_name(pascal)
1646+
}
1647+
1648+
fn finalize_enum_variant_name(pascal: String) -> String {
16341649
// Handle empty string
16351650
if pascal.is_empty() {
16361651
return "Value".to_string();
@@ -1649,6 +1664,56 @@ fn enum_variant_name(s: &str) -> String {
16491664
pascal
16501665
}
16511666

1667+
fn enum_serde_rename_all(values: &EnumValues, config: &SeaOrmConfig) -> &'static str {
1668+
match values {
1669+
EnumValues::String(string_values) if uses_screaming_snake_variants(string_values) => {
1670+
"SCREAMING_SNAKE_CASE"
1671+
}
1672+
_ => config.enum_naming_case().serde_rename_all(),
1673+
}
1674+
}
1675+
1676+
fn uses_screaming_snake_variants(values: &[String]) -> bool {
1677+
!values.is_empty() && values.iter().all(|value| is_screaming_snake_value(value))
1678+
}
1679+
1680+
fn is_screaming_snake_value(value: &str) -> bool {
1681+
let mut has_ascii_upper = false;
1682+
1683+
for ch in value.chars() {
1684+
if ch.is_ascii_lowercase() {
1685+
return false;
1686+
}
1687+
if ch.is_ascii_uppercase() {
1688+
has_ascii_upper = true;
1689+
continue;
1690+
}
1691+
if ch.is_ascii_digit() || ch == '_' {
1692+
continue;
1693+
}
1694+
return false;
1695+
}
1696+
1697+
has_ascii_upper
1698+
}
1699+
1700+
fn screaming_snake_to_pascal_case(value: &str) -> String {
1701+
value
1702+
.split('_')
1703+
.filter(|segment| !segment.is_empty())
1704+
.map(|segment| {
1705+
let mut chars = segment.chars();
1706+
let first = chars
1707+
.next()
1708+
.expect("empty segments are filtered before PascalCase conversion");
1709+
let mut out = String::new();
1710+
out.push(first.to_ascii_uppercase());
1711+
out.extend(chars.map(|ch| ch.to_ascii_lowercase()));
1712+
out
1713+
})
1714+
.collect()
1715+
}
1716+
16521717
fn to_pascal_case(s: &str) -> String {
16531718
let mut result = String::new();
16541719
let mut capitalize = true;
@@ -2182,6 +2247,8 @@ mod helper_tests {
21822247
#[case("pending", "Pending")]
21832248
#[case("in_stock", "InStock")]
21842249
#[case("info-level", "InfoLevel")]
2250+
#[case("ACTIVE", "ACTIVE")]
2251+
#[case("ERROR_LEVEL", "ERRORLEVEL")]
21852252
#[case("1critical", "N1critical")]
21862253
#[case("123abc", "N123abc")]
21872254
#[case("1_critical", "N1Critical")]
@@ -2190,6 +2257,33 @@ mod helper_tests {
21902257
assert_eq!(enum_variant_name(input), expected);
21912258
}
21922259

2260+
#[test]
2261+
fn test_render_enum_uses_screaming_snake_serde_for_uppercase_values() {
2262+
let mut lines = Vec::new();
2263+
let config = SeaOrmConfig::default();
2264+
let values = EnumValues::String(vec!["PENDING".into(), "IN_PROGRESS".into()]);
2265+
2266+
render_enum(&mut lines, "orders", "order_status", &values, &config);
2267+
2268+
let result = lines.join("\n");
2269+
assert!(result.contains("#[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]"));
2270+
assert!(result.contains(" #[sea_orm(string_value = \"PENDING\")]\n Pending,"));
2271+
assert!(result.contains(" #[sea_orm(string_value = \"IN_PROGRESS\")]\n InProgress,"));
2272+
}
2273+
2274+
#[test]
2275+
fn test_is_screaming_snake_value_rejects_invalid_symbol() {
2276+
assert!(!is_screaming_snake_value("PENDING-REVIEW"));
2277+
}
2278+
2279+
#[test]
2280+
fn test_screaming_snake_to_pascal_case_ignores_empty_segments() {
2281+
assert_eq!(
2282+
screaming_snake_to_pascal_case("PENDING__REVIEW"),
2283+
"PendingReview"
2284+
);
2285+
}
2286+
21932287
fn string_enum_order_status() -> (&'static str, EnumValues) {
21942288
(
21952289
"order_status",

crates/vespertide-planner/src/validate.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,11 @@ pub fn validate_migration_plan(plan: &MigrationPlan) -> Result<(), PlannerError>
417417
nullable,
418418
fill_with,
419419
delete_null_rows,
420-
} => {
420+
}
421421
// If changing from nullable to non-nullable, fill_with is required
422-
if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) {
422+
if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) => {
423423
return Err(PlannerError::MissingFillWith(table.clone(), column.clone()));
424424
}
425-
}
426425
MigrationAction::ModifyColumnType {
427426
table,
428427
column,
@@ -484,9 +483,9 @@ pub fn find_missing_fill_with(
484483
table,
485484
column,
486485
fill_with,
487-
} => {
486+
}
488487
// If column is NOT NULL and has no default, fill_with is required
489-
if !column.nullable && column.default.is_none() && fill_with.is_none() {
488+
if !column.nullable && column.default.is_none() && fill_with.is_none() => {
490489
missing.push(FillWithRequired {
491490
action_index: idx,
492491
table: table.clone(),
@@ -498,17 +497,16 @@ pub fn find_missing_fill_with(
498497
has_foreign_key: false,
499498
});
500499
}
501-
}
502500
MigrationAction::ModifyColumnNullable {
503501
table,
504502
column,
505503
nullable,
506504
fill_with,
507505
delete_null_rows,
508-
} => {
506+
}
509507
// If changing from nullable to non-nullable, fill_with is required
510508
// UNLESS the column already has a default value (which will be used)
511-
if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) {
509+
if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) => {
512510
// Look up column from the current schema
513511
let table_def = current_schema.iter().find(|t| t.name == *table);
514512

@@ -542,7 +540,6 @@ pub fn find_missing_fill_with(
542540
has_foreign_key,
543541
});
544542
}
545-
}
546543
_ => {}
547544
}
548545
}

examples/app/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ tokio = { version = "1", features = ["full"] }
1010
sea-orm = { version = "2.0.0-rc.37", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
1111
anyhow = "1"
1212
serde = { version = "1", features = ["derive"] }
13-
vespera = "0.1.48"
13+
vespera = "0.1.50"

0 commit comments

Comments
 (0)