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//! (SRD-66 §"Surface 4").
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 (SRD-66 §"Surface 4
17//! §Semantics"):
18//!
19//! ```text
20//! exactly_one_value: expected unary structure (1 row × 1 column),
21//!                    found <r> rows × <c> columns
22//! ```
23
24use crate::ast::Value;
25
26/// Assert that the input value is a unary structure and return its
27/// single cell as a String. See module docs.
28///
29/// **Output is always `Str`** (the function returns `String`, so the
30/// macro emits a fixed `PortType::Str` output port). The function
31/// accepts any `Value` variant the upstream wire produces and
32/// renders the unwrapped leaf to its display form. This matches the
33/// load-bearing workload pattern `schema_text := exactly_one_value(body)`
34/// where downstream nodes (regex_match, format, …) take `&str`.
35///
36/// A polymorphic-output variant that preserves leaf types
37/// (Bool/U64/F64/…) would require a port-type declaration that
38/// reflects the runtime unwrap result rather than the input port,
39/// which the macro can't express today without generic-over-Wire
40/// instantiation. When such a variant is needed it becomes its own
41/// node.
42#[crate::polydat_node(category = Diagnostic)]
43fn exactly_one_value(body: Value) -> String {
44    if crate::library::debug_nodes_enabled() {
45        // Per-cycle visibility into what the structural unwrap
46        // saw and produced. Body's display form is truncated for
47        // long Json arrays so the trace stays scannable.
48        let body_disp = body.to_display_string();
49        let snippet: String = body_disp.chars().take(400).collect();
50        let ellipsis = if body_disp.len() > snippet.len() {
51            "…"
52        } else {
53            ""
54        };
55        eprintln!(
56            "[DEBUG] exactly_one_value: body.variant={:?} body.len={} snippet={}{ellipsis}",
57            body.port_type(),
58            body_disp.len(),
59            snippet,
60        );
61    }
62    let leaf_value: Value = match &body {
63        // Already-scalar values pass through unchanged. They came
64        // from a body projection that already collapsed the row ×
65        // column structure.
66        Value::Str(_)
67        | Value::Bool(_)
68        | Value::U64(_)
69        | Value::I64(_)
70        | Value::U128(_)
71        | Value::I128(_)
72        | Value::F64(_) => body.clone(),
73
74        // Typed vector carriers: the structural shape is "1 row × 1
75        // column" iff the slice has exactly one element.
76        Value::VecF32(arc) => {
77            if arc.len() != 1 {
78                panic!(
79                    "exactly_one_value: expected unary structure \
80                     (1 row × 1 column), found vec_f32 of length {}",
81                    arc.len()
82                );
83            }
84            Value::F64(arc[0] as f64)
85        }
86        Value::VecI32(arc) => {
87            if arc.len() != 1 {
88                panic!(
89                    "exactly_one_value: expected unary structure \
90                     (1 row × 1 column), found vec_i32 of length {}",
91                    arc.len()
92                );
93            }
94            Value::I64(arc[0] as i64)
95        }
96        Value::VecF64(arc) => {
97            if arc.len() != 1 {
98                panic!(
99                    "exactly_one_value: expected unary structure \
100                     (1 row × 1 column), found vec_f64 of length {}",
101                    arc.len()
102                );
103            }
104            Value::F64(arc[0])
105        }
106        Value::VecI64(arc) => {
107            if arc.len() != 1 {
108                panic!(
109                    "exactly_one_value: expected unary structure \
110                     (1 row × 1 column), found vec_i64 of length {}",
111                    arc.len()
112                );
113            }
114            Value::I64(arc[0])
115        }
116        Value::VecF16(arc) => {
117            if arc.len() != 1 {
118                panic!(
119                    "exactly_one_value: expected unary structure \
120                     (1 row × 1 column), found vec_f16 of length {}",
121                    arc.len()
122                );
123            }
124            Value::F64(arc[0].to_f32() as f64)
125        }
126        Value::VecI16(arc) => {
127            if arc.len() != 1 {
128                panic!(
129                    "exactly_one_value: expected unary structure \
130                     (1 row × 1 column), found vec_i16 of length {}",
131                    arc.len()
132                );
133            }
134            Value::I64(arc[0] as i64)
135        }
136        Value::VecI8(arc) => {
137            if arc.len() != 1 {
138                panic!(
139                    "exactly_one_value: expected unary structure \
140                     (1 row × 1 column), found vec_i8 of length {}",
141                    arc.len()
142                );
143            }
144            Value::I64(arc[0] as i64)
145        }
146
147        Value::None => panic!(
148            "exactly_one_value: empty body (Value::None); the upstream \
149             op produced no result to unwrap"
150        ),
151
152        // Structural body walk: array = row dim, object = column
153        // dim, leaf = result. Unary shape = 1 × 1 × 1.
154        Value::Json(j) => unwrap_unary_json(j),
155
156        // Other carriers pass through (already collapsed).
157        // Register words render via their view's display form.
158        Value::Bytes(_) | Value::Ext(_) | Value::Handle(_) | Value::Reg128(_, _) => body.clone(),
159    };
160    // Render to String for the declared Str output port. Non-Str
161    // leaves render via the Value display form (Bool → "true"/"false",
162    // U64 → "42", F64 → "3.14", etc.).
163    match leaf_value {
164        Value::Str(s) => s.to_string(),
165        other => other.to_display_string(),
166    }
167}
168
169/// Walk a JSON value asserting unary row × column × leaf shape.
170/// Returns the matching `Value` variant for the leaf cell.
171///
172/// The shape diagnostic names actual dimensions when the input
173/// doesn't match the unary contract.
174fn unwrap_unary_json(j: &serde_json::Value) -> Value {
175    use serde_json::Value as J;
176    // Row dimension: an array. Length 0 / >1 → shape error.
177    let row = match j {
178        J::Array(arr) => match arr.len() {
179            0 => panic!(
180                "exactly_one_value: expected unary structure (1 row × 1 column), \
181                 found 0 rows"
182            ),
183            1 => &arr[0],
184            n => panic!(
185                "exactly_one_value: expected unary structure (1 row × 1 column), \
186                 found {n} rows"
187            ),
188        },
189        // No row wrapper — treat the whole value as the single
190        // row and continue to column inspection. Adapters that
191        // produce a single-row unwrapped projection (rare; CQL
192        // doesn't) take this path naturally.
193        other => other,
194    };
195    // Column dimension: an object. Length 0 / >1 → shape error.
196    let leaf = match row {
197        J::Object(obj) => match obj.len() {
198            0 => panic!(
199                "exactly_one_value: expected unary structure (1 row × 1 column), \
200                 found 1 row × 0 columns"
201            ),
202            1 => obj.values().next().expect("len==1"),
203            n => panic!(
204                "exactly_one_value: expected unary structure (1 row × 1 column), \
205                 found 1 row × {n} columns"
206            ),
207        },
208        // No column wrapper — the row IS the leaf. Common for
209        // non-tabular bodies (e.g. a HTTP body that's a bare
210        // string).
211        other => other,
212    };
213    match leaf {
214        J::String(s) => Value::Str(s.as_str().into()),
215        J::Bool(b) => Value::Bool(*b),
216        // Total, lossless number extraction mirroring
217        // serde_json::Number's three leaves (PosInt / NegInt /
218        // Float) — a negative integer lands in the honest signed
219        // carrier instead of degrading to F64.
220        J::Number(n) => {
221            if let Some(u) = n.as_u64() {
222                Value::U64(u)
223            } else if let Some(i) = n.as_i64() {
224                Value::I64(i)
225            } else if let Some(f) = n.as_f64() {
226                Value::F64(f)
227            } else {
228                panic!(
229                    "exactly_one_value: numeric leaf is not representable as u64, i64, or f64: {n}"
230                )
231            }
232        }
233        J::Null => panic!("exactly_one_value: leaf cell is null; expected a non-null value"),
234        // Nested structural leaf — the body has more than two
235        // levels of nesting. Not a unary shape per the SRD; 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    // SRD-66 §"Surface 4 §Semantics" — 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}