rullst_orm/schema/
enums.rs1use crate::Error;
2
3pub const MAX_DATABASE_ENUM_VARIANTS: usize = 64;
5pub const MAX_DATABASE_ENUM_LABEL_BYTES: usize = 63;
8
9pub trait DatabaseEnum {
15 const TYPE_NAME: &'static str;
17 const VARIANTS: &'static [&'static str];
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub(super) struct NativeEnumDefinition {
23 pub type_name: &'static str,
24 pub variants: &'static [&'static str],
25}
26
27pub(super) fn validated_definition<E: DatabaseEnum>() -> Result<NativeEnumDefinition, Error> {
28 validate_native_enum(E::TYPE_NAME, E::VARIANTS)?;
29 Ok(NativeEnumDefinition {
30 type_name: E::TYPE_NAME,
31 variants: E::VARIANTS,
32 })
33}
34
35pub(super) fn validate_native_enum(type_name: &str, variants: &[&str]) -> Result<(), Error> {
36 let type_name_bytes = type_name.as_bytes();
37 if type_name_bytes.is_empty()
38 || type_name_bytes.len() > MAX_DATABASE_ENUM_LABEL_BYTES
39 || !(type_name_bytes[0].is_ascii_alphabetic() || type_name_bytes[0] == b'_')
40 || !type_name_bytes
41 .iter()
42 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
43 {
44 return Err(Error::Validation(format!(
45 "database enum type names must be 1-{MAX_DATABASE_ENUM_LABEL_BYTES} byte unqualified ASCII identifiers"
46 )));
47 }
48 if variants.is_empty() || variants.len() > MAX_DATABASE_ENUM_VARIANTS {
49 return Err(Error::Validation(format!(
50 "database enums require 1-{MAX_DATABASE_ENUM_VARIANTS} variants"
51 )));
52 }
53
54 let mut seen = std::collections::BTreeSet::new();
55 for variant in variants {
56 if variant.is_empty()
57 || variant.len() > MAX_DATABASE_ENUM_LABEL_BYTES
58 || !variant
59 .bytes()
60 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b' '))
61 {
62 return Err(Error::Validation(format!(
63 "database enum labels must be 1-{MAX_DATABASE_ENUM_LABEL_BYTES} ASCII letters, digits, spaces, underscores or hyphens"
64 )));
65 }
66 if !seen.insert(*variant) {
67 return Err(Error::Validation(
68 "database enum labels must be unique".to_string(),
69 ));
70 }
71 }
72 Ok(())
73}
74
75pub(super) fn quoted_label(value: &str) -> String {
76 format!("'{}'", value.replace('\'', "''"))
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn enum_metadata_rejects_unsafe_or_ambiguous_shapes() {
85 assert!(validate_native_enum("account_status", &["Active", "Paused"]).is_ok());
86 assert!(validate_native_enum("public.account_status", &["Active"]).is_err());
87 assert!(validate_native_enum("1account_status", &["Active"]).is_err());
88 assert!(validate_native_enum("status;drop", &["Active"]).is_err());
89 assert!(
90 validate_native_enum(&"x".repeat(MAX_DATABASE_ENUM_LABEL_BYTES + 1), &["Active"])
91 .is_err()
92 );
93 assert!(validate_native_enum("status", &[]).is_err());
94 assert!(validate_native_enum("status", &["Active", "Active"]).is_err());
95 assert!(validate_native_enum("status", &["line\nfeed"]).is_err());
96 assert!(validate_native_enum("status", &["owner's"]).is_err());
97 assert!(validate_native_enum("status", &["back\\slash"]).is_err());
98 assert!(
99 validate_native_enum("status", &[&"x".repeat(MAX_DATABASE_ENUM_LABEL_BYTES + 1)])
100 .is_err()
101 );
102 }
103
104 #[test]
105 fn enum_labels_are_sql_quoted_defensively() {
106 assert_eq!(quoted_label("awaiting_review"), "'awaiting_review'");
107 assert_eq!(quoted_label("owner's"), "'owner''s'");
108 }
109}