1use crate::FieldType;
2use serde::de::{self, Deserializer, MapAccess, Visitor};
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Serialize)]
19pub struct ItemsSpec {
20 #[serde(rename = "type")]
21 pub field_type: FieldType,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub min: Option<serde_json::Value>,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub max: Option<serde_json::Value>,
28
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub format: Option<String>,
31
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub values: Option<Vec<String>>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")]
36 pub reference: Option<String>,
37}
38
39impl ItemsSpec {
40 pub fn of(field_type: FieldType) -> Self {
42 Self {
43 field_type,
44 min: None,
45 max: None,
46 format: None,
47 values: None,
48 reference: None,
49 }
50 }
51}
52
53impl<'de> Deserialize<'de> for ItemsSpec {
54 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
55 struct ItemsSpecVisitor;
56
57 impl<'de> Visitor<'de> for ItemsSpecVisitor {
58 type Value = ItemsSpec;
59
60 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
61 f.write_str("a type name (e.g. \"string\") or a constraint map with `type:`")
62 }
63
64 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
65 let field_type = FieldType::deserialize(de::value::StrDeserializer::new(v))?;
66 Ok(ItemsSpec::of(field_type))
67 }
68
69 fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
70 #[derive(Deserialize)]
71 #[serde(deny_unknown_fields)]
72 struct Inner {
73 #[serde(rename = "type")]
74 field_type: FieldType,
75 #[serde(default)]
76 min: Option<serde_json::Value>,
77 #[serde(default)]
78 max: Option<serde_json::Value>,
79 #[serde(default)]
80 format: Option<String>,
81 #[serde(default)]
82 values: Option<Vec<String>>,
83 #[serde(default, rename = "ref")]
84 reference: Option<String>,
85 }
86 let inner = Inner::deserialize(de::value::MapAccessDeserializer::new(map))?;
87 Ok(ItemsSpec {
88 field_type: inner.field_type,
89 min: inner.min,
90 max: inner.max,
91 format: inner.format,
92 values: inner.values,
93 reference: inner.reference,
94 })
95 }
96 }
97
98 deserializer.deserialize_any(ItemsSpecVisitor)
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct FieldSchema {
111 #[serde(rename = "type")]
113 pub field_type: FieldType,
114
115 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
117 pub primary: bool,
118
119 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
121 pub generated: bool,
122
123 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
125 pub required: bool,
126
127 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
129 pub unique: bool,
130
131 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
133 pub nullable: bool,
134
135 #[serde(default, skip_serializing_if = "Option::is_none", rename = "ref")]
137 pub reference: Option<String>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub min: Option<serde_json::Value>,
142
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub max: Option<serde_json::Value>,
146
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub format: Option<String>,
150
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub values: Option<Vec<String>>,
154
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub default: Option<serde_json::Value>,
158
159 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
161 pub sensitive: bool,
162
163 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
165 pub search: bool,
166
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub items: Option<ItemsSpec>,
170
171 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
175 pub transient: bool,
176}
177
178impl FieldSchema {
179 pub fn is_persisted(&self) -> bool {
182 !self.transient
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn field_schema_minimal() {
192 let json = r#"{"type": "string"}"#;
193 let fs: FieldSchema = serde_json::from_str(json).unwrap();
194 assert_eq!(fs.field_type, FieldType::String);
195 assert!(!fs.primary);
196 assert!(!fs.generated);
197 assert!(!fs.required);
198 assert!(!fs.unique);
199 assert!(!fs.nullable);
200 assert!(fs.reference.is_none());
201 assert!(fs.min.is_none());
202 assert!(fs.max.is_none());
203 assert!(fs.format.is_none());
204 assert!(fs.values.is_none());
205 assert!(fs.default.is_none());
206 assert!(!fs.sensitive);
207 assert!(!fs.search);
208 assert!(fs.items.is_none());
209 assert!(!fs.transient);
210 }
211
212 #[test]
213 fn field_schema_full() {
214 let json = r#"{
215 "type": "enum",
216 "primary": false,
217 "generated": false,
218 "required": true,
219 "unique": false,
220 "nullable": false,
221 "values": ["admin", "member", "viewer"],
222 "default": "member"
223 }"#;
224 let fs: FieldSchema = serde_json::from_str(json).unwrap();
225 assert_eq!(fs.field_type, FieldType::Enum);
226 assert!(fs.required);
227 assert_eq!(fs.values.as_ref().unwrap().len(), 3);
228 assert_eq!(fs.default.as_ref().unwrap(), "member");
229 }
230
231 #[test]
232 fn field_schema_with_ref() {
233 let json = r#"{"type": "uuid", "ref": "organizations.id", "required": true}"#;
234 let fs: FieldSchema = serde_json::from_str(json).unwrap();
235 assert_eq!(fs.reference.as_deref(), Some("organizations.id"));
236 }
237
238 #[test]
239 fn field_schema_serde_roundtrip() {
240 let fs = FieldSchema {
241 field_type: FieldType::String,
242 primary: false,
243 generated: false,
244 required: true,
245 unique: true,
246 nullable: false,
247 reference: None,
248 min: Some(serde_json::json!(1)),
249 max: Some(serde_json::json!(200)),
250 format: Some("email".to_string()),
251 values: None,
252 default: None,
253 sensitive: false,
254 search: true,
255 items: None,
256 transient: false,
257 };
258 let json = serde_json::to_string(&fs).unwrap();
259 let back: FieldSchema = serde_json::from_str(&json).unwrap();
260 assert_eq!(fs, back);
261 }
262
263 #[test]
264 fn items_spec_bare_string_form() {
265 let yaml = r#"type: array
266items: string"#;
267 let fs: FieldSchema = serde_yaml::from_str(yaml).unwrap();
268 let items = fs.items.expect("items present");
269 assert_eq!(items.field_type, FieldType::String);
270 assert!(items.min.is_none());
271 assert!(items.max.is_none());
272 assert!(items.values.is_none());
273 assert!(items.reference.is_none());
274 assert!(items.format.is_none());
275 }
276
277 #[test]
278 fn items_spec_full_map_form() {
279 let yaml = r#"type: array
280items: { type: string, min: 3, max: 3 }"#;
281 let fs: FieldSchema = serde_yaml::from_str(yaml).unwrap();
282 let items = fs.items.expect("items present");
283 assert_eq!(items.field_type, FieldType::String);
284 assert_eq!(items.min, Some(serde_json::json!(3)));
285 assert_eq!(items.max, Some(serde_json::json!(3)));
286 }
287
288 #[test]
289 fn items_spec_enum_form() {
290 let yaml = r#"type: array
291items: { type: enum, values: [a, b, c] }"#;
292 let fs: FieldSchema = serde_yaml::from_str(yaml).unwrap();
293 let items = fs.items.expect("items present");
294 assert_eq!(items.field_type, FieldType::Enum);
295 assert_eq!(
296 items.values.as_deref(),
297 Some(["a".to_string(), "b".to_string(), "c".to_string()].as_slice())
298 );
299 }
300
301 #[test]
302 fn items_spec_uuid_ref_form() {
303 let yaml = r#"type: array
304items: { type: uuid, ref: organizations.id }"#;
305 let fs: FieldSchema = serde_yaml::from_str(yaml).unwrap();
306 let items = fs.items.expect("items present");
307 assert_eq!(items.field_type, FieldType::Uuid);
308 assert_eq!(items.reference.as_deref(), Some("organizations.id"));
309 }
310
311 #[test]
312 fn items_spec_unknown_field_rejected() {
313 let yaml = r#"type: array
314items: { type: string, unknown_key: 1 }"#;
315 let result: Result<FieldSchema, _> = serde_yaml::from_str(yaml);
316 assert!(
317 result.is_err(),
318 "unknown field on ItemsSpec must be rejected"
319 );
320 }
321}