Skip to main content

sqlmodel_core/
validate.rs

1//! Runtime validation helpers for SQLModel.
2//!
3//! This module provides validation functions that can be called from
4//! generated validation code (via the `#[derive(Validate)]` macro).
5//!
6//! It also provides `model_validate()` functionality for creating and
7//! validating models from various input types (similar to Pydantic).
8
9use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use regex::Regex;
13use serde::de::DeserializeOwned;
14
15use crate::Value;
16use crate::error::{ValidationError, ValidationErrorKind};
17
18/// Thread-safe regex cache for compiled patterns.
19///
20/// This avoids recompiling regex patterns on every validation call.
21/// Patterns are compiled lazily on first use and cached for the lifetime
22/// of the program.
23struct RegexCache {
24    cache: std::sync::RwLock<std::collections::HashMap<String, Regex>>,
25}
26
27impl RegexCache {
28    fn new() -> Self {
29        Self {
30            cache: std::sync::RwLock::new(std::collections::HashMap::new()),
31        }
32    }
33
34    fn get_or_compile(&self, pattern: &str) -> Result<Regex, regex::Error> {
35        // Fast path: check if already cached
36        // Use unwrap_or_else to recover from poisoned lock (another thread panicked)
37        {
38            let cache = self.cache.read().unwrap_or_else(|e| e.into_inner());
39            if let Some(regex) = cache.get(pattern) {
40                return Ok(regex.clone());
41            }
42        }
43
44        // Slow path: compile and cache
45        let regex = Regex::new(pattern)?;
46        {
47            let mut cache = self.cache.write().unwrap_or_else(|e| e.into_inner());
48            cache.insert(pattern.to_string(), regex.clone());
49        }
50        Ok(regex)
51    }
52}
53
54/// Global regex cache singleton.
55fn regex_cache() -> &'static RegexCache {
56    static CACHE: OnceLock<RegexCache> = OnceLock::new();
57    CACHE.get_or_init(RegexCache::new)
58}
59
60/// Check if a string matches a regex pattern.
61///
62/// This function is designed to be called from generated validation code.
63/// It caches compiled regex patterns for efficiency.
64///
65/// # Arguments
66///
67/// * `value` - The string to validate
68/// * `pattern` - The regex pattern to match against
69///
70/// # Returns
71///
72/// `true` if the value matches the pattern, `false` otherwise.
73/// Returns `false` if the pattern is invalid (logs a warning).
74///
75/// # Example
76///
77/// ```ignore
78/// use sqlmodel_core::validate::matches_pattern;
79///
80/// assert!(matches_pattern("test@example.com", r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"));
81/// assert!(!matches_pattern("invalid", r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"));
82/// ```
83pub fn matches_pattern(value: &str, pattern: &str) -> bool {
84    match regex_cache().get_or_compile(pattern) {
85        Ok(regex) => regex.is_match(value),
86        Err(e) => {
87            // Log the error but don't panic - validation should be resilient
88            tracing::warn!(
89                pattern = pattern,
90                error = %e,
91                "Invalid regex pattern in validation, treating as non-match"
92            );
93            false
94        }
95    }
96}
97
98/// Validate a regex pattern at compile time (for use in proc macros).
99///
100/// Returns an error message if the pattern is invalid, None if valid.
101pub fn validate_pattern(pattern: &str) -> Option<String> {
102    match Regex::new(pattern) {
103        Ok(_) => None,
104        Err(e) => Some(format!("invalid regex pattern: {e}")),
105    }
106}
107
108// ============================================================================
109// Built-in Validators
110// ============================================================================
111
112/// Validate a credit card number using the Luhn algorithm.
113///
114/// The Luhn algorithm (also known as the "modulus 10" algorithm) is a simple
115/// checksum formula used to validate identification numbers such as credit card
116/// numbers, IMEI numbers, and others.
117///
118/// # Algorithm
119///
120/// 1. Starting from the rightmost digit (check digit) and moving left,
121///    double the value of every second digit.
122/// 2. If the result of doubling is greater than 9, subtract 9.
123/// 3. Sum all the digits.
124/// 4. The total modulo 10 must equal 0.
125///
126/// # Arguments
127///
128/// * `value` - The credit card number as a string (may contain spaces or hyphens)
129///
130/// # Returns
131///
132/// `true` if the number is valid according to the Luhn algorithm, `false` otherwise.
133///
134/// # Example
135///
136/// ```ignore
137/// use sqlmodel_core::validate::is_valid_credit_card;
138///
139/// assert!(is_valid_credit_card("4539578763621486"));  // Valid Visa
140/// assert!(is_valid_credit_card("4539-5787-6362-1486")); // With dashes
141/// assert!(is_valid_credit_card("4539 5787 6362 1486")); // With spaces
142/// assert!(!is_valid_credit_card("1234567890123456")); // Invalid
143/// ```
144pub fn is_valid_credit_card(value: &str) -> bool {
145    // Remove all non-digit characters (spaces, hyphens, etc.)
146    let digits: Vec<u32> = value
147        .chars()
148        .filter(|c| c.is_ascii_digit())
149        .filter_map(|c| c.to_digit(10))
150        .collect();
151
152    // Credit card numbers are typically 13-19 digits
153    if digits.len() < 13 || digits.len() > 19 {
154        return false;
155    }
156
157    // Luhn algorithm
158    let mut sum = 0u32;
159    let len = digits.len();
160
161    for (i, &digit) in digits.iter().enumerate() {
162        // Count from right: rightmost is position 1 (odd)
163        // We double every second digit starting from the second-to-last
164        let position_from_right = len - i;
165        let is_double_position = position_from_right % 2 == 0;
166
167        let value = if is_double_position {
168            let doubled = digit * 2;
169            if doubled > 9 { doubled - 9 } else { doubled }
170        } else {
171            digit
172        };
173
174        sum += value;
175    }
176
177    sum % 10 == 0
178}
179
180// ============================================================================
181// Model Validation (model_validate)
182// ============================================================================
183
184/// Input types for model_validate().
185///
186/// Supports creating models from various input formats.
187#[derive(Debug, Clone)]
188pub enum ValidateInput {
189    /// A HashMap of field names to values.
190    Dict(HashMap<String, Value>),
191    /// A JSON string to parse.
192    Json(String),
193    /// A serde_json::Value for direct deserialization.
194    JsonValue(serde_json::Value),
195}
196
197impl From<HashMap<String, Value>> for ValidateInput {
198    fn from(map: HashMap<String, Value>) -> Self {
199        ValidateInput::Dict(map)
200    }
201}
202
203impl From<String> for ValidateInput {
204    fn from(json: String) -> Self {
205        ValidateInput::Json(json)
206    }
207}
208
209impl From<&str> for ValidateInput {
210    fn from(json: &str) -> Self {
211        ValidateInput::Json(json.to_string())
212    }
213}
214
215impl From<serde_json::Value> for ValidateInput {
216    fn from(value: serde_json::Value) -> Self {
217        ValidateInput::JsonValue(value)
218    }
219}
220
221/// Options for model_validate().
222///
223/// Controls the validation behavior.
224#[derive(Debug, Clone, Default)]
225pub struct ValidateOptions {
226    /// If true, use strict type coercion (no implicit conversions).
227    pub strict: bool,
228    /// If true, read from object attributes (ORM mode).
229    /// Currently unused - reserved for future from_attributes support.
230    pub from_attributes: bool,
231    /// Optional context dictionary passed to custom validators.
232    pub context: Option<HashMap<String, serde_json::Value>>,
233    /// Additional values to merge into the result after parsing.
234    pub update: Option<HashMap<String, serde_json::Value>>,
235}
236
237impl ValidateOptions {
238    /// Create new default options.
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Enable strict mode (no implicit type conversions).
244    pub fn strict(mut self) -> Self {
245        self.strict = true;
246        self
247    }
248
249    /// Enable from_attributes mode (read from object attributes).
250    pub fn from_attributes(mut self) -> Self {
251        self.from_attributes = true;
252        self
253    }
254
255    /// Set context for custom validators.
256    pub fn with_context(mut self, context: HashMap<String, serde_json::Value>) -> Self {
257        self.context = Some(context);
258        self
259    }
260
261    /// Set values to merge into result.
262    pub fn with_update(mut self, update: HashMap<String, serde_json::Value>) -> Self {
263        self.update = Some(update);
264        self
265    }
266}
267
268/// Result type for model_validate operations.
269pub type ValidateResult<T> = std::result::Result<T, ValidationError>;
270
271/// Trait for models that support model_validate().
272///
273/// This is typically implemented via derive macro or blanket impl
274/// for models that implement Deserialize.
275pub trait ModelValidate: Sized {
276    /// Create and validate a model from input.
277    ///
278    /// # Arguments
279    ///
280    /// * `input` - The input to validate (Dict, Json, or JsonValue)
281    /// * `options` - Validation options
282    ///
283    /// # Returns
284    ///
285    /// The validated model or validation errors.
286    ///
287    /// # Example
288    ///
289    /// ```ignore
290    /// use sqlmodel_core::validate::{ModelValidate, ValidateInput, ValidateOptions};
291    ///
292    /// let user = User::model_validate(
293    ///     r#"{"name": "Alice", "age": 30}"#,
294    ///     ValidateOptions::default()
295    /// )?;
296    /// ```
297    fn model_validate(
298        input: impl Into<ValidateInput>,
299        options: ValidateOptions,
300    ) -> ValidateResult<Self>;
301
302    /// Create and validate a model from JSON string with default options.
303    fn model_validate_json(json: &str) -> ValidateResult<Self> {
304        Self::model_validate(json, ValidateOptions::default())
305    }
306
307    /// Create and validate a model from a HashMap with default options.
308    fn model_validate_dict(dict: HashMap<String, Value>) -> ValidateResult<Self> {
309        Self::model_validate(dict, ValidateOptions::default())
310    }
311}
312
313/// Blanket implementation of ModelValidate for types that implement DeserializeOwned.
314///
315/// This provides model_validate() for any model that can be deserialized from JSON.
316impl<T: DeserializeOwned> ModelValidate for T {
317    fn model_validate(
318        input: impl Into<ValidateInput>,
319        options: ValidateOptions,
320    ) -> ValidateResult<Self> {
321        let input = input.into();
322
323        // Convert input to serde_json::Value
324        let mut json_value = match input {
325            ValidateInput::Dict(dict) => {
326                // Convert HashMap<String, Value> to serde_json::Value
327                let map: serde_json::Map<String, serde_json::Value> = dict
328                    .into_iter()
329                    .map(|(k, v)| (k, value_to_json(v)))
330                    .collect();
331                serde_json::Value::Object(map)
332            }
333            ValidateInput::Json(json_str) => serde_json::from_str(&json_str).map_err(|e| {
334                let mut err = ValidationError::new();
335                err.add(
336                    "_json",
337                    ValidationErrorKind::Custom,
338                    format!("Invalid JSON: {e}"),
339                );
340                err
341            })?,
342            ValidateInput::JsonValue(value) => value,
343        };
344
345        // Apply update values if provided
346        if let Some(update) = options.update {
347            if let serde_json::Value::Object(ref mut map) = json_value {
348                for (key, value) in update {
349                    map.insert(key, value);
350                }
351            }
352        }
353
354        // Deserialize with appropriate strictness
355        if options.strict {
356            // In strict mode, we use serde's strict deserialization
357            // (default behavior - no implicit conversions)
358            serde_json::from_value(json_value).map_err(|e| {
359                let mut err = ValidationError::new();
360                err.add(
361                    "_model",
362                    ValidationErrorKind::Custom,
363                    format!("Validation failed: {e}"),
364                );
365                err
366            })
367        } else {
368            // Non-strict mode - same for now, but could add coercion logic
369            serde_json::from_value(json_value).map_err(|e| {
370                let mut err = ValidationError::new();
371                err.add(
372                    "_model",
373                    ValidationErrorKind::Custom,
374                    format!("Validation failed: {e}"),
375                );
376                err
377            })
378        }
379    }
380}
381
382// ============================================================================
383// Model Dump (model_dump)
384// ============================================================================
385
386/// Output mode for model_dump().
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
388pub enum DumpMode {
389    /// JSON-compatible types (strings, numbers, booleans, null)
390    #[default]
391    Json,
392    /// Rust native types (preserves Value variants)
393    Python,
394}
395
396/// Options for model_dump() and model_dump_json().
397///
398/// Controls the serialization behavior.
399#[derive(Debug, Clone, Default)]
400pub struct DumpOptions {
401    /// Output mode: Json or Python (Rust native).
402    ///
403    /// **Note:** This option is currently NOT IMPLEMENTED. In Pydantic,
404    /// `mode='json'` returns JSON-compatible types (strings for dates) while
405    /// `mode='python'` returns native Python types (datetime objects). In Rust
406    /// with serde, type serialization is determined by Serialize implementations,
407    /// so both modes currently produce identical `serde_json::Value` output.
408    pub mode: DumpMode,
409    /// Only include these fields (if Some)
410    pub include: Option<std::collections::HashSet<String>>,
411    /// Exclude these fields
412    pub exclude: Option<std::collections::HashSet<String>>,
413    /// Use field aliases in output.
414    ///
415    /// When true, `sql_model_dump()` will rename fields to their
416    /// `serialization_alias` (or `alias` as fallback) in the output.
417    pub by_alias: bool,
418    /// Exclude fields that were not explicitly set.
419    ///
420    /// **Note:** This option is currently NOT IMPLEMENTED. It is defined for
421    /// API compatibility with Pydantic but has no effect. Full implementation
422    /// requires compile-time code generation to track which fields were
423    /// explicitly provided during construction (a `fields_set: HashSet<String>`).
424    /// This differs from `exclude_defaults` which compares current values to
425    /// known defaults.
426    ///
427    /// Pydantic semantics: If a field has a default value and the user explicitly
428    /// sets it to that default, it should still be included (it was "set").
429    /// With `exclude_defaults`, it would be excluded regardless of whether
430    /// it was explicitly provided.
431    pub exclude_unset: bool,
432    /// Exclude fields with default values
433    pub exclude_defaults: bool,
434    /// Exclude fields with None/null values
435    pub exclude_none: bool,
436    /// Exclude computed fields (for future computed_field support)
437    pub exclude_computed_fields: bool,
438    /// Enable round-trip mode (preserves types for re-parsing).
439    ///
440    /// **Note:** This option is currently NOT IMPLEMENTED. In Pydantic,
441    /// round_trip mode serializes values in a way that can be deserialized
442    /// back to the exact same model. In Rust with serde, round-trip fidelity
443    /// is generally handled by the Serialize/Deserialize implementations.
444    pub round_trip: bool,
445    /// Indentation for JSON output (None = compact, Some(n) = n spaces)
446    pub indent: Option<usize>,
447}
448
449impl DumpOptions {
450    /// Create new default options.
451    pub fn new() -> Self {
452        Self::default()
453    }
454
455    /// Set output mode to JSON.
456    pub fn json(mut self) -> Self {
457        self.mode = DumpMode::Json;
458        self
459    }
460
461    /// Set output mode to Python (Rust native).
462    pub fn python(mut self) -> Self {
463        self.mode = DumpMode::Python;
464        self
465    }
466
467    /// Set fields to include.
468    pub fn include(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
469        self.include = Some(fields.into_iter().map(Into::into).collect());
470        self
471    }
472
473    /// Set fields to exclude.
474    pub fn exclude(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
475        self.exclude = Some(fields.into_iter().map(Into::into).collect());
476        self
477    }
478
479    /// Enable by_alias mode.
480    pub fn by_alias(mut self) -> Self {
481        self.by_alias = true;
482        self
483    }
484
485    /// Enable exclude_unset mode.
486    pub fn exclude_unset(mut self) -> Self {
487        self.exclude_unset = true;
488        self
489    }
490
491    /// Enable exclude_defaults mode.
492    pub fn exclude_defaults(mut self) -> Self {
493        self.exclude_defaults = true;
494        self
495    }
496
497    /// Enable exclude_none mode.
498    pub fn exclude_none(mut self) -> Self {
499        self.exclude_none = true;
500        self
501    }
502
503    /// Enable exclude_computed_fields mode.
504    pub fn exclude_computed_fields(mut self) -> Self {
505        self.exclude_computed_fields = true;
506        self
507    }
508
509    /// Enable round_trip mode.
510    pub fn round_trip(mut self) -> Self {
511        self.round_trip = true;
512        self
513    }
514
515    /// Set indentation for JSON output.
516    ///
517    /// When set, JSON output will be pretty-printed with the specified number
518    /// of spaces for indentation. When None (default), JSON is compact.
519    pub fn indent(mut self, spaces: usize) -> Self {
520        self.indent = Some(spaces);
521        self
522    }
523}
524
525/// Result type for model_dump operations.
526pub type DumpResult = std::result::Result<serde_json::Value, serde_json::Error>;
527
528/// Trait for models that support model_dump().
529///
530/// This is typically implemented via blanket impl for models that implement Serialize.
531pub trait ModelDump {
532    /// Serialize a model to a JSON value.
533    ///
534    /// # Arguments
535    ///
536    /// * `options` - Dump options controlling serialization behavior
537    ///
538    /// # Returns
539    ///
540    /// A serde_json::Value representing the serialized model.
541    ///
542    /// # Example
543    ///
544    /// ```ignore
545    /// use sqlmodel_core::validate::{ModelDump, DumpOptions};
546    ///
547    /// let json = user.model_dump(DumpOptions::default())?;
548    /// ```
549    fn model_dump(&self, options: DumpOptions) -> DumpResult;
550
551    /// Serialize a model to a JSON string with default options.
552    fn model_dump_json(&self) -> std::result::Result<String, serde_json::Error> {
553        let value = self.model_dump(DumpOptions::default())?;
554        serde_json::to_string(&value)
555    }
556
557    /// Serialize a model to a pretty-printed JSON string.
558    fn model_dump_json_pretty(&self) -> std::result::Result<String, serde_json::Error> {
559        let value = self.model_dump(DumpOptions::default())?;
560        serde_json::to_string_pretty(&value)
561    }
562
563    /// Serialize a model to a JSON string with full options support.
564    ///
565    /// This method supports all DumpOptions including the `indent` option:
566    /// - `indent: None` - compact JSON output
567    /// - `indent: Some(n)` - pretty-printed with n spaces indentation
568    ///
569    /// # Example
570    ///
571    /// ```ignore
572    /// use sqlmodel_core::validate::{ModelDump, DumpOptions};
573    ///
574    /// // Compact JSON with exclusions
575    /// let json = user.model_dump_json_with_options(
576    ///     DumpOptions::default().exclude(["password"])
577    /// )?;
578    ///
579    /// // Pretty-printed with 4-space indent
580    /// let json = user.model_dump_json_with_options(
581    ///     DumpOptions::default().indent(4)
582    /// )?;
583    /// ```
584    fn model_dump_json_with_options(
585        &self,
586        options: DumpOptions,
587    ) -> std::result::Result<String, serde_json::Error> {
588        let value = self.model_dump(DumpOptions {
589            indent: None, // Don't pass indent to model_dump (it returns Value, not String)
590            ..options.clone()
591        })?;
592
593        match options.indent {
594            Some(spaces) => {
595                let indent_bytes = " ".repeat(spaces).into_bytes();
596                let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
597                let mut writer = Vec::new();
598                let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
599                serde::Serialize::serialize(&value, &mut ser)?;
600                // serde_json always produces valid UTF-8, but propagate error instead of panicking
601                String::from_utf8(writer).map_err(|e| {
602                    serde_json::Error::io(std::io::Error::new(
603                        std::io::ErrorKind::InvalidData,
604                        format!("UTF-8 encoding error: {e}"),
605                    ))
606                })
607            }
608            None => serde_json::to_string(&value),
609        }
610    }
611}
612
613/// Blanket implementation of ModelDump for types that implement Serialize.
614impl<T: serde::Serialize> ModelDump for T {
615    fn model_dump(&self, options: DumpOptions) -> DumpResult {
616        // First, serialize to JSON value
617        let mut value = serde_json::to_value(self)?;
618
619        // Apply options
620        if let serde_json::Value::Object(ref mut map) = value {
621            // Apply include filter
622            if let Some(ref include) = options.include {
623                map.retain(|k, _| include.contains(k));
624            }
625
626            // Apply exclude filter
627            if let Some(ref exclude) = options.exclude {
628                map.retain(|k, _| !exclude.contains(k));
629            }
630
631            // Apply exclude_none filter
632            if options.exclude_none {
633                map.retain(|_, v| !v.is_null());
634            }
635
636            // Note: This is the generic ModelDump implementation for Serialize types.
637            // - exclude_defaults: Requires Model::fields() metadata. Use SqlModelDump instead.
638            // - exclude_unset: Requires fields_set tracking. NOT IMPLEMENTED in either trait.
639            // - exclude_computed_fields: Requires Model::fields() metadata. Use SqlModelDump instead.
640            // - by_alias: Requires Model::fields() metadata. Use SqlModelDump instead.
641            // - mode/round_trip: Currently not implemented (no effect).
642        }
643
644        Ok(value)
645    }
646}
647
648/// Convert a Value to serde_json::Value.
649fn value_to_json(value: Value) -> serde_json::Value {
650    match value {
651        Value::Null => serde_json::Value::Null,
652        Value::Bool(b) => serde_json::Value::Bool(b),
653        Value::TinyInt(i) => serde_json::Value::Number(i.into()),
654        Value::SmallInt(i) => serde_json::Value::Number(i.into()),
655        Value::Int(i) => serde_json::Value::Number(i.into()),
656        Value::BigInt(i) => serde_json::Value::Number(i.into()),
657        Value::Float(f) => serde_json::Number::from_f64(f64::from(f))
658            .map_or(serde_json::Value::Null, serde_json::Value::Number),
659        Value::Double(f) => serde_json::Number::from_f64(f)
660            .map_or(serde_json::Value::Null, serde_json::Value::Number),
661        Value::Decimal(s) => serde_json::Value::String(s),
662        Value::Text(s) => serde_json::Value::String(s),
663        Value::Bytes(b) => {
664            // Encode bytes as hex string
665            use std::fmt::Write;
666            let hex = b
667                .iter()
668                .fold(String::with_capacity(b.len() * 2), |mut acc, byte| {
669                    let _ = write!(acc, "{byte:02x}");
670                    acc
671                });
672            serde_json::Value::String(hex)
673        }
674        // Date is i32 (days since epoch) - convert to number
675        Value::Date(d) => serde_json::Value::Number(d.into()),
676        // Time is i64 (microseconds since midnight)
677        Value::Time(t) => serde_json::Value::Number(t.into()),
678        // Timestamp is i64 (microseconds since epoch)
679        Value::Timestamp(ts) => serde_json::Value::Number(ts.into()),
680        // TimestampTz is i64 (microseconds since epoch, UTC)
681        Value::TimestampTz(ts) => serde_json::Value::Number(ts.into()),
682        // UUID is [u8; 16] - format as UUID string with dashes
683        Value::Uuid(u) => {
684            use std::fmt::Write;
685            let hex = u.iter().fold(String::with_capacity(32), |mut acc, b| {
686                let _ = write!(acc, "{b:02x}");
687                acc
688            });
689            // Format as UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
690            let formatted = format!(
691                "{}-{}-{}-{}-{}",
692                &hex[0..8],
693                &hex[8..12],
694                &hex[12..16],
695                &hex[16..20],
696                &hex[20..32]
697            );
698            serde_json::Value::String(formatted)
699        }
700        Value::Json(j) => j,
701        Value::Array(arr) => serde_json::Value::Array(arr.into_iter().map(value_to_json).collect()),
702        Value::Default => serde_json::Value::Null,
703    }
704}
705
706// ============================================================================
707// Alias-Aware Validation and Serialization
708// ============================================================================
709
710use crate::Model;
711
712/// Apply validation aliases to JSON input.
713///
714/// This transforms input keys that match validation_alias or alias to their
715/// corresponding field names, enabling deserialization to work correctly.
716///
717/// # Arguments
718///
719/// * `json` - The JSON value to transform (modified in place)
720/// * `fields` - The field metadata containing alias information
721pub fn apply_validation_aliases(json: &mut serde_json::Value, fields: &[crate::FieldInfo]) {
722    if let serde_json::Value::Object(map) = json {
723        // Build a mapping from alias -> field_name
724        let mut alias_map: HashMap<&str, &str> = HashMap::new();
725        for field in fields {
726            // validation_alias takes precedence for input
727            if let Some(alias) = field.validation_alias {
728                alias_map.insert(alias, field.name);
729            }
730            // Regular alias also works for input
731            if let Some(alias) = field.alias {
732                alias_map.entry(alias).or_insert(field.name);
733            }
734        }
735
736        // Collect keys that need to be renamed
737        let renames: Vec<(String, &str)> = map
738            .keys()
739            .filter_map(|k| alias_map.get(k.as_str()).map(|v| (k.clone(), *v)))
740            .collect();
741
742        // Apply renames
743        for (old_key, new_key) in renames {
744            if let Some(value) = map.remove(&old_key) {
745                // Only insert if the target key doesn't already exist
746                map.entry(new_key.to_string()).or_insert(value);
747            }
748        }
749    }
750}
751
752/// Apply serialization aliases to JSON output.
753///
754/// This transforms output keys from field names to their serialization_alias
755/// or alias, enabling proper JSON output format.
756///
757/// # Arguments
758///
759/// * `json` - The JSON value to transform (modified in place)
760/// * `fields` - The field metadata containing alias information
761pub fn apply_serialization_aliases(json: &mut serde_json::Value, fields: &[crate::FieldInfo]) {
762    if let serde_json::Value::Object(map) = json {
763        // Build a mapping from field_name -> output_alias
764        let mut alias_map: HashMap<&str, &str> = HashMap::new();
765        for field in fields {
766            // serialization_alias takes precedence for output
767            if let Some(alias) = field.serialization_alias {
768                alias_map.insert(field.name, alias);
769            } else if let Some(alias) = field.alias {
770                // Regular alias is fallback for output
771                alias_map.insert(field.name, alias);
772            }
773        }
774
775        // Collect keys that need to be renamed
776        let renames: Vec<(String, &str)> = map
777            .keys()
778            .filter_map(|k| alias_map.get(k.as_str()).map(|v| (k.clone(), *v)))
779            .collect();
780
781        // Apply renames
782        for (old_key, new_key) in renames {
783            if let Some(value) = map.remove(&old_key) {
784                map.insert(new_key.to_string(), value);
785            }
786        }
787    }
788}
789
790/// Model-aware validation that supports field aliases.
791///
792/// Unlike the generic `ModelValidate`, this trait uses the `Model::fields()`
793/// metadata to transform aliased input keys to their actual field names
794/// before deserialization.
795///
796/// # Example
797///
798/// ```ignore
799/// #[derive(Model, Serialize, Deserialize)]
800/// struct User {
801///     #[sqlmodel(validation_alias = "userName")]
802///     name: String,
803/// }
804///
805/// // Input with alias key works
806/// let user = User::sql_model_validate(r#"{"userName": "Alice"}"#)?;
807/// assert_eq!(user.name, "Alice");
808/// ```
809pub trait SqlModelValidate: Model + DeserializeOwned + Sized {
810    /// Create and validate a model from input, applying validation aliases.
811    fn sql_model_validate(
812        input: impl Into<ValidateInput>,
813        options: ValidateOptions,
814    ) -> ValidateResult<Self> {
815        let input = input.into();
816
817        // Convert input to serde_json::Value
818        let mut json_value = match input {
819            ValidateInput::Dict(dict) => {
820                let map: serde_json::Map<String, serde_json::Value> = dict
821                    .into_iter()
822                    .map(|(k, v)| (k, value_to_json(v)))
823                    .collect();
824                serde_json::Value::Object(map)
825            }
826            ValidateInput::Json(json_str) => serde_json::from_str(&json_str).map_err(|e| {
827                let mut err = ValidationError::new();
828                err.add(
829                    "_json",
830                    ValidationErrorKind::Custom,
831                    format!("Invalid JSON: {e}"),
832                );
833                err
834            })?,
835            ValidateInput::JsonValue(value) => value,
836        };
837
838        // Apply validation aliases before deserialization
839        apply_validation_aliases(&mut json_value, Self::fields());
840
841        // Apply update values if provided
842        if let Some(update) = options.update {
843            if let serde_json::Value::Object(ref mut map) = json_value {
844                for (key, value) in update {
845                    map.insert(key, value);
846                }
847            }
848        }
849
850        // Deserialize
851        serde_json::from_value(json_value).map_err(|e| {
852            let mut err = ValidationError::new();
853            err.add(
854                "_model",
855                ValidationErrorKind::Custom,
856                format!("Validation failed: {e}"),
857            );
858            err
859        })
860    }
861
862    /// Create and validate a model from JSON string with default options.
863    fn sql_model_validate_json(json: &str) -> ValidateResult<Self> {
864        Self::sql_model_validate(json, ValidateOptions::default())
865    }
866
867    /// Create and validate a model from a HashMap with default options.
868    fn sql_model_validate_dict(dict: HashMap<String, Value>) -> ValidateResult<Self> {
869        Self::sql_model_validate(dict, ValidateOptions::default())
870    }
871}
872
873/// Blanket implementation for all Model types that implement DeserializeOwned.
874impl<T: Model + DeserializeOwned> SqlModelValidate for T {}
875
876/// Model-aware dump that supports field aliases and computed field exclusion.
877///
878/// Unlike the generic `ModelDump`, this trait uses the `Model::fields()`
879/// metadata to transform field names to their serialization aliases
880/// in the output and to handle computed fields properly.
881///
882/// # Example
883///
884/// ```ignore
885/// #[derive(Model, Serialize, Deserialize)]
886/// struct User {
887///     #[sqlmodel(serialization_alias = "userName")]
888///     name: String,
889///     #[sqlmodel(computed)]
890///     full_name: String, // Derived field, not in DB
891/// }
892///
893/// let user = User { name: "Alice".to_string(), full_name: "Alice Smith".to_string() };
894/// let json = user.sql_model_dump(DumpOptions::default().by_alias())?;
895/// assert_eq!(json["userName"], "Alice");
896///
897/// // Exclude computed fields
898/// let json = user.sql_model_dump(DumpOptions::default().exclude_computed_fields())?;
899/// assert!(json.get("full_name").is_none());
900/// ```
901pub trait SqlModelDump: Model + serde::Serialize {
902    /// Serialize a model to a JSON value, optionally applying aliases.
903    fn sql_model_dump(&self, options: DumpOptions) -> DumpResult {
904        // First, serialize to JSON value
905        let mut value = serde_json::to_value(self)?;
906
907        // Apply options that work on original field names BEFORE alias renaming
908        if let serde_json::Value::Object(ref mut map) = value {
909            // Exclude computed fields if requested (must happen before alias renaming)
910            if options.exclude_computed_fields {
911                let computed_field_names: std::collections::HashSet<&str> = Self::fields()
912                    .iter()
913                    .filter(|f| f.computed)
914                    .map(|f| f.name)
915                    .collect();
916                map.retain(|k, _| !computed_field_names.contains(k.as_str()));
917            }
918
919            // Exclude fields with default values if requested
920            if options.exclude_defaults {
921                for field in Self::fields() {
922                    if let Some(default_json) = field.default_json {
923                        if let Some(current_value) = map.get(field.name) {
924                            // Parse the default JSON and compare
925                            if let Ok(default_value) =
926                                serde_json::from_str::<serde_json::Value>(default_json)
927                            {
928                                if current_value == &default_value {
929                                    map.remove(field.name);
930                                }
931                            }
932                        }
933                    }
934                }
935            }
936        }
937
938        // Apply serialization aliases if by_alias is set
939        if options.by_alias {
940            apply_serialization_aliases(&mut value, Self::fields());
941        }
942
943        // Apply remaining options (include/exclude work on the final key names)
944        if let serde_json::Value::Object(ref mut map) = value {
945            // Apply include filter
946            if let Some(ref include) = options.include {
947                map.retain(|k, _| include.contains(k));
948            }
949
950            // Apply exclude filter
951            if let Some(ref exclude) = options.exclude {
952                map.retain(|k, _| !exclude.contains(k));
953            }
954
955            // Apply exclude_none filter
956            if options.exclude_none {
957                map.retain(|_, v| !v.is_null());
958            }
959        }
960
961        Ok(value)
962    }
963
964    /// Serialize a model to a JSON string with default options.
965    fn sql_model_dump_json(&self) -> std::result::Result<String, serde_json::Error> {
966        let value = self.sql_model_dump(DumpOptions::default())?;
967        serde_json::to_string(&value)
968    }
969
970    /// Serialize a model to a pretty-printed JSON string.
971    fn sql_model_dump_json_pretty(&self) -> std::result::Result<String, serde_json::Error> {
972        let value = self.sql_model_dump(DumpOptions::default())?;
973        serde_json::to_string_pretty(&value)
974    }
975
976    /// Serialize with aliases to a JSON string.
977    fn sql_model_dump_json_by_alias(&self) -> std::result::Result<String, serde_json::Error> {
978        let value = self.sql_model_dump(DumpOptions::default().by_alias())?;
979        serde_json::to_string(&value)
980    }
981
982    /// Serialize a model to a JSON string with full options support.
983    ///
984    /// This method supports all DumpOptions including the `indent` option:
985    /// - `indent: None` - compact JSON output
986    /// - `indent: Some(n)` - pretty-printed with n spaces indentation
987    ///
988    /// Compared to `model_dump_json_with_options`, this method also applies
989    /// Model-specific transformations like serialization aliases.
990    ///
991    /// # Example
992    ///
993    /// ```ignore
994    /// use sqlmodel_core::validate::{SqlModelDump, DumpOptions};
995    ///
996    /// // With aliases and 2-space indent
997    /// let json = user.sql_model_dump_json_with_options(
998    ///     DumpOptions::default().by_alias().indent(2)
999    /// )?;
1000    /// ```
1001    fn sql_model_dump_json_with_options(
1002        &self,
1003        options: DumpOptions,
1004    ) -> std::result::Result<String, serde_json::Error> {
1005        let value = self.sql_model_dump(DumpOptions {
1006            indent: None, // Don't pass indent to sql_model_dump (it returns Value, not String)
1007            ..options.clone()
1008        })?;
1009
1010        match options.indent {
1011            Some(spaces) => {
1012                let indent_bytes = " ".repeat(spaces).into_bytes();
1013                let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
1014                let mut writer = Vec::new();
1015                let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
1016                serde::Serialize::serialize(&value, &mut ser)?;
1017                // serde_json always produces valid UTF-8, but propagate error instead of panicking
1018                String::from_utf8(writer).map_err(|e| {
1019                    serde_json::Error::io(std::io::Error::new(
1020                        std::io::ErrorKind::InvalidData,
1021                        format!("UTF-8 encoding error: {e}"),
1022                    ))
1023                })
1024            }
1025            None => serde_json::to_string(&value),
1026        }
1027    }
1028}
1029
1030/// Blanket implementation for all Model types that implement Serialize.
1031impl<T: Model + serde::Serialize> SqlModelDump for T {}
1032
1033// ============================================================================
1034// Model Update (sqlmodel_update)
1035// ============================================================================
1036
1037/// Input types for sqlmodel_update().
1038///
1039/// Supports updating models from various input formats.
1040#[derive(Debug, Clone)]
1041pub enum UpdateInput {
1042    /// A HashMap of field names to JSON values.
1043    Dict(HashMap<String, serde_json::Value>),
1044    /// A serde_json::Value for direct updating.
1045    JsonValue(serde_json::Value),
1046}
1047
1048impl From<HashMap<String, serde_json::Value>> for UpdateInput {
1049    fn from(map: HashMap<String, serde_json::Value>) -> Self {
1050        UpdateInput::Dict(map)
1051    }
1052}
1053
1054impl From<serde_json::Value> for UpdateInput {
1055    fn from(value: serde_json::Value) -> Self {
1056        UpdateInput::JsonValue(value)
1057    }
1058}
1059
1060impl From<HashMap<String, Value>> for UpdateInput {
1061    fn from(map: HashMap<String, Value>) -> Self {
1062        let json_map: HashMap<String, serde_json::Value> = map
1063            .into_iter()
1064            .map(|(k, v)| (k, value_to_json(v)))
1065            .collect();
1066        UpdateInput::Dict(json_map)
1067    }
1068}
1069
1070/// Options for sqlmodel_update().
1071#[derive(Debug, Clone, Default)]
1072pub struct UpdateOptions {
1073    /// Only update these fields (if Some). Other fields in the source are ignored.
1074    pub update_fields: Option<std::collections::HashSet<String>>,
1075}
1076
1077impl UpdateOptions {
1078    /// Create new default options.
1079    pub fn new() -> Self {
1080        Self::default()
1081    }
1082
1083    /// Set fields to update.
1084    pub fn update_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
1085        self.update_fields = Some(fields.into_iter().map(Into::into).collect());
1086        self
1087    }
1088}
1089
1090/// Trait for models that support sqlmodel_update().
1091///
1092/// This enables updating a model instance from a dictionary or another model's values.
1093///
1094/// # Example
1095///
1096/// ```ignore
1097/// use sqlmodel_core::validate::{SqlModelUpdate, UpdateInput, UpdateOptions};
1098///
1099/// let mut user = User { id: 1, name: "Alice".to_string(), age: 30 };
1100///
1101/// // Update from a HashMap
1102/// user.sqlmodel_update(
1103///     HashMap::from([("name".to_string(), serde_json::json!("Bob"))]),
1104///     UpdateOptions::default()
1105/// )?;
1106/// assert_eq!(user.name, "Bob");
1107///
1108/// // Update only specific fields
1109/// user.sqlmodel_update(
1110///     HashMap::from([
1111///         ("name".to_string(), serde_json::json!("Carol")),
1112///         ("age".to_string(), serde_json::json!(25))
1113///     ]),
1114///     UpdateOptions::default().update_fields(["name"])
1115/// )?;
1116/// assert_eq!(user.name, "Carol");
1117/// assert_eq!(user.age, 30); // age was not updated
1118/// ```
1119pub trait SqlModelUpdate: Model + serde::Serialize + DeserializeOwned {
1120    /// Update a model instance from input.
1121    ///
1122    /// This method merges values from the input into the current model.
1123    /// Only fields present in the input (and allowed by `update_fields` option)
1124    /// are updated.
1125    ///
1126    /// # Arguments
1127    ///
1128    /// * `input` - The source of update values (Dict or JsonValue)
1129    /// * `options` - Update options controlling which fields to update
1130    ///
1131    /// # Returns
1132    ///
1133    /// Ok(()) if the update succeeds, or a validation error if the resulting
1134    /// model fails validation.
1135    fn sqlmodel_update(
1136        &mut self,
1137        input: impl Into<UpdateInput>,
1138        options: UpdateOptions,
1139    ) -> ValidateResult<()> {
1140        let input = input.into();
1141
1142        // Convert input to a map
1143        let update_map = match input {
1144            UpdateInput::Dict(map) => map,
1145            UpdateInput::JsonValue(value) => {
1146                if let serde_json::Value::Object(map) = value {
1147                    map.into_iter().collect()
1148                } else {
1149                    let mut err = ValidationError::new();
1150                    err.add(
1151                        "_update",
1152                        ValidationErrorKind::Custom,
1153                        "Update input must be an object".to_string(),
1154                    );
1155                    return Err(err);
1156                }
1157            }
1158        };
1159
1160        // Serialize current model to JSON
1161        let mut current = serde_json::to_value(&*self).map_err(|e| {
1162            let mut err = ValidationError::new();
1163            err.add(
1164                "_model",
1165                ValidationErrorKind::Custom,
1166                format!("Failed to serialize model: {e}"),
1167            );
1168            err
1169        })?;
1170
1171        // Get valid field names from model metadata
1172        let valid_fields: std::collections::HashSet<&str> =
1173            Self::fields().iter().map(|f| f.name).collect();
1174
1175        // Update the current JSON with new values
1176        if let serde_json::Value::Object(ref mut current_map) = current {
1177            for (key, value) in update_map {
1178                // Check if field is valid
1179                if !valid_fields.contains(key.as_str()) {
1180                    let mut err = ValidationError::new();
1181                    err.add(
1182                        &key,
1183                        ValidationErrorKind::Custom,
1184                        format!("Unknown field: {key}"),
1185                    );
1186                    return Err(err);
1187                }
1188
1189                // Check if field is allowed by update_fields option
1190                if let Some(ref allowed) = options.update_fields {
1191                    if !allowed.contains(&key) {
1192                        continue; // Skip fields not in update_fields
1193                    }
1194                }
1195
1196                // Update the field
1197                current_map.insert(key, value);
1198            }
1199        }
1200
1201        // Deserialize back to model (this also validates)
1202        let updated: Self = serde_json::from_value(current).map_err(|e| {
1203            let mut err = ValidationError::new();
1204            err.add(
1205                "_model",
1206                ValidationErrorKind::Custom,
1207                format!("Update failed validation: {e}"),
1208            );
1209            err
1210        })?;
1211
1212        // Replace self with the updated model
1213        *self = updated;
1214
1215        Ok(())
1216    }
1217
1218    /// Update a model instance from a HashMap with default options.
1219    fn sqlmodel_update_dict(
1220        &mut self,
1221        dict: HashMap<String, serde_json::Value>,
1222    ) -> ValidateResult<()> {
1223        self.sqlmodel_update(dict, UpdateOptions::default())
1224    }
1225
1226    /// Copy non-None/non-null values from another model into this one.
1227    ///
1228    /// This is useful for partial updates where you have a "patch" model
1229    /// with only the fields that should be updated (non-None values).
1230    ///
1231    /// # Arguments
1232    ///
1233    /// * `source` - The source model to copy values from
1234    /// * `options` - Update options controlling which fields to update
1235    ///
1236    /// # Example
1237    ///
1238    /// ```ignore
1239    /// let mut user = User { id: 1, name: "Alice".to_string(), age: Some(30) };
1240    /// let patch = User { id: 0, name: "Bob".to_string(), age: None };
1241    ///
1242    /// // Only update name, skip None age
1243    /// user.sqlmodel_update_from(&patch, UpdateOptions::default())?;
1244    /// assert_eq!(user.name, "Bob");
1245    /// assert_eq!(user.age, Some(30)); // Unchanged because patch.age is None
1246    /// ```
1247    fn sqlmodel_update_from(&mut self, source: &Self, options: UpdateOptions) -> ValidateResult<()>
1248    where
1249        Self: Sized,
1250    {
1251        // Serialize source to JSON
1252        let source_json = serde_json::to_value(source).map_err(|e| {
1253            let mut err = ValidationError::new();
1254            err.add(
1255                "_source",
1256                ValidationErrorKind::Custom,
1257                format!("Failed to serialize source model: {e}"),
1258            );
1259            err
1260        })?;
1261
1262        // Filter out null values (None fields)
1263        let update_map: HashMap<String, serde_json::Value> =
1264            if let serde_json::Value::Object(map) = source_json {
1265                map.into_iter().filter(|(_, v)| !v.is_null()).collect()
1266            } else {
1267                let mut err = ValidationError::new();
1268                err.add(
1269                    "_source",
1270                    ValidationErrorKind::Custom,
1271                    "Source model must serialize to an object".to_string(),
1272                );
1273                return Err(err);
1274            };
1275
1276        self.sqlmodel_update(update_map, options)
1277    }
1278}
1279
1280/// Blanket implementation for all Model types that implement Serialize + DeserializeOwned.
1281impl<T: Model + serde::Serialize + DeserializeOwned> SqlModelUpdate for T {}
1282
1283#[cfg(test)]
1284mod tests {
1285    use super::*;
1286    use serde::{Deserialize, Serialize};
1287
1288    #[test]
1289    fn test_matches_email_pattern() {
1290        let email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
1291
1292        assert!(matches_pattern("test@example.com", email_pattern));
1293        assert!(matches_pattern("user.name+tag@domain.org", email_pattern));
1294        assert!(!matches_pattern("invalid", email_pattern));
1295        assert!(!matches_pattern("@example.com", email_pattern));
1296        assert!(!matches_pattern("test@", email_pattern));
1297    }
1298
1299    #[test]
1300    fn test_matches_url_pattern() {
1301        let url_pattern = r"^https?://[^\s/$.?#].[^\s]*$";
1302
1303        assert!(matches_pattern("https://example.com", url_pattern));
1304        assert!(matches_pattern("http://example.com/path", url_pattern));
1305        assert!(!matches_pattern("ftp://example.com", url_pattern));
1306        assert!(!matches_pattern("not a url", url_pattern));
1307    }
1308
1309    #[test]
1310    fn test_matches_phone_pattern() {
1311        let phone_pattern = r"^\+?[1-9]\d{1,14}$";
1312
1313        assert!(matches_pattern("+12025551234", phone_pattern));
1314        assert!(matches_pattern("12025551234", phone_pattern));
1315        assert!(!matches_pattern("0123456789", phone_pattern)); // Can't start with 0
1316        assert!(!matches_pattern("abc", phone_pattern));
1317    }
1318
1319    #[test]
1320    fn test_matches_uuid_pattern() {
1321        let uuid_pattern =
1322            r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
1323
1324        assert!(matches_pattern(
1325            "550e8400-e29b-41d4-a716-446655440000",
1326            uuid_pattern
1327        ));
1328        assert!(matches_pattern(
1329            "550E8400-E29B-41D4-A716-446655440000",
1330            uuid_pattern
1331        ));
1332        assert!(!matches_pattern("invalid-uuid", uuid_pattern));
1333        assert!(!matches_pattern(
1334            "550e8400e29b41d4a716446655440000",
1335            uuid_pattern
1336        ));
1337    }
1338
1339    #[test]
1340    fn test_matches_alphanumeric_pattern() {
1341        let alphanumeric_pattern = r"^[a-zA-Z0-9]+$";
1342
1343        assert!(matches_pattern("abc123", alphanumeric_pattern));
1344        assert!(matches_pattern("ABC", alphanumeric_pattern));
1345        assert!(matches_pattern("123", alphanumeric_pattern));
1346        assert!(!matches_pattern("abc-123", alphanumeric_pattern));
1347        assert!(!matches_pattern("hello world", alphanumeric_pattern));
1348    }
1349
1350    #[test]
1351    fn test_invalid_pattern_returns_false() {
1352        // Invalid regex pattern (unclosed bracket)
1353        let invalid_pattern = r"[unclosed";
1354        assert!(!matches_pattern("anything", invalid_pattern));
1355    }
1356
1357    #[test]
1358    fn test_validate_pattern_valid() {
1359        assert!(validate_pattern(r"^[a-z]+$").is_none());
1360        assert!(validate_pattern(r"^\d{4}-\d{2}-\d{2}$").is_none());
1361    }
1362
1363    #[test]
1364    fn test_validate_pattern_invalid() {
1365        let result = validate_pattern(r"[unclosed");
1366        assert!(result.is_some());
1367        assert!(result.unwrap().contains("invalid regex pattern"));
1368    }
1369
1370    #[test]
1371    fn test_regex_caching() {
1372        let pattern = r"^test\d+$";
1373
1374        // First call compiles the regex
1375        assert!(matches_pattern("test123", pattern));
1376
1377        // Second call should use cached regex
1378        assert!(matches_pattern("test456", pattern));
1379        assert!(!matches_pattern("invalid", pattern));
1380    }
1381
1382    #[test]
1383    fn test_empty_string() {
1384        let pattern = r"^.+$"; // At least one character
1385        assert!(!matches_pattern("", pattern));
1386
1387        let empty_allowed = r"^.*$"; // Zero or more characters
1388        assert!(matches_pattern("", empty_allowed));
1389    }
1390
1391    #[test]
1392    fn test_special_characters() {
1393        let pattern = r"^[a-z]+$";
1394        assert!(!matches_pattern("hello<script>", pattern));
1395        assert!(!matches_pattern("test'; DROP TABLE users;--", pattern));
1396    }
1397
1398    // =========================================================================
1399    // model_validate tests
1400    // =========================================================================
1401
1402    #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
1403    struct TestUser {
1404        name: String,
1405        age: i32,
1406        #[serde(default)]
1407        active: bool,
1408    }
1409
1410    #[test]
1411    fn test_model_validate_from_json() {
1412        let json = r#"{"name": "Alice", "age": 30}"#;
1413        let user: TestUser = TestUser::model_validate_json(json).unwrap();
1414        assert_eq!(user.name, "Alice");
1415        assert_eq!(user.age, 30);
1416        assert!(!user.active); // default
1417    }
1418
1419    #[test]
1420    fn test_model_validate_from_json_value() {
1421        let json_value = serde_json::json!({"name": "Bob", "age": 25, "active": true});
1422        let user: TestUser =
1423            TestUser::model_validate(json_value, ValidateOptions::default()).unwrap();
1424        assert_eq!(user.name, "Bob");
1425        assert_eq!(user.age, 25);
1426        assert!(user.active);
1427    }
1428
1429    #[test]
1430    fn test_model_validate_from_dict() {
1431        let mut dict = HashMap::new();
1432        dict.insert("name".to_string(), Value::Text("Charlie".to_string()));
1433        dict.insert("age".to_string(), Value::Int(35));
1434        dict.insert("active".to_string(), Value::Bool(true));
1435
1436        let user: TestUser = TestUser::model_validate_dict(dict).unwrap();
1437        assert_eq!(user.name, "Charlie");
1438        assert_eq!(user.age, 35);
1439        assert!(user.active);
1440    }
1441
1442    #[test]
1443    fn test_model_validate_invalid_json() {
1444        let json = r#"{"name": "Invalid"}"#; // missing required 'age' field
1445        let result: ValidateResult<TestUser> = TestUser::model_validate_json(json);
1446        assert!(result.is_err());
1447        let err = result.unwrap_err();
1448        assert!(!err.is_empty());
1449    }
1450
1451    #[test]
1452    fn test_model_validate_malformed_json() {
1453        let json = r#"{"name": "Alice", age: 30}"#; // invalid JSON syntax
1454        let result: ValidateResult<TestUser> = TestUser::model_validate_json(json);
1455        assert!(result.is_err());
1456        let err = result.unwrap_err();
1457        assert!(
1458            err.errors
1459                .iter()
1460                .any(|e| e.message.contains("Invalid JSON"))
1461        );
1462    }
1463
1464    #[test]
1465    fn test_model_validate_with_update() {
1466        let json = r#"{"name": "Original", "age": 20}"#;
1467        let mut update = HashMap::new();
1468        update.insert("name".to_string(), serde_json::json!("Updated"));
1469
1470        let options = ValidateOptions::new().with_update(update);
1471        let user: TestUser = TestUser::model_validate(json, options).unwrap();
1472        assert_eq!(user.name, "Updated"); // overridden by update
1473        assert_eq!(user.age, 20);
1474    }
1475
1476    #[test]
1477    fn test_model_validate_strict_mode() {
1478        let json = r#"{"name": "Alice", "age": 30}"#;
1479        let options = ValidateOptions::new().strict();
1480        let user: TestUser = TestUser::model_validate(json, options).unwrap();
1481        assert_eq!(user.name, "Alice");
1482        assert_eq!(user.age, 30);
1483    }
1484
1485    #[test]
1486    fn test_validate_options_builder() {
1487        let mut context = HashMap::new();
1488        context.insert("key".to_string(), serde_json::json!("value"));
1489
1490        let options = ValidateOptions::new()
1491            .strict()
1492            .from_attributes()
1493            .with_context(context.clone());
1494
1495        assert!(options.strict);
1496        assert!(options.from_attributes);
1497        assert!(options.context.is_some());
1498        assert_eq!(
1499            options.context.unwrap().get("key"),
1500            Some(&serde_json::json!("value"))
1501        );
1502    }
1503
1504    #[test]
1505    fn test_validate_input_from_conversions() {
1506        // From String
1507        let input: ValidateInput = "{}".to_string().into();
1508        assert!(matches!(input, ValidateInput::Json(_)));
1509
1510        // From &str
1511        let input: ValidateInput = "{}".into();
1512        assert!(matches!(input, ValidateInput::Json(_)));
1513
1514        // From serde_json::Value
1515        let input: ValidateInput = serde_json::json!({}).into();
1516        assert!(matches!(input, ValidateInput::JsonValue(_)));
1517
1518        // From HashMap
1519        let map: HashMap<String, Value> = HashMap::new();
1520        let input: ValidateInput = map.into();
1521        assert!(matches!(input, ValidateInput::Dict(_)));
1522    }
1523
1524    #[test]
1525    fn test_value_to_json_conversions() {
1526        assert_eq!(value_to_json(Value::Null), serde_json::Value::Null);
1527        assert_eq!(value_to_json(Value::Bool(true)), serde_json::json!(true));
1528        assert_eq!(value_to_json(Value::Int(42)), serde_json::json!(42));
1529        assert_eq!(value_to_json(Value::BigInt(100)), serde_json::json!(100));
1530        assert_eq!(
1531            value_to_json(Value::Text("hello".to_string())),
1532            serde_json::json!("hello")
1533        );
1534        // UUID is [u8; 16]
1535        let uuid_bytes: [u8; 16] = [
1536            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
1537            0x00, 0x00,
1538        ];
1539        assert_eq!(
1540            value_to_json(Value::Uuid(uuid_bytes)),
1541            serde_json::json!("550e8400-e29b-41d4-a716-446655440000")
1542        );
1543
1544        // Array conversion
1545        let arr = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
1546        assert_eq!(
1547            value_to_json(Value::Array(arr)),
1548            serde_json::json!([1, 2, 3])
1549        );
1550    }
1551
1552    // =========================================================================
1553    // model_dump tests
1554    // =========================================================================
1555
1556    #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
1557    struct TestProduct {
1558        name: String,
1559        price: f64,
1560        #[serde(skip_serializing_if = "Option::is_none")]
1561        description: Option<String>,
1562    }
1563
1564    #[test]
1565    fn test_model_dump_default() {
1566        let product = TestProduct {
1567            name: "Widget".to_string(),
1568            price: 19.99,
1569            description: Some("A useful widget".to_string()),
1570        };
1571        let json = product.model_dump(DumpOptions::default()).unwrap();
1572        assert_eq!(json["name"], "Widget");
1573        assert_eq!(json["price"], 19.99);
1574        assert_eq!(json["description"], "A useful widget");
1575    }
1576
1577    #[test]
1578    fn test_model_dump_json() {
1579        let product = TestProduct {
1580            name: "Gadget".to_string(),
1581            price: 29.99,
1582            description: None,
1583        };
1584        let json_str = product.model_dump_json().unwrap();
1585        assert!(json_str.contains("Gadget"));
1586        assert!(json_str.contains("29.99"));
1587    }
1588
1589    #[test]
1590    fn test_model_dump_json_pretty() {
1591        let product = TestProduct {
1592            name: "Gadget".to_string(),
1593            price: 29.99,
1594            description: None,
1595        };
1596        let json_str = product.model_dump_json_pretty().unwrap();
1597        // Pretty print should have newlines
1598        assert!(json_str.contains('\n'));
1599        assert!(json_str.contains("Gadget"));
1600    }
1601
1602    #[test]
1603    fn test_model_dump_json_with_options_compact() {
1604        let product = TestProduct {
1605            name: "Widget".to_string(),
1606            price: 19.99,
1607            description: Some("A widget".to_string()),
1608        };
1609
1610        // Compact JSON (no indent)
1611        let json_str = product
1612            .model_dump_json_with_options(DumpOptions::default())
1613            .unwrap();
1614        assert!(!json_str.contains('\n')); // No newlines in compact mode
1615        assert!(json_str.contains("Widget"));
1616        assert!(json_str.contains("19.99"));
1617    }
1618
1619    #[test]
1620    fn test_model_dump_json_with_options_indent() {
1621        let product = TestProduct {
1622            name: "Widget".to_string(),
1623            price: 19.99,
1624            description: Some("A widget".to_string()),
1625        };
1626
1627        // 2-space indentation
1628        let json_str = product
1629            .model_dump_json_with_options(DumpOptions::default().indent(2))
1630            .unwrap();
1631        assert!(json_str.contains('\n')); // Has newlines
1632        assert!(json_str.contains("  \"name\"")); // 2-space indent
1633        assert!(json_str.contains("Widget"));
1634
1635        // 4-space indentation
1636        let json_str = product
1637            .model_dump_json_with_options(DumpOptions::default().indent(4))
1638            .unwrap();
1639        assert!(json_str.contains("    \"name\"")); // 4-space indent
1640    }
1641
1642    #[test]
1643    fn test_model_dump_json_with_options_combined() {
1644        let product = TestProduct {
1645            name: "Widget".to_string(),
1646            price: 19.99,
1647            description: Some("A widget".to_string()),
1648        };
1649
1650        // Combine indent with exclude
1651        let json_str = product
1652            .model_dump_json_with_options(DumpOptions::default().exclude(["price"]).indent(2))
1653            .unwrap();
1654        assert!(json_str.contains('\n')); // Has newlines
1655        assert!(json_str.contains("Widget"));
1656        assert!(!json_str.contains("19.99")); // price is excluded
1657    }
1658
1659    #[test]
1660    fn test_dump_options_indent_builder() {
1661        let options = DumpOptions::new().indent(4);
1662        assert_eq!(options.indent, Some(4));
1663
1664        // Can combine with other options
1665        let options2 = DumpOptions::new()
1666            .indent(2)
1667            .by_alias()
1668            .exclude(["password"]);
1669        assert_eq!(options2.indent, Some(2));
1670        assert!(options2.by_alias);
1671        assert!(options2.exclude.unwrap().contains("password"));
1672    }
1673
1674    #[test]
1675    fn test_model_dump_include() {
1676        let product = TestProduct {
1677            name: "Widget".to_string(),
1678            price: 19.99,
1679            description: Some("A widget".to_string()),
1680        };
1681        let options = DumpOptions::new().include(["name"]);
1682        let json = product.model_dump(options).unwrap();
1683        assert!(json.get("name").is_some());
1684        assert!(json.get("price").is_none());
1685        assert!(json.get("description").is_none());
1686    }
1687
1688    #[test]
1689    fn test_model_dump_exclude() {
1690        let product = TestProduct {
1691            name: "Widget".to_string(),
1692            price: 19.99,
1693            description: Some("A widget".to_string()),
1694        };
1695        let options = DumpOptions::new().exclude(["description"]);
1696        let json = product.model_dump(options).unwrap();
1697        assert!(json.get("name").is_some());
1698        assert!(json.get("price").is_some());
1699        assert!(json.get("description").is_none());
1700    }
1701
1702    #[test]
1703    fn test_model_dump_exclude_none() {
1704        let product = TestProduct {
1705            name: "Widget".to_string(),
1706            price: 19.99,
1707            description: None,
1708        };
1709        // Note: serde skip_serializing_if already handles this
1710        // But we can still test the exclude_none flag
1711        let options = DumpOptions::new().exclude_none();
1712        let json = product.model_dump(options).unwrap();
1713        assert!(json.get("name").is_some());
1714        // description would be None, but serde already skips it
1715    }
1716
1717    #[test]
1718    fn test_dump_options_builder() {
1719        let options = DumpOptions::new()
1720            .json()
1721            .include(["name", "age"])
1722            .exclude(["password"])
1723            .by_alias()
1724            .exclude_none()
1725            .exclude_defaults()
1726            .round_trip();
1727
1728        assert_eq!(options.mode, DumpMode::Json);
1729        assert!(options.include.is_some());
1730        assert!(options.exclude.is_some());
1731        assert!(options.by_alias);
1732        assert!(options.exclude_none);
1733        assert!(options.exclude_defaults);
1734        assert!(options.round_trip);
1735    }
1736
1737    #[test]
1738    fn test_dump_mode_default() {
1739        assert_eq!(DumpMode::default(), DumpMode::Json);
1740    }
1741
1742    #[test]
1743    fn test_model_dump_include_exclude_combined() {
1744        let user = TestUser {
1745            name: "Alice".to_string(),
1746            age: 30,
1747            active: true,
1748        };
1749        // Include name and age, but exclude age
1750        let options = DumpOptions::new().include(["name", "age"]).exclude(["age"]);
1751        let json = user.model_dump(options).unwrap();
1752        // Include is applied first, then exclude
1753        assert!(json.get("name").is_some());
1754        assert!(json.get("age").is_none());
1755        assert!(json.get("active").is_none());
1756    }
1757
1758    // ========================================================================
1759    // Alias Tests
1760    // ========================================================================
1761
1762    use crate::{FieldInfo, Row, SqlType};
1763
1764    /// Test model with aliases for validation and serialization tests.
1765    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1766    struct TestAliasedUser {
1767        id: i64,
1768        name: String,
1769        email: String,
1770    }
1771
1772    impl Model for TestAliasedUser {
1773        const TABLE_NAME: &'static str = "users";
1774        const PRIMARY_KEY: &'static [&'static str] = &["id"];
1775
1776        fn fields() -> &'static [FieldInfo] {
1777            static FIELDS: &[FieldInfo] = &[
1778                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
1779                FieldInfo::new("name", "name", SqlType::Text)
1780                    .validation_alias("userName")
1781                    .serialization_alias("displayName"),
1782                FieldInfo::new("email", "email", SqlType::Text).alias("emailAddress"), // Both input and output
1783            ];
1784            FIELDS
1785        }
1786
1787        fn to_row(&self) -> Vec<(&'static str, Value)> {
1788            vec![
1789                ("id", Value::BigInt(self.id)),
1790                ("name", Value::Text(self.name.clone())),
1791                ("email", Value::Text(self.email.clone())),
1792            ]
1793        }
1794
1795        fn from_row(row: &Row) -> crate::Result<Self> {
1796            Ok(Self {
1797                id: row.get_named("id")?,
1798                name: row.get_named("name")?,
1799                email: row.get_named("email")?,
1800            })
1801        }
1802
1803        fn primary_key_value(&self) -> Vec<Value> {
1804            vec![Value::BigInt(self.id)]
1805        }
1806
1807        fn is_new(&self) -> bool {
1808            false
1809        }
1810    }
1811
1812    #[test]
1813    fn test_apply_validation_aliases() {
1814        let fields = TestAliasedUser::fields();
1815
1816        // Test with validation_alias
1817        let mut json = serde_json::json!({
1818            "id": 1,
1819            "userName": "Alice",
1820            "email": "alice@example.com"
1821        });
1822        apply_validation_aliases(&mut json, fields);
1823
1824        // userName should be renamed to name
1825        assert_eq!(json["name"], "Alice");
1826        assert!(json.get("userName").is_none());
1827
1828        // Test with regular alias
1829        let mut json2 = serde_json::json!({
1830            "id": 1,
1831            "name": "Bob",
1832            "emailAddress": "bob@example.com"
1833        });
1834        apply_validation_aliases(&mut json2, fields);
1835
1836        // emailAddress should be renamed to email
1837        assert_eq!(json2["email"], "bob@example.com");
1838        assert!(json2.get("emailAddress").is_none());
1839    }
1840
1841    #[test]
1842    fn test_apply_serialization_aliases() {
1843        let fields = TestAliasedUser::fields();
1844
1845        let mut json = serde_json::json!({
1846            "id": 1,
1847            "name": "Alice",
1848            "email": "alice@example.com"
1849        });
1850        apply_serialization_aliases(&mut json, fields);
1851
1852        // name should be renamed to displayName (serialization_alias)
1853        assert_eq!(json["displayName"], "Alice");
1854        assert!(json.get("name").is_none());
1855
1856        // email should be renamed to emailAddress (regular alias)
1857        assert_eq!(json["emailAddress"], "alice@example.com");
1858        assert!(json.get("email").is_none());
1859    }
1860
1861    #[test]
1862    fn test_sql_model_validate_with_validation_alias() {
1863        // Use validation_alias in input
1864        let json = r#"{"id": 1, "userName": "Alice", "email": "alice@example.com"}"#;
1865        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1866
1867        assert_eq!(user.id, 1);
1868        assert_eq!(user.name, "Alice");
1869        assert_eq!(user.email, "alice@example.com");
1870    }
1871
1872    #[test]
1873    fn test_sql_model_validate_with_regular_alias() {
1874        // Use regular alias in input
1875        let json = r#"{"id": 1, "name": "Bob", "emailAddress": "bob@example.com"}"#;
1876        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1877
1878        assert_eq!(user.id, 1);
1879        assert_eq!(user.name, "Bob");
1880        assert_eq!(user.email, "bob@example.com");
1881    }
1882
1883    #[test]
1884    fn test_sql_model_validate_with_field_name() {
1885        // Use actual field name (should still work)
1886        let json = r#"{"id": 1, "name": "Charlie", "email": "charlie@example.com"}"#;
1887        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1888
1889        assert_eq!(user.id, 1);
1890        assert_eq!(user.name, "Charlie");
1891        assert_eq!(user.email, "charlie@example.com");
1892    }
1893
1894    #[test]
1895    fn test_sql_model_dump_by_alias() {
1896        let user = TestAliasedUser {
1897            id: 1,
1898            name: "Alice".to_string(),
1899            email: "alice@example.com".to_string(),
1900        };
1901
1902        let json = user
1903            .sql_model_dump(DumpOptions::default().by_alias())
1904            .unwrap();
1905
1906        // name should be serialized as displayName
1907        assert_eq!(json["displayName"], "Alice");
1908        assert!(json.get("name").is_none());
1909
1910        // email should be serialized as emailAddress
1911        assert_eq!(json["emailAddress"], "alice@example.com");
1912        assert!(json.get("email").is_none());
1913    }
1914
1915    #[test]
1916    fn test_sql_model_dump_without_alias() {
1917        let user = TestAliasedUser {
1918            id: 1,
1919            name: "Alice".to_string(),
1920            email: "alice@example.com".to_string(),
1921        };
1922
1923        // Without by_alias, use original field names
1924        let json = user.sql_model_dump(DumpOptions::default()).unwrap();
1925
1926        assert_eq!(json["name"], "Alice");
1927        assert_eq!(json["email"], "alice@example.com");
1928        assert!(json.get("displayName").is_none());
1929        assert!(json.get("emailAddress").is_none());
1930    }
1931
1932    #[test]
1933    fn test_alias_does_not_overwrite_existing() {
1934        let fields = TestAliasedUser::fields();
1935
1936        // If both alias and field name are present, field name wins
1937        let mut json = serde_json::json!({
1938            "id": 1,
1939            "name": "FieldName",
1940            "userName": "AliasName",
1941            "email": "test@example.com"
1942        });
1943        apply_validation_aliases(&mut json, fields);
1944
1945        // Original "name" field should be preserved
1946        assert_eq!(json["name"], "FieldName");
1947        // userName should be removed (but couldn't insert because "name" exists)
1948        assert!(json.get("userName").is_none());
1949    }
1950
1951    // ========================================================================
1952    // Computed Field Tests
1953    // ========================================================================
1954
1955    /// Test model with computed fields.
1956    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1957    struct TestUserWithComputed {
1958        id: i64,
1959        first_name: String,
1960        last_name: String,
1961        #[serde(default)]
1962        full_name: String, // Computed field - derived from first_name + last_name
1963    }
1964
1965    impl Model for TestUserWithComputed {
1966        const TABLE_NAME: &'static str = "users";
1967        const PRIMARY_KEY: &'static [&'static str] = &["id"];
1968
1969        fn fields() -> &'static [FieldInfo] {
1970            static FIELDS: &[FieldInfo] = &[
1971                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
1972                FieldInfo::new("first_name", "first_name", SqlType::Text),
1973                FieldInfo::new("last_name", "last_name", SqlType::Text),
1974                FieldInfo::new("full_name", "full_name", SqlType::Text).computed(true),
1975            ];
1976            FIELDS
1977        }
1978
1979        fn to_row(&self) -> Vec<(&'static str, Value)> {
1980            // Computed field is NOT included in DB operations
1981            vec![
1982                ("id", Value::BigInt(self.id)),
1983                ("first_name", Value::Text(self.first_name.clone())),
1984                ("last_name", Value::Text(self.last_name.clone())),
1985            ]
1986        }
1987
1988        fn from_row(row: &Row) -> crate::Result<Self> {
1989            Ok(Self {
1990                id: row.get_named("id")?,
1991                first_name: row.get_named("first_name")?,
1992                last_name: row.get_named("last_name")?,
1993                // Computed field initialized with Default (empty string)
1994                full_name: String::new(),
1995            })
1996        }
1997
1998        fn primary_key_value(&self) -> Vec<Value> {
1999            vec![Value::BigInt(self.id)]
2000        }
2001
2002        fn is_new(&self) -> bool {
2003            false
2004        }
2005    }
2006
2007    #[test]
2008    fn test_computed_field_included_by_default() {
2009        let user = TestUserWithComputed {
2010            id: 1,
2011            first_name: "John".to_string(),
2012            last_name: "Doe".to_string(),
2013            full_name: "John Doe".to_string(),
2014        };
2015
2016        // By default, computed fields ARE included in model_dump
2017        let json = user.sql_model_dump(DumpOptions::default()).unwrap();
2018
2019        assert_eq!(json["id"], 1);
2020        assert_eq!(json["first_name"], "John");
2021        assert_eq!(json["last_name"], "Doe");
2022        assert_eq!(json["full_name"], "John Doe"); // Computed field is present
2023    }
2024
2025    #[test]
2026    fn test_computed_field_excluded_with_option() {
2027        let user = TestUserWithComputed {
2028            id: 1,
2029            first_name: "John".to_string(),
2030            last_name: "Doe".to_string(),
2031            full_name: "John Doe".to_string(),
2032        };
2033
2034        // With exclude_computed_fields, computed fields are excluded
2035        let json = user
2036            .sql_model_dump(DumpOptions::default().exclude_computed_fields())
2037            .unwrap();
2038
2039        assert_eq!(json["id"], 1);
2040        assert_eq!(json["first_name"], "John");
2041        assert_eq!(json["last_name"], "Doe");
2042        assert!(json.get("full_name").is_none()); // Computed field is excluded
2043    }
2044
2045    #[test]
2046    fn test_computed_field_not_in_to_row() {
2047        let user = TestUserWithComputed {
2048            id: 1,
2049            first_name: "Jane".to_string(),
2050            last_name: "Smith".to_string(),
2051            full_name: "Jane Smith".to_string(),
2052        };
2053
2054        // to_row() should not include computed field (for DB INSERT/UPDATE)
2055        let row = user.to_row();
2056
2057        // Should have 3 fields: id, first_name, last_name
2058        assert_eq!(row.len(), 3);
2059        let field_names: Vec<&str> = row.iter().map(|(name, _)| *name).collect();
2060        assert!(field_names.contains(&"id"));
2061        assert!(field_names.contains(&"first_name"));
2062        assert!(field_names.contains(&"last_name"));
2063        assert!(!field_names.contains(&"full_name")); // Computed field NOT in row
2064    }
2065
2066    #[test]
2067    fn test_computed_field_select_fields_excludes() {
2068        let fields = TestUserWithComputed::fields();
2069
2070        // Check that computed field is marked
2071        let computed: Vec<&FieldInfo> = fields.iter().filter(|f| f.computed).collect();
2072        assert_eq!(computed.len(), 1);
2073        assert_eq!(computed[0].name, "full_name");
2074
2075        // Non-computed fields
2076        let non_computed: Vec<&FieldInfo> = fields.iter().filter(|f| !f.computed).collect();
2077        assert_eq!(non_computed.len(), 3);
2078    }
2079
2080    #[test]
2081    fn test_computed_field_with_other_dump_options() {
2082        let user = TestUserWithComputed {
2083            id: 1,
2084            first_name: "John".to_string(),
2085            last_name: "Doe".to_string(),
2086            full_name: "John Doe".to_string(),
2087        };
2088
2089        // Combine exclude_computed_fields with include filter
2090        let json = user
2091            .sql_model_dump(DumpOptions::default().exclude_computed_fields().include([
2092                "id",
2093                "first_name",
2094                "full_name",
2095            ]))
2096            .unwrap();
2097
2098        // full_name is excluded because it's computed, even though in include list
2099        // (exclude_computed_fields is applied before include filter)
2100        assert!(json.get("id").is_some());
2101        assert!(json.get("first_name").is_some());
2102        assert!(json.get("full_name").is_none()); // Excluded as computed
2103        assert!(json.get("last_name").is_none()); // Not in include list
2104    }
2105
2106    #[test]
2107    fn test_dump_options_exclude_computed_fields_builder() {
2108        let options = DumpOptions::new().exclude_computed_fields();
2109        assert!(options.exclude_computed_fields);
2110
2111        // Can combine with other options
2112        let options2 = DumpOptions::new()
2113            .exclude_computed_fields()
2114            .by_alias()
2115            .exclude_none();
2116        assert!(options2.exclude_computed_fields);
2117        assert!(options2.by_alias);
2118        assert!(options2.exclude_none);
2119    }
2120
2121    /// Test model with both computed fields AND serialization aliases.
2122    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2123    struct TestUserWithComputedAndAlias {
2124        id: i64,
2125        first_name: String,
2126        #[serde(default)]
2127        display_name: String, // Computed field that also has an alias
2128    }
2129
2130    impl Model for TestUserWithComputedAndAlias {
2131        const TABLE_NAME: &'static str = "users";
2132        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2133
2134        fn fields() -> &'static [FieldInfo] {
2135            static FIELDS: &[FieldInfo] = &[
2136                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2137                FieldInfo::new("first_name", "first_name", SqlType::Text)
2138                    .serialization_alias("firstName"),
2139                FieldInfo::new("display_name", "display_name", SqlType::Text)
2140                    .computed(true)
2141                    .serialization_alias("displayName"),
2142            ];
2143            FIELDS
2144        }
2145
2146        fn to_row(&self) -> Vec<(&'static str, Value)> {
2147            vec![
2148                ("id", Value::BigInt(self.id)),
2149                ("first_name", Value::Text(self.first_name.clone())),
2150            ]
2151        }
2152
2153        fn from_row(row: &Row) -> crate::Result<Self> {
2154            Ok(Self {
2155                id: row.get_named("id")?,
2156                first_name: row.get_named("first_name")?,
2157                display_name: String::new(),
2158            })
2159        }
2160
2161        fn primary_key_value(&self) -> Vec<Value> {
2162            vec![Value::BigInt(self.id)]
2163        }
2164
2165        fn is_new(&self) -> bool {
2166            false
2167        }
2168    }
2169
2170    #[test]
2171    fn test_exclude_computed_with_by_alias() {
2172        // This test verifies that computed field exclusion works correctly
2173        // even when combined with by_alias (which renames keys)
2174        let user = TestUserWithComputedAndAlias {
2175            id: 1,
2176            first_name: "John".to_string(),
2177            display_name: "John Doe".to_string(),
2178        };
2179
2180        // Test with by_alias only - computed field should still appear (aliased)
2181        let json = user
2182            .sql_model_dump(DumpOptions::default().by_alias())
2183            .unwrap();
2184        assert_eq!(json["firstName"], "John"); // first_name aliased
2185        assert_eq!(json["displayName"], "John Doe"); // display_name aliased (computed but not excluded)
2186        assert!(json.get("first_name").is_none()); // Original name should not exist
2187        assert!(json.get("display_name").is_none()); // Original name should not exist
2188
2189        // Test with exclude_computed_fields only - computed field should be excluded
2190        let json = user
2191            .sql_model_dump(DumpOptions::default().exclude_computed_fields())
2192            .unwrap();
2193        assert_eq!(json["first_name"], "John");
2194        assert!(json.get("display_name").is_none()); // Computed field excluded
2195
2196        // Test with BOTH by_alias AND exclude_computed_fields
2197        // This was buggy before the fix - computed field wasn't excluded
2198        // because exclusion happened after aliasing
2199        let json = user
2200            .sql_model_dump(DumpOptions::default().by_alias().exclude_computed_fields())
2201            .unwrap();
2202        assert_eq!(json["firstName"], "John"); // first_name aliased
2203        assert!(json.get("displayName").is_none()); // Computed field excluded (even though aliased)
2204        assert!(json.get("display_name").is_none()); // Original name doesn't exist either
2205    }
2206
2207    // ========================================================================
2208    // Exclude Defaults Tests
2209    // ========================================================================
2210
2211    /// Test model with default values for exclude_defaults testing.
2212    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2213    struct TestModelWithDefaults {
2214        id: i64,
2215        name: String,
2216        count: i32,    // default: 0
2217        active: bool,  // default: false
2218        score: f64,    // default: 0.0
2219        label: String, // default: "default"
2220    }
2221
2222    impl Model for TestModelWithDefaults {
2223        const TABLE_NAME: &'static str = "test_defaults";
2224        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2225
2226        fn fields() -> &'static [FieldInfo] {
2227            static FIELDS: &[FieldInfo] = &[
2228                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2229                FieldInfo::new("name", "name", SqlType::Text),
2230                FieldInfo::new("count", "count", SqlType::Integer).default_json("0"),
2231                FieldInfo::new("active", "active", SqlType::Boolean).default_json("false"),
2232                FieldInfo::new("score", "score", SqlType::Double).default_json("0.0"),
2233                FieldInfo::new("label", "label", SqlType::Text).default_json("\"default\""),
2234            ];
2235            FIELDS
2236        }
2237
2238        fn to_row(&self) -> Vec<(&'static str, Value)> {
2239            vec![
2240                ("id", Value::BigInt(self.id)),
2241                ("name", Value::Text(self.name.clone())),
2242                ("count", Value::Int(self.count)),
2243                ("active", Value::Bool(self.active)),
2244                ("score", Value::Double(self.score)),
2245                ("label", Value::Text(self.label.clone())),
2246            ]
2247        }
2248
2249        fn from_row(row: &Row) -> crate::Result<Self> {
2250            Ok(Self {
2251                id: row.get_named("id")?,
2252                name: row.get_named("name")?,
2253                count: row.get_named("count")?,
2254                active: row.get_named("active")?,
2255                score: row.get_named("score")?,
2256                label: row.get_named("label")?,
2257            })
2258        }
2259
2260        fn primary_key_value(&self) -> Vec<Value> {
2261            vec![Value::BigInt(self.id)]
2262        }
2263
2264        fn is_new(&self) -> bool {
2265            false
2266        }
2267    }
2268
2269    #[test]
2270    fn test_exclude_defaults_all_at_default() {
2271        let model = TestModelWithDefaults {
2272            id: 1,
2273            name: "Test".to_string(),
2274            count: 0,                     // at default
2275            active: false,                // at default
2276            score: 0.0,                   // at default
2277            label: "default".to_string(), // at default
2278        };
2279
2280        let json = model
2281            .sql_model_dump(DumpOptions::default().exclude_defaults())
2282            .unwrap();
2283
2284        // id and name have no default_json, so they're always included
2285        assert!(json.get("id").is_some());
2286        assert!(json.get("name").is_some());
2287
2288        // Fields at default value should be excluded
2289        assert!(json.get("count").is_none());
2290        assert!(json.get("active").is_none());
2291        assert!(json.get("score").is_none());
2292        assert!(json.get("label").is_none());
2293    }
2294
2295    #[test]
2296    fn test_exclude_defaults_none_at_default() {
2297        let model = TestModelWithDefaults {
2298            id: 1,
2299            name: "Test".to_string(),
2300            count: 42,                   // not at default
2301            active: true,                // not at default
2302            score: 3.5,                  // not at default
2303            label: "custom".to_string(), // not at default
2304        };
2305
2306        let json = model
2307            .sql_model_dump(DumpOptions::default().exclude_defaults())
2308            .unwrap();
2309
2310        // All fields should be present since none are at defaults
2311        assert!(json.get("id").is_some());
2312        assert!(json.get("name").is_some());
2313        assert!(json.get("count").is_some());
2314        assert!(json.get("active").is_some());
2315        assert!(json.get("score").is_some());
2316        assert!(json.get("label").is_some());
2317
2318        // Verify values
2319        assert_eq!(json["count"], 42);
2320        assert_eq!(json["active"], true);
2321        assert_eq!(json["score"], 3.5);
2322        assert_eq!(json["label"], "custom");
2323    }
2324
2325    #[test]
2326    fn test_exclude_defaults_mixed() {
2327        let model = TestModelWithDefaults {
2328            id: 1,
2329            name: "Test".to_string(),
2330            count: 0,                    // at default
2331            active: true,                // not at default
2332            score: 0.0,                  // at default
2333            label: "custom".to_string(), // not at default
2334        };
2335
2336        let json = model
2337            .sql_model_dump(DumpOptions::default().exclude_defaults())
2338            .unwrap();
2339
2340        assert!(json.get("id").is_some());
2341        assert!(json.get("name").is_some());
2342
2343        // At default - excluded
2344        assert!(json.get("count").is_none());
2345        assert!(json.get("score").is_none());
2346
2347        // Not at default - included
2348        assert!(json.get("active").is_some());
2349        assert!(json.get("label").is_some());
2350        assert_eq!(json["active"], true);
2351        assert_eq!(json["label"], "custom");
2352    }
2353
2354    #[test]
2355    fn test_exclude_defaults_without_flag() {
2356        let model = TestModelWithDefaults {
2357            id: 1,
2358            name: "Test".to_string(),
2359            count: 0,                     // at default
2360            active: false,                // at default
2361            score: 0.0,                   // at default
2362            label: "default".to_string(), // at default
2363        };
2364
2365        // Without exclude_defaults, all fields should be included
2366        let json = model.sql_model_dump(DumpOptions::default()).unwrap();
2367
2368        assert!(json.get("id").is_some());
2369        assert!(json.get("name").is_some());
2370        assert!(json.get("count").is_some());
2371        assert!(json.get("active").is_some());
2372        assert!(json.get("score").is_some());
2373        assert!(json.get("label").is_some());
2374    }
2375
2376    #[test]
2377    fn test_exclude_defaults_with_by_alias() {
2378        // Test that exclude_defaults works correctly with by_alias
2379
2380        /// Model with defaults and aliases
2381        #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2382        struct TestAliasWithDefaults {
2383            id: i64,
2384            count: i32,
2385        }
2386
2387        impl Model for TestAliasWithDefaults {
2388            const TABLE_NAME: &'static str = "test";
2389            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2390
2391            fn fields() -> &'static [FieldInfo] {
2392                static FIELDS: &[FieldInfo] = &[
2393                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2394                    FieldInfo::new("count", "count", SqlType::Integer)
2395                        .default_json("0")
2396                        .serialization_alias("itemCount"),
2397                ];
2398                FIELDS
2399            }
2400
2401            fn to_row(&self) -> Vec<(&'static str, Value)> {
2402                vec![
2403                    ("id", Value::BigInt(self.id)),
2404                    ("count", Value::Int(self.count)),
2405                ]
2406            }
2407
2408            fn from_row(row: &Row) -> crate::Result<Self> {
2409                Ok(Self {
2410                    id: row.get_named("id")?,
2411                    count: row.get_named("count")?,
2412                })
2413            }
2414
2415            fn primary_key_value(&self) -> Vec<Value> {
2416                vec![Value::BigInt(self.id)]
2417            }
2418
2419            fn is_new(&self) -> bool {
2420                false
2421            }
2422        }
2423
2424        // At default value
2425        let model_at_default = TestAliasWithDefaults { id: 1, count: 0 };
2426        let json = model_at_default
2427            .sql_model_dump(DumpOptions::default().exclude_defaults().by_alias())
2428            .unwrap();
2429
2430        // count is at default (0), so neither count nor itemCount should appear
2431        assert!(json.get("count").is_none());
2432        assert!(json.get("itemCount").is_none());
2433
2434        // Not at default
2435        let model_not_at_default = TestAliasWithDefaults { id: 1, count: 5 };
2436        let json = model_not_at_default
2437            .sql_model_dump(DumpOptions::default().exclude_defaults().by_alias())
2438            .unwrap();
2439
2440        // count is not at default, should appear with alias
2441        assert!(json.get("count").is_none()); // Original name not present
2442        assert_eq!(json["itemCount"], 5); // Alias is present
2443    }
2444
2445    #[test]
2446    fn test_field_info_default_json() {
2447        // Test the FieldInfo builder methods for default_json
2448        let field1 = FieldInfo::new("count", "count", SqlType::Integer).default_json("0");
2449        assert_eq!(field1.default_json, Some("0"));
2450        assert!(field1.has_default);
2451
2452        let field2 =
2453            FieldInfo::new("name", "name", SqlType::Text).default_json_opt(Some("\"hello\""));
2454        assert_eq!(field2.default_json, Some("\"hello\""));
2455        assert!(field2.has_default);
2456
2457        let field3 = FieldInfo::new("name", "name", SqlType::Text).default_json_opt(None);
2458        assert_eq!(field3.default_json, None);
2459        assert!(!field3.has_default);
2460
2461        let field4 = FieldInfo::new("flag", "flag", SqlType::Boolean).has_default(true);
2462        assert!(field4.has_default);
2463        assert_eq!(field4.default_json, None); // has_default alone doesn't set default_json
2464    }
2465
2466    // ==================== SqlModelUpdate Tests ====================
2467
2468    #[test]
2469    fn test_sqlmodel_update_from_dict() {
2470        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2471        struct TestUser {
2472            id: i64,
2473            name: String,
2474            age: i32,
2475        }
2476
2477        impl Model for TestUser {
2478            const TABLE_NAME: &'static str = "users";
2479            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2480
2481            fn fields() -> &'static [FieldInfo] {
2482                static FIELDS: &[FieldInfo] = &[
2483                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2484                    FieldInfo::new("name", "name", SqlType::Text),
2485                    FieldInfo::new("age", "age", SqlType::Integer),
2486                ];
2487                FIELDS
2488            }
2489
2490            fn to_row(&self) -> Vec<(&'static str, Value)> {
2491                vec![
2492                    ("id", Value::BigInt(self.id)),
2493                    ("name", Value::Text(self.name.clone())),
2494                    ("age", Value::Int(self.age)),
2495                ]
2496            }
2497
2498            fn from_row(row: &Row) -> crate::Result<Self> {
2499                Ok(Self {
2500                    id: row.get_named("id")?,
2501                    name: row.get_named("name")?,
2502                    age: row.get_named("age")?,
2503                })
2504            }
2505
2506            fn primary_key_value(&self) -> Vec<Value> {
2507                vec![Value::BigInt(self.id)]
2508            }
2509
2510            fn is_new(&self) -> bool {
2511                false
2512            }
2513        }
2514
2515        let mut user = TestUser {
2516            id: 1,
2517            name: "Alice".to_string(),
2518            age: 30,
2519        };
2520
2521        // Update name only
2522        let update = HashMap::from([("name".to_string(), serde_json::json!("Bob"))]);
2523        user.sqlmodel_update(update, UpdateOptions::default())
2524            .unwrap();
2525
2526        assert_eq!(user.name, "Bob");
2527        assert_eq!(user.age, 30); // Unchanged
2528    }
2529
2530    #[test]
2531    fn test_sqlmodel_update_with_update_fields_filter() {
2532        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2533        struct TestUser {
2534            id: i64,
2535            name: String,
2536            age: i32,
2537        }
2538
2539        impl Model for TestUser {
2540            const TABLE_NAME: &'static str = "users";
2541            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2542
2543            fn fields() -> &'static [FieldInfo] {
2544                static FIELDS: &[FieldInfo] = &[
2545                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2546                    FieldInfo::new("name", "name", SqlType::Text),
2547                    FieldInfo::new("age", "age", SqlType::Integer),
2548                ];
2549                FIELDS
2550            }
2551
2552            fn to_row(&self) -> Vec<(&'static str, Value)> {
2553                vec![
2554                    ("id", Value::BigInt(self.id)),
2555                    ("name", Value::Text(self.name.clone())),
2556                    ("age", Value::Int(self.age)),
2557                ]
2558            }
2559
2560            fn from_row(row: &Row) -> crate::Result<Self> {
2561                Ok(Self {
2562                    id: row.get_named("id")?,
2563                    name: row.get_named("name")?,
2564                    age: row.get_named("age")?,
2565                })
2566            }
2567
2568            fn primary_key_value(&self) -> Vec<Value> {
2569                vec![Value::BigInt(self.id)]
2570            }
2571
2572            fn is_new(&self) -> bool {
2573                false
2574            }
2575        }
2576
2577        let mut user = TestUser {
2578            id: 1,
2579            name: "Alice".to_string(),
2580            age: 30,
2581        };
2582
2583        // Try to update both name and age, but only allow name
2584        let update = HashMap::from([
2585            ("name".to_string(), serde_json::json!("Bob")),
2586            ("age".to_string(), serde_json::json!(25)),
2587        ]);
2588        user.sqlmodel_update(update, UpdateOptions::default().update_fields(["name"]))
2589            .unwrap();
2590
2591        assert_eq!(user.name, "Bob"); // Updated
2592        assert_eq!(user.age, 30); // Not updated because not in update_fields
2593    }
2594
2595    #[test]
2596    fn test_sqlmodel_update_invalid_field_error() {
2597        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2598        struct TestUser {
2599            id: i64,
2600            name: String,
2601        }
2602
2603        impl Model for TestUser {
2604            const TABLE_NAME: &'static str = "users";
2605            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2606
2607            fn fields() -> &'static [FieldInfo] {
2608                static FIELDS: &[FieldInfo] = &[
2609                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2610                    FieldInfo::new("name", "name", SqlType::Text),
2611                ];
2612                FIELDS
2613            }
2614
2615            fn to_row(&self) -> Vec<(&'static str, Value)> {
2616                vec![
2617                    ("id", Value::BigInt(self.id)),
2618                    ("name", Value::Text(self.name.clone())),
2619                ]
2620            }
2621
2622            fn from_row(row: &Row) -> crate::Result<Self> {
2623                Ok(Self {
2624                    id: row.get_named("id")?,
2625                    name: row.get_named("name")?,
2626                })
2627            }
2628
2629            fn primary_key_value(&self) -> Vec<Value> {
2630                vec![Value::BigInt(self.id)]
2631            }
2632
2633            fn is_new(&self) -> bool {
2634                false
2635            }
2636        }
2637
2638        let mut user = TestUser {
2639            id: 1,
2640            name: "Alice".to_string(),
2641        };
2642
2643        // Try to update an invalid field
2644        let update = HashMap::from([("invalid_field".to_string(), serde_json::json!("value"))]);
2645        let result = user.sqlmodel_update(update, UpdateOptions::default());
2646
2647        assert!(result.is_err());
2648        let err = result.unwrap_err();
2649        assert!(err.errors.iter().any(|e| e.field == "invalid_field"));
2650    }
2651
2652    #[test]
2653    fn test_sqlmodel_update_from_model() {
2654        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2655        struct TestUser {
2656            id: i64,
2657            name: String,
2658            email: Option<String>,
2659        }
2660
2661        impl Model for TestUser {
2662            const TABLE_NAME: &'static str = "users";
2663            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2664
2665            fn fields() -> &'static [FieldInfo] {
2666                static FIELDS: &[FieldInfo] = &[
2667                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2668                    FieldInfo::new("name", "name", SqlType::Text),
2669                    FieldInfo::new("email", "email", SqlType::Text).nullable(true),
2670                ];
2671                FIELDS
2672            }
2673
2674            fn to_row(&self) -> Vec<(&'static str, Value)> {
2675                vec![
2676                    ("id", Value::BigInt(self.id)),
2677                    ("name", Value::Text(self.name.clone())),
2678                    ("email", self.email.clone().map_or(Value::Null, Value::Text)),
2679                ]
2680            }
2681
2682            fn from_row(row: &Row) -> crate::Result<Self> {
2683                Ok(Self {
2684                    id: row.get_named("id")?,
2685                    name: row.get_named("name")?,
2686                    email: row.get_named("email").ok(),
2687                })
2688            }
2689
2690            fn primary_key_value(&self) -> Vec<Value> {
2691                vec![Value::BigInt(self.id)]
2692            }
2693
2694            fn is_new(&self) -> bool {
2695                false
2696            }
2697        }
2698
2699        let mut user = TestUser {
2700            id: 1,
2701            name: "Alice".to_string(),
2702            email: Some("alice@example.com".to_string()),
2703        };
2704
2705        // Patch with only name set (email is None)
2706        let patch = TestUser {
2707            id: 0, // Will be ignored since we're copying non-null values
2708            name: "Bob".to_string(),
2709            email: None, // Should not overwrite existing email
2710        };
2711
2712        user.sqlmodel_update_from(&patch, UpdateOptions::default())
2713            .unwrap();
2714
2715        assert_eq!(user.name, "Bob"); // Updated
2716        assert_eq!(user.email, Some("alice@example.com".to_string())); // Not updated (patch.email was None)
2717    }
2718
2719    #[test]
2720    fn test_sqlmodel_update_dict_convenience() {
2721        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2722        struct TestItem {
2723            id: i64,
2724            count: i32,
2725        }
2726
2727        impl Model for TestItem {
2728            const TABLE_NAME: &'static str = "items";
2729            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2730
2731            fn fields() -> &'static [FieldInfo] {
2732                static FIELDS: &[FieldInfo] = &[
2733                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2734                    FieldInfo::new("count", "count", SqlType::Integer),
2735                ];
2736                FIELDS
2737            }
2738
2739            fn to_row(&self) -> Vec<(&'static str, Value)> {
2740                vec![
2741                    ("id", Value::BigInt(self.id)),
2742                    ("count", Value::Int(self.count)),
2743                ]
2744            }
2745
2746            fn from_row(row: &Row) -> crate::Result<Self> {
2747                Ok(Self {
2748                    id: row.get_named("id")?,
2749                    count: row.get_named("count")?,
2750                })
2751            }
2752
2753            fn primary_key_value(&self) -> Vec<Value> {
2754                vec![Value::BigInt(self.id)]
2755            }
2756
2757            fn is_new(&self) -> bool {
2758                false
2759            }
2760        }
2761
2762        let mut item = TestItem { id: 1, count: 10 };
2763
2764        // Use the convenience method
2765        item.sqlmodel_update_dict(HashMap::from([(
2766            "count".to_string(),
2767            serde_json::json!(20),
2768        )]))
2769        .unwrap();
2770
2771        assert_eq!(item.count, 20);
2772    }
2773
2774    // ========================================================================
2775    // Credit Card Validation Tests (Luhn Algorithm)
2776    // ========================================================================
2777
2778    #[test]
2779    fn test_credit_card_valid_visa() {
2780        // Valid Visa test number
2781        assert!(is_valid_credit_card("4539578763621486"));
2782    }
2783
2784    #[test]
2785    fn test_credit_card_valid_mastercard() {
2786        // Valid Mastercard test number
2787        assert!(is_valid_credit_card("5425233430109903"));
2788    }
2789
2790    #[test]
2791    fn test_credit_card_valid_amex() {
2792        // Valid American Express test number
2793        assert!(is_valid_credit_card("374245455400126"));
2794    }
2795
2796    #[test]
2797    fn test_credit_card_with_spaces() {
2798        // Spaces should be stripped
2799        assert!(is_valid_credit_card("4539 5787 6362 1486"));
2800    }
2801
2802    #[test]
2803    fn test_credit_card_with_dashes() {
2804        // Dashes should be stripped
2805        assert!(is_valid_credit_card("4539-5787-6362-1486"));
2806    }
2807
2808    #[test]
2809    fn test_credit_card_invalid_luhn() {
2810        // Invalid Luhn checksum
2811        assert!(!is_valid_credit_card("1234567890123456"));
2812    }
2813
2814    #[test]
2815    fn test_credit_card_too_short() {
2816        // Less than 13 digits
2817        assert!(!is_valid_credit_card("123456789012"));
2818    }
2819
2820    #[test]
2821    fn test_credit_card_too_long() {
2822        // More than 19 digits
2823        assert!(!is_valid_credit_card("12345678901234567890"));
2824    }
2825
2826    #[test]
2827    fn test_credit_card_empty() {
2828        assert!(!is_valid_credit_card(""));
2829    }
2830
2831    #[test]
2832    fn test_credit_card_non_numeric() {
2833        // Contains letters
2834        assert!(!is_valid_credit_card("453957876362abcd"));
2835    }
2836
2837    #[test]
2838    fn test_credit_card_all_zeros() {
2839        // All zeros - 16 digits, technically passes Luhn (sum=0, 0%10=0)
2840        // but not a realistic card number
2841        assert!(is_valid_credit_card("0000000000000000"));
2842    }
2843
2844    #[test]
2845    fn test_credit_card_valid_discover() {
2846        // Valid Discover test number
2847        assert!(is_valid_credit_card("6011111111111117"));
2848    }
2849
2850    // =========================================================================
2851    // Nested Model Tests
2852    // =========================================================================
2853
2854    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2855    struct Address {
2856        street: String,
2857        city: String,
2858        #[serde(skip_serializing_if = "Option::is_none")]
2859        zip: Option<String>,
2860    }
2861
2862    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2863    struct Person {
2864        name: String,
2865        age: i32,
2866        address: Address,
2867        #[serde(skip_serializing_if = "Option::is_none")]
2868        spouse: Option<Box<Person>>,
2869    }
2870
2871    #[test]
2872    fn test_nested_model_dump_basic() {
2873        let person = Person {
2874            name: "Alice".to_string(),
2875            age: 30,
2876            address: Address {
2877                street: "123 Main St".to_string(),
2878                city: "Springfield".to_string(),
2879                zip: Some("12345".to_string()),
2880            },
2881            spouse: None,
2882        };
2883
2884        let json = person.model_dump(DumpOptions::default()).unwrap();
2885        assert_eq!(json["name"], "Alice");
2886        assert_eq!(json["age"], 30);
2887        assert_eq!(json["address"]["street"], "123 Main St");
2888        assert_eq!(json["address"]["city"], "Springfield");
2889        assert_eq!(json["address"]["zip"], "12345");
2890    }
2891
2892    #[test]
2893    fn test_nested_model_dump_exclude_top_level() {
2894        let person = Person {
2895            name: "Alice".to_string(),
2896            age: 30,
2897            address: Address {
2898                street: "123 Main St".to_string(),
2899                city: "Springfield".to_string(),
2900                zip: Some("12345".to_string()),
2901            },
2902            spouse: None,
2903        };
2904
2905        // Exclude only applies to top-level fields
2906        let json = person
2907            .model_dump(DumpOptions::default().exclude(["age"]))
2908            .unwrap();
2909        assert!(json.get("name").is_some());
2910        assert!(json.get("age").is_none());
2911        assert!(json.get("address").is_some()); // Still present
2912        // Nested fields are NOT affected by top-level exclude
2913        assert_eq!(json["address"]["city"], "Springfield");
2914    }
2915
2916    #[test]
2917    fn test_nested_model_dump_exclude_nested_limitation() {
2918        // NOTE: This test documents a LIMITATION.
2919        // In Pydantic, you can exclude nested fields with dot notation: exclude={"address.zip"}
2920        // Our current implementation only supports top-level field exclusion.
2921        let person = Person {
2922            name: "Alice".to_string(),
2923            age: 30,
2924            address: Address {
2925                street: "123 Main St".to_string(),
2926                city: "Springfield".to_string(),
2927                zip: Some("12345".to_string()),
2928            },
2929            spouse: None,
2930        };
2931
2932        // Trying to exclude "address.zip" won't work - it treats it as a top-level field name
2933        let json = person
2934            .model_dump(DumpOptions::default().exclude(["address.zip"]))
2935            .unwrap();
2936        // address.zip is still present because we don't support nested path exclusion
2937        assert_eq!(json["address"]["zip"], "12345");
2938    }
2939
2940    #[test]
2941    fn test_deeply_nested_model_dump() {
2942        let person = Person {
2943            name: "Alice".to_string(),
2944            age: 30,
2945            address: Address {
2946                street: "123 Main St".to_string(),
2947                city: "Springfield".to_string(),
2948                zip: None,
2949            },
2950            spouse: Some(Box::new(Person {
2951                name: "Bob".to_string(),
2952                age: 32,
2953                address: Address {
2954                    street: "456 Oak Ave".to_string(),
2955                    city: "Springfield".to_string(),
2956                    zip: Some("12346".to_string()),
2957                },
2958                spouse: None,
2959            })),
2960        };
2961
2962        let json = person.model_dump(DumpOptions::default()).unwrap();
2963        assert_eq!(json["name"], "Alice");
2964        assert_eq!(json["spouse"]["name"], "Bob");
2965        assert_eq!(json["spouse"]["address"]["street"], "456 Oak Ave");
2966    }
2967
2968    #[test]
2969    fn test_nested_model_exclude_none() {
2970        let person = Person {
2971            name: "Alice".to_string(),
2972            age: 30,
2973            address: Address {
2974                street: "123 Main St".to_string(),
2975                city: "Springfield".to_string(),
2976                zip: None, // Will be skipped by serde skip_serializing_if
2977            },
2978            spouse: None, // Will be skipped by serde skip_serializing_if
2979        };
2980
2981        let json = person
2982            .model_dump(DumpOptions::default().exclude_none())
2983            .unwrap();
2984        assert!(json.get("name").is_some());
2985        // spouse is None and serde skips it, so it's not in the output
2986        assert!(json.get("spouse").is_none());
2987        // Note: exclude_none only affects top-level nulls in model_dump
2988        // Nested nulls are handled by serde's skip_serializing_if
2989    }
2990}