Skip to main content

ucp_api/
lib.rs

1//! High-level API for UCP.
2
3use std::str::FromStr;
4
5use ucl_parser::{parse, parse_commands, UclDocument};
6use ucm_core::{Block, BlockId, Content, Document, EdgeType, Error, Result};
7use ucm_engine::{Engine, Operation, OperationResult};
8
9#[cfg(not(target_arch = "wasm32"))]
10pub mod codegraph;
11
12#[cfg(not(target_arch = "wasm32"))]
13pub use codegraph::{
14    build_code_graph, canonical_codegraph_json, canonical_fingerprint, codegraph_prompt_projection,
15    validate_code_graph_profile, CodeGraphBuildInput, CodeGraphBuildResult, CodeGraphBuildStatus,
16    CodeGraphDiagnostic, CodeGraphExtractorConfig, CodeGraphSeverity, CodeGraphStats,
17    CodeGraphValidationResult, PortableDocument, CODEGRAPH_EXTRACTOR_VERSION,
18    CODEGRAPH_PROFILE_MARKER, CODEGRAPH_PROFILE_VERSION,
19};
20
21/// UCP client for document manipulation
22pub struct UcpClient {
23    engine: Engine,
24}
25
26impl UcpClient {
27    pub fn new() -> Self {
28        Self {
29            engine: Engine::new(),
30        }
31    }
32
33    /// Create a new document
34    pub fn create_document(&self) -> Document {
35        Document::create()
36    }
37
38    /// Execute UCL commands on a document
39    pub fn execute_ucl(&self, doc: &mut Document, ucl: &str) -> Result<Vec<OperationResult>> {
40        let commands =
41            parse_commands(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))?;
42
43        let ops = self.commands_to_operations(commands)?;
44        self.engine.execute_batch(doc, ops)
45    }
46
47    /// Parse a full UCL document
48    pub fn parse_ucl(&self, ucl: &str) -> Result<UclDocument> {
49        parse(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))
50    }
51
52    /// Add a text block
53    pub fn add_text(
54        &self,
55        doc: &mut Document,
56        parent: &BlockId,
57        text: &str,
58        role: Option<&str>,
59    ) -> Result<BlockId> {
60        let block = Block::new(Content::text(text), role);
61        doc.add_block(block, parent)
62    }
63
64    /// Add a code block  
65    pub fn add_code(
66        &self,
67        doc: &mut Document,
68        parent: &BlockId,
69        lang: &str,
70        code: &str,
71    ) -> Result<BlockId> {
72        let block = Block::new(Content::code(lang, code), None);
73        doc.add_block(block, parent)
74    }
75
76    /// Get document as JSON
77    pub fn to_json(&self, doc: &Document) -> Result<String> {
78        // Serialize blocks
79        let blocks: Vec<_> = doc.blocks.values().collect();
80        serde_json::to_string_pretty(&blocks)
81            .map_err(|e| Error::Internal(format!("Serialization error: {}", e)))
82    }
83
84    fn commands_to_operations(&self, commands: Vec<ucl_parser::Command>) -> Result<Vec<Operation>> {
85        let mut ops = Vec::new();
86        for cmd in commands {
87            match cmd {
88                ucl_parser::Command::Edit(e) => {
89                    let block_id: BlockId = e
90                        .block_id
91                        .parse()
92                        .map_err(|_| Error::InvalidBlockId(e.block_id.clone()))?;
93                    ops.push(Operation::Edit {
94                        block_id,
95                        path: e.path.to_string(),
96                        value: e.value.to_json(),
97                        operator: match e.operator {
98                            ucl_parser::Operator::Set => ucm_engine::EditOperator::Set,
99                            ucl_parser::Operator::Append => ucm_engine::EditOperator::Append,
100                            ucl_parser::Operator::Remove => ucm_engine::EditOperator::Remove,
101                            ucl_parser::Operator::Increment => ucm_engine::EditOperator::Increment,
102                            ucl_parser::Operator::Decrement => ucm_engine::EditOperator::Decrement,
103                        },
104                    });
105                }
106                ucl_parser::Command::Append(a) => {
107                    let parent_id: BlockId = a
108                        .parent_id
109                        .parse()
110                        .map_err(|_| Error::InvalidBlockId(a.parent_id.clone()))?;
111                    let content = match a.content_type {
112                        ucl_parser::ContentType::Text => Content::text(&a.content),
113                        ucl_parser::ContentType::Code => Content::code("", &a.content),
114                        _ => Content::text(&a.content),
115                    };
116                    ops.push(Operation::Append {
117                        parent_id,
118                        content,
119                        label: a.properties.get("label").and_then(|v| match v {
120                            ucl_parser::Value::String(s) => Some(s.clone()),
121                            _ => None,
122                        }),
123                        tags: Vec::new(),
124                        semantic_role: a.properties.get("role").and_then(|v| match v {
125                            ucl_parser::Value::String(s) => Some(s.clone()),
126                            _ => None,
127                        }),
128                        index: a.index,
129                    });
130                }
131                ucl_parser::Command::Delete(d) => {
132                    if let Some(id) = d.block_id {
133                        let block_id: BlockId =
134                            id.parse().map_err(|_| Error::InvalidBlockId(id.clone()))?;
135                        ops.push(Operation::Delete {
136                            block_id,
137                            cascade: d.cascade,
138                            preserve_children: d.preserve_children,
139                        });
140                    }
141                }
142                ucl_parser::Command::Move(m) => {
143                    let block_id: BlockId = m
144                        .block_id
145                        .parse()
146                        .map_err(|_| Error::InvalidBlockId(m.block_id.clone()))?;
147                    match m.target {
148                        ucl_parser::MoveTarget::ToParent { parent_id, index } => {
149                            let new_parent: BlockId = parent_id
150                                .parse()
151                                .map_err(|_| Error::InvalidBlockId(parent_id.clone()))?;
152                            ops.push(Operation::MoveToTarget {
153                                block_id,
154                                target: ucm_engine::MoveTarget::ToParent {
155                                    parent_id: new_parent,
156                                    index,
157                                },
158                            });
159                        }
160                        ucl_parser::MoveTarget::Before { sibling_id } => {
161                            let sibling: BlockId = sibling_id
162                                .parse()
163                                .map_err(|_| Error::InvalidBlockId(sibling_id.clone()))?;
164                            ops.push(Operation::MoveToTarget {
165                                block_id,
166                                target: ucm_engine::MoveTarget::Before {
167                                    sibling_id: sibling,
168                                },
169                            });
170                        }
171                        ucl_parser::MoveTarget::After { sibling_id } => {
172                            let sibling: BlockId = sibling_id
173                                .parse()
174                                .map_err(|_| Error::InvalidBlockId(sibling_id.clone()))?;
175                            ops.push(Operation::MoveToTarget {
176                                block_id,
177                                target: ucm_engine::MoveTarget::After {
178                                    sibling_id: sibling,
179                                },
180                            });
181                        }
182                    }
183                }
184                ucl_parser::Command::Prune(p) => {
185                    let condition = match p.target {
186                        ucl_parser::PruneTarget::Unreachable => {
187                            Some(ucm_engine::PruneCondition::Unreachable)
188                        }
189                        _ => None,
190                    };
191                    ops.push(Operation::Prune { condition });
192                }
193                ucl_parser::Command::Link(l) => {
194                    let source: BlockId = l
195                        .source_id
196                        .parse()
197                        .map_err(|_| Error::InvalidBlockId(l.source_id.clone()))?;
198                    let target: BlockId = l
199                        .target_id
200                        .parse()
201                        .map_err(|_| Error::InvalidBlockId(l.target_id.clone()))?;
202                    let edge_type =
203                        EdgeType::from_str(&l.edge_type).unwrap_or(EdgeType::References);
204                    ops.push(Operation::Link {
205                        source,
206                        edge_type,
207                        target,
208                        metadata: None,
209                    });
210                }
211                ucl_parser::Command::Snapshot(s) => match s {
212                    ucl_parser::SnapshotCommand::Create { name, description } => {
213                        ops.push(Operation::CreateSnapshot { name, description });
214                    }
215                    ucl_parser::SnapshotCommand::Restore { name } => {
216                        ops.push(Operation::RestoreSnapshot { name });
217                    }
218                    _ => {}
219                },
220                _ => {} // Other commands
221            }
222        }
223        Ok(ops)
224    }
225}
226
227impl Default for UcpClient {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_create_document() {
239        let client = UcpClient::new();
240        let doc = client.create_document();
241        assert_eq!(doc.block_count(), 1);
242    }
243
244    #[test]
245    fn test_add_text() {
246        let client = UcpClient::new();
247        let mut doc = client.create_document();
248        let root = doc.root;
249
250        let id = client
251            .add_text(&mut doc, &root, "Hello, world!", Some("intro"))
252            .unwrap();
253        assert_eq!(doc.block_count(), 2);
254        assert!(doc.get_block(&id).is_some());
255    }
256}