1use 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
21pub struct UcpClient {
23 engine: Engine,
24}
25
26impl UcpClient {
27 pub fn new() -> Self {
28 Self {
29 engine: Engine::new(),
30 }
31 }
32
33 pub fn create_document(&self) -> Document {
35 Document::create()
36 }
37
38 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 pub fn parse_ucl(&self, ucl: &str) -> Result<UclDocument> {
49 parse(ucl).map_err(|e| Error::Internal(format!("Parse error: {}", e)))
50 }
51
52 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 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 pub fn to_json(&self, doc: &Document) -> Result<String> {
78 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 _ => {} }
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}