Skip to main content

ops_rs/
structured_queries.rs

1use crate::prelude::*;
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4
5/// A hierarchical table of contents entry that can represent various Outline structures
6///
7/// This flexible model can handle:
8/// - Simple flat TOCs: title + page
9/// - Chapter-based TOCs: chapters with subsections
10/// - Part-based TOCs: parts with chapters with sections
11/// - Mixed hierarchies: any combination of the above
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct OutlineEntry {
14    /// The title/heading text (required)
15    pub title: String,
16
17    /// Page number or range (e.g., "15", "15-20", "iv", "A-1")
18    /// Optional because some entries might be section dividers
19    pub page: Option<String>,
20
21    /// Hierarchical level (0 = top level, 1 = subsection, etc.)
22    /// Helps maintain structure without complex nesting
23    pub level: u8,
24
25    /// Optional type/category for semantic meaning
26    /// Examples: "part", "chapter", "section", "appendix", "index"
27    pub entry_type: Option<String>,
28
29    /// Child entries for hierarchical structures
30    /// Empty for leaf entries
31    pub children: Vec<OutlineEntry>,
32}
33
34impl OutlineEntry {
35    pub fn new(title: String, page: Option<String>, level: u8) -> Self {
36        Self {
37            title,
38            page,
39            level,
40            entry_type: None,
41            children: Vec::new(),
42        }
43    }
44
45    pub fn with_type(mut self, entry_type: String) -> Self {
46        self.entry_type = Some(entry_type);
47        self
48    }
49
50    pub fn with_children(mut self, children: Vec<OutlineEntry>) -> Self {
51        self.children = children;
52        self
53    }
54
55    /// Add a child entry
56    pub fn add_child(&mut self, child: OutlineEntry) {
57        self.children.push(child);
58    }
59
60    /// Get all entries flattened with their hierarchical context
61    pub fn flatten(&self) -> Vec<FlatOutlineEntry> {
62        let mut result = Vec::new();
63        self.flatten_recursive(&mut result, Vec::new());
64        result
65    }
66
67    fn flatten_recursive(&self, result: &mut Vec<FlatOutlineEntry>, mut path: Vec<String>) {
68        path.push(self.title.clone());
69
70        result.push(FlatOutlineEntry {
71            title: self.title.clone(),
72            page: self.page.clone(),
73            level: self.level,
74            entry_type: self.entry_type.clone(),
75            path: path.clone(),
76        });
77
78        for child in &self.children {
79            child.flatten_recursive(result, path.clone());
80        }
81    }
82}
83
84/// A flattened representation of a Outline entry with full hierarchical path
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct FlatOutlineEntry {
87    pub title: String,
88    pub page: Option<String>,
89    pub level: u8,
90    pub entry_type: Option<String>,
91    /// Full path from root to this entry (e.g., ["Part I", "Chapter 1", "Section 1.1"])
92    pub path: Vec<String>,
93}
94
95/// Complete table of contents structure
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ListingOutline {
98    /// Document title (optional)
99    pub document_title: Option<String>,
100
101    /// Main Outline entries
102    pub entries: Vec<OutlineEntry>,
103
104    /// Extraction confidence (0.0 - 1.0)
105    pub confidence: f64,
106
107    /// Additional metadata about the Outline structure
108    pub metadata: OutlineMetadata,
109}
110
111impl ListingOutline {
112    pub fn new() -> Self {
113        Self {
114            document_title: None,
115            entries: Vec::new(),
116            confidence: 0.0,
117            metadata: OutlineMetadata::default(),
118        }
119    }
120
121    /// Get all entries as a flat list
122    pub fn flatten(&self) -> Vec<FlatOutlineEntry> {
123        self.entries
124            .iter()
125            .flat_map(|entry| entry.flatten())
126            .collect()
127    }
128
129    /// Get entries at a specific level
130    pub fn entries_at_level(&self, level: u8) -> Vec<&OutlineEntry> {
131        fn collect_at_level<'a>(
132            entries: &'a [OutlineEntry],
133            target_level: u8,
134            result: &mut Vec<&'a OutlineEntry>,
135        ) {
136            for entry in entries {
137                if entry.level == target_level {
138                    result.push(entry);
139                }
140                collect_at_level(&entry.children, target_level, result);
141            }
142        }
143
144        let mut result = Vec::new();
145        collect_at_level(&self.entries, level, &mut result);
146        result
147    }
148
149    /// Get the maximum depth of the Outline
150    pub fn max_depth(&self) -> u8 {
151        fn max_depth_recursive(entries: &[OutlineEntry]) -> u8 {
152            entries
153                .iter()
154                .map(|entry| {
155                    let child_depth = if entry.children.is_empty() {
156                        0
157                    } else {
158                        max_depth_recursive(&entry.children)
159                    };
160                    entry.level.max(child_depth)
161                })
162                .max()
163                .unwrap_or(0)
164        }
165
166        max_depth_recursive(&self.entries)
167    }
168}
169
170impl Default for ListingOutline {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176/// Metadata about the table of contents structure
177#[derive(Debug, Clone, Serialize, Deserialize, Default)]
178pub struct OutlineMetadata {
179    /// Detected Outline style (e.g., "numeric", "roman", "alphabetic", "mixed")
180    pub numbering_style: Option<String>,
181
182    /// Whether the Outline uses dots or other leaders
183    pub has_leaders: bool,
184
185    /// Page numbering style (e.g., "arabic", "roman", "mixed")
186    pub page_style: Option<String>,
187
188    /// Total number of entries
189    pub total_entries: usize,
190
191    /// Number of hierarchical levels
192    pub levels: u8,
193
194    /// Detected structure type (e.g., "chapters", "parts_chapters", "sections")
195    pub structure_type: Option<String>,
196}
197
198/// JSON Schema generation for table of contents
199pub fn generate_outline_schema() -> serde_json::Value {
200    json!({
201        "$schema": "http://json-schema.org/draft-07/schema#",
202        "title": "Table of Contents",
203        "description": "Hierarchical table of contents structure that can represent various Outline formats",
204        "type": "object",
205        "properties": {
206            "document_title": {
207                "type": ["string", "null"],
208                "description": "Title of the document (optional)"
209            },
210            "entries": {
211                "type": "array",
212                "description": "Main table of contents entries",
213                "items": {
214                    "$ref": "#/definitions/OutlineEntry"
215                }
216            },
217            "confidence": {
218                "type": "number",
219                "minimum": 0.0,
220                "maximum": 1.0,
221                "description": "Confidence level of the extraction (0.0 - 1.0)"
222            },
223            "metadata": {
224                "$ref": "#/definitions/OutlineMetadata"
225            }
226        },
227        "required": ["entries", "confidence"],
228        "definitions": {
229            "OutlineEntry": {
230                "type": "object",
231                "description": "A single table of contents entry with optional hierarchy",
232                "properties": {
233                    "title": {
234                        "type": "string",
235                        "description": "The heading or title text"
236                    },
237                    "page": {
238                        "type": ["string", "null"],
239                        "description": "Page number or range (e.g., '15', '15-20', 'iv', 'A-1')"
240                    },
241                    "level": {
242                        "type": "integer",
243                        "minimum": 0,
244                        "maximum": 10,
245                        "description": "Hierarchical level (0 = top level, 1 = subsection, etc.)"
246                    },
247                    "entry_type": {
248                        "type": ["string", "null"],
249                        "description": "Optional semantic type (e.g., 'part', 'chapter', 'section', 'appendix')",
250                        "enum": ["part", "chapter", "section", "subsection", "appendix", "index", "bibliography", "preface", "introduction", "conclusion", null]
251                    },
252                    "children": {
253                        "type": "array",
254                        "description": "Child entries for hierarchical structures",
255                        "items": {
256                            "$ref": "#/definitions/OutlineEntry"
257                        }
258                    }
259                },
260                "required": ["title", "level"]
261            },
262            "OutlineMetadata": {
263                "type": "object",
264                "description": "Metadata about the table of contents structure",
265                "properties": {
266                    "numbering_style": {
267                        "type": ["string", "null"],
268                        "description": "Detected numbering style",
269                        "enum": ["numeric", "roman", "alphabetic", "mixed", null]
270                    },
271                    "has_leaders": {
272                        "type": "boolean",
273                        "description": "Whether the Outline uses dots or other leaders"
274                    },
275                    "page_style": {
276                        "type": ["string", "null"],
277                        "description": "Page numbering style",
278                        "enum": ["arabic", "roman", "alphabetic", "mixed", null]
279                    },
280                    "total_entries": {
281                        "type": "integer",
282                        "minimum": 0,
283                        "description": "Total number of entries"
284                    },
285                    "levels": {
286                        "type": "integer",
287                        "minimum": 1,
288                        "maximum": 10,
289                        "description": "Number of hierarchical levels"
290                    },
291                    "structure_type": {
292                        "type": ["string", "null"],
293                        "description": "Detected overall structure type",
294                        "enum": ["flat", "chapters", "parts_chapters", "sections", "mixed", null]
295                    }
296                },
297                "required": ["has_leaders", "total_entries", "levels"]
298            }
299        }
300    })
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    // TEST0024: Build a flat ListingOutline with depth-0 entries and verify max_depth, levels, and flatten count
308    #[test]
309    fn test0024_simple_flat_outline() {
310        let mut outline = ListingOutline::new();
311        outline.entries = vec![
312            OutlineEntry::new("Introduction".to_string(), Some("1".to_string()), 0),
313            OutlineEntry::new(
314                "Chapter 1: Getting Started".to_string(),
315                Some("5".to_string()),
316                0,
317            ),
318            OutlineEntry::new(
319                "Chapter 2: Advanced Topics".to_string(),
320                Some("15".to_string()),
321                0,
322            ),
323            OutlineEntry::new("Conclusion".to_string(), Some("25".to_string()), 0),
324        ];
325
326        assert_eq!(outline.max_depth(), 0);
327        assert_eq!(outline.entries_at_level(0).len(), 4);
328        assert_eq!(outline.flatten().len(), 4);
329    }
330
331    // TEST0025: Build a two-level outline with chapters and sections and verify depth, level counts, and flatten
332    #[test]
333    fn test0025_hierarchical_outline() {
334        let mut outline = ListingOutline::new();
335
336        let mut chapter1 =
337            OutlineEntry::new("Chapter 1: Basics".to_string(), Some("10".to_string()), 0)
338                .with_type("chapter".to_string());
339        chapter1.add_child(OutlineEntry::new(
340            "1.1 Introduction".to_string(),
341            Some("10".to_string()),
342            1,
343        ));
344        chapter1.add_child(OutlineEntry::new(
345            "1.2 Fundamentals".to_string(),
346            Some("15".to_string()),
347            1,
348        ));
349
350        let mut chapter2 =
351            OutlineEntry::new("Chapter 2: Advanced".to_string(), Some("20".to_string()), 0)
352                .with_type("chapter".to_string());
353        chapter2.add_child(OutlineEntry::new(
354            "2.1 Complex Topics".to_string(),
355            Some("20".to_string()),
356            1,
357        ));
358
359        outline.entries = vec![chapter1, chapter2];
360
361        assert_eq!(outline.max_depth(), 1);
362        assert_eq!(outline.entries_at_level(0).len(), 2);
363        assert_eq!(outline.entries_at_level(1).len(), 3);
364        assert_eq!(outline.flatten().len(), 5); // 2 chapters + 3 sections
365    }
366
367    // TEST0026: Build a three-level part/chapter/section outline and verify depth and per-level entry counts
368    #[test]
369    fn test0026_complex_part_based_outline() {
370        let mut outline = ListingOutline::new();
371
372        // Part I with chapters
373        let mut part1 =
374            OutlineEntry::new("Part I: Foundations".to_string(), Some("1".to_string()), 0)
375                .with_type("part".to_string());
376
377        let mut chapter1 = OutlineEntry::new(
378            "Chapter 1: Introduction".to_string(),
379            Some("3".to_string()),
380            1,
381        )
382        .with_type("chapter".to_string());
383        chapter1.add_child(OutlineEntry::new(
384            "1.1 Overview".to_string(),
385            Some("3".to_string()),
386            2,
387        ));
388        chapter1.add_child(OutlineEntry::new(
389            "1.2 Scope".to_string(),
390            Some("5".to_string()),
391            2,
392        ));
393
394        let chapter2 = OutlineEntry::new(
395            "Chapter 2: Background".to_string(),
396            Some("8".to_string()),
397            1,
398        )
399        .with_type("chapter".to_string());
400
401        part1.add_child(chapter1);
402        part1.add_child(chapter2);
403
404        // Part II
405        let part2 = OutlineEntry::new(
406            "Part II: Applications".to_string(),
407            Some("15".to_string()),
408            0,
409        )
410        .with_type("part".to_string());
411
412        outline.entries = vec![part1, part2];
413
414        assert_eq!(outline.max_depth(), 2);
415        assert_eq!(outline.entries_at_level(0).len(), 2); // 2 parts
416        assert_eq!(outline.entries_at_level(1).len(), 2); // 2 chapters
417        assert_eq!(outline.entries_at_level(2).len(), 2); // 2 sections
418        assert_eq!(outline.flatten().len(), 6); // 2 parts + 2 chapters + 2 sections
419    }
420
421    // TEST0027: Flatten a nested outline and verify each entry's path reflects its ancestry correctly
422    #[test]
423    fn test0027_flatten_preserves_hierarchy() {
424        let mut outline = ListingOutline::new();
425
426        let mut part = OutlineEntry::new("Part I".to_string(), Some("1".to_string()), 0);
427        let mut chapter = OutlineEntry::new("Chapter 1".to_string(), Some("3".to_string()), 1);
428        chapter.add_child(OutlineEntry::new(
429            "Section 1.1".to_string(),
430            Some("3".to_string()),
431            2,
432        ));
433        part.add_child(chapter);
434        outline.entries = vec![part];
435
436        let flat = outline.flatten();
437        assert_eq!(flat.len(), 3);
438
439        // Check paths
440        assert_eq!(flat[0].path, vec!["Part I"]);
441        assert_eq!(flat[1].path, vec!["Part I", "Chapter 1"]);
442        assert_eq!(flat[2].path, vec!["Part I", "Chapter 1", "Section 1.1"]);
443    }
444
445    // TEST0028: Call generate_outline_schema and verify the returned JSON contains all required definitions
446    #[test]
447    fn test0028_schema_generation() {
448        let schema = generate_outline_schema();
449        assert!(schema.is_object());
450        assert!(schema["properties"]["entries"].is_object());
451        assert!(schema["definitions"]["OutlineEntry"].is_object());
452        assert!(schema["definitions"]["OutlineMetadata"].is_object());
453    }
454}