capnweb_client/
recorder.rs

1use capnweb_core::{CapId, Op, Plan, Source};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::sync::{Arc, Mutex};
5
6/// Records method calls to build an IL Plan
7#[derive(Clone)]
8pub struct Recorder {
9    inner: Arc<Mutex<RecorderInner>>,
10}
11
12struct RecorderInner {
13    captures: Vec<CapId>,
14    ops: Vec<Op>,
15    next_result_index: u32,
16    capability_map: BTreeMap<String, u32>,
17}
18
19impl Recorder {
20    /// Create a new recorder
21    pub fn new() -> Self {
22        Self {
23            inner: Arc::new(Mutex::new(RecorderInner {
24                captures: Vec::new(),
25                ops: Vec::new(),
26                next_result_index: 0,
27                capability_map: BTreeMap::new(),
28            })),
29        }
30    }
31
32    /// Record a capability capture
33    pub fn capture(&self, name: &str, cap_id: CapId) -> RecordedCapability {
34        let mut inner = self.inner.lock().unwrap();
35        let index = inner.captures.len() as u32;
36        inner.captures.push(cap_id);
37        inner.capability_map.insert(name.to_string(), index);
38
39        RecordedCapability {
40            recorder: self.clone(),
41            index,
42            name: name.to_string(),
43        }
44    }
45
46    /// Record a method call
47    pub fn call(&self, target: Source, method: &str, args: Vec<Source>) -> RecordedResult {
48        let mut inner = self.inner.lock().unwrap();
49        let result_index = inner.next_result_index;
50        inner.next_result_index += 1;
51
52        inner
53            .ops
54            .push(Op::call(target, method.to_string(), args, result_index));
55
56        RecordedResult {
57            recorder: self.clone(),
58            index: result_index,
59        }
60    }
61
62    /// Record object construction
63    pub fn object(&self, fields: BTreeMap<String, Source>) -> RecordedResult {
64        let mut inner = self.inner.lock().unwrap();
65        let result_index = inner.next_result_index;
66        inner.next_result_index += 1;
67
68        inner.ops.push(Op::object(fields, result_index));
69
70        RecordedResult {
71            recorder: self.clone(),
72            index: result_index,
73        }
74    }
75
76    /// Record array construction
77    pub fn array(&self, items: Vec<Source>) -> RecordedResult {
78        let mut inner = self.inner.lock().unwrap();
79        let result_index = inner.next_result_index;
80        inner.next_result_index += 1;
81
82        inner.ops.push(Op::array(items, result_index));
83
84        RecordedResult {
85            recorder: self.clone(),
86            index: result_index,
87        }
88    }
89
90    /// Build the final plan
91    pub fn build(&self, result: Source) -> Plan {
92        let inner = self.inner.lock().unwrap();
93        Plan::new(inner.captures.clone(), inner.ops.clone(), result)
94    }
95
96    /// Get a capability source by name
97    pub fn cap(&self, name: &str) -> Option<Source> {
98        let inner = self.inner.lock().unwrap();
99        inner
100            .capability_map
101            .get(name)
102            .map(|&index| Source::capture(index))
103    }
104}
105
106impl Default for Recorder {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112/// Represents a recorded capability
113pub struct RecordedCapability {
114    recorder: Recorder,
115    index: u32,
116    #[allow(dead_code)]
117    name: String,
118}
119
120impl RecordedCapability {
121    /// Call a method on this capability
122    pub fn call(&self, method: &str, args: Vec<Source>) -> RecordedResult {
123        self.recorder
124            .call(Source::capture(self.index), method, args)
125    }
126
127    /// Get the source for this capability
128    pub fn as_source(&self) -> Source {
129        Source::capture(self.index)
130    }
131}
132
133/// Represents a recorded result
134pub struct RecordedResult {
135    recorder: Recorder,
136    index: u32,
137}
138
139impl RecordedResult {
140    /// Call a method on this result
141    pub fn call(&self, method: &str, args: Vec<Source>) -> RecordedResult {
142        self.recorder.call(Source::result(self.index), method, args)
143    }
144
145    /// Get the source for this result
146    pub fn as_source(&self) -> Source {
147        Source::result(self.index)
148    }
149
150    /// Access a field of this result
151    pub fn field(&self, name: &str) -> RecordedField {
152        RecordedField {
153            recorder: self.recorder.clone(),
154            result_index: self.index,
155            field_name: name.to_string(),
156        }
157    }
158}
159
160/// Represents a field access on a result
161pub struct RecordedField {
162    recorder: Recorder,
163    result_index: u32,
164    #[allow(dead_code)]
165    field_name: String,
166}
167
168impl RecordedField {
169    /// Call a method on this field
170    pub fn call(&self, method: &str, args: Vec<Source>) -> RecordedResult {
171        // First, get the field value
172        let field_source = self.as_source();
173        self.recorder.call(field_source, method, args)
174    }
175
176    /// Get the source for this field
177    pub fn as_source(&self) -> Source {
178        // In a real implementation, this would extract the field
179        // For now, we'll use the parent result
180        Source::result(self.result_index)
181    }
182}
183
184/// Helper to create parameter sources
185pub struct Param;
186
187impl Param {
188    /// Create a parameter source from a path
189    pub fn path(segments: &[&str]) -> Source {
190        Source::param(segments.iter().map(|s| s.to_string()).collect())
191    }
192
193    /// Create a value source
194    pub fn value(val: Value) -> Source {
195        Source::by_value(val)
196    }
197}
198
199/// Convenience builder for recorded plans
200pub struct RecordedPlan {
201    recorder: Recorder,
202}
203
204impl RecordedPlan {
205    /// Create a new plan recorder
206    pub fn new() -> Self {
207        Self {
208            recorder: Recorder::new(),
209        }
210    }
211
212    /// Capture a capability
213    pub fn capture(&self, name: &str, cap_id: CapId) -> RecordedCapability {
214        self.recorder.capture(name, cap_id)
215    }
216
217    /// Build an object
218    pub fn object(&self, fields: BTreeMap<String, Source>) -> RecordedResult {
219        self.recorder.object(fields)
220    }
221
222    /// Build an array
223    pub fn array(&self, items: Vec<Source>) -> RecordedResult {
224        self.recorder.array(items)
225    }
226
227    /// Finish building the plan
228    pub fn finish(self, result: Source) -> Plan {
229        self.recorder.build(result)
230    }
231}
232
233impl Default for RecordedPlan {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_recorder_basic() {
245        let recorder = Recorder::new();
246
247        // Capture a capability
248        let cap = recorder.capture("calculator", CapId::new(1));
249
250        // Call a method
251        let result = cap.call(
252            "add",
253            vec![
254                Param::value(serde_json::json!(5)),
255                Param::value(serde_json::json!(3)),
256            ],
257        );
258
259        // Build the plan
260        let plan = recorder.build(result.as_source());
261
262        // Verify the plan structure
263        assert_eq!(plan.captures.len(), 1);
264        assert_eq!(plan.ops.len(), 1);
265    }
266
267    #[test]
268    fn test_recorded_plan_builder() {
269        let plan_builder = RecordedPlan::new();
270
271        // Capture capabilities
272        let calc = plan_builder.capture("calc", CapId::new(1));
273
274        // Perform operations
275        let sum = calc.call("add", vec![Param::path(&["a"]), Param::path(&["b"])]);
276
277        // Build final plan
278        let plan = plan_builder.finish(sum.as_source());
279
280        assert_eq!(plan.captures.len(), 1);
281        assert_eq!(plan.ops.len(), 1);
282    }
283
284    #[test]
285    fn test_chained_calls() {
286        let recorder = Recorder::new();
287
288        let api = recorder.capture("api", CapId::new(1));
289
290        // Chain multiple calls
291        let result = api
292            .call("getUser", vec![Param::value(serde_json::json!(123))])
293            .call("getName", vec![]);
294
295        let plan = recorder.build(result.as_source());
296
297        assert_eq!(plan.ops.len(), 2);
298    }
299
300    #[test]
301    fn test_object_construction() {
302        let recorder = Recorder::new();
303
304        let api = recorder.capture("api", CapId::new(1));
305        let name = api.call("getName", vec![]);
306        let age = api.call("getAge", vec![]);
307
308        let mut fields = BTreeMap::new();
309        fields.insert("name".to_string(), name.as_source());
310        fields.insert("age".to_string(), age.as_source());
311
312        let obj = recorder.object(fields);
313        let plan = recorder.build(obj.as_source());
314
315        assert_eq!(plan.ops.len(), 3); // 2 calls + 1 object
316    }
317
318    #[test]
319    fn test_array_construction() {
320        let recorder = Recorder::new();
321
322        let api = recorder.capture("api", CapId::new(1));
323
324        let items = vec![
325            api.call("getValue", vec![Param::value(serde_json::json!(1))])
326                .as_source(),
327            api.call("getValue", vec![Param::value(serde_json::json!(2))])
328                .as_source(),
329            api.call("getValue", vec![Param::value(serde_json::json!(3))])
330                .as_source(),
331        ];
332
333        let arr = recorder.array(items);
334        let plan = recorder.build(arr.as_source());
335
336        assert_eq!(plan.ops.len(), 4); // 3 calls + 1 array
337    }
338}