Skip to main content

polydat_core/library/
exactly_one.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `exactly_one_value` — explicit unwrap of a unary structural body
5//! (library_catalog.md, "`exactly_one_value` — motivation and semantics").
6//!
7//! The motivating use case has a CQL `describe keyspace` op whose
8//! body is a single row × single text column. To regex-match the
9//! schema text, the workload asserts unary shape and unwraps. No
10//! implicit modal projection — identical workload source against a
11//! non-unary body must surface a clear shape diagnostic, not silently
12//! diverge from intent.
13//!
14//! Scalars pass through; typed vectors must have exactly one element;
15//! a `Json` body is walked as rows × columns × leaf and rejected with
16//! the diagnostic below when not unary:
17//!
18//! ```text
19//! exactly_one_value: expected unary structure (1 row × 1 column),
20//!                    found <r> rows × <c> columns
21//! ```
22
23use crate::ast::Value;
24
25/// Assert that the input value is a unary structure and return its
26/// single cell as a String. See module docs.
27///
28/// **Output is always `Str`** (the function returns `String`, so the
29/// macro emits a fixed `PortType::Str` output port). The function
30/// accepts any `Value` variant the upstream wire produces and
31/// renders the unwrapped leaf to its display form. This matches the
32/// load-bearing workload pattern `schema_text := exactly_one_value(body)`
33/// where downstream nodes (regex_match, format, …) take `&str`.
34///
35/// A polymorphic-output variant that preserves leaf types
36/// (Bool/U64/F64/…) would require a port-type declaration that
37/// reflects the runtime unwrap result rather than the input port,
38/// which the macro can't express without generic-over-Wire
39/// instantiation. When such a variant is needed it becomes its own
40/// node.
41#[crate::polydat_node(category = Diagnostic)]
42fn exactly_one_value(body: Value) -> String {
43    if crate::library::debug_nodes_enabled() {
44        // Per-cycle visibility into what the structural unwrap
45        // saw and produced. Body's display form is truncated for
46        // long Json arrays so the trace stays scannable.
47        let body_disp = body.to_display_string();
48        let snippet: String = body_disp.chars().take(400).collect();
49        let ellipsis = if body_disp.len() > snippet.len() {
50            "…"
51        } else {
52            ""
53        };
54        eprintln!(
55            "[DEBUG] exactly_one_value: body.variant={:?} body.len={} snippet={}{ellipsis}",
56            body.port_type(),
57            body_disp.len(),
58            snippet,
59        );
60    }
61    let leaf_value: Value = match &body {
62        // Already-scalar values pass through unchanged. They came
63        // from a body projection that already collapsed the row ×
64        // column structure.
65        Value::Str(_)
66        | Value::Bool(_)
67        | Value::U64(_)
68        | Value::I64(_)
69        | Value::U128(_)
70        | Value::I128(_)
71        | Value::F64(_) => body.clone(),
72
73        // Typed vector carriers: the structural shape is "1 row × 1
74        // column" iff the slice has exactly one element.
75        Value::VecF32(arc) => {
76            if arc.len() != 1 {
77                panic!(
78                    "exactly_one_value: expected unary structure \
79                     (1 row × 1 column), found vec_f32 of length {}",
80                    arc.len()
81                );
82            }
83            Value::F64(arc[0] as f64)
84        }
85        Value::VecI32(arc) => {
86            if arc.len() != 1 {
87                panic!(
88                    "exactly_one_value: expected unary structure \
89                     (1 row × 1 column), found vec_i32 of length {}",
90                    arc.len()
91                );
92            }
93            Value::I64(arc[0] as i64)
94        }
95        Value::VecF64(arc) => {
96            if arc.len() != 1 {
97                panic!(
98                    "exactly_one_value: expected unary structure \
99                     (1 row × 1 column), found vec_f64 of length {}",
100                    arc.len()
101                );
102            }
103            Value::F64(arc[0])
104        }
105        Value::VecI64(arc) => {
106            if arc.len() != 1 {
107                panic!(
108                    "exactly_one_value: expected unary structure \
109                     (1 row × 1 column), found vec_i64 of length {}",
110                    arc.len()
111                );
112            }
113            Value::I64(arc[0])
114        }
115        Value::VecF16(arc) => {
116            if arc.len() != 1 {
117                panic!(
118                    "exactly_one_value: expected unary structure \
119                     (1 row × 1 column), found vec_f16 of length {}",
120                    arc.len()
121                );
122            }
123            Value::F64(arc[0].to_f32() as f64)
124        }
125        Value::VecI16(arc) => {
126            if arc.len() != 1 {
127                panic!(
128                    "exactly_one_value: expected unary structure \
129                     (1 row × 1 column), found vec_i16 of length {}",
130                    arc.len()
131                );
132            }
133            Value::I64(arc[0] as i64)
134        }
135        Value::VecI8(arc) => {
136            if arc.len() != 1 {
137                panic!(
138                    "exactly_one_value: expected unary structure \
139                     (1 row × 1 column), found vec_i8 of length {}",
140                    arc.len()
141                );
142            }
143            Value::I64(arc[0] as i64)
144        }
145
146        Value::None => panic!(
147            "exactly_one_value: empty body (Value::None); the upstream \
148             op produced no result to unwrap"
149        ),
150
151        // Structural body walk: array = row dim, object = column
152        // dim, leaf = result. Unary shape = 1 × 1 × 1.
153        Value::Json(j) => unwrap_unary_json(j),
154
155        // Other carriers pass through (already collapsed).
156        // Register words render via their view's display form.
157        Value::Bytes(_) | Value::Ext(_) | Value::Handle(_) | Value::Reg128(_, _) => body.clone(),
158    };
159    // Render to String for the declared Str output port. Non-Str
160    // leaves render via the Value display form (Bool → "true"/"false",
161    // U64 → "42", F64 → "3.14", etc.).
162    match leaf_value {
163        Value::Str(s) => s.to_string(),
164        other => other.to_display_string(),
165    }
166}
167
168/// Walk a JSON value asserting unary row × column × leaf shape.
169/// Returns the matching `Value` variant for the leaf cell.
170///
171/// The shape diagnostic names actual dimensions when the input
172/// doesn't match the unary contract.
173fn unwrap_unary_json(j: &serde_json::Value) -> Value {
174    use serde_json::Value as J;
175    // Row dimension: an array. Length 0 / >1 → shape error.
176    let row = match j {
177        J::Array(arr) => match arr.len() {
178            0 => panic!(
179                "exactly_one_value: expected unary structure (1 row × 1 column), \
180                 found 0 rows"
181            ),
182            1 => &arr[0],
183            n => panic!(
184                "exactly_one_value: expected unary structure (1 row × 1 column), \
185                 found {n} rows"
186            ),
187        },
188        // No row wrapper — treat the whole value as the single
189        // row and continue to column inspection. Adapters that
190        // produce a single-row unwrapped projection (rare; CQL
191        // doesn't) take this path naturally.
192        other => other,
193    };
194    // Column dimension: an object. Length 0 / >1 → shape error.
195    let leaf = match row {
196        J::Object(obj) => match obj.len() {
197            0 => panic!(
198                "exactly_one_value: expected unary structure (1 row × 1 column), \
199                 found 1 row × 0 columns"
200            ),
201            1 => obj.values().next().expect("len==1"),
202            n => panic!(
203                "exactly_one_value: expected unary structure (1 row × 1 column), \
204                 found 1 row × {n} columns"
205            ),
206        },
207        // No column wrapper — the row IS the leaf. Common for
208        // non-tabular bodies (e.g. a HTTP body that's a bare
209        // string).
210        other => other,
211    };
212    match leaf {
213        J::String(s) => Value::Str(s.as_str().into()),
214        J::Bool(b) => Value::Bool(*b),
215        // Total, lossless number extraction mirroring
216        // serde_json::Number's three leaves (PosInt / NegInt /
217        // Float) — a negative integer lands in the honest signed
218        // carrier instead of degrading to F64.
219        J::Number(n) => {
220            if let Some(u) = n.as_u64() {
221                Value::U64(u)
222            } else if let Some(i) = n.as_i64() {
223                Value::I64(i)
224            } else if let Some(f) = n.as_f64() {
225                Value::F64(f)
226            } else {
227                panic!(
228                    "exactly_one_value: numeric leaf is not representable as u64, i64, or f64: {n}"
229                )
230            }
231        }
232        J::Null => panic!("exactly_one_value: leaf cell is null; expected a non-null value"),
233        // Nested structural leaf — the body has more than two
234        // levels of nesting. That is not a unary shape
235        // (library_catalog.md `exactly_one_value`); the
236        // diagnostic names what was found.
237        J::Array(_) | J::Object(_) => panic!(
238            "exactly_one_value: leaf cell is itself structural ({}); \
239             expected a scalar (string, number, or boolean)",
240            describe_json_kind(leaf)
241        ),
242    }
243}
244
245fn describe_json_kind(j: &serde_json::Value) -> &'static str {
246    use serde_json::Value as J;
247    match j {
248        J::Null => "null",
249        J::Bool(_) => "bool",
250        J::Number(_) => "number",
251        J::String(_) => "string",
252        J::Array(_) => "array",
253        J::Object(_) => "object",
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::ast::PolydatNode;
261
262    fn run(input: Value) -> Value {
263        // Macro emits `ExactlyOneValue::new(input_type)` for a
264        // PolyWire-in node; pass the input's port type so the
265        // input slot's port-type metadata matches the upstream
266        // wire. (Output port is fixed `Str` regardless of input.)
267        let node = ExactlyOneValue::new(input.port_type());
268        let mut out = [Value::None];
269        node.eval(&[input], &mut out);
270        out.into_iter().next().unwrap()
271    }
272
273    #[test]
274    fn passes_through_str() {
275        let v = run(Value::Str("hello".into()));
276        assert_eq!(v.as_str(), "hello");
277    }
278
279    #[test]
280    fn passes_through_bool() {
281        // Bool leaves render to "true"/"false" (Str output).
282        let v = run(Value::Bool(true));
283        assert_eq!(v.as_str(), "true");
284    }
285
286    #[test]
287    fn passes_through_u64() {
288        // Numeric leaves render to their display form.
289        let v = run(Value::U64(42));
290        assert_eq!(v.as_str(), "42");
291    }
292
293    #[test]
294    fn passes_through_f64() {
295        let v = run(Value::F64(2.5));
296        assert_eq!(v.as_str(), "2.5");
297    }
298
299    #[test]
300    fn unwraps_singleton_vec_f32() {
301        let v = Value::VecF32(crate::ast::SliceArc::from_vec(vec![1.5_f32]));
302        let out = run(v);
303        assert_eq!(out.as_str(), "1.5");
304    }
305
306    #[test]
307    #[should_panic(expected = "expected unary structure")]
308    fn rejects_multi_element_vec_f32() {
309        let v = Value::VecF32(crate::ast::SliceArc::from_vec(vec![1.0_f32, 2.0]));
310        run(v);
311    }
312
313    #[test]
314    #[should_panic(expected = "exactly_one_value: empty body")]
315    fn rejects_none() {
316        run(Value::None);
317    }
318
319    // ---------------------------------------------------------------
320    // Structural Json walk
321    // ---------------------------------------------------------------
322
323    #[test]
324    fn unwraps_unary_json_describe_keyspace_shape() {
325        let j = serde_json::json!([
326            {"create_statement": "VIRTUAL TABLE system_views.sai_column_indexes (\n  ...\n)"}
327        ]);
328        let out = run(Value::Json(std::sync::Arc::new(j)));
329        let s = out.as_str();
330        assert!(s.starts_with("VIRTUAL TABLE"), "got: {s:?}");
331    }
332
333    #[test]
334    fn unwraps_unary_json_string_leaf() {
335        let j = serde_json::json!([{"value": "hello"}]);
336        let out = run(Value::Json(std::sync::Arc::new(j)));
337        assert_eq!(out.as_str(), "hello");
338    }
339
340    #[test]
341    fn unwraps_unary_json_numeric_leaf() {
342        let j = serde_json::json!([{"n": 42}]);
343        let out = run(Value::Json(std::sync::Arc::new(j)));
344        assert_eq!(out.as_str(), "42");
345    }
346
347    #[test]
348    fn unwraps_unary_json_bool_leaf() {
349        let j = serde_json::json!([{"b": true}]);
350        let out = run(Value::Json(std::sync::Arc::new(j)));
351        assert_eq!(out.as_str(), "true");
352    }
353
354    #[test]
355    #[should_panic(expected = "found 0 rows")]
356    fn rejects_empty_json_array() {
357        run(Value::Json(std::sync::Arc::new(serde_json::json!([]))));
358    }
359
360    #[test]
361    #[should_panic(expected = "found 2 rows")]
362    fn rejects_multi_row_json() {
363        let j = serde_json::json!([{"a": 1}, {"a": 2}]);
364        run(Value::Json(std::sync::Arc::new(j)));
365    }
366
367    #[test]
368    #[should_panic(expected = "found 1 row × 2 columns")]
369    fn rejects_multi_column_json() {
370        let j = serde_json::json!([{"a": 1, "b": 2}]);
371        run(Value::Json(std::sync::Arc::new(j)));
372    }
373
374    #[test]
375    #[should_panic(expected = "leaf cell is null")]
376    fn rejects_json_null_leaf() {
377        let j = serde_json::json!([{"a": null}]);
378        run(Value::Json(std::sync::Arc::new(j)));
379    }
380
381    #[test]
382    #[should_panic(expected = "leaf cell is itself structural")]
383    fn rejects_json_nested_structural_leaf() {
384        let j = serde_json::json!([{"a": {"nested": 1}}]);
385        run(Value::Json(std::sync::Arc::new(j)));
386    }
387}