Skip to main content

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}