Skip to main content

reifydb_sub_server/
wire.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, sync::Arc};
5
6use reifydb_codec::json::{from::parse_value, wire_type::WireValueType};
7use reifydb_value::{
8	params::Params,
9	value::{Value, duration::Duration},
10};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct WireValue {
15	#[serde(rename = "type")]
16	pub r#type: WireValueType,
17	pub value: String,
18}
19
20#[derive(Debug, Serialize, Deserialize)]
21#[serde(untagged)]
22pub enum WireParams {
23	Positional(Vec<WireValue>),
24	Named(HashMap<String, WireValue>),
25}
26
27fn wire_value_to_value(parameter: &str, wire: WireValue) -> Result<Value, String> {
28	parse_value(&wire.r#type.0, &wire.value).map_err(|e| format!("parameter {parameter}: {e}"))
29}
30
31impl WireValue {
32	pub fn into_duration(self, option: &str) -> Result<Duration, String> {
33		match parse_value(&self.r#type.0, &self.value).map_err(|e| format!("option {option}: {e}"))? {
34			Value::Duration(duration) => Ok(duration),
35			other => Err(format!("option {option}: expected Duration, got {}", other.get_type())),
36		}
37	}
38}
39
40impl WireParams {
41	pub fn into_params(self) -> Result<Params, String> {
42		match self {
43			WireParams::Positional(items) => {
44				let mut values = Vec::with_capacity(items.len());
45				for (index, item) in items.into_iter().enumerate() {
46					values.push(wire_value_to_value(&format!("${}", index + 1), item)?);
47				}
48				Ok(Params::Positional(Arc::new(values)))
49			}
50			WireParams::Named(map) => {
51				let mut result = HashMap::with_capacity(map.len());
52				for (key, wire) in map {
53					let value = wire_value_to_value(&format!("${key}"), wire)?;
54					result.insert(key, value);
55				}
56				Ok(Params::Named(Arc::new(result)))
57			}
58		}
59	}
60}
61
62#[cfg(test)]
63mod tests {
64	use reifydb_codec::json::{NONE_MARKER, none_marker};
65	use reifydb_value::value::value_type::ValueType;
66	use serde_json::from_str;
67
68	use super::*;
69
70	fn positional(json: &str) -> Result<Params, String> {
71		from_str::<WireParams>(json).expect("wire params should deserialise").into_params()
72	}
73
74	fn named(json: &str) -> Result<Params, String> {
75		from_str::<WireParams>(json).expect("wire params should deserialise").into_params()
76	}
77
78	fn option(inner: ValueType) -> ValueType {
79		ValueType::Option(Box::new(inner))
80	}
81
82	fn option_option_int4(value: &str) -> String {
83		format!(
84			r#"{{"type":{{"id":"Option","underlying":{{"id":"Option","underlying":{{"id":"Int4"}}}}}},"value":"{value}"}}"#
85		)
86	}
87
88	#[test]
89	fn a_none_under_one_some_layer_of_option_option_int4_is_a_none_of_int4() {
90		let params = positional(&format!("[{}]", option_option_int4(&none_marker(1)))).unwrap();
91		assert_eq!(params.get_positional(0), Some(&Value::none_of(ValueType::Int4)));
92
93		let params = named(&format!(r#"{{"v":{}}}"#, option_option_int4(&none_marker(1)))).unwrap();
94		assert_eq!(params.get_named("v"), Some(&Value::none_of(ValueType::Int4)));
95	}
96
97	#[test]
98	fn a_bare_marker_on_option_option_int4_is_a_none_of_option_int4() {
99		let params = positional(&format!("[{}]", option_option_int4(NONE_MARKER))).unwrap();
100		assert_eq!(params.get_positional(0), Some(&Value::none_of(option(ValueType::Int4))));
101
102		let params = named(&format!(r#"{{"v":{}}}"#, option_option_int4(NONE_MARKER))).unwrap();
103		assert_eq!(params.get_named("v"), Some(&Value::none_of(option(ValueType::Int4))));
104	}
105
106	#[test]
107	fn a_present_value_on_option_option_int4_is_the_base_value() {
108		let params = positional(&format!("[{}]", option_option_int4("5"))).unwrap();
109		assert_eq!(params.get_positional(0), Some(&Value::Int4(5)));
110
111		let params = named(&format!(r#"{{"v":{}}}"#, option_option_int4("5"))).unwrap();
112		assert_eq!(params.get_named("v"), Some(&Value::Int4(5)));
113	}
114
115	#[test]
116	fn a_scalar_descriptor_is_a_base_type() {
117		let params = positional(r#"[{"type":{"id":"Int4"},"value":"7"}]"#).unwrap();
118		assert_eq!(params.get_positional(0), Some(&Value::Int4(7)));
119	}
120
121	#[test]
122	fn nested_descriptors_carry_the_option_layers() {
123		let params = positional(
124			r#"[{"type":{"id":"Option","underlying":{"id":"Option","underlying":{"id":"Utf8"}}},"value":"⟪none:1⟫"},{"type":{"id":"Option","underlying":{"id":"Utf8"}},"value":"seven"}]"#,
125		)
126		.unwrap();
127		assert_eq!(params.get_positional(0), Some(&Value::none_of(ValueType::Utf8)));
128		assert_eq!(params.get_positional(1), Some(&Value::Utf8("seven".to_string())));
129	}
130
131	#[test]
132	fn the_none_type_name_is_unknown() {
133		let err = from_str::<WireValue>(r#"{"type":{"id":"None"},"value":"⟪none⟫"}"#).unwrap_err().to_string();
134		assert!(err.contains("unknown type id `None`"), "{err}");
135	}
136
137	#[test]
138	fn a_positional_error_names_the_position_and_the_type() {
139		let err = positional(r#"[{"type":{"id":"Int4"},"value":"1"},{"type":{"id":"Int4"},"value":"abc"}]"#)
140			.unwrap_err();
141		assert_eq!(err, "parameter $2: invalid data: cannot parse 'abc' as Int4");
142	}
143
144	#[test]
145	fn a_named_error_names_the_parameter_and_the_type() {
146		let params = from_str::<WireParams>(r#"{"id":{"type":{"id":"Int4"},"value":"⟪none⟫"}}"#).unwrap();
147		let err = params.into_params().unwrap_err();
148		assert_eq!(err, "parameter $id: invalid data: none marker for non-Option type Int4");
149	}
150}