p2panda_rs/schema/system/
schema_views.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::convert::TryFrom;
4
5use crate::document::{DocumentView, DocumentViewId};
6use crate::operation::{OperationValue, PinnedRelationList};
7use crate::schema::system::SystemSchemaError;
8use crate::schema::FieldType;
9
10/// View onto materialised schema which has fields "name", "description" and "fields".
11///
12/// The fields are validated when converting a DocumentView struct into this type.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct SchemaView {
15    /// ID of this schema view.
16    id: DocumentViewId,
17
18    /// Name of this schema.
19    name: String,
20
21    /// Description of this schema.
22    description: String,
23
24    /// The fields in this schema.
25    fields: PinnedRelationList,
26}
27
28#[allow(dead_code)] // These methods aren't used yet...
29impl SchemaView {
30    /// The id of this schema view.
31    pub fn view_id(&self) -> &DocumentViewId {
32        &self.id
33    }
34
35    /// The name of this schema.
36    pub fn name(&self) -> &str {
37        &self.name
38    }
39
40    /// The description of this schema.
41    pub fn description(&self) -> &str {
42        &self.description
43    }
44
45    /// A list of fields assigned to this schema identified by their document id.
46    pub fn fields(&self) -> &PinnedRelationList {
47        &self.fields
48    }
49}
50
51impl TryFrom<DocumentView> for SchemaView {
52    type Error = SystemSchemaError;
53
54    fn try_from(document_view: DocumentView) -> Result<Self, Self::Error> {
55        if document_view.len() != 3 {
56            return Err(SystemSchemaError::AdditionalFields);
57        };
58
59        let name = match document_view.get("name") {
60            Some(document_view_value) => {
61                if let OperationValue::String(value) = document_view_value.value() {
62                    Ok(value)
63                } else {
64                    Err(SystemSchemaError::InvalidField(
65                        "name".into(),
66                        document_view_value.clone(),
67                    ))
68                }
69            }
70            None => Err(SystemSchemaError::MissingField("name".into())),
71        }?;
72
73        let description = match document_view.get("description") {
74            Some(document_view_value) => {
75                if let OperationValue::String(value) = document_view_value.value() {
76                    Ok(value)
77                } else {
78                    Err(SystemSchemaError::InvalidField(
79                        "description".into(),
80                        document_view_value.clone(),
81                    ))
82                }
83            }
84            None => Err(SystemSchemaError::MissingField("description".into())),
85        }?;
86
87        let fields = match document_view.get("fields") {
88            Some(document_view_value) => {
89                if let OperationValue::PinnedRelationList(value) = document_view_value.value() {
90                    Ok(value)
91                } else {
92                    Err(SystemSchemaError::InvalidField(
93                        "fields".into(),
94                        document_view_value.clone(),
95                    ))
96                }
97            }
98            None => Err(SystemSchemaError::MissingField("fields".into())),
99        }?;
100
101        Ok(Self {
102            id: document_view.id().clone(),
103            name: name.to_string(),
104            description: description.to_string(),
105            fields: fields.to_owned(),
106        })
107    }
108}
109
110/// View onto materialised schema field which has fields "name" and "type".
111///
112/// The fields are validated when converting a DocumentView struct into this type.
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct SchemaFieldView {
115    // Identifier of this schema field view.
116    id: DocumentViewId,
117
118    /// Name of this schema field.
119    name: String,
120
121    /// Type of this schema field.
122    field_type: FieldType,
123}
124
125#[allow(dead_code)] // These methods aren't used yet...
126impl SchemaFieldView {
127    /// The id of this schema view.
128    pub fn id(&self) -> &DocumentViewId {
129        &self.id
130    }
131
132    /// The name of this schema field.
133    pub fn name(&self) -> &str {
134        &self.name
135    }
136
137    /// The type of this schema field represented as a FieldType enum variant.
138    pub fn field_type(&self) -> &FieldType {
139        &self.field_type
140    }
141}
142
143impl TryFrom<DocumentView> for SchemaFieldView {
144    type Error = SystemSchemaError;
145
146    fn try_from(document_view: DocumentView) -> Result<Self, Self::Error> {
147        if document_view.len() != 2 {
148            return Err(SystemSchemaError::AdditionalFields);
149        };
150
151        let name = match document_view.get("name") {
152            Some(document_view_value) => {
153                if let OperationValue::String(value) = document_view_value.value() {
154                    Ok(value)
155                } else {
156                    Err(SystemSchemaError::InvalidField(
157                        "name".into(),
158                        document_view_value.clone(),
159                    ))
160                }
161            }
162            None => Err(SystemSchemaError::MissingField("name".into())),
163        }?;
164
165        let field_type = match document_view.get("type") {
166            Some(document_view_value) => {
167                if let OperationValue::String(type_str) = document_view_value.value() {
168                    // Validate the type string parses into a FieldType
169                    Ok(type_str.parse::<FieldType>()?)
170                } else {
171                    Err(SystemSchemaError::InvalidField(
172                        "type".into(),
173                        document_view_value.to_owned(),
174                    ))
175                }
176            }
177            None => Err(SystemSchemaError::MissingField("type".to_string())),
178        }?;
179
180        Ok(Self {
181            id: document_view.id().clone(),
182            name: name.to_string(),
183            field_type,
184        })
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use std::convert::TryFrom;
191
192    use rstest::rstest;
193
194    use crate::document::{DocumentView, DocumentViewFields, DocumentViewId, DocumentViewValue};
195    use crate::operation::{OperationId, OperationValue, PinnedRelationList};
196    use crate::schema::system::SchemaFieldView;
197    use crate::schema::SchemaId;
198    use crate::test_utils::fixtures::schema_id;
199    use crate::test_utils::fixtures::{document_view_id, random_operation_id};
200
201    use super::{FieldType, SchemaView};
202
203    #[rstest]
204    fn from_document_view(
205        #[from(random_operation_id)] operation_id: OperationId,
206        #[from(random_operation_id)] relation: OperationId,
207        #[from(random_operation_id)] view_id: OperationId,
208    ) {
209        let mut venue_schema = DocumentViewFields::new();
210        venue_schema.insert(
211            "name",
212            DocumentViewValue::new(
213                &operation_id,
214                &OperationValue::String("venue_name".to_string()),
215            ),
216        );
217        venue_schema.insert(
218            "description",
219            DocumentViewValue::new(
220                &operation_id,
221                &OperationValue::String("Describes a venue".to_string()),
222            ),
223        );
224        venue_schema.insert(
225            "fields",
226            DocumentViewValue::new(
227                &operation_id,
228                &OperationValue::PinnedRelationList(PinnedRelationList::new(vec![
229                    DocumentViewId::new(&[relation]),
230                ])),
231            ),
232        );
233        let document_view_id = DocumentViewId::from(view_id);
234        let document_view = DocumentView::new(&document_view_id, &venue_schema);
235
236        assert!(SchemaView::try_from(document_view).is_ok());
237    }
238
239    #[rstest]
240    fn field_type_from_document_view(
241        #[from(random_operation_id)] operation_id: OperationId,
242        document_view_id: DocumentViewId,
243        #[from(schema_id)] address_schema: SchemaId,
244    ) {
245        // Create first schema field "is_accessible"
246        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
247
248        let mut bool_field = DocumentViewFields::new();
249        bool_field.insert(
250            "name",
251            DocumentViewValue::new(
252                &operation_id,
253                &OperationValue::String("is_accessible".to_string()),
254            ),
255        );
256        bool_field.insert(
257            "type",
258            DocumentViewValue::new(&operation_id, &FieldType::Boolean.into()),
259        );
260
261        let document_view = DocumentView::new(&document_view_id, &bool_field);
262        let field_view = SchemaFieldView::try_from(document_view);
263        assert!(field_view.is_ok());
264
265        let field_view = field_view.unwrap();
266        assert_eq!(field_view.field_type(), &FieldType::Boolean);
267        assert_eq!(field_view.name(), "is_accessible");
268
269        // Create second schema field "capacity"
270        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
271
272        let mut capacity_field = DocumentViewFields::new();
273        capacity_field.insert(
274            "name",
275            DocumentViewValue::new(
276                &operation_id,
277                &OperationValue::String("capacity".to_string()),
278            ),
279        );
280        capacity_field.insert(
281            "type",
282            DocumentViewValue::new(&operation_id, &FieldType::Integer.into()),
283        );
284
285        let document_view = DocumentView::new(&document_view_id, &capacity_field);
286        let field_view = SchemaFieldView::try_from(document_view);
287        assert!(field_view.is_ok());
288        assert_eq!(field_view.unwrap().field_type(), &FieldType::Integer);
289
290        // Create third schema field "ticket_price"
291        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
292
293        let mut float_field = DocumentViewFields::new();
294        float_field.insert(
295            "name",
296            DocumentViewValue::new(
297                &operation_id,
298                &OperationValue::String("ticket_price".to_string()),
299            ),
300        );
301        float_field.insert(
302            "type",
303            DocumentViewValue::new(&operation_id, &FieldType::Float.into()),
304        );
305
306        let document_view = DocumentView::new(&document_view_id, &float_field);
307        let field_view = SchemaFieldView::try_from(document_view);
308        assert!(field_view.is_ok());
309        assert_eq!(field_view.unwrap().field_type(), &FieldType::Float);
310
311        // Create fourth schema field "venue_name"
312        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
313
314        let mut str_field = DocumentViewFields::new();
315        str_field.insert(
316            "name",
317            DocumentViewValue::new(
318                &operation_id,
319                &OperationValue::String("venue_name".to_string()),
320            ),
321        );
322        str_field.insert(
323            "type",
324            DocumentViewValue::new(&operation_id, &FieldType::String.into()),
325        );
326
327        let document_view = DocumentView::new(&document_view_id, &str_field);
328        let field_view = SchemaFieldView::try_from(document_view);
329        assert!(field_view.is_ok());
330        assert_eq!(field_view.unwrap().field_type(), &FieldType::String);
331
332        // Create fifth schema field "address"
333        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
334
335        let mut relation_field = DocumentViewFields::new();
336        relation_field.insert(
337            "name",
338            DocumentViewValue::new(
339                &operation_id,
340                &OperationValue::String("address".to_string()),
341            ),
342        );
343        relation_field.insert(
344            "type",
345            DocumentViewValue::new(
346                &operation_id,
347                &FieldType::Relation(address_schema.clone()).into(),
348            ),
349        );
350
351        let document_view = DocumentView::new(&document_view_id, &relation_field);
352        let field_view = SchemaFieldView::try_from(document_view);
353        assert!(field_view.is_ok());
354        assert_eq!(
355            field_view.unwrap().field_type(),
356            &FieldType::Relation(address_schema)
357        );
358    }
359
360    #[rstest]
361    fn invalid_schema_field(
362        #[from(random_operation_id)] operation_id: OperationId,
363        document_view_id: DocumentViewId,
364    ) {
365        let mut invalid_field = DocumentViewFields::new();
366        invalid_field.insert(
367            "name",
368            DocumentViewValue::new(
369                &operation_id,
370                &OperationValue::String("address".to_string()),
371            ),
372        );
373        invalid_field.insert(
374            "type",
375            DocumentViewValue::new(&operation_id, &OperationValue::String("hash".to_string())),
376        );
377
378        let document_view = DocumentView::new(&document_view_id, &invalid_field);
379        let field_view = SchemaFieldView::try_from(document_view);
380        assert!(field_view.is_err());
381    }
382
383    #[rstest]
384    fn too_many_fields(
385        #[from(random_operation_id)] operation_id: OperationId,
386        document_view_id: DocumentViewId,
387    ) {
388        let mut invalid_field = DocumentViewFields::new();
389        invalid_field.insert(
390            "name",
391            DocumentViewValue::new(
392                &operation_id,
393                &OperationValue::String("address".to_string()),
394            ),
395        );
396        invalid_field.insert(
397            "type",
398            DocumentViewValue::new(&operation_id, &OperationValue::String("hash".to_string())),
399        );
400        invalid_field.insert(
401            "monkey",
402            DocumentViewValue::new(&operation_id, &OperationValue::String("monkey".to_string())),
403        );
404        invalid_field.insert(
405            "penguin",
406            DocumentViewValue::new(
407                &operation_id,
408                &OperationValue::String("penguin".to_string()),
409            ),
410        );
411
412        let document_view = DocumentView::new(&document_view_id, &invalid_field);
413        let field_view = SchemaFieldView::try_from(document_view);
414        assert!(field_view.is_err());
415
416        let document_view = DocumentView::new(&document_view_id, &invalid_field);
417        let field_view = SchemaView::try_from(document_view);
418        assert!(field_view.is_err());
419    }
420}