Skip to main content

docx_core/control/
data.rs

1use docx_store::models::{DocBlock, DocSource, RelationRecord, Symbol};
2use docx_store::schema::{
3    REL_CONTAINS,
4    REL_INHERITS,
5    REL_MEMBER_OF,
6    REL_PARAM_TYPE,
7    REL_REFERENCES,
8    REL_RETURNS,
9    REL_SEE_ALSO,
10};
11use surrealdb::Connection;
12
13use super::{ControlError, DocxControlPlane};
14
15impl<C: Connection> DocxControlPlane<C> {
16    /// Fetches a symbol by project and key.
17    ///
18    /// # Errors
19    /// Returns `ControlError` if the store query fails.
20    pub async fn get_symbol(
21        &self,
22        project_id: &str,
23        symbol_key: &str,
24    ) -> Result<Option<Symbol>, ControlError> {
25        let record = self
26            .store
27            .get_symbol_by_project(project_id, symbol_key)
28            .await?;
29        if record.is_some() {
30            return Ok(record);
31        }
32        Ok(self.store.get_symbol(symbol_key).await?)
33    }
34
35    /// Lists document blocks for a symbol, optionally scoping by ingest id.
36    ///
37    /// # Errors
38    /// Returns `ControlError` if the store query fails.
39    pub async fn list_doc_blocks(
40        &self,
41        project_id: &str,
42        symbol_key: &str,
43        ingest_id: Option<&str>,
44    ) -> Result<Vec<DocBlock>, ControlError> {
45        Ok(self
46            .store
47            .list_doc_blocks(project_id, symbol_key, ingest_id)
48            .await?)
49    }
50
51    /// Searches symbols by name.
52    ///
53    /// # Errors
54    /// Returns `ControlError` if the store query fails.
55    pub async fn search_symbols(
56        &self,
57        project_id: &str,
58        name: &str,
59        limit: usize,
60    ) -> Result<Vec<Symbol>, ControlError> {
61        Ok(self
62            .store
63            .list_symbols_by_name(project_id, name, limit)
64            .await?)
65    }
66
67    /// Searches document blocks by text.
68    ///
69    /// # Errors
70    /// Returns `ControlError` if the store query fails.
71    pub async fn search_doc_blocks(
72        &self,
73        project_id: &str,
74        text: &str,
75        limit: usize,
76    ) -> Result<Vec<DocBlock>, ControlError> {
77        Ok(self.store.search_doc_blocks(project_id, text, limit).await?)
78    }
79
80    /// Lists distinct symbol kinds for a project.
81    ///
82    /// # Errors
83    /// Returns `ControlError` if the store query fails.
84    pub async fn list_symbol_kinds(
85        &self,
86        project_id: &str,
87    ) -> Result<Vec<String>, ControlError> {
88        Ok(self.store.list_symbol_kinds(project_id).await?)
89    }
90
91    /// Lists members by scope prefix or glob pattern.
92    ///
93    /// # Errors
94    /// Returns `ControlError` if the store query fails.
95    pub async fn list_members_by_scope(
96        &self,
97        project_id: &str,
98        scope: &str,
99        limit: usize,
100    ) -> Result<Vec<Symbol>, ControlError> {
101        Ok(self
102            .store
103            .list_members_by_scope(project_id, scope, limit)
104            .await?)
105    }
106
107    /// Fetches adjacency information for a symbol, including relations and related symbols.
108    ///
109    /// # Errors
110    /// Returns `ControlError` if the store query fails.
111    pub async fn get_symbol_adjacency(
112        &self,
113        project_id: &str,
114        symbol_key: &str,
115        limit: usize,
116    ) -> Result<SymbolAdjacency, ControlError> {
117        let limit = limit.max(1);
118        let symbol = self.get_symbol(project_id, symbol_key).await?;
119        let Some(symbol) = symbol else {
120            return Ok(SymbolAdjacency::default());
121        };
122        let doc_blocks = self
123            .list_doc_blocks(project_id, symbol_key, None)
124            .await?;
125        let mut ingest_ids = doc_blocks
126            .iter()
127            .filter_map(|block| block.ingest_id.clone())
128            .collect::<Vec<_>>();
129        ingest_ids.sort();
130        ingest_ids.dedup();
131        let doc_sources = self.store.list_doc_sources(project_id, &ingest_ids).await?;
132        let symbol_id = symbol.id.clone().unwrap_or_else(|| symbol.symbol_key.clone());
133
134        let member_of = self
135            .list_relations(REL_MEMBER_OF, project_id, &symbol_id, limit)
136            .await?;
137        let contains = self
138            .list_relations(REL_CONTAINS, project_id, &symbol_id, limit)
139            .await?;
140        let returns = self
141            .list_relations(REL_RETURNS, project_id, &symbol_id, limit)
142            .await?;
143        let param_types = self
144            .list_relations(REL_PARAM_TYPE, project_id, &symbol_id, limit)
145            .await?;
146        let see_also = self
147            .list_relations(REL_SEE_ALSO, project_id, &symbol_id, limit)
148            .await?;
149        let inherits = self
150            .list_relations(REL_INHERITS, project_id, &symbol_id, limit)
151            .await?;
152        let references = self
153            .list_relations(REL_REFERENCES, project_id, &symbol_id, limit)
154            .await?;
155
156        let mut related_symbols = Vec::new();
157        for relation in member_of
158            .iter()
159            .chain(contains.iter())
160            .chain(returns.iter())
161            .chain(param_types.iter())
162            .chain(see_also.iter())
163            .chain(inherits.iter())
164            .chain(references.iter())
165        {
166            if let Some(symbol_key) = record_id_to_symbol_key(&relation.in_id)
167                && let Some(found) = self.get_symbol(project_id, symbol_key).await?
168            {
169                related_symbols.push(found);
170            }
171            if let Some(symbol_key) = record_id_to_symbol_key(&relation.out_id)
172                && let Some(found) = self.get_symbol(project_id, symbol_key).await?
173            {
174                related_symbols.push(found);
175            }
176        }
177
178        related_symbols.sort_by(|left, right| left.symbol_key.cmp(&right.symbol_key));
179        related_symbols.dedup_by(|left, right| left.symbol_key == right.symbol_key);
180
181        Ok(SymbolAdjacency {
182            symbol: Some(symbol),
183            doc_blocks,
184            doc_sources,
185            member_of,
186            contains,
187            returns,
188            param_types,
189            see_also,
190            inherits,
191            references,
192            related_symbols,
193        })
194    }
195
196    async fn list_relations(
197        &self,
198        table: &str,
199        project_id: &str,
200        symbol_id: &str,
201        limit: usize,
202    ) -> Result<Vec<RelationRecord>, ControlError> {
203        let outgoing = self
204            .store
205            .list_relations_from_symbol(table, project_id, symbol_id, limit)
206            .await?;
207        let incoming = self
208            .store
209            .list_relations_to_symbol(table, project_id, symbol_id, limit)
210            .await?;
211        Ok(merge_relations(outgoing, incoming))
212    }
213}
214
215/// Relation graph data for a symbol.
216#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
217pub struct SymbolAdjacency {
218    pub symbol: Option<Symbol>,
219    pub doc_blocks: Vec<DocBlock>,
220    pub doc_sources: Vec<DocSource>,
221    pub member_of: Vec<RelationRecord>,
222    pub contains: Vec<RelationRecord>,
223    pub returns: Vec<RelationRecord>,
224    pub param_types: Vec<RelationRecord>,
225    pub see_also: Vec<RelationRecord>,
226    pub inherits: Vec<RelationRecord>,
227    pub references: Vec<RelationRecord>,
228    pub related_symbols: Vec<Symbol>,
229}
230
231/// Extracts the symbol key from a table-qualified record id.
232fn record_id_to_symbol_key(record_id: &str) -> Option<&str> {
233    record_id.strip_prefix("symbol:")
234}
235
236/// Merges relation records while de-duplicating by record identity.
237fn merge_relations(
238    mut left: Vec<RelationRecord>,
239    right: Vec<RelationRecord>,
240) -> Vec<RelationRecord> {
241    let mut seen = std::collections::HashSet::new();
242    for relation in &left {
243        seen.insert(relation_key(relation));
244    }
245    for relation in right {
246        let key = relation_key(&relation);
247        if seen.insert(key) {
248            left.push(relation);
249        }
250    }
251    left
252}
253
254/// Creates a deduplication key for relation records.
255fn relation_key(relation: &RelationRecord) -> (String, String, Option<String>) {
256    (
257        relation.in_id.clone(),
258        relation.out_id.clone(),
259        relation.kind.clone(),
260    )
261}