Skip to main content

grc_20/validate/
mod.rs

1//! Semantic validation for GRC-20 edits.
2//!
3//! This module provides validation beyond structural encoding checks.
4//! Structural validation happens during decode; semantic validation
5//! requires additional context (schema, entity state).
6//!
7//! **Note:** With the per-edit typing model, type enforcement is advisory.
8//! The protocol does not enforce that a property always uses the same type
9//! across edits. Applications can use SchemaContext to opt-in to type checking.
10
11use std::collections::HashMap;
12
13use crate::error::ValidationError;
14use crate::model::{DataType, Edit, Id, Op, PropertyValue, Value};
15
16/// Schema context for semantic validation.
17///
18/// Applications can use this to register expected types for properties
19/// and validate that values match those types. This is advisory—the
20/// protocol does not enforce global type consistency.
21#[derive(Debug, Clone, Default)]
22pub struct SchemaContext {
23    /// Known property data types (advisory).
24    properties: HashMap<Id, DataType>,
25}
26
27impl SchemaContext {
28    /// Creates a new empty schema context.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Registers a property with its expected data type.
34    pub fn add_property(&mut self, id: Id, data_type: DataType) {
35        self.properties.insert(id, data_type);
36    }
37
38    /// Gets the expected data type for a property, if registered.
39    pub fn get_property_type(&self, id: &Id) -> Option<DataType> {
40        self.properties.get(id).copied()
41    }
42}
43
44/// Validates an edit against a schema context.
45///
46/// This performs semantic validation that requires context:
47/// - Value types match property data types (when registered in schema)
48///
49/// Note: Type checking is advisory. Unknown properties are allowed.
50/// Entity lifecycle (DELETED/ACTIVE) validation requires state context
51/// and is not performed here.
52pub 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
68/// Validates that property values match their declared types.
69fn 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        // Note: If property is not in schema, we allow it (might be defined elsewhere)
84    }
85    Ok(())
86}
87
88/// Validates a single value (independent of property context).
89///
90/// This checks value-level constraints like:
91/// - NaN not allowed in floats
92/// - Point bounds
93/// - Decimal normalization
94/// - Position string format
95pub fn validate_value(value: &Value) -> Option<&'static str> {
96    value.validate()
97}
98
99/// Validates a position string according to spec rules.
100///
101/// Position strings must:
102/// - Only contain characters 0-9, A-Z, a-z (62 chars)
103/// - Not exceed 64 characters
104pub 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(); // Empty schema
169
170        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], // Unknown property
179                    value: Value::Text {
180                        value: Cow::Owned("test".to_string()),
181                        language: None,
182                    },
183                }],
184                context: None,
185            })],
186        };
187
188        // Unknown properties are allowed (advisory type checking)
189        let result = validate_edit(&edit, &schema);
190        assert!(result.is_ok());
191    }
192}