tensorlogic_adapters/builder.rs
1//! Builder patterns for convenient schema construction.
2//!
3//! This module provides fluent builder APIs for constructing symbol tables,
4//! making it easier to programmatically create complex schemas.
5
6use anyhow::{bail, Result};
7
8use crate::{DomainHierarchy, DomainInfo, Metadata, PredicateInfo, SymbolTable};
9
10/// Builder for constructing SymbolTable instances.
11///
12/// Provides a fluent API for building schemas step by step.
13///
14/// # Example
15///
16/// ```rust
17/// use tensorlogic_adapters::SchemaBuilder;
18///
19/// let table = SchemaBuilder::new()
20/// .domain("Person", 100)
21/// .domain("Location", 50)
22/// .predicate("at", vec!["Person", "Location"])
23/// .build()
24/// .unwrap();
25///
26/// assert_eq!(table.domains.len(), 2);
27/// assert_eq!(table.predicates.len(), 1);
28/// ```
29#[derive(Clone, Debug, Default)]
30pub struct SchemaBuilder {
31 domains: Vec<DomainInfo>,
32 predicates: Vec<PredicateInfo>,
33 variables: Vec<(String, String)>,
34 hierarchy: Option<DomainHierarchy>,
35}
36
37impl SchemaBuilder {
38 /// Create a new schema builder.
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 /// Add a domain with the given name and cardinality.
44 ///
45 /// # Example
46 ///
47 /// ```rust
48 /// use tensorlogic_adapters::SchemaBuilder;
49 ///
50 /// let builder = SchemaBuilder::new()
51 /// .domain("Person", 100)
52 /// .domain("Location", 50);
53 /// ```
54 pub fn domain(mut self, name: impl Into<String>, cardinality: usize) -> Self {
55 self.domains.push(DomainInfo::new(name.into(), cardinality));
56 self
57 }
58
59 /// Add a domain with description.
60 ///
61 /// # Example
62 ///
63 /// ```rust
64 /// use tensorlogic_adapters::SchemaBuilder;
65 ///
66 /// let builder = SchemaBuilder::new()
67 /// .domain_with_desc("Person", 100, "Human entities");
68 /// ```
69 pub fn domain_with_desc(
70 mut self,
71 name: impl Into<String>,
72 cardinality: usize,
73 description: impl Into<String>,
74 ) -> Self {
75 self.domains
76 .push(DomainInfo::new(name.into(), cardinality).with_description(description.into()));
77 self
78 }
79
80 /// Add a domain with metadata.
81 ///
82 /// # Example
83 ///
84 /// ```rust
85 /// use tensorlogic_adapters::{SchemaBuilder, Metadata};
86 ///
87 /// let mut meta = Metadata::new();
88 /// meta.add_tag("core");
89 ///
90 /// let builder = SchemaBuilder::new()
91 /// .domain_with_metadata("Person", 100, meta);
92 /// ```
93 pub fn domain_with_metadata(
94 mut self,
95 name: impl Into<String>,
96 cardinality: usize,
97 metadata: Metadata,
98 ) -> Self {
99 self.domains
100 .push(DomainInfo::new(name.into(), cardinality).with_metadata(metadata));
101 self
102 }
103
104 /// Add a predicate with the given name and argument domains.
105 ///
106 /// # Example
107 ///
108 /// ```rust
109 /// use tensorlogic_adapters::SchemaBuilder;
110 ///
111 /// let builder = SchemaBuilder::new()
112 /// .domain("Person", 100)
113 /// .predicate("knows", vec!["Person", "Person"]);
114 /// ```
115 pub fn predicate<S: Into<String>>(
116 mut self,
117 name: impl Into<String>,
118 arg_domains: Vec<S>,
119 ) -> Self {
120 let domains: Vec<String> = arg_domains.into_iter().map(|s| s.into()).collect();
121 self.predicates
122 .push(PredicateInfo::new(name.into(), domains));
123 self
124 }
125
126 /// Add a predicate with description.
127 ///
128 /// # Example
129 ///
130 /// ```rust
131 /// use tensorlogic_adapters::SchemaBuilder;
132 ///
133 /// let builder = SchemaBuilder::new()
134 /// .domain("Person", 100)
135 /// .domain("Location", 50)
136 /// .predicate_with_desc("at", vec!["Person", "Location"], "Person at location");
137 /// ```
138 pub fn predicate_with_desc<S: Into<String>>(
139 mut self,
140 name: impl Into<String>,
141 arg_domains: Vec<S>,
142 description: impl Into<String>,
143 ) -> Self {
144 let domains: Vec<String> = arg_domains.into_iter().map(|s| s.into()).collect();
145 self.predicates
146 .push(PredicateInfo::new(name.into(), domains).with_description(description.into()));
147 self
148 }
149
150 /// Bind a variable to a domain.
151 ///
152 /// # Example
153 ///
154 /// ```rust
155 /// use tensorlogic_adapters::SchemaBuilder;
156 ///
157 /// let builder = SchemaBuilder::new()
158 /// .domain("Person", 100)
159 /// .variable("x", "Person")
160 /// .variable("y", "Person");
161 /// ```
162 pub fn variable(mut self, var: impl Into<String>, domain: impl Into<String>) -> Self {
163 self.variables.push((var.into(), domain.into()));
164 self
165 }
166
167 /// Add a domain hierarchy relationship.
168 ///
169 /// # Example
170 ///
171 /// ```rust
172 /// use tensorlogic_adapters::SchemaBuilder;
173 ///
174 /// let builder = SchemaBuilder::new()
175 /// .domain("Agent", 200)
176 /// .domain("Person", 100)
177 /// .subtype("Person", "Agent");
178 /// ```
179 pub fn subtype(mut self, subtype: impl Into<String>, supertype: impl Into<String>) -> Self {
180 if self.hierarchy.is_none() {
181 self.hierarchy = Some(DomainHierarchy::new());
182 }
183
184 if let Some(hierarchy) = &mut self.hierarchy {
185 hierarchy.add_subtype(subtype.into(), supertype.into());
186 }
187
188 self
189 }
190
191 /// Build the final SymbolTable.
192 ///
193 /// # Errors
194 ///
195 /// Returns an error if:
196 /// - A predicate references an undefined domain
197 /// - A variable is bound to an undefined domain
198 /// - The domain hierarchy is cyclic
199 ///
200 /// # Example
201 ///
202 /// ```rust
203 /// use tensorlogic_adapters::SchemaBuilder;
204 ///
205 /// let result = SchemaBuilder::new()
206 /// .domain("Person", 100)
207 /// .predicate("person", vec!["Person"])
208 /// .build();
209 ///
210 /// assert!(result.is_ok());
211 /// ```
212 pub fn build(self) -> Result<SymbolTable> {
213 let mut table = SymbolTable::new();
214
215 // Add all domains first
216 for domain in self.domains {
217 table.add_domain(domain)?;
218 }
219
220 // Add predicates (will validate domain references)
221 for predicate in self.predicates {
222 table.add_predicate(predicate)?;
223 }
224
225 // Bind variables (will validate domain references)
226 for (var, domain) in self.variables {
227 table.bind_variable(var, domain)?;
228 }
229
230 // Note: Hierarchy support is a future enhancement
231 // This would require adding a hierarchy field to SymbolTable
232 // For now, hierarchy information is validated but not stored
233 if let Some(hierarchy) = &self.hierarchy {
234 // Validate that all referenced domains exist
235 for domain in hierarchy.all_domains() {
236 if !table.domains.contains_key(&domain) {
237 bail!("Hierarchy references unknown domain: {}", domain);
238 }
239 }
240 // Validate hierarchy is acyclic
241 hierarchy.validate_acyclic()?;
242 // Future: Store hierarchy in table once SymbolTable supports it
243 }
244
245 Ok(table)
246 }
247
248 /// Build and validate the schema.
249 ///
250 /// This performs additional validation beyond the basic build.
251 ///
252 /// # Example
253 ///
254 /// ```rust
255 /// use tensorlogic_adapters::SchemaBuilder;
256 ///
257 /// let result = SchemaBuilder::new()
258 /// .domain("Person", 100)
259 /// .predicate("knows", vec!["Person", "Person"])
260 /// .build_and_validate();
261 ///
262 /// assert!(result.is_ok());
263 /// ```
264 pub fn build_and_validate(self) -> Result<SymbolTable> {
265 let table = self.build()?;
266
267 // Run validation
268 let validator = crate::SchemaValidator::new(&table);
269 let report = validator.validate()?;
270
271 if !report.errors.is_empty() {
272 anyhow::bail!("Schema validation failed: {}", report.errors.join(", "));
273 }
274
275 Ok(table)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_basic_builder() {
285 let table = SchemaBuilder::new()
286 .domain("Person", 100)
287 .domain("Location", 50)
288 .predicate("at", vec!["Person", "Location"])
289 .build()
290 .unwrap();
291
292 assert_eq!(table.domains.len(), 2);
293 assert_eq!(table.predicates.len(), 1);
294 assert!(table.get_domain("Person").is_some());
295 assert!(table.get_domain("Location").is_some());
296 assert!(table.get_predicate("at").is_some());
297 }
298
299 #[test]
300 fn test_builder_with_variables() {
301 let table = SchemaBuilder::new()
302 .domain("Person", 100)
303 .variable("x", "Person")
304 .variable("y", "Person")
305 .build()
306 .unwrap();
307
308 assert_eq!(table.variables.len(), 2);
309 assert_eq!(table.get_variable_domain("x"), Some("Person"));
310 assert_eq!(table.get_variable_domain("y"), Some("Person"));
311 }
312
313 #[test]
314 fn test_builder_with_descriptions() {
315 let table = SchemaBuilder::new()
316 .domain_with_desc("Person", 100, "Human entities")
317 .predicate_with_desc("knows", vec!["Person", "Person"], "Knows relation")
318 .build()
319 .unwrap();
320
321 let domain = table.get_domain("Person").unwrap();
322 assert_eq!(domain.description.as_ref().unwrap(), "Human entities");
323
324 let predicate = table.get_predicate("knows").unwrap();
325 assert_eq!(predicate.description.as_ref().unwrap(), "Knows relation");
326 }
327
328 #[test]
329 fn test_builder_error_undefined_domain() {
330 let result = SchemaBuilder::new()
331 .predicate("knows", vec!["UndefinedDomain"])
332 .build();
333
334 assert!(result.is_err());
335 }
336
337 #[test]
338 fn test_builder_fluent_api() {
339 // Test that chaining works smoothly
340 let table = SchemaBuilder::new()
341 .domain("A", 10)
342 .domain("B", 20)
343 .domain("C", 30)
344 .predicate("p1", vec!["A"])
345 .predicate("p2", vec!["A", "B"])
346 .predicate("p3", vec!["A", "B", "C"])
347 .variable("x", "A")
348 .variable("y", "B")
349 .variable("z", "C")
350 .build()
351 .unwrap();
352
353 assert_eq!(table.domains.len(), 3);
354 assert_eq!(table.predicates.len(), 3);
355 assert_eq!(table.variables.len(), 3);
356 }
357
358 #[test]
359 fn test_builder_with_metadata() {
360 let mut meta = Metadata::new();
361 meta.add_tag("core");
362 meta.add_tag("reasoning");
363
364 let table = SchemaBuilder::new()
365 .domain_with_metadata("Person", 100, meta)
366 .build()
367 .unwrap();
368
369 let domain = table.get_domain("Person").unwrap();
370 assert!(domain.metadata.is_some());
371 let metadata = domain.metadata.as_ref().unwrap();
372 assert!(metadata.tags.contains("core"));
373 assert!(metadata.tags.contains("reasoning"));
374 }
375
376 #[test]
377 fn test_build_and_validate() {
378 // Valid schema should pass
379 let result = SchemaBuilder::new()
380 .domain("Person", 100)
381 .predicate("person", vec!["Person"])
382 .build_and_validate();
383
384 assert!(result.is_ok());
385 }
386}