Skip to main content

vespertide_core/schema/
str_or_bool.rs

1use serde::{Deserialize, Serialize};
2
3/// A JSON value that can be a string, an array of strings, or a boolean.
4///
5/// Used for inline `"unique"` and `"index"` declarations on a [`ColumnDef`]:
6/// - `true` / `false` — enable or disable the constraint with an auto-generated name.
7/// - A single string — the constraint name (used to group columns into a composite constraint).
8/// - An array of strings — a list of constraint names for multi-group membership.
9///
10/// This enum is `#[non_exhaustive]`: new variants may be added in future releases.
11/// Downstream `match` expressions should include a wildcard arm.
12///
13/// [`ColumnDef`]: crate::schema::ColumnDef
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[serde(rename_all = "snake_case", untagged)]
17#[non_exhaustive]
18pub enum StrOrBoolOrArray {
19    /// A named constraint or group identifier.
20    Str(String),
21    /// Multiple constraint group names for multi-group membership.
22    Array(Vec<String>),
23    /// `true` to enable with an auto-generated name; `false` to disable.
24    Bool(bool),
25}
26
27impl StrOrBoolOrArray {
28    /// Returns the string value when this is `Str`.
29    #[must_use]
30    pub fn as_str(&self) -> Option<&str> {
31        match self {
32            StrOrBoolOrArray::Str(s) => Some(s.as_str()),
33            StrOrBoolOrArray::Array(_) | StrOrBoolOrArray::Bool(_) => None,
34        }
35    }
36}
37
38/// A column default value that can be a boolean, integer, float, or SQL expression string.
39///
40/// In JSON model files the `"default"` field accepts any of these forms:
41/// - `true` / `false` — boolean literal.
42/// - `0`, `42` — integer literal.
43/// - `0.0`, `1.5` — floating-point literal.
44/// - `"'pending'"` — SQL string literal (note the inner single quotes).
45/// - `"NOW()"` — SQL function call (no surrounding quotes).
46///
47/// Use [`DefaultValue::to_sql`] to convert to the SQL representation for DDL generation.
48///
49/// `StringOrBool` is a backwards-compatibility alias for this type.
50///
51/// This enum is `#[non_exhaustive]`: new variants may be added in future releases.
52/// Downstream `match` expressions should include a wildcard arm.
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55#[serde(untagged)]
56#[non_exhaustive]
57pub enum DefaultValue {
58    /// A boolean default (`true` or `false`).
59    Bool(bool),
60    /// An integer default value.
61    Integer(i64),
62    /// A floating-point default value.
63    Float(f64),
64    /// A SQL expression or string literal default (e.g. `"'pending'"` or `"NOW()"`).
65    String(String),
66}
67
68impl Eq for DefaultValue {}
69
70impl DefaultValue {
71    /// Returns the boolean value when this is `Bool`.
72    #[must_use]
73    pub fn as_bool(&self) -> Option<bool> {
74        match self {
75            DefaultValue::Bool(b) => Some(*b),
76            DefaultValue::Integer(_) | DefaultValue::Float(_) | DefaultValue::String(_) => None,
77        }
78    }
79
80    /// Convert to SQL string representation
81    /// Empty strings are converted to '' (SQL empty string literal)
82    pub fn to_sql(&self) -> String {
83        match self {
84            DefaultValue::Bool(b) => b.to_string(),
85            DefaultValue::Integer(n) => n.to_string(),
86            DefaultValue::Float(f) => f.to_string(),
87            DefaultValue::String(s) => {
88                if s.is_empty() {
89                    "''".to_string()
90                } else {
91                    s.clone()
92                }
93            }
94        }
95    }
96
97    /// Check if this is a string type (needs quoting for certain column types)
98    pub fn is_string(&self) -> bool {
99        matches!(self, DefaultValue::String(_))
100    }
101
102    /// Check if this is an empty string
103    pub fn is_empty_string(&self) -> bool {
104        matches!(self, DefaultValue::String(s) if s.is_empty())
105    }
106}
107
108impl From<bool> for DefaultValue {
109    fn from(b: bool) -> Self {
110        DefaultValue::Bool(b)
111    }
112}
113
114impl From<i64> for DefaultValue {
115    fn from(n: i64) -> Self {
116        DefaultValue::Integer(n)
117    }
118}
119
120impl From<i32> for DefaultValue {
121    fn from(n: i32) -> Self {
122        DefaultValue::Integer(i64::from(n))
123    }
124}
125
126impl From<f64> for DefaultValue {
127    fn from(f: f64) -> Self {
128        DefaultValue::Float(f)
129    }
130}
131
132impl From<String> for DefaultValue {
133    fn from(s: String) -> Self {
134        DefaultValue::String(s)
135    }
136}
137
138impl From<&str> for DefaultValue {
139    fn from(s: &str) -> Self {
140        DefaultValue::String(s.to_string())
141    }
142}
143
144/// Backwards compatibility alias
145pub type StringOrBool = DefaultValue;
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_default_value_to_sql_bool() {
153        let val = DefaultValue::Bool(true);
154        assert_eq!(val.to_sql(), "true");
155
156        let val = DefaultValue::Bool(false);
157        assert_eq!(val.to_sql(), "false");
158    }
159
160    #[test]
161    fn test_default_value_to_sql_integer() {
162        let val = DefaultValue::Integer(42);
163        assert_eq!(val.to_sql(), "42");
164
165        let val = DefaultValue::Integer(-100);
166        assert_eq!(val.to_sql(), "-100");
167    }
168
169    #[test]
170    fn test_default_value_to_sql_float() {
171        let val = DefaultValue::Float(1.5);
172        assert_eq!(val.to_sql(), "1.5");
173    }
174
175    #[test]
176    fn test_default_value_to_sql_string() {
177        let val = DefaultValue::String("hello".into());
178        assert_eq!(val.to_sql(), "hello");
179    }
180
181    #[test]
182    fn test_default_value_to_sql_empty_string() {
183        let val = DefaultValue::String(String::new());
184        assert_eq!(val.to_sql(), "''");
185    }
186
187    #[test]
188    fn test_default_value_is_empty_string() {
189        assert!(DefaultValue::String(String::new()).is_empty_string());
190        assert!(!DefaultValue::String("hello".into()).is_empty_string());
191        assert!(!DefaultValue::Bool(true).is_empty_string());
192        assert!(!DefaultValue::Integer(0).is_empty_string());
193    }
194
195    #[test]
196    fn test_default_value_from_bool() {
197        let val: DefaultValue = true.into();
198        assert_eq!(val, DefaultValue::Bool(true));
199
200        let val: DefaultValue = false.into();
201        assert_eq!(val, DefaultValue::Bool(false));
202    }
203
204    #[test]
205    fn test_default_value_from_integer() {
206        let val: DefaultValue = 42i64.into();
207        assert_eq!(val, DefaultValue::Integer(42));
208
209        let val: DefaultValue = 100i32.into();
210        assert_eq!(val, DefaultValue::Integer(100));
211    }
212
213    #[test]
214    fn test_default_value_from_float() {
215        let val: DefaultValue = 1.5f64.into();
216        assert_eq!(val, DefaultValue::Float(1.5));
217    }
218
219    #[test]
220    fn test_default_value_from_string() {
221        let val: DefaultValue = String::from("test").into();
222        assert_eq!(val, DefaultValue::String("test".into()));
223    }
224
225    #[test]
226    fn test_default_value_from_str() {
227        let val: DefaultValue = "test".into();
228        assert_eq!(val, DefaultValue::String("test".into()));
229    }
230
231    #[test]
232    fn test_default_value_is_string() {
233        assert!(DefaultValue::String("test".into()).is_string());
234        assert!(!DefaultValue::Bool(true).is_string());
235        assert!(!DefaultValue::Integer(42).is_string());
236        assert!(!DefaultValue::Float(1.5).is_string());
237    }
238
239    #[test]
240    fn test_string_or_bool_as_bool() {
241        assert_eq!(StringOrBool::Bool(true).as_bool(), Some(true));
242        assert_eq!(StringOrBool::Bool(false).as_bool(), Some(false));
243        assert_eq!(StringOrBool::String("value".into()).as_bool(), None);
244    }
245
246    #[test]
247    fn test_str_or_bool_or_array_as_str() {
248        assert_eq!(StrOrBoolOrArray::Str("name".into()).as_str(), Some("name"));
249        assert_eq!(StrOrBoolOrArray::Bool(true).as_str(), None);
250        assert_eq!(StrOrBoolOrArray::Array(vec!["name".into()]).as_str(), None);
251    }
252}