plotnik_lib/engine/
value.rs

1//! Output value types for materialization.
2
3use arborium_tree_sitter::Node;
4use serde::ser::{SerializeMap, SerializeSeq};
5use serde::{Serialize, Serializer};
6
7use crate::Colors;
8
9/// Lifetime-free node handle for output.
10///
11/// Captures enough information to represent a node without holding
12/// a reference to the tree.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct NodeHandle {
15    /// Node kind name (e.g., "identifier", "number").
16    pub kind: String,
17    /// Source text of the node.
18    pub text: String,
19    /// Byte span [start, end).
20    pub span: (u32, u32),
21}
22
23impl NodeHandle {
24    /// Create from a tree-sitter node and source text.
25    pub fn from_node(node: Node<'_>, source: &str) -> Self {
26        let text = node
27            .utf8_text(source.as_bytes())
28            .expect("node text extraction failed")
29            .to_owned();
30        Self {
31            kind: node.kind().to_owned(),
32            text,
33            span: (node.start_byte() as u32, node.end_byte() as u32),
34        }
35    }
36}
37
38impl Serialize for NodeHandle {
39    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
40    where
41        S: Serializer,
42    {
43        use serde::ser::SerializeStruct;
44        let mut s = serializer.serialize_struct("NodeHandle", 3)?;
45        s.serialize_field("kind", &self.kind)?;
46        s.serialize_field("text", &self.text)?;
47        s.serialize_field("span", &[self.span.0, self.span.1])?;
48        s.end()
49    }
50}
51
52/// Self-contained output value.
53///
54/// `Object` uses `Vec<(String, Value)>` to preserve field order from type metadata.
55#[derive(Clone, Debug, PartialEq)]
56pub enum Value {
57    Null,
58    String(String),
59    Node(NodeHandle),
60    Array(Vec<Value>),
61    /// Object with ordered fields.
62    Object(Vec<(String, Value)>),
63    /// Tagged union. `data` is None for Void payloads.
64    Tagged {
65        tag: String,
66        data: Option<Box<Value>>,
67    },
68}
69
70impl Serialize for Value {
71    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72    where
73        S: Serializer,
74    {
75        match self {
76            Value::Null => serializer.serialize_none(),
77            Value::String(s) => serializer.serialize_str(s),
78            Value::Node(h) => h.serialize(serializer),
79            Value::Array(arr) => {
80                let mut seq = serializer.serialize_seq(Some(arr.len()))?;
81                for item in arr {
82                    seq.serialize_element(item)?;
83                }
84                seq.end()
85            }
86            Value::Object(fields) => {
87                let mut map = serializer.serialize_map(Some(fields.len()))?;
88                for (key, value) in fields {
89                    map.serialize_entry(key, value)?;
90                }
91                map.end()
92            }
93            Value::Tagged { tag, data } => {
94                let len = if data.is_some() { 2 } else { 1 };
95                let mut map = serializer.serialize_map(Some(len))?;
96                map.serialize_entry("$tag", tag)?;
97                if let Some(d) = data {
98                    map.serialize_entry("$data", d)?;
99                }
100                map.end()
101            }
102        }
103    }
104}
105
106impl Value {
107    /// Format value as colored JSON.
108    ///
109    /// Color scheme (jq-inspired):
110    /// - Keys: Blue
111    /// - String values: Green
112    /// - Numbers, booleans: Normal
113    /// - null: Dim
114    /// - Structure `{}[]:,`: Dim
115    pub fn format(&self, pretty: bool, colors: Colors) -> String {
116        let mut out = String::new();
117        format_value(&mut out, self, &colors, pretty, 0);
118        out
119    }
120}
121
122fn format_value(out: &mut String, value: &Value, c: &Colors, pretty: bool, indent: usize) {
123    match value {
124        Value::Null => {
125            out.push_str(c.dim);
126            out.push_str("null");
127            out.push_str(c.reset);
128        }
129        Value::String(s) => {
130            out.push_str(c.green);
131            out.push('"');
132            out.push_str(&escape_json_string(s));
133            out.push('"');
134            out.push_str(c.reset);
135        }
136        Value::Node(h) => {
137            format_node_handle(out, h, c, pretty, indent);
138        }
139        Value::Array(arr) => {
140            format_array(out, arr, c, pretty, indent);
141        }
142        Value::Object(fields) => {
143            format_object(out, fields, c, pretty, indent);
144        }
145        Value::Tagged { tag, data } => {
146            format_tagged(out, tag, data, c, pretty, indent);
147        }
148    }
149}
150
151fn format_node_handle(out: &mut String, h: &NodeHandle, c: &Colors, pretty: bool, indent: usize) {
152    out.push_str(c.dim);
153    out.push('{');
154    out.push_str(c.reset);
155
156    let field_indent = if pretty { indent + 2 } else { 0 };
157
158    // Field 1: "kind"
159    if pretty {
160        out.push('\n');
161        out.push_str(&" ".repeat(field_indent));
162    }
163    out.push_str(c.blue);
164    out.push_str("\"kind\"");
165    out.push_str(c.reset);
166    out.push_str(c.dim);
167    out.push(':');
168    out.push_str(c.reset);
169    if pretty {
170        out.push(' ');
171    }
172    out.push_str(c.green);
173    out.push('"');
174    out.push_str(&escape_json_string(&h.kind));
175    out.push('"');
176    out.push_str(c.reset);
177
178    // Field 2: "text"
179    out.push_str(c.dim);
180    out.push(',');
181    out.push_str(c.reset);
182    if pretty {
183        out.push('\n');
184        out.push_str(&" ".repeat(field_indent));
185    }
186    out.push_str(c.blue);
187    out.push_str("\"text\"");
188    out.push_str(c.reset);
189    out.push_str(c.dim);
190    out.push(':');
191    out.push_str(c.reset);
192    if pretty {
193        out.push(' ');
194    }
195    out.push_str(c.green);
196    out.push('"');
197    out.push_str(&escape_json_string(&h.text));
198    out.push('"');
199    out.push_str(c.reset);
200
201    // Field 3: "span"
202    out.push_str(c.dim);
203    out.push(',');
204    out.push_str(c.reset);
205    if pretty {
206        out.push('\n');
207        out.push_str(&" ".repeat(field_indent));
208    }
209    out.push_str(c.blue);
210    out.push_str("\"span\"");
211    out.push_str(c.reset);
212    out.push_str(c.dim);
213    out.push(':');
214    out.push_str(c.reset);
215    if pretty {
216        out.push(' ');
217    }
218    out.push_str(c.dim);
219    out.push('[');
220    out.push_str(c.reset);
221    out.push_str(&h.span.0.to_string());
222    out.push_str(c.dim);
223    out.push_str(", ");
224    out.push_str(c.reset);
225    out.push_str(&h.span.1.to_string());
226    out.push_str(c.dim);
227    out.push(']');
228    out.push_str(c.reset);
229
230    if pretty {
231        out.push('\n');
232        out.push_str(&" ".repeat(indent));
233    }
234
235    out.push_str(c.dim);
236    out.push('}');
237    out.push_str(c.reset);
238}
239
240fn format_array(out: &mut String, arr: &[Value], c: &Colors, pretty: bool, indent: usize) {
241    out.push_str(c.dim);
242    out.push('[');
243    out.push_str(c.reset);
244
245    if arr.is_empty() {
246        out.push_str(c.dim);
247        out.push(']');
248        out.push_str(c.reset);
249        return;
250    }
251
252    let elem_indent = if pretty { indent + 2 } else { 0 };
253
254    for (i, item) in arr.iter().enumerate() {
255        if i > 0 {
256            out.push_str(c.dim);
257            out.push(',');
258            out.push_str(c.reset);
259        }
260
261        if pretty {
262            out.push('\n');
263            out.push_str(&" ".repeat(elem_indent));
264        }
265
266        format_value(out, item, c, pretty, elem_indent);
267    }
268
269    if pretty {
270        out.push('\n');
271        out.push_str(&" ".repeat(indent));
272    }
273
274    out.push_str(c.dim);
275    out.push(']');
276    out.push_str(c.reset);
277}
278
279fn format_object(
280    out: &mut String,
281    fields: &[(String, Value)],
282    c: &Colors,
283    pretty: bool,
284    indent: usize,
285) {
286    out.push_str(c.dim);
287    out.push('{');
288    out.push_str(c.reset);
289
290    if fields.is_empty() {
291        out.push_str(c.dim);
292        out.push('}');
293        out.push_str(c.reset);
294        return;
295    }
296
297    let field_indent = if pretty { indent + 2 } else { 0 };
298
299    for (i, (key, value)) in fields.iter().enumerate() {
300        if i > 0 {
301            out.push_str(c.dim);
302            out.push(',');
303            out.push_str(c.reset);
304        }
305
306        if pretty {
307            out.push('\n');
308            out.push_str(&" ".repeat(field_indent));
309        }
310
311        // Key in blue
312        out.push_str(c.blue);
313        out.push('"');
314        out.push_str(&escape_json_string(key));
315        out.push('"');
316        out.push_str(c.reset);
317
318        out.push_str(c.dim);
319        out.push(':');
320        out.push_str(c.reset);
321
322        if pretty {
323            out.push(' ');
324        }
325
326        format_value(out, value, c, pretty, field_indent);
327    }
328
329    if pretty {
330        out.push('\n');
331        out.push_str(&" ".repeat(indent));
332    }
333
334    out.push_str(c.dim);
335    out.push('}');
336    out.push_str(c.reset);
337}
338
339fn format_tagged(
340    out: &mut String,
341    tag: &str,
342    data: &Option<Box<Value>>,
343    c: &Colors,
344    pretty: bool,
345    indent: usize,
346) {
347    out.push_str(c.dim);
348    out.push('{');
349    out.push_str(c.reset);
350
351    let field_indent = if pretty { indent + 2 } else { 0 };
352
353    if pretty {
354        out.push('\n');
355        out.push_str(&" ".repeat(field_indent));
356    }
357
358    // $tag key in blue
359    out.push_str(c.blue);
360    out.push_str("\"$tag\"");
361    out.push_str(c.reset);
362
363    out.push_str(c.dim);
364    out.push(':');
365    out.push_str(c.reset);
366
367    if pretty {
368        out.push(' ');
369    }
370
371    // Tag value is green (string)
372    out.push_str(c.green);
373    out.push('"');
374    out.push_str(&escape_json_string(tag));
375    out.push('"');
376    out.push_str(c.reset);
377
378    // Only emit $data if present (Void payloads omit it)
379    if let Some(d) = data {
380        out.push_str(c.dim);
381        out.push(',');
382        out.push_str(c.reset);
383
384        if pretty {
385            out.push('\n');
386            out.push_str(&" ".repeat(field_indent));
387        }
388
389        // $data key in blue
390        out.push_str(c.blue);
391        out.push_str("\"$data\"");
392        out.push_str(c.reset);
393
394        out.push_str(c.dim);
395        out.push(':');
396        out.push_str(c.reset);
397
398        if pretty {
399            out.push(' ');
400        }
401
402        format_value(out, d, c, pretty, field_indent);
403    }
404
405    if pretty {
406        out.push('\n');
407        out.push_str(&" ".repeat(indent));
408    }
409
410    out.push_str(c.dim);
411    out.push('}');
412    out.push_str(c.reset);
413}
414
415fn escape_json_string(s: &str) -> String {
416    let mut result = String::with_capacity(s.len());
417    for ch in s.chars() {
418        match ch {
419            '"' => result.push_str("\\\""),
420            '\\' => result.push_str("\\\\"),
421            '\n' => result.push_str("\\n"),
422            '\r' => result.push_str("\\r"),
423            '\t' => result.push_str("\\t"),
424            c if c.is_control() => {
425                result.push_str(&format!("\\u{:04x}", c as u32));
426            }
427            c => result.push(c),
428        }
429    }
430    result
431}