toasty_core/schema/app/relation/has_one.rs
1use crate::{
2 schema::app::{BelongsTo, FieldTy, HasKind, Model, ModelId, Schema},
3 stmt,
4};
5
6/// The inverse side of a one-to-one relationship.
7///
8/// A `HasOne` field on model A means "A has exactly one B". A direct `HasOne`
9/// pairs with a [`BelongsTo`] field on model B that holds the foreign key; a
10/// multi-step (`via`) `HasOne` reaches B by following a path of existing
11/// relations. Which one it is is recorded in [`kind`](HasOne::kind).
12///
13/// # Examples
14///
15/// ```ignore
16/// // Given a `User` model that has one `Profile`:
17/// let has_one: &HasOne = user_field.ty.as_has_one_unwrap();
18/// let profile_model = has_one.target(&schema);
19/// let inverse = has_one.pair(&schema); // the BelongsTo on Profile
20/// ```
21#[derive(Debug, Clone)]
22pub struct HasOne {
23 /// The [`ModelId`] of the associated (target) model.
24 pub target: ModelId,
25
26 /// The expression type this field evaluates to from the application's
27 /// perspective.
28 pub expr_ty: stmt::Type,
29
30 /// How this relation reaches its target — a paired `BelongsTo`
31 /// ([`HasKind::Direct`]) or a [`Via`](super::Via) path
32 /// ([`HasKind::Via`]).
33 pub kind: HasKind,
34}
35
36impl HasOne {
37 /// Resolves the target [`Model`] from the given schema.
38 pub fn target<'a>(&self, schema: &'a Schema) -> &'a Model {
39 schema.model(self.target)
40 }
41
42 /// Resolves the paired [`BelongsTo`] relation on the target model.
43 ///
44 /// # Panics
45 ///
46 /// Panics if this is a multi-step (`via`) relation — it has no pair — or
47 /// if the paired field is not a `BelongsTo` variant.
48 pub fn pair<'a>(&self, schema: &'a Schema) -> &'a BelongsTo {
49 let pair = self
50 .kind
51 .pair_id()
52 .expect("`via` relation has no paired `BelongsTo`");
53 schema.field(pair).ty.as_belongs_to_unwrap()
54 }
55}
56
57impl From<HasOne> for FieldTy {
58 fn from(value: HasOne) -> Self {
59 Self::HasOne(value)
60 }
61}