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}