toasty_core/schema/app/field.rs
1mod primitive;
2pub use primitive::{FieldPrimitive, SerializeFormat};
3
4use super::{
5 AutoStrategy, BelongsTo, Constraint, Embedded, HasMany, HasOne, Model, ModelId, Schema,
6 VariantId,
7};
8use crate::{Result, driver, stmt};
9use std::fmt;
10
11/// A single field within a model.
12///
13/// Fields are the building blocks of a model's data structure. Each field has a
14/// unique [`FieldId`], a name, a type (primitive, embedded, or relation), and
15/// metadata such as nullability, primary-key membership, auto-population
16/// strategy, and validation constraints.
17///
18/// # Examples
19///
20/// ```ignore
21/// use toasty_core::schema::app::{Field, Schema};
22///
23/// let schema: Schema = /* ... */;
24/// let model = schema.model(model_id).as_root_unwrap();
25/// for field in &model.fields {
26/// println!("{}: primary_key={}", field.name, field.primary_key);
27/// }
28/// ```
29#[derive(Debug, Clone)]
30pub struct Field {
31 /// Uniquely identifies this field within its containing model.
32 pub id: FieldId,
33
34 /// The field's application and storage names.
35 pub name: FieldName,
36
37 /// The field's type: primitive, embedded, or a relation variant.
38 pub ty: FieldTy,
39
40 /// `true` if this field accepts `None` / `NULL` values.
41 pub nullable: bool,
42
43 /// `true` if this field is part of the model's primary key.
44 pub primary_key: bool,
45
46 /// If set, Toasty automatically populates this field on insert.
47 pub auto: Option<AutoStrategy>,
48
49 /// Validation constraints applied to this field's values.
50 pub constraints: Vec<Constraint>,
51
52 /// If this field belongs to an enum variant, identifies that variant.
53 /// `None` for fields on root models and embedded structs.
54 pub variant: Option<VariantId>,
55}
56
57/// Uniquely identifies a [`Field`] within a schema.
58///
59/// Composed of the owning model's [`ModelId`] and a positional index into that
60/// model's field list.
61///
62/// # Examples
63///
64/// ```
65/// use toasty_core::schema::app::{FieldId, ModelId};
66///
67/// let id = FieldId { model: ModelId(0), index: 2 };
68/// assert_eq!(id.index, 2);
69/// ```
70#[derive(Copy, Clone, PartialEq, Eq, Hash)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct FieldId {
73 /// The model this field belongs to.
74 pub model: ModelId,
75 /// Positional index within the model's field list.
76 pub index: usize,
77}
78
79/// The name of a field, with separate application and storage representations.
80///
81/// The `app` field is the Rust-facing name (e.g., `user_name`). It is
82/// `Option<String>` to support unnamed (tuple) fields in the future; for now it
83/// is always `Some`. The optional `storage` field overrides the column name used
84/// in the database; when `None`, `app` is used as the storage name.
85///
86/// # Examples
87///
88/// ```
89/// use toasty_core::schema::app::FieldName;
90///
91/// let name = FieldName {
92/// app: Some("user_name".to_string()),
93/// storage: Some("username".to_string()),
94/// };
95/// assert_eq!(name.storage_name(), Some("username"));
96///
97/// let default_name = FieldName {
98/// app: Some("email".to_string()),
99/// storage: None,
100/// };
101/// assert_eq!(default_name.storage_name(), Some("email"));
102/// ```
103#[derive(Debug, Clone)]
104pub struct FieldName {
105 /// The application-level (Rust) name of the field. `None` for unnamed
106 /// (tuple) fields.
107 pub app: Option<String>,
108 /// Optional override for the database column name. When `None`, `app` is
109 /// used.
110 pub storage: Option<String>,
111}
112
113impl FieldName {
114 /// Returns the application-level (Rust) name of this field.
115 ///
116 /// This is a convenience accessor that unwraps the `app` field, which is
117 /// `Option<String>` to support unnamed (tuple) fields. Most fields have an
118 /// application name, and this method provides direct access without manual
119 /// unwrapping.
120 ///
121 /// # Panics
122 ///
123 /// Panics if `app` is `None` (i.e., the field is unnamed).
124 ///
125 /// # Examples
126 ///
127 /// ```
128 /// use toasty_core::schema::app::FieldName;
129 ///
130 /// let name = FieldName {
131 /// app: Some("user_name".to_string()),
132 /// storage: None,
133 /// };
134 /// assert_eq!(name.app_unwrap(), "user_name");
135 /// ```
136 #[track_caller]
137 pub fn app_unwrap(&self) -> &str {
138 self.app.as_deref().unwrap()
139 }
140
141 /// Returns the storage (database column) name for this field, if one can
142 /// be determined.
143 ///
144 /// Returns `storage` if set, otherwise falls back to `app`. Returns `None`
145 /// only when both fields are `None`.
146 pub fn storage_name(&self) -> Option<&str> {
147 self.storage.as_deref().or(self.app.as_deref())
148 }
149
150 /// Returns the storage (database column) name for this field.
151 ///
152 /// This is a convenience wrapper around [`storage_name`](FieldName::storage_name)
153 /// for callers that expect a name to always be present.
154 ///
155 /// # Panics
156 ///
157 /// Panics if both `storage` and `app` are `None`.
158 pub fn storage_name_unwrap(&self) -> &str {
159 self.storage_name()
160 .expect("must specify app name or storage name")
161 }
162}
163
164impl fmt::Display for FieldName {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(self.app.as_deref().unwrap_or("<unnamed>"))
167 }
168}
169
170/// The type of a [`Field`], distinguishing primitives, embedded types, and
171/// relation variants.
172///
173/// # Examples
174///
175/// ```
176/// use toasty_core::schema::app::{FieldPrimitive, FieldTy};
177/// use toasty_core::stmt::Type;
178///
179/// let ty = FieldTy::Primitive(FieldPrimitive {
180/// ty: Type::String,
181/// storage_ty: None,
182/// serialize: None,
183/// });
184/// assert!(ty.is_primitive());
185/// assert!(!ty.is_relation());
186/// ```
187#[derive(Clone)]
188pub enum FieldTy {
189 /// A primitive (scalar) field backed by a single column.
190 Primitive(FieldPrimitive),
191 /// An embedded struct or enum, flattened into the parent table.
192 Embedded(Embedded),
193 /// The owning side of a relationship (stores the foreign key).
194 BelongsTo(BelongsTo),
195 /// The inverse side of a one-to-many relationship.
196 HasMany(HasMany),
197 /// The inverse side of a one-to-one relationship.
198 HasOne(HasOne),
199}
200
201impl Field {
202 /// Returns this field's [`FieldId`].
203 pub fn id(&self) -> FieldId {
204 self.id
205 }
206
207 /// Returns a reference to this field's [`FieldName`].
208 pub fn name(&self) -> &FieldName {
209 &self.name
210 }
211
212 /// Returns a reference to this field's [`FieldTy`].
213 pub fn ty(&self) -> &FieldTy {
214 &self.ty
215 }
216
217 /// Returns `true` if this field is nullable.
218 pub fn nullable(&self) -> bool {
219 self.nullable
220 }
221
222 /// Returns `true` if this field is part of the primary key.
223 pub fn primary_key(&self) -> bool {
224 self.primary_key
225 }
226
227 /// Returns the auto-population strategy, if one is configured.
228 pub fn auto(&self) -> Option<&AutoStrategy> {
229 self.auto.as_ref()
230 }
231
232 /// Returns `true` if this field uses auto-increment for value generation.
233 pub fn is_auto_increment(&self) -> bool {
234 self.auto().map(|auto| auto.is_increment()).unwrap_or(false)
235 }
236
237 /// Returns `true` if this field is a relation (`BelongsTo`, `HasMany`, or
238 /// `HasOne`).
239 pub fn is_relation(&self) -> bool {
240 self.ty.is_relation()
241 }
242
243 /// Returns a fully qualified name for the field.
244 pub fn full_name(&self, schema: &Schema) -> Option<String> {
245 self.name.app.as_ref().map(|app_name| {
246 let model = schema.model(self.id.model);
247 format!("{}::{}", model.name().upper_camel_case(), app_name)
248 })
249 }
250
251 /// If the field is a relation, return the relation's target ModelId.
252 pub fn relation_target_id(&self) -> Option<ModelId> {
253 match &self.ty {
254 FieldTy::BelongsTo(belongs_to) => Some(belongs_to.target),
255 FieldTy::HasMany(has_many) => Some(has_many.target),
256 _ => None,
257 }
258 }
259
260 /// If the field is a relation, return the target of the relation.
261 pub fn relation_target<'a>(&self, schema: &'a Schema) -> Option<&'a Model> {
262 self.relation_target_id().map(|id| schema.model(id))
263 }
264
265 /// Returns the expression type this field evaluates to.
266 ///
267 /// For primitives this is the scalar type; for relations and embedded types
268 /// it is the type visible to the application layer.
269 pub fn expr_ty(&self) -> &stmt::Type {
270 match &self.ty {
271 FieldTy::Primitive(primitive) => &primitive.ty,
272 FieldTy::Embedded(embedded) => &embedded.expr_ty,
273 FieldTy::BelongsTo(belongs_to) => &belongs_to.expr_ty,
274 FieldTy::HasMany(has_many) => &has_many.expr_ty,
275 FieldTy::HasOne(has_one) => &has_one.expr_ty,
276 }
277 }
278
279 /// Returns the paired relation field, if this field is a relation.
280 ///
281 /// For `BelongsTo` this returns the inverse `HasMany`/`HasOne` (if linked).
282 /// For `HasMany` and `HasOne` this returns the paired `BelongsTo`.
283 /// Returns `None` for primitive and embedded fields.
284 pub fn pair(&self) -> Option<FieldId> {
285 match &self.ty {
286 FieldTy::Primitive(_) => None,
287 FieldTy::Embedded(_) => None,
288 FieldTy::BelongsTo(belongs_to) => belongs_to.pair,
289 FieldTy::HasMany(has_many) => Some(has_many.pair),
290 FieldTy::HasOne(has_one) => Some(has_one.pair),
291 }
292 }
293
294 pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
295 if let FieldTy::Primitive(primitive) = &self.ty
296 && let Some(storage_ty) = &primitive.storage_ty
297 {
298 storage_ty.verify(db)?;
299 }
300
301 Ok(())
302 }
303}
304
305impl FieldTy {
306 /// Returns `true` if this is a [`FieldTy::Primitive`].
307 pub fn is_primitive(&self) -> bool {
308 matches!(self, Self::Primitive(..))
309 }
310
311 /// Returns the inner [`FieldPrimitive`] if this is a primitive field.
312 pub fn as_primitive(&self) -> Option<&FieldPrimitive> {
313 match self {
314 Self::Primitive(primitive) => Some(primitive),
315 _ => None,
316 }
317 }
318
319 /// Returns the inner [`FieldPrimitive`], panicking if this is not a
320 /// primitive field.
321 ///
322 /// # Panics
323 ///
324 /// Panics if `self` is not [`FieldTy::Primitive`].
325 #[track_caller]
326 pub fn as_primitive_unwrap(&self) -> &FieldPrimitive {
327 match self {
328 Self::Primitive(simple) => simple,
329 _ => panic!("expected simple field, but was {self:?}"),
330 }
331 }
332
333 /// Returns a mutable reference to the inner [`FieldPrimitive`], panicking
334 /// if this is not a primitive field.
335 ///
336 /// # Panics
337 ///
338 /// Panics if `self` is not [`FieldTy::Primitive`].
339 #[track_caller]
340 pub fn as_primitive_mut_unwrap(&mut self) -> &mut FieldPrimitive {
341 match self {
342 Self::Primitive(simple) => simple,
343 _ => panic!("expected simple field, but was {self:?}"),
344 }
345 }
346
347 /// Returns `true` if this is a [`FieldTy::Embedded`].
348 pub fn is_embedded(&self) -> bool {
349 matches!(self, Self::Embedded(..))
350 }
351
352 /// Returns the inner [`Embedded`] if this is an embedded field.
353 pub fn as_embedded(&self) -> Option<&Embedded> {
354 match self {
355 Self::Embedded(embedded) => Some(embedded),
356 _ => None,
357 }
358 }
359
360 /// Returns the inner [`Embedded`], panicking if this is not an embedded
361 /// field.
362 ///
363 /// # Panics
364 ///
365 /// Panics if `self` is not [`FieldTy::Embedded`].
366 #[track_caller]
367 pub fn as_embedded_unwrap(&self) -> &Embedded {
368 match self {
369 Self::Embedded(embedded) => embedded,
370 _ => panic!("expected embedded field, but was {self:?}"),
371 }
372 }
373
374 /// Returns a mutable reference to the inner [`Embedded`], panicking if
375 /// this is not an embedded field.
376 ///
377 /// # Panics
378 ///
379 /// Panics if `self` is not [`FieldTy::Embedded`].
380 #[track_caller]
381 pub fn as_embedded_mut_unwrap(&mut self) -> &mut Embedded {
382 match self {
383 Self::Embedded(embedded) => embedded,
384 _ => panic!("expected embedded field, but was {self:?}"),
385 }
386 }
387
388 /// Returns `true` if this is a relation type (`BelongsTo`, `HasMany`, or
389 /// `HasOne`).
390 pub fn is_relation(&self) -> bool {
391 matches!(
392 self,
393 Self::BelongsTo(..) | Self::HasMany(..) | Self::HasOne(..)
394 )
395 }
396
397 /// Returns `true` if this is a `HasMany` or `HasOne` relation.
398 pub fn is_has_n(&self) -> bool {
399 matches!(self, Self::HasMany(..) | Self::HasOne(..))
400 }
401
402 /// Returns `true` if this is a [`FieldTy::HasMany`].
403 pub fn is_has_many(&self) -> bool {
404 matches!(self, Self::HasMany(..))
405 }
406
407 /// Returns the inner [`HasMany`] if this is a has-many field.
408 pub fn as_has_many(&self) -> Option<&HasMany> {
409 match self {
410 Self::HasMany(has_many) => Some(has_many),
411 _ => None,
412 }
413 }
414
415 /// Returns the inner [`HasMany`], panicking if this is not a has-many
416 /// field.
417 ///
418 /// # Panics
419 ///
420 /// Panics if `self` is not [`FieldTy::HasMany`].
421 #[track_caller]
422 pub fn as_has_many_unwrap(&self) -> &HasMany {
423 match self {
424 Self::HasMany(has_many) => has_many,
425 _ => panic!("expected field to be `HasMany`, but was {self:?}"),
426 }
427 }
428
429 /// Returns a mutable reference to the inner [`HasMany`], panicking if
430 /// this is not a has-many field.
431 ///
432 /// # Panics
433 ///
434 /// Panics if `self` is not [`FieldTy::HasMany`].
435 #[track_caller]
436 pub fn as_has_many_mut_unwrap(&mut self) -> &mut HasMany {
437 match self {
438 Self::HasMany(has_many) => has_many,
439 _ => panic!("expected field to be `HasMany`, but was {self:?}"),
440 }
441 }
442
443 /// Returns the inner [`HasOne`] if this is a has-one field.
444 pub fn as_has_one(&self) -> Option<&HasOne> {
445 match self {
446 Self::HasOne(has_one) => Some(has_one),
447 _ => None,
448 }
449 }
450
451 /// Returns `true` if this is a [`FieldTy::HasOne`].
452 pub fn is_has_one(&self) -> bool {
453 matches!(self, Self::HasOne(..))
454 }
455
456 /// Returns the inner [`HasOne`], panicking if this is not a has-one field.
457 ///
458 /// # Panics
459 ///
460 /// Panics if `self` is not [`FieldTy::HasOne`].
461 #[track_caller]
462 pub fn as_has_one_unwrap(&self) -> &HasOne {
463 match self {
464 Self::HasOne(has_one) => has_one,
465 _ => panic!("expected field to be `HasOne`, but it was {self:?}"),
466 }
467 }
468
469 /// Returns a mutable reference to the inner [`HasOne`], panicking if
470 /// this is not a has-one field.
471 ///
472 /// # Panics
473 ///
474 /// Panics if `self` is not [`FieldTy::HasOne`].
475 #[track_caller]
476 pub fn as_has_one_mut_unwrap(&mut self) -> &mut HasOne {
477 match self {
478 Self::HasOne(has_one) => has_one,
479 _ => panic!("expected field to be `HasOne`, but it was {self:?}"),
480 }
481 }
482
483 /// Returns `true` if this is a [`FieldTy::BelongsTo`].
484 pub fn is_belongs_to(&self) -> bool {
485 matches!(self, Self::BelongsTo(..))
486 }
487
488 /// Returns the inner [`BelongsTo`] if this is a belongs-to field.
489 pub fn as_belongs_to(&self) -> Option<&BelongsTo> {
490 match self {
491 Self::BelongsTo(belongs_to) => Some(belongs_to),
492 _ => None,
493 }
494 }
495
496 /// Returns the inner [`BelongsTo`], panicking if this is not a belongs-to
497 /// field.
498 ///
499 /// # Panics
500 ///
501 /// Panics if `self` is not [`FieldTy::BelongsTo`].
502 #[track_caller]
503 pub fn as_belongs_to_unwrap(&self) -> &BelongsTo {
504 match self {
505 Self::BelongsTo(belongs_to) => belongs_to,
506 _ => panic!("expected field to be `BelongsTo`, but was {self:?}"),
507 }
508 }
509
510 /// Returns a mutable reference to the inner [`BelongsTo`], panicking if
511 /// this is not a belongs-to field.
512 ///
513 /// # Panics
514 ///
515 /// Panics if `self` is not [`FieldTy::BelongsTo`].
516 #[track_caller]
517 pub fn as_belongs_to_mut_unwrap(&mut self) -> &mut BelongsTo {
518 match self {
519 Self::BelongsTo(belongs_to) => belongs_to,
520 _ => panic!("expected field to be `BelongsTo`, but was {self:?}"),
521 }
522 }
523}
524
525impl fmt::Debug for FieldTy {
526 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
527 match self {
528 Self::Primitive(ty) => ty.fmt(fmt),
529 Self::Embedded(ty) => ty.fmt(fmt),
530 Self::BelongsTo(ty) => ty.fmt(fmt),
531 Self::HasMany(ty) => ty.fmt(fmt),
532 Self::HasOne(ty) => ty.fmt(fmt),
533 }
534 }
535}
536
537impl FieldId {
538 pub(crate) fn placeholder() -> Self {
539 Self {
540 model: ModelId::placeholder(),
541 index: usize::MAX,
542 }
543 }
544}
545
546impl From<&Self> for FieldId {
547 fn from(val: &Self) -> Self {
548 *val
549 }
550}
551
552impl From<&Field> for FieldId {
553 fn from(val: &Field) -> Self {
554 val.id
555 }
556}
557
558impl From<FieldId> for usize {
559 fn from(val: FieldId) -> Self {
560 val.index
561 }
562}
563
564impl fmt::Debug for FieldId {
565 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
566 write!(fmt, "FieldId({}/{})", self.model.0, self.index)
567 }
568}