Skip to main content

shaperail_core/
schema.rs

1use crate::FieldType;
2use serde::{Deserialize, Serialize};
3
4/// Definition of a single field in a resource schema.
5///
6/// Matches the inline YAML format:
7/// ```yaml
8/// email: { type: string, format: email, unique: true, required: true }
9/// ```
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct FieldSchema {
13    /// The data type of this field.
14    #[serde(rename = "type")]
15    pub field_type: FieldType,
16
17    /// Whether this field is the primary key.
18    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
19    pub primary: bool,
20
21    /// Whether this field is auto-generated (e.g., uuid v4, timestamps).
22    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
23    pub generated: bool,
24
25    /// Whether this field is required (NOT NULL + validated on input).
26    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
27    pub required: bool,
28
29    /// Whether this field has a unique constraint.
30    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
31    pub unique: bool,
32
33    /// Whether this field is explicitly nullable.
34    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
35    pub nullable: bool,
36
37    /// Foreign key reference in `resource.field` format.
38    #[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")]
39    pub reference: Option<String>,
40
41    /// Minimum value (number) or length (string).
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub min: Option<serde_json::Value>,
44
45    /// Maximum value (number) or length (string).
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub max: Option<serde_json::Value>,
48
49    /// String format validation (email, url, uuid).
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub format: Option<String>,
52
53    /// Allowed values for enum-type fields.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub values: Option<Vec<String>>,
56
57    /// Default value for this field.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub default: Option<serde_json::Value>,
60
61    /// Whether this field contains sensitive data (redacted in logs).
62    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
63    pub sensitive: bool,
64
65    /// Whether this field is included in full-text search.
66    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
67    pub search: bool,
68
69    /// Element type for array fields.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub items: Option<String>,
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn field_schema_minimal() {
80        let json = r#"{"type": "string"}"#;
81        let fs: FieldSchema = serde_json::from_str(json).unwrap();
82        assert_eq!(fs.field_type, FieldType::String);
83        assert!(!fs.primary);
84        assert!(!fs.generated);
85        assert!(!fs.required);
86        assert!(!fs.unique);
87        assert!(!fs.nullable);
88        assert!(fs.reference.is_none());
89        assert!(fs.min.is_none());
90        assert!(fs.max.is_none());
91        assert!(fs.format.is_none());
92        assert!(fs.values.is_none());
93        assert!(fs.default.is_none());
94        assert!(!fs.sensitive);
95        assert!(!fs.search);
96        assert!(fs.items.is_none());
97    }
98
99    #[test]
100    fn field_schema_full() {
101        let json = r#"{
102            "type": "enum",
103            "primary": false,
104            "generated": false,
105            "required": true,
106            "unique": false,
107            "nullable": false,
108            "values": ["admin", "member", "viewer"],
109            "default": "member"
110        }"#;
111        let fs: FieldSchema = serde_json::from_str(json).unwrap();
112        assert_eq!(fs.field_type, FieldType::Enum);
113        assert!(fs.required);
114        assert_eq!(fs.values.as_ref().unwrap().len(), 3);
115        assert_eq!(fs.default.as_ref().unwrap(), "member");
116    }
117
118    #[test]
119    fn field_schema_with_ref() {
120        let json = r#"{"type": "uuid", "ref": "organizations.id", "required": true}"#;
121        let fs: FieldSchema = serde_json::from_str(json).unwrap();
122        assert_eq!(fs.reference.as_deref(), Some("organizations.id"));
123    }
124
125    #[test]
126    fn field_schema_serde_roundtrip() {
127        let fs = FieldSchema {
128            field_type: FieldType::String,
129            primary: false,
130            generated: false,
131            required: true,
132            unique: true,
133            nullable: false,
134            reference: None,
135            min: Some(serde_json::json!(1)),
136            max: Some(serde_json::json!(200)),
137            format: Some("email".to_string()),
138            values: None,
139            default: None,
140            sensitive: false,
141            search: true,
142            items: None,
143        };
144        let json = serde_json::to_string(&fs).unwrap();
145        let back: FieldSchema = serde_json::from_str(&json).unwrap();
146        assert_eq!(fs, back);
147    }
148}