aether_schema/
lib.rs

1use aether_ast::{AttributeId, PredicateId};
2use indexmap::IndexMap;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
7pub enum ValueType {
8    Bool,
9    I64,
10    U64,
11    F64,
12    String,
13    Bytes,
14    Entity,
15    List(Box<ValueType>),
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
19pub enum AttributeClass {
20    ScalarLww,
21    SetAddWins,
22    SequenceRga,
23    RefScalar,
24    RefSet,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
28pub struct AttributeSchema {
29    pub id: AttributeId,
30    pub name: String,
31    pub class: AttributeClass,
32    pub value_type: ValueType,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
36pub struct PredicateSignature {
37    pub id: PredicateId,
38    pub name: String,
39    pub fields: Vec<ValueType>,
40}
41
42#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
43pub struct Schema {
44    pub version: String,
45    pub attributes: IndexMap<AttributeId, AttributeSchema>,
46    pub predicates: IndexMap<PredicateId, PredicateSignature>,
47}
48
49impl Schema {
50    pub fn new(version: impl Into<String>) -> Self {
51        Self {
52            version: version.into(),
53            attributes: IndexMap::new(),
54            predicates: IndexMap::new(),
55        }
56    }
57
58    pub fn register_attribute(&mut self, attribute: AttributeSchema) -> Result<(), SchemaError> {
59        if self.attributes.contains_key(&attribute.id) {
60            return Err(SchemaError::DuplicateAttributeId(attribute.id));
61        }
62        if self
63            .attributes
64            .values()
65            .any(|existing| existing.name == attribute.name)
66        {
67            return Err(SchemaError::DuplicateAttributeName(attribute.name));
68        }
69        self.attributes.insert(attribute.id, attribute);
70        Ok(())
71    }
72
73    pub fn register_predicate(&mut self, predicate: PredicateSignature) -> Result<(), SchemaError> {
74        if self.predicates.contains_key(&predicate.id) {
75            return Err(SchemaError::DuplicatePredicateId(predicate.id));
76        }
77        if self
78            .predicates
79            .values()
80            .any(|existing| existing.name == predicate.name)
81        {
82            return Err(SchemaError::DuplicatePredicateName(predicate.name));
83        }
84        self.predicates.insert(predicate.id, predicate);
85        Ok(())
86    }
87
88    pub fn attribute(&self, id: &AttributeId) -> Option<&AttributeSchema> {
89        self.attributes.get(id)
90    }
91
92    pub fn predicate(&self, id: &PredicateId) -> Option<&PredicateSignature> {
93        self.predicates.get(id)
94    }
95
96    pub fn validate_predicate_arity(
97        &self,
98        id: &PredicateId,
99        arity: usize,
100    ) -> Result<(), SchemaError> {
101        let signature = self
102            .predicate(id)
103            .ok_or(SchemaError::UnknownPredicate(*id))?;
104        if signature.fields.len() != arity {
105            return Err(SchemaError::PredicateArityMismatch {
106                predicate: *id,
107                expected: signature.fields.len(),
108                actual: arity,
109            });
110        }
111        Ok(())
112    }
113}
114
115#[derive(Debug, Error)]
116pub enum SchemaError {
117    #[error("duplicate attribute id {0}")]
118    DuplicateAttributeId(AttributeId),
119    #[error("duplicate attribute name {0}")]
120    DuplicateAttributeName(String),
121    #[error("duplicate predicate id {0}")]
122    DuplicatePredicateId(PredicateId),
123    #[error("duplicate predicate name {0}")]
124    DuplicatePredicateName(String),
125    #[error("unknown attribute {0}")]
126    UnknownAttribute(AttributeId),
127    #[error("unknown predicate {0}")]
128    UnknownPredicate(PredicateId),
129    #[error("predicate {predicate} has arity {actual}, expected {expected}")]
130    PredicateArityMismatch {
131        predicate: PredicateId,
132        expected: usize,
133        actual: usize,
134    },
135}
136
137#[cfg(test)]
138mod tests {
139    use super::{
140        AttributeClass, AttributeSchema, PredicateSignature, Schema, SchemaError, ValueType,
141    };
142    use aether_ast::{AttributeId, PredicateId};
143
144    fn sample_attribute(id: u64, name: &str) -> AttributeSchema {
145        AttributeSchema {
146            id: AttributeId::new(id),
147            name: name.into(),
148            class: AttributeClass::ScalarLww,
149            value_type: ValueType::String,
150        }
151    }
152
153    fn sample_predicate(id: u64, name: &str, arity: usize) -> PredicateSignature {
154        PredicateSignature {
155            id: PredicateId::new(id),
156            name: name.into(),
157            fields: vec![ValueType::Entity; arity],
158        }
159    }
160
161    #[test]
162    fn duplicate_attribute_ids_and_names_are_rejected() {
163        let mut schema = Schema::new("v1");
164        schema
165            .register_attribute(sample_attribute(1, "task.status"))
166            .expect("register first attribute");
167
168        let duplicate_id = schema.register_attribute(sample_attribute(1, "task.owner"));
169        assert!(matches!(
170            duplicate_id,
171            Err(SchemaError::DuplicateAttributeId(id)) if id == AttributeId::new(1)
172        ));
173
174        let duplicate_name = schema.register_attribute(sample_attribute(2, "task.status"));
175        assert!(matches!(
176            duplicate_name,
177            Err(SchemaError::DuplicateAttributeName(name)) if name == "task.status"
178        ));
179    }
180
181    #[test]
182    fn duplicate_predicate_ids_and_names_are_rejected() {
183        let mut schema = Schema::new("v1");
184        schema
185            .register_predicate(sample_predicate(1, "ready", 1))
186            .expect("register first predicate");
187
188        let duplicate_id = schema.register_predicate(sample_predicate(1, "blocked", 1));
189        assert!(matches!(
190            duplicate_id,
191            Err(SchemaError::DuplicatePredicateId(id)) if id == PredicateId::new(1)
192        ));
193
194        let duplicate_name = schema.register_predicate(sample_predicate(2, "ready", 1));
195        assert!(matches!(
196            duplicate_name,
197            Err(SchemaError::DuplicatePredicateName(name)) if name == "ready"
198        ));
199    }
200
201    #[test]
202    fn predicate_lookup_and_arity_validation_report_errors() {
203        let mut schema = Schema::new("v1");
204        schema
205            .register_predicate(sample_predicate(5, "depends_on", 2))
206            .expect("register predicate");
207
208        assert_eq!(
209            schema
210                .predicate(&PredicateId::new(5))
211                .map(|predicate| predicate.name.as_str()),
212            Some("depends_on")
213        );
214        assert!(matches!(
215            schema.validate_predicate_arity(&PredicateId::new(99), 1),
216            Err(SchemaError::UnknownPredicate(id)) if id == PredicateId::new(99)
217        ));
218        assert!(matches!(
219            schema.validate_predicate_arity(&PredicateId::new(5), 1),
220            Err(SchemaError::PredicateArityMismatch {
221                predicate,
222                expected: 2,
223                actual: 1,
224            }) if predicate == PredicateId::new(5)
225        ));
226        assert!(schema
227            .validate_predicate_arity(&PredicateId::new(5), 2)
228            .is_ok());
229    }
230}