1use std::collections::HashMap;
12
13use crate::error::ValidationError;
14use crate::model::{DataType, Edit, Id, Op, PropertyValue, Value};
15
16#[derive(Debug, Clone, Default)]
22pub struct SchemaContext {
23 properties: HashMap<Id, DataType>,
25}
26
27impl SchemaContext {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn add_property(&mut self, id: Id, data_type: DataType) {
35 self.properties.insert(id, data_type);
36 }
37
38 pub fn get_property_type(&self, id: &Id) -> Option<DataType> {
40 self.properties.get(id).copied()
41 }
42}
43
44pub fn validate_edit(edit: &Edit, schema: &SchemaContext) -> Result<(), ValidationError> {
53 for op in &edit.ops {
54 match op {
55 Op::CreateEntity(ce) => {
56 validate_property_values(&ce.values, schema)?;
57 }
58 Op::UpdateEntity(ue) => {
59 validate_property_values(&ue.set_properties, schema)?;
60 }
61 _ => {}
62 }
63 }
64
65 Ok(())
66}
67
68fn validate_property_values(
70 values: &[PropertyValue],
71 schema: &SchemaContext,
72) -> Result<(), ValidationError> {
73 for pv in values {
74 if let Some(expected_type) = schema.get_property_type(&pv.property) {
75 let actual_type = pv.value.data_type();
76 if expected_type != actual_type {
77 return Err(ValidationError::TypeMismatch {
78 property: pv.property,
79 expected: expected_type,
80 });
81 }
82 }
83 }
85 Ok(())
86}
87
88pub fn validate_value(value: &Value) -> Option<&'static str> {
96 value.validate()
97}
98
99pub fn validate_position(pos: &str) -> Result<(), &'static str> {
105 crate::model::validate_position(pos)
106}
107
108#[cfg(test)]
109mod tests {
110 use std::borrow::Cow;
111
112 use super::*;
113 use crate::model::CreateEntity;
114
115 #[test]
116 fn test_validate_type_mismatch() {
117 let mut schema = SchemaContext::new();
118 schema.add_property([1u8; 16], DataType::Int64);
119
120 let edit = Edit {
121 id: [0u8; 16],
122 name: Cow::Borrowed(""),
123 authors: vec![],
124 created_at: 0,
125 ops: vec![Op::CreateEntity(CreateEntity {
126 id: [2u8; 16],
127 values: vec![PropertyValue {
128 property: [1u8; 16],
129 value: Value::Text {
130 value: Cow::Owned("not an int".to_string()),
131 language: None,
132 },
133 }],
134 context: None,
135 })],
136 };
137
138 let result = validate_edit(&edit, &schema);
139 assert!(matches!(result, Err(ValidationError::TypeMismatch { .. })));
140 }
141
142 #[test]
143 fn test_validate_type_match() {
144 let mut schema = SchemaContext::new();
145 schema.add_property([1u8; 16], DataType::Int64);
146
147 let edit = Edit {
148 id: [0u8; 16],
149 name: Cow::Borrowed(""),
150 authors: vec![],
151 created_at: 0,
152 ops: vec![Op::CreateEntity(CreateEntity {
153 id: [2u8; 16],
154 values: vec![PropertyValue {
155 property: [1u8; 16],
156 value: Value::Int64 { value: 42, unit: None },
157 }],
158 context: None,
159 })],
160 };
161
162 let result = validate_edit(&edit, &schema);
163 assert!(result.is_ok());
164 }
165
166 #[test]
167 fn test_validate_unknown_property() {
168 let schema = SchemaContext::new(); let edit = Edit {
171 id: [0u8; 16],
172 name: Cow::Borrowed(""),
173 authors: vec![],
174 created_at: 0,
175 ops: vec![Op::CreateEntity(CreateEntity {
176 id: [2u8; 16],
177 values: vec![PropertyValue {
178 property: [99u8; 16], value: Value::Text {
180 value: Cow::Owned("test".to_string()),
181 language: None,
182 },
183 }],
184 context: None,
185 })],
186 };
187
188 let result = validate_edit(&edit, &schema);
190 assert!(result.is_ok());
191 }
192}