Skip to main content

shaperail_core/
relation.rs

1use serde::{Deserialize, Serialize};
2
3/// Type of relationship between two resources.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum RelationType {
7    /// This resource has a foreign key pointing to the related resource.
8    BelongsTo,
9    /// The related resource has a foreign key pointing to this resource (many records).
10    HasMany,
11    /// The related resource has a foreign key pointing to this resource (one record).
12    HasOne,
13}
14
15impl std::fmt::Display for RelationType {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        let s = match self {
18            Self::BelongsTo => "belongs_to",
19            Self::HasMany => "has_many",
20            Self::HasOne => "has_one",
21        };
22        write!(f, "{s}")
23    }
24}
25
26/// Specification for a relationship to another resource.
27///
28/// ```yaml
29/// organization: { resource: organizations, type: belongs_to, key: org_id }
30/// ```
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct RelationSpec {
34    /// Name of the related resource.
35    pub resource: String,
36
37    /// Type of relationship.
38    #[serde(rename = "type")]
39    pub relation_type: RelationType,
40
41    /// Local foreign key field (for belongs_to).
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub key: Option<String>,
44
45    /// Foreign key on the related resource (for has_many/has_one).
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub foreign_key: Option<String>,
48}
49
50/// Specification for a database index.
51///
52/// ```yaml
53/// indexes:
54///   - fields: [org_id, role]
55///   - fields: [created_at], order: desc
56/// ```
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct IndexSpec {
60    /// Fields included in this index.
61    pub fields: Vec<String>,
62
63    /// Whether this is a unique index.
64    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
65    pub unique: bool,
66
67    /// Sort order for the index (asc/desc).
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub order: Option<String>,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn relation_type_display() {
78        assert_eq!(RelationType::BelongsTo.to_string(), "belongs_to");
79        assert_eq!(RelationType::HasMany.to_string(), "has_many");
80        assert_eq!(RelationType::HasOne.to_string(), "has_one");
81    }
82
83    #[test]
84    fn relation_type_serde() {
85        let rt: RelationType = serde_json::from_str("\"belongs_to\"").unwrap();
86        assert_eq!(rt, RelationType::BelongsTo);
87        let rt: RelationType = serde_json::from_str("\"has_many\"").unwrap();
88        assert_eq!(rt, RelationType::HasMany);
89        let rt: RelationType = serde_json::from_str("\"has_one\"").unwrap();
90        assert_eq!(rt, RelationType::HasOne);
91    }
92
93    #[test]
94    fn relation_spec_belongs_to() {
95        let json = r#"{"resource": "organizations", "type": "belongs_to", "key": "org_id"}"#;
96        let rs: RelationSpec = serde_json::from_str(json).unwrap();
97        assert_eq!(rs.resource, "organizations");
98        assert_eq!(rs.relation_type, RelationType::BelongsTo);
99        assert_eq!(rs.key.as_deref(), Some("org_id"));
100        assert!(rs.foreign_key.is_none());
101    }
102
103    #[test]
104    fn relation_spec_has_many() {
105        let json = r#"{"resource": "orders", "type": "has_many", "foreign_key": "user_id"}"#;
106        let rs: RelationSpec = serde_json::from_str(json).unwrap();
107        assert_eq!(rs.relation_type, RelationType::HasMany);
108        assert_eq!(rs.foreign_key.as_deref(), Some("user_id"));
109        assert!(rs.key.is_none());
110    }
111
112    #[test]
113    fn relation_spec_has_one() {
114        let json = r#"{"resource": "profiles", "type": "has_one", "foreign_key": "user_id"}"#;
115        let rs: RelationSpec = serde_json::from_str(json).unwrap();
116        assert_eq!(rs.relation_type, RelationType::HasOne);
117    }
118
119    #[test]
120    fn index_spec_composite() {
121        let json = r#"{"fields": ["org_id", "role"]}"#;
122        let idx: IndexSpec = serde_json::from_str(json).unwrap();
123        assert_eq!(idx.fields, vec!["org_id", "role"]);
124        assert!(!idx.unique);
125        assert!(idx.order.is_none());
126    }
127
128    #[test]
129    fn index_spec_with_order() {
130        let json = r#"{"fields": ["created_at"], "order": "desc"}"#;
131        let idx: IndexSpec = serde_json::from_str(json).unwrap();
132        assert_eq!(idx.order.as_deref(), Some("desc"));
133    }
134
135    #[test]
136    fn index_spec_unique() {
137        let json = r#"{"fields": ["email"], "unique": true}"#;
138        let idx: IndexSpec = serde_json::from_str(json).unwrap();
139        assert!(idx.unique);
140    }
141}