Skip to main content

rango_orm_core/
fields.rs

1use argon2::Argon2;
2use argon2::password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash};
3use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Marker trait for all Rango field types.
8pub trait RangoField {
9    type Inner;
10    fn column_type() -> crate::schema::ColumnType;
11    fn into_inner(self) -> Self::Inner;
12}
13
14// ─── Primitive fields ────────────────────────────────────────────────────────
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct FieldBool(pub bool);
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct FieldSmallInt(pub i16);
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct FieldInt(pub i32);
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct FieldBigInt(pub i64);
27
28/// Auto-increment `INTEGER` primary key. `None` until the row is inserted;
29/// the database generates the value.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
31pub struct FieldAutoInt(pub Option<i32>);
32
33/// Auto-increment `BIGINT` primary key. `None` until the row is inserted;
34/// the database generates the value.
35#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
36pub struct FieldAutoBigInt(pub Option<i64>);
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct FieldFloat(pub f32);
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct FieldDouble(pub f64);
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct FieldDecimal<const PRECISION: u8, const SCALE: u8>(pub rust_decimal::Decimal);
46
47impl<const P: u8, const S: u8> FieldDecimal<P, S> {
48    /// Wraps `value` at the column scale (`12.5` becomes `12.50` for `FieldDecimal<10, 2>`),
49    /// so a value read back has the same digits the database stores.
50    pub fn from_decimal(mut value: rust_decimal::Decimal) -> Self {
51        value.rescale(u32::from(S));
52        Self(value)
53    }
54}
55
56// ─── String fields ───────────────────────────────────────────────────────────
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct FieldText(pub String);
60
61/// VARCHAR with min/max length validation (in characters, not bytes).
62///
63/// The inner value is private: the only ways to build one are the validating
64/// [`new`](Self::new) / `TryFrom` / `Deserialize`, and decoding from a database row.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(try_from = "String")]
67pub struct FieldVarchar<const MIN: usize, const MAX: usize>(String);
68
69impl<const MIN: usize, const MAX: usize> FieldVarchar<MIN, MAX> {
70    pub fn new(s: impl Into<String>) -> Result<Self, FieldError> {
71        let s = s.into();
72        let len = s.chars().count();
73        if len < MIN {
74            return Err(FieldError(format!("Value too short (min {})", MIN)));
75        }
76        if len > MAX {
77            return Err(FieldError(format!("Value too long (max {})", MAX)));
78        }
79        Ok(Self(s))
80    }
81
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85
86    pub fn into_inner(self) -> String {
87        self.0
88    }
89
90    /// Wraps a value read from the database without re-validating it, so rows written
91    /// before a bound changed stay readable. Not for application code.
92    #[doc(hidden)]
93    pub fn from_db(s: String) -> Self {
94        Self(s)
95    }
96}
97
98impl<const MIN: usize, const MAX: usize> TryFrom<String> for FieldVarchar<MIN, MAX> {
99    type Error = FieldError;
100    fn try_from(s: String) -> Result<Self, FieldError> {
101        Self::new(s)
102    }
103}
104
105impl<const MIN: usize, const MAX: usize> std::ops::Deref for FieldVarchar<MIN, MAX> {
106    type Target = str;
107    fn deref(&self) -> &str {
108        &self.0
109    }
110}
111
112impl<const MIN: usize, const MAX: usize> std::fmt::Display for FieldVarchar<MIN, MAX> {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.write_str(&self.0)
115    }
116}
117
118/// Email field — validated on construction (inner value is private, see [`FieldVarchar`]).
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120#[serde(try_from = "String")]
121pub struct FieldEmail(String);
122
123impl FieldEmail {
124    pub fn new(s: impl Into<String>) -> Result<Self, FieldError> {
125        let s = s.into();
126        if !s.contains('@') || s.chars().count() > 254 {
127            return Err(FieldError("Invalid email address".into()));
128        }
129        Ok(Self(s))
130    }
131
132    pub fn as_str(&self) -> &str {
133        &self.0
134    }
135
136    pub fn into_inner(self) -> String {
137        self.0
138    }
139
140    /// Wraps a value read from the database without re-validating it. Not for application code.
141    #[doc(hidden)]
142    pub fn from_db(s: String) -> Self {
143        Self(s)
144    }
145}
146
147impl TryFrom<String> for FieldEmail {
148    type Error = FieldError;
149    fn try_from(s: String) -> Result<Self, FieldError> {
150        Self::new(s)
151    }
152}
153
154impl std::ops::Deref for FieldEmail {
155    type Target = str;
156    fn deref(&self) -> &str {
157        &self.0
158    }
159}
160
161impl std::fmt::Display for FieldEmail {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        f.write_str(&self.0)
164    }
165}
166
167/// Password field — stores an Argon2id PHC hash, never the plaintext.
168///
169/// The plaintext is length-validated, then hashed with a random salt.
170/// `Debug` prints a redacted value. This type intentionally does not implement
171/// `Serialize`/`Deserialize`; if a model must be serialized, skip the field
172/// or use a DTO:
173///
174/// ```rust,ignore
175/// #[derive(serde::Serialize)]
176/// struct UserDto {
177///     #[serde(skip)]
178///     password: FieldPassword<8, 128>,
179/// }
180/// ```
181///
182/// Serializing the field is a compile error:
183///
184/// ```compile_fail,E0277
185/// fn needs_serialize<T: serde::Serialize>() {}
186/// needs_serialize::<rango_orm_core::FieldPassword<8, 128>>();
187/// ```
188///
189/// So is deserializing it:
190///
191/// ```compile_fail,E0277
192/// fn needs_deserialize<T: serde::de::DeserializeOwned>() {}
193/// needs_deserialize::<rango_orm_core::FieldPassword<8, 128>>();
194/// ```
195#[derive(Clone, PartialEq)]
196pub struct FieldPassword<const MIN: usize, const MAX: usize> {
197    hash: String,
198}
199
200impl<const MIN: usize, const MAX: usize> std::fmt::Debug for FieldPassword<MIN, MAX> {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        write!(f, "FieldPassword(<redacted>)")
203    }
204}
205
206impl<const MIN: usize, const MAX: usize> FieldPassword<MIN, MAX> {
207    pub fn new(plain: impl AsRef<str>) -> Result<Self, FieldError> {
208        let plain = plain.as_ref();
209        let len = plain.chars().count();
210        if len < MIN {
211            return Err(FieldError(format!("Password too short (min {})", MIN)));
212        }
213        if len > MAX {
214            return Err(FieldError(format!("Password too long (max {})", MAX)));
215        }
216
217        let hash = Argon2::default()
218            .hash_password(plain.as_bytes())
219            .map_err(|e| FieldError(e.to_string()))?;
220
221        Ok(Self {
222            hash: hash.to_string(),
223        })
224    }
225
226    pub fn verify(&self, candidate: &str) -> bool {
227        let Ok(parsed) = PasswordHash::new(&self.hash) else {
228            return false;
229        };
230
231        Argon2::default()
232            .verify_password(candidate.as_bytes(), &parsed)
233            .is_ok()
234    }
235
236    pub fn from_hash(hash: String) -> Self {
237        Self { hash }
238    }
239
240    pub fn hash(&self) -> &str {
241        &self.hash
242    }
243}
244
245/// URL field — basic validation (inner value is private, see [`FieldVarchar`]).
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247#[serde(try_from = "String")]
248pub struct FieldUrl(String);
249
250impl FieldUrl {
251    pub fn new(s: impl Into<String>) -> Result<Self, FieldError> {
252        let s = s.into();
253        if !s.starts_with("http://") && !s.starts_with("https://") {
254            return Err(FieldError("URL must start with http:// or https://".into()));
255        }
256        Ok(Self(s))
257    }
258
259    pub fn as_str(&self) -> &str {
260        &self.0
261    }
262
263    pub fn into_inner(self) -> String {
264        self.0
265    }
266
267    /// Wraps a value read from the database without re-validating it. Not for application code.
268    #[doc(hidden)]
269    pub fn from_db(s: String) -> Self {
270        Self(s)
271    }
272}
273
274impl TryFrom<String> for FieldUrl {
275    type Error = FieldError;
276    fn try_from(s: String) -> Result<Self, FieldError> {
277        Self::new(s)
278    }
279}
280
281impl std::ops::Deref for FieldUrl {
282    type Target = str;
283    fn deref(&self) -> &str {
284        &self.0
285    }
286}
287
288impl std::fmt::Display for FieldUrl {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        f.write_str(&self.0)
291    }
292}
293
294// ─── Binary ──────────────────────────────────────────────────────────────────
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct FieldBytes(pub Vec<u8>);
298
299// ─── UUID ─────────────────────────────────────────────────────────────────────
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct FieldUuid(pub Uuid);
303
304impl FieldUuid {
305    /// Generate a new random UUID v4.
306    pub fn new() -> Self {
307        Self(Uuid::new_v4())
308    }
309}
310
311impl Default for FieldUuid {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317// ─── Date / Time ─────────────────────────────────────────────────────────────
318
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct FieldDate(pub NaiveDate);
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323pub struct FieldTime(pub NaiveTime);
324
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326pub struct FieldDateTime(pub DateTime<Utc>);
327
328impl FieldDateTime {
329    pub fn now() -> Self {
330        Self(Utc::now())
331    }
332}
333
334// ─── JSON ────────────────────────────────────────────────────────────────────
335
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub struct FieldJson(pub serde_json::Value);
338
339// ─── Range ───────────────────────────────────────────────────────────────────
340
341/// Integer bounded to `MIN..=MAX` (inner value is private, see [`FieldVarchar`]).
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343#[serde(try_from = "i64")]
344pub struct FieldRange<const MIN: i64, const MAX: i64>(i64);
345
346impl<const MIN: i64, const MAX: i64> FieldRange<MIN, MAX> {
347    pub fn new(val: i64) -> Result<Self, FieldError> {
348        if val < MIN || val > MAX {
349            return Err(FieldError(format!(
350                "Value {} out of range [{}, {}]",
351                val, MIN, MAX
352            )));
353        }
354        Ok(Self(val))
355    }
356
357    pub fn get(&self) -> i64 {
358        self.0
359    }
360
361    /// Wraps a value read from the database without re-validating it. Not for application code.
362    #[doc(hidden)]
363    pub fn from_db(v: i64) -> Self {
364        Self(v)
365    }
366}
367
368impl<const MIN: i64, const MAX: i64> TryFrom<i64> for FieldRange<MIN, MAX> {
369    type Error = FieldError;
370    fn try_from(v: i64) -> Result<Self, FieldError> {
371        Self::new(v)
372    }
373}
374
375// ─── Error ───────────────────────────────────────────────────────────────────
376
377#[derive(Debug, Clone, PartialEq)]
378pub struct FieldError(pub String);
379
380impl std::fmt::Display for FieldError {
381    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382        write!(f, "{}", self.0)
383    }
384}
385
386impl std::error::Error for FieldError {}
387
388// ─── ToSqlValue implementations ──────────────────────────────────────────────
389
390use crate::query::{SqlValue, ToSqlValue};
391
392impl ToSqlValue for FieldBool {
393    fn to_sql_value(&self) -> SqlValue {
394        SqlValue::Bool(self.0)
395    }
396}
397impl ToSqlValue for FieldSmallInt {
398    fn to_sql_value(&self) -> SqlValue {
399        SqlValue::SmallInt(self.0)
400    }
401}
402impl ToSqlValue for FieldInt {
403    fn to_sql_value(&self) -> SqlValue {
404        SqlValue::Int(self.0)
405    }
406}
407impl ToSqlValue for FieldBigInt {
408    fn to_sql_value(&self) -> SqlValue {
409        SqlValue::BigInt(self.0)
410    }
411}
412impl ToSqlValue for FieldAutoInt {
413    fn to_sql_value(&self) -> SqlValue {
414        self.0.map_or(SqlValue::NullInt, SqlValue::Int)
415    }
416}
417impl ToSqlValue for FieldAutoBigInt {
418    fn to_sql_value(&self) -> SqlValue {
419        self.0.map_or(SqlValue::NullBigInt, SqlValue::BigInt)
420    }
421}
422impl ToSqlValue for FieldFloat {
423    fn to_sql_value(&self) -> SqlValue {
424        SqlValue::Float(self.0)
425    }
426}
427impl ToSqlValue for FieldDouble {
428    fn to_sql_value(&self) -> SqlValue {
429        SqlValue::Double(self.0)
430    }
431}
432impl ToSqlValue for FieldText {
433    fn to_sql_value(&self) -> SqlValue {
434        SqlValue::Text(self.0.clone())
435    }
436}
437impl ToSqlValue for FieldEmail {
438    fn to_sql_value(&self) -> SqlValue {
439        SqlValue::Text(self.0.clone())
440    }
441}
442impl ToSqlValue for FieldUrl {
443    fn to_sql_value(&self) -> SqlValue {
444        SqlValue::Text(self.0.clone())
445    }
446}
447impl ToSqlValue for FieldBytes {
448    fn to_sql_value(&self) -> SqlValue {
449        SqlValue::Bytes(self.0.clone())
450    }
451}
452// Raw key values: what `ForeignKey<M>` holds (`Model::Pk`).
453impl ToSqlValue for Uuid {
454    fn to_sql_value(&self) -> SqlValue {
455        SqlValue::Uuid(*self)
456    }
457}
458impl ToSqlValue for i32 {
459    fn to_sql_value(&self) -> SqlValue {
460        SqlValue::Int(*self)
461    }
462}
463impl ToSqlValue for i64 {
464    fn to_sql_value(&self) -> SqlValue {
465        SqlValue::BigInt(*self)
466    }
467}
468impl ToSqlValue for FieldUuid {
469    fn to_sql_value(&self) -> SqlValue {
470        SqlValue::Uuid(self.0)
471    }
472}
473impl ToSqlValue for FieldDateTime {
474    fn to_sql_value(&self) -> SqlValue {
475        SqlValue::DateTime(self.0)
476    }
477}
478impl ToSqlValue for FieldDate {
479    fn to_sql_value(&self) -> SqlValue {
480        SqlValue::Date(self.0)
481    }
482}
483impl ToSqlValue for FieldTime {
484    fn to_sql_value(&self) -> SqlValue {
485        SqlValue::Time(self.0)
486    }
487}
488impl ToSqlValue for FieldJson {
489    fn to_sql_value(&self) -> SqlValue {
490        SqlValue::Json(self.0.clone())
491    }
492}
493
494impl<const MIN: usize, const MAX: usize> ToSqlValue for FieldVarchar<MIN, MAX> {
495    fn to_sql_value(&self) -> SqlValue {
496        SqlValue::Text(self.0.clone())
497    }
498}
499impl<const MIN: usize, const MAX: usize> ToSqlValue for FieldPassword<MIN, MAX> {
500    fn to_sql_value(&self) -> SqlValue {
501        SqlValue::Text(self.hash().to_string())
502    }
503}
504impl<const MIN: i64, const MAX: i64> ToSqlValue for FieldRange<MIN, MAX> {
505    fn to_sql_value(&self) -> SqlValue {
506        SqlValue::BigInt(self.0)
507    }
508}
509impl<const P: u8, const S: u8> ToSqlValue for FieldDecimal<P, S> {
510    fn to_sql_value(&self) -> SqlValue {
511        SqlValue::Decimal(self.0)
512    }
513}
514impl<const P: u8, const S: u8> ToSqlValue for Option<FieldDecimal<P, S>> {
515    fn to_sql_value(&self) -> SqlValue {
516        self.as_ref()
517            .map_or(SqlValue::NullDecimal, |v| v.to_sql_value())
518    }
519}
520
521// Option<T> support — typed nulls so Postgres knows the column type
522impl ToSqlValue for Option<FieldBool> {
523    fn to_sql_value(&self) -> SqlValue {
524        self.as_ref()
525            .map_or(SqlValue::NullBool, |v| v.to_sql_value())
526    }
527}
528impl ToSqlValue for Option<FieldSmallInt> {
529    fn to_sql_value(&self) -> SqlValue {
530        self.as_ref()
531            .map_or(SqlValue::NullSmallInt, |v| v.to_sql_value())
532    }
533}
534impl ToSqlValue for Option<FieldInt> {
535    fn to_sql_value(&self) -> SqlValue {
536        self.as_ref()
537            .map_or(SqlValue::NullInt, |v| v.to_sql_value())
538    }
539}
540impl ToSqlValue for Option<FieldBigInt> {
541    fn to_sql_value(&self) -> SqlValue {
542        self.as_ref()
543            .map_or(SqlValue::NullBigInt, |v| v.to_sql_value())
544    }
545}
546impl ToSqlValue for Option<FieldFloat> {
547    fn to_sql_value(&self) -> SqlValue {
548        self.as_ref()
549            .map_or(SqlValue::NullFloat, |v| v.to_sql_value())
550    }
551}
552impl ToSqlValue for Option<FieldDouble> {
553    fn to_sql_value(&self) -> SqlValue {
554        self.as_ref()
555            .map_or(SqlValue::NullDouble, |v| v.to_sql_value())
556    }
557}
558impl ToSqlValue for Option<FieldText> {
559    fn to_sql_value(&self) -> SqlValue {
560        self.as_ref()
561            .map_or(SqlValue::NullText, |v| v.to_sql_value())
562    }
563}
564impl ToSqlValue for Option<FieldEmail> {
565    fn to_sql_value(&self) -> SqlValue {
566        self.as_ref()
567            .map_or(SqlValue::NullText, |v| v.to_sql_value())
568    }
569}
570impl ToSqlValue for Option<FieldUrl> {
571    fn to_sql_value(&self) -> SqlValue {
572        self.as_ref()
573            .map_or(SqlValue::NullText, |v| v.to_sql_value())
574    }
575}
576impl ToSqlValue for Option<FieldBytes> {
577    fn to_sql_value(&self) -> SqlValue {
578        self.as_ref()
579            .map_or(SqlValue::NullBytes, |v| v.to_sql_value())
580    }
581}
582impl ToSqlValue for Option<FieldUuid> {
583    fn to_sql_value(&self) -> SqlValue {
584        self.as_ref()
585            .map_or(SqlValue::NullUuid, |v| v.to_sql_value())
586    }
587}
588impl ToSqlValue for Option<FieldDateTime> {
589    fn to_sql_value(&self) -> SqlValue {
590        self.as_ref()
591            .map_or(SqlValue::NullDateTime, |v| v.to_sql_value())
592    }
593}
594impl ToSqlValue for Option<FieldDate> {
595    fn to_sql_value(&self) -> SqlValue {
596        self.as_ref()
597            .map_or(SqlValue::NullDate, |v| v.to_sql_value())
598    }
599}
600impl ToSqlValue for Option<FieldTime> {
601    fn to_sql_value(&self) -> SqlValue {
602        self.as_ref()
603            .map_or(SqlValue::NullTime, |v| v.to_sql_value())
604    }
605}
606impl ToSqlValue for Option<FieldJson> {
607    fn to_sql_value(&self) -> SqlValue {
608        self.as_ref()
609            .map_or(SqlValue::NullJson, |v| v.to_sql_value())
610    }
611}
612
613// Parametric types
614impl<const MIN: usize, const MAX: usize> ToSqlValue for Option<FieldVarchar<MIN, MAX>> {
615    fn to_sql_value(&self) -> SqlValue {
616        self.as_ref()
617            .map_or(SqlValue::NullText, |v| v.to_sql_value())
618    }
619}
620impl<const MIN: usize, const MAX: usize> ToSqlValue for Option<FieldPassword<MIN, MAX>> {
621    fn to_sql_value(&self) -> SqlValue {
622        self.as_ref()
623            .map_or(SqlValue::NullText, |v| v.to_sql_value())
624    }
625}
626impl<const MIN: i64, const MAX: i64> ToSqlValue for Option<FieldRange<MIN, MAX>> {
627    fn to_sql_value(&self) -> SqlValue {
628        self.as_ref()
629            .map_or(SqlValue::NullBigInt, |v| v.to_sql_value())
630    }
631}
632impl<M: crate::Model> ToSqlValue for Option<ForeignKey<M>> {
633    fn to_sql_value(&self) -> SqlValue {
634        self.as_ref()
635            .map_or_else(<M::Pk as crate::PrimaryKey>::null_value, |v| {
636                v.to_sql_value()
637            })
638    }
639}
640
641// ─── Many to Many ────────────────────────────────────────────────────────────
642
643/// A many-to-many relation field.
644/// The pivot table is auto-generated by `rango makemigrations`.
645///
646/// # Example
647/// ```rust,ignore
648/// #[derive(Model)]
649/// struct Article {
650///     id: FieldUuid,
651///     tags: ManyToMany<Tag>,                      // auto pivot table
652///
653///     #[field(through = "ArticleTag")]
654///     contributors: ManyToMany<User>,             // explicit pivot table
655/// }
656/// ```
657#[derive(Serialize, Deserialize)]
658#[serde(bound(
659    serialize = "<M as crate::Model>::Pk: Serialize",
660    deserialize = "<M as crate::Model>::Pk: serde::de::DeserializeOwned"
661))]
662pub struct ManyToMany<M: crate::Model>(
663    /// Always empty when loaded from the database: use `.prefetch_related(Model::FIELD)`
664    /// and `.prefetched::<M>()` to load the related rows.
665    pub Vec<<M as crate::Model>::Pk>,
666    std::marker::PhantomData<M>,
667);
668
669// Manual impls: derives would require `M: Clone + Default + Debug`, but `M` is only a marker.
670impl<M: crate::Model> Clone for ManyToMany<M> {
671    fn clone(&self) -> Self {
672        Self(self.0.clone(), std::marker::PhantomData)
673    }
674}
675
676impl<M: crate::Model> Default for ManyToMany<M> {
677    fn default() -> Self {
678        Self::empty()
679    }
680}
681
682impl<M: crate::Model> std::fmt::Debug for ManyToMany<M> {
683    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684        f.debug_tuple("ManyToMany").field(&self.0).finish()
685    }
686}
687
688impl<M: crate::Model> ManyToMany<M> {
689    pub fn empty() -> Self {
690        Self(Vec::new(), std::marker::PhantomData)
691    }
692}
693
694// ─── Foreign Key ─────────────────────────────────────────────────────────────
695
696/// A foreign key reference: stores the primary key value of the related model.
697/// The value type follows the target's primary key (`Model::Pk`): `Uuid` for `FieldUuid`,
698/// `i32` for `FieldAutoInt`, `i64` for `FieldAutoBigInt`. The column type in the schema
699/// and migrations follows it too.
700///
701/// # Example
702/// ```rust,ignore
703/// #[derive(Model)]
704/// struct Article {
705///     id: FieldUuid,
706///     author_id: ForeignKey<User>,
707/// }
708/// ```
709#[derive(Serialize, Deserialize)]
710#[serde(bound(
711    serialize = "<M as crate::Model>::Pk: Serialize",
712    deserialize = "<M as crate::Model>::Pk: serde::de::DeserializeOwned"
713))]
714pub struct ForeignKey<M: crate::Model>(pub <M as crate::Model>::Pk, std::marker::PhantomData<M>);
715
716// Manual impls: derives would require `M: Clone + Debug + PartialEq`, but `M` is only a marker.
717impl<M: crate::Model> Clone for ForeignKey<M> {
718    fn clone(&self) -> Self {
719        *self
720    }
721}
722
723impl<M: crate::Model> Copy for ForeignKey<M> {}
724
725impl<M: crate::Model> PartialEq for ForeignKey<M> {
726    fn eq(&self, other: &Self) -> bool {
727        self.0 == other.0
728    }
729}
730
731impl<M: crate::Model> Eq for ForeignKey<M> {}
732
733impl<M: crate::Model> std::hash::Hash for ForeignKey<M> {
734    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
735        self.0.hash(state);
736    }
737}
738
739impl<M: crate::Model> std::fmt::Debug for ForeignKey<M> {
740    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741        f.debug_tuple("ForeignKey").field(&self.0).finish()
742    }
743}
744
745impl<M: crate::Model> ForeignKey<M> {
746    pub fn new(id: <M as crate::Model>::Pk) -> Self {
747        Self(id, std::marker::PhantomData)
748    }
749
750    pub fn id(&self) -> <M as crate::Model>::Pk {
751        self.0
752    }
753}
754
755impl<M: crate::Model> ToSqlValue for ForeignKey<M> {
756    fn to_sql_value(&self) -> SqlValue {
757        self.0.to_sql_value()
758    }
759}
760
761// ─── From<T> conversions (ergonomics) ────────────────────────────────────────
762
763impl From<bool> for FieldBool {
764    fn from(v: bool) -> Self {
765        Self(v)
766    }
767}
768impl From<i16> for FieldSmallInt {
769    fn from(v: i16) -> Self {
770        Self(v)
771    }
772}
773impl From<i32> for FieldInt {
774    fn from(v: i32) -> Self {
775        Self(v)
776    }
777}
778impl From<i64> for FieldBigInt {
779    fn from(v: i64) -> Self {
780        Self(v)
781    }
782}
783impl From<i32> for FieldAutoInt {
784    fn from(v: i32) -> Self {
785        Self(Some(v))
786    }
787}
788impl From<i64> for FieldAutoBigInt {
789    fn from(v: i64) -> Self {
790        Self(Some(v))
791    }
792}
793impl From<f32> for FieldFloat {
794    fn from(v: f32) -> Self {
795        Self(v)
796    }
797}
798impl From<f64> for FieldDouble {
799    fn from(v: f64) -> Self {
800        Self(v)
801    }
802}
803impl From<String> for FieldText {
804    fn from(v: String) -> Self {
805        Self(v)
806    }
807}
808impl From<&str> for FieldText {
809    fn from(v: &str) -> Self {
810        Self(v.to_string())
811    }
812}
813impl From<Vec<u8>> for FieldBytes {
814    fn from(v: Vec<u8>) -> Self {
815        Self(v)
816    }
817}
818impl From<Uuid> for FieldUuid {
819    fn from(v: Uuid) -> Self {
820        Self(v)
821    }
822}
823impl From<NaiveDate> for FieldDate {
824    fn from(v: NaiveDate) -> Self {
825        Self(v)
826    }
827}
828impl From<NaiveTime> for FieldTime {
829    fn from(v: NaiveTime) -> Self {
830        Self(v)
831    }
832}
833impl From<DateTime<Utc>> for FieldDateTime {
834    fn from(v: DateTime<Utc>) -> Self {
835        Self(v)
836    }
837}
838impl From<serde_json::Value> for FieldJson {
839    fn from(v: serde_json::Value) -> Self {
840        Self(v)
841    }
842}
843
844#[cfg(test)]
845mod tests {
846    use super::*;
847
848    #[test]
849    fn validated_fields_display_their_value() {
850        let name = FieldVarchar::<1, 10>::new("ada").unwrap();
851        let email = FieldEmail::new("ada@x.io").unwrap();
852        assert_eq!(format!("{name} <{email}>"), "ada <ada@x.io>");
853    }
854
855    #[test]
856    fn varchar_counts_chars_not_bytes() {
857        // 3 chars, 6 bytes
858        assert!(FieldVarchar::<1, 3>::new("éèà").is_ok());
859        assert!(FieldVarchar::<4, 10>::new("éèà").is_err());
860    }
861
862    #[test]
863    fn varchar_rejects_out_of_bounds() {
864        assert!(FieldVarchar::<1, 100>::new("").is_err());
865        assert!(FieldVarchar::<1, 100>::new("a".repeat(101)).is_err());
866        assert_eq!(FieldVarchar::<1, 100>::new("ok").unwrap().as_str(), "ok");
867    }
868
869    #[test]
870    fn deserialize_validates() {
871        assert!(serde_json::from_str::<FieldVarchar<1, 5>>("\"\"").is_err());
872        assert!(serde_json::from_str::<FieldEmail>("\"nope\"").is_err());
873        assert!(serde_json::from_str::<FieldUrl>("\"ftp://x\"").is_err());
874        assert!(serde_json::from_str::<FieldRange<0, 10>>("11").is_err());
875        assert!(serde_json::from_str::<FieldRange<0, 10>>("10").is_ok());
876    }
877
878    #[test]
879    fn field_password_new_rejects_too_short() {
880        assert!(FieldPassword::<8, 128>::new("short").is_err());
881    }
882
883    #[test]
884    fn field_password_new_rejects_too_long() {
885        let long = "a".repeat(129);
886        assert!(FieldPassword::<8, 128>::new(long).is_err());
887    }
888
889    #[test]
890    fn field_password_verify_accepts_correct_and_rejects_wrong() {
891        let password = FieldPassword::<8, 128>::new("correct horse battery staple").unwrap();
892        assert!(password.verify("correct horse battery staple"));
893        assert!(!password.verify("wrong password"));
894    }
895
896    #[test]
897    fn field_password_hash_is_argon2id() {
898        let password = FieldPassword::<8, 128>::new("secret password").unwrap();
899        assert!(password.hash().starts_with("$argon2id$"));
900    }
901
902    #[test]
903    fn field_password_debug_does_not_leak_plaintext() {
904        let password = FieldPassword::<8, 128>::new("super secret").unwrap();
905        let debug = format!("{:?}", password);
906        assert!(!debug.contains("super secret"));
907        assert!(debug.contains("FieldPassword(<redacted>)"));
908    }
909}
910
911// ─── FilterValue: plain Rust values accepted by typed column filters ────────
912
913use crate::column::FilterValue;
914
915macro_rules! filter_values {
916    ($($field:ty => $($value:ty),+;)+) => {$($(
917        impl FilterValue<$field> for $value {
918            fn into_sql_value(self) -> SqlValue {
919                self.into()
920            }
921        }
922    )+)+};
923}
924
925filter_values! {
926    FieldBool => bool;
927    FieldSmallInt => i16;
928    FieldInt => i32;
929    FieldBigInt => i64;
930    FieldAutoInt => i32;
931    FieldAutoBigInt => i64;
932    FieldFloat => f32;
933    FieldDouble => f64;
934    FieldText => String, &str;
935    FieldEmail => String, &str;
936    FieldUrl => String, &str;
937    FieldBytes => Vec<u8>;
938    FieldUuid => Uuid;
939    FieldDateTime => DateTime<Utc>;
940    FieldDate => NaiveDate;
941    FieldTime => NaiveTime;
942    FieldJson => serde_json::Value;
943}
944
945impl<const MIN: usize, const MAX: usize> FilterValue<FieldVarchar<MIN, MAX>> for String {
946    fn into_sql_value(self) -> SqlValue {
947        SqlValue::Text(self)
948    }
949}
950impl<const MIN: usize, const MAX: usize> FilterValue<FieldVarchar<MIN, MAX>> for &str {
951    fn into_sql_value(self) -> SqlValue {
952        SqlValue::Text(self.to_string())
953    }
954}
955impl<const MIN: i64, const MAX: i64> FilterValue<FieldRange<MIN, MAX>> for i64 {
956    fn into_sql_value(self) -> SqlValue {
957        SqlValue::BigInt(self)
958    }
959}
960impl<const P: u8, const S: u8> FilterValue<FieldDecimal<P, S>> for rust_decimal::Decimal {
961    fn into_sql_value(self) -> SqlValue {
962        SqlValue::Decimal(self)
963    }
964}
965impl<M: crate::Model<Pk = Uuid>> FilterValue<ForeignKey<M>> for Uuid {
966    fn into_sql_value(self) -> SqlValue {
967        SqlValue::Uuid(self)
968    }
969}
970impl<M: crate::Model<Pk = i32>> FilterValue<ForeignKey<M>> for i32 {
971    fn into_sql_value(self) -> SqlValue {
972        SqlValue::Int(self)
973    }
974}
975impl<M: crate::Model<Pk = i64>> FilterValue<ForeignKey<M>> for i64 {
976    fn into_sql_value(self) -> SqlValue {
977        SqlValue::BigInt(self)
978    }
979}
980impl<M: crate::Model<Pk = Uuid>> FilterValue<ForeignKey<M>> for FieldUuid {
981    fn into_sql_value(self) -> SqlValue {
982        SqlValue::Uuid(self.0)
983    }
984}