1use std::collections::BTreeMap;
2
3use serde::de::{MapAccess, Visitor};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::batch::{BatchQuery, ReadBatch, WriteBatch};
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum QueryParamType {
11 Bool,
13 I64,
15 F64,
17 F32,
19 String,
21 DateTime,
23 Bytes,
25 Value,
27 Object,
29 Array(Box<QueryParamType>),
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum QueryRequestType {
37 Read,
39 Write,
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45#[serde(untagged)]
46pub enum QueryValue {
47 Null,
49 Bool(bool),
51 I64(i64),
53 F64(f64),
55 F32(f32),
57 String(String),
59 Array(Vec<QueryValue>),
61 Object(BTreeMap<String, QueryValue>),
63}
64
65#[derive(Debug)]
67pub enum QueryError {
68 Serialize(sonic_rs::Error),
70 Utf8(std::string::FromUtf8Error),
72 UnsupportedBytesParameter(String),
74 InvalidDateTimeParameter {
76 path: String,
78 millis: i64,
80 },
81 InvalidParameterName,
83 DuplicateParameterName(String),
85 MixedParameterModes,
87 ParameterTypeMismatch {
89 path: String,
91 expected: QueryParamType,
93 actual: &'static str,
95 },
96 ParameterNameMismatch {
98 missing_values: Vec<String>,
100 extra_values: Vec<String>,
102 },
103}
104
105impl QueryError {
106 pub fn unsupported_bytes(path: impl Into<String>) -> Self {
108 Self::UnsupportedBytesParameter(path.into())
109 }
110
111 pub fn invalid_datetime(path: impl Into<String>, millis: i64) -> Self {
113 Self::InvalidDateTimeParameter {
114 path: path.into(),
115 millis,
116 }
117 }
118}
119
120impl std::fmt::Display for QueryError {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 Self::Serialize(err) => write!(f, "json serialization error: {err}"),
124 Self::Utf8(err) => write!(f, "utf8 conversion error: {err}"),
125 Self::UnsupportedBytesParameter(path) => write!(
126 f,
127 "parameter '{path}' uses bytes, which the query JSON route cannot represent"
128 ),
129 Self::InvalidDateTimeParameter { path, millis } => write!(
130 f,
131 "parameter '{path}' uses datetime millis '{millis}', which cannot be rendered as RFC3339"
132 ),
133 Self::InvalidParameterName => write!(f, "parameter name must not be empty"),
134 Self::DuplicateParameterName(name) => {
135 write!(f, "parameter name '{name}' is duplicated")
136 }
137 Self::MixedParameterModes => {
138 write!(f, "typed and untyped query parameters cannot be mixed")
139 }
140 Self::ParameterTypeMismatch {
141 path,
142 expected,
143 actual,
144 } => write!(
145 f,
146 "parameter '{path}' expected {expected:?}, but received {actual}"
147 ),
148 Self::ParameterNameMismatch {
149 missing_values,
150 extra_values,
151 } => write!(
152 f,
153 "parameter schema names do not match values (missing values: {missing_values:?}, extra values: {extra_values:?})"
154 ),
155 }
156 }
157}
158
159impl std::error::Error for QueryError {}
160
161impl From<sonic_rs::Error> for QueryError {
162 fn from(value: sonic_rs::Error) -> Self {
163 Self::Serialize(value)
164 }
165}
166
167impl From<std::string::FromUtf8Error> for QueryError {
168 fn from(value: std::string::FromUtf8Error) -> Self {
169 Self::Utf8(value)
170 }
171}
172
173#[derive(Debug, Clone, PartialEq)]
174enum QueryParameters {
175 Untyped(BTreeMap<String, QueryValue>),
176 Typed {
177 values: BTreeMap<String, QueryValue>,
178 types: BTreeMap<String, QueryParamType>,
179 },
180}
181
182impl Default for QueryParameters {
183 fn default() -> Self {
184 Self::Untyped(BTreeMap::new())
185 }
186}
187
188#[derive(Debug, Clone, PartialEq)]
194pub struct QueryRequest {
195 query_name: Option<String>,
197 query: BatchQuery,
199 parameters: QueryParameters,
200}
201
202impl QueryRequest {
203 fn new(query: BatchQuery) -> Self {
204 Self {
205 query_name: None,
206 query,
207 parameters: QueryParameters::default(),
208 }
209 }
210
211 pub fn read(query: ReadBatch) -> Self {
213 Self::new(BatchQuery::Read(query))
214 }
215
216 pub fn write(query: WriteBatch) -> Self {
218 Self::new(BatchQuery::Write(query))
219 }
220
221 pub const fn request_type(&self) -> QueryRequestType {
223 match self.query {
224 BatchQuery::Read(_) => QueryRequestType::Read,
225 BatchQuery::Write(_) => QueryRequestType::Write,
226 }
227 }
228
229 pub const fn query(&self) -> &BatchQuery {
231 &self.query
232 }
233
234 pub fn query_name(&self) -> Option<&str> {
236 self.query_name.as_deref()
237 }
238
239 pub fn parameters(&self) -> Option<&BTreeMap<String, QueryValue>> {
241 let values = match &self.parameters {
242 QueryParameters::Untyped(values) | QueryParameters::Typed { values, .. } => values,
243 };
244 (!values.is_empty()).then_some(values)
245 }
246
247 pub fn parameter_types(&self) -> Option<&BTreeMap<String, QueryParamType>> {
249 match &self.parameters {
250 QueryParameters::Untyped(_) => None,
251 QueryParameters::Typed { types, .. } => Some(types),
252 }
253 }
254
255 pub fn into_query(self) -> (BatchQuery, BTreeMap<String, QueryValue>) {
257 let values = match self.parameters {
258 QueryParameters::Untyped(values) | QueryParameters::Typed { values, .. } => values,
259 };
260 (self.query, values)
261 }
262
263 pub fn try_insert_untyped_parameter(
265 &mut self,
266 name: impl Into<String>,
267 value: QueryValue,
268 ) -> Result<(), QueryError> {
269 let name = name.into();
270 validate_parameter_name(&name)?;
271 validate_json_value(&value, &name)?;
272 match &mut self.parameters {
273 QueryParameters::Untyped(values) => {
274 if values.contains_key(&name) {
275 return Err(QueryError::DuplicateParameterName(name));
276 }
277 values.insert(name, value);
278 Ok(())
279 }
280 QueryParameters::Typed { .. } => Err(QueryError::MixedParameterModes),
281 }
282 }
283
284 pub fn insert_parameter_value(&mut self, name: impl Into<String>, value: QueryValue) {
289 self.try_insert_untyped_parameter(name, value)
290 .expect("untyped query parameter must be valid");
291 }
292
293 pub fn try_insert_typed_parameter(
295 &mut self,
296 name: impl Into<String>,
297 ty: QueryParamType,
298 value: QueryValue,
299 ) -> Result<(), QueryError> {
300 let name = name.into();
301 validate_parameter_name(&name)?;
302 let value = normalize_typed_value(&ty, value, &name)?;
303 if matches!(&self.parameters, QueryParameters::Untyped(values) if values.is_empty()) {
304 self.parameters = QueryParameters::Typed {
305 values: BTreeMap::new(),
306 types: BTreeMap::new(),
307 };
308 }
309 match &mut self.parameters {
310 QueryParameters::Untyped(_) => Err(QueryError::MixedParameterModes),
311 QueryParameters::Typed { values, types } => {
312 if values.contains_key(&name) {
313 return Err(QueryError::DuplicateParameterName(name));
314 }
315 values.insert(name.clone(), value);
316 types.insert(name, ty);
317 Ok(())
318 }
319 }
320 }
321
322 pub fn set_query_name(&mut self, name: impl Into<String>) {
324 self.query_name = Some(name.into());
325 }
326
327 pub fn clear_query_name(&mut self) {
329 self.query_name = None;
330 }
331
332 pub fn with_parameter_value(mut self, name: impl Into<String>, value: QueryValue) -> Self {
334 self.insert_parameter_value(name, value);
335 self
336 }
337
338 pub fn with_typed_parameter(
340 mut self,
341 name: impl Into<String>,
342 ty: QueryParamType,
343 value: QueryValue,
344 ) -> Result<Self, QueryError> {
345 self.try_insert_typed_parameter(name, ty, value)?;
346 Ok(self)
347 }
348
349 pub fn with_query_name(mut self, name: impl Into<String>) -> Self {
351 self.set_query_name(name);
352 self
353 }
354
355 pub fn to_json_bytes(&self) -> Result<Vec<u8>, QueryError> {
357 Ok(sonic_rs::to_vec(self)?)
358 }
359
360 pub fn to_json_string(&self) -> Result<String, QueryError> {
362 Ok(String::from_utf8(self.to_json_bytes()?)?)
363 }
364}
365
366#[derive(Serialize)]
367struct QueryRequestRef<'a> {
368 request_type: QueryRequestType,
369 query_name: &'a Option<String>,
370 query: &'a BatchQuery,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 parameters: Option<&'a BTreeMap<String, QueryValue>>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 parameter_types: Option<&'a BTreeMap<String, QueryParamType>>,
375}
376
377impl Serialize for QueryRequest {
378 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
379 where
380 S: Serializer,
381 {
382 QueryRequestRef {
383 request_type: self.request_type(),
384 query_name: &self.query_name,
385 query: &self.query,
386 parameters: self.parameters(),
387 parameter_types: self.parameter_types(),
388 }
389 .serialize(serializer)
390 }
391}
392
393#[derive(Deserialize)]
394struct RawQueryRequest {
395 request_type: QueryRequestType,
396 #[serde(default)]
397 query_name: Option<String>,
398 query: BatchQuery,
399 #[serde(default)]
400 parameters: Option<UniqueMap<QueryValue>>,
401 #[serde(default)]
402 parameter_types: Option<UniqueMap<QueryParamType>>,
403}
404
405struct UniqueMap<T>(BTreeMap<String, T>);
406
407impl<'de, T> Deserialize<'de> for UniqueMap<T>
408where
409 T: Deserialize<'de>,
410{
411 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
412 where
413 D: Deserializer<'de>,
414 {
415 struct UniqueMapVisitor<T>(std::marker::PhantomData<T>);
416
417 impl<'de, T> Visitor<'de> for UniqueMapVisitor<T>
418 where
419 T: Deserialize<'de>,
420 {
421 type Value = UniqueMap<T>;
422
423 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 formatter.write_str("an object with unique parameter names")
425 }
426
427 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
428 where
429 M: MapAccess<'de>,
430 {
431 let mut values = BTreeMap::new();
432 while let Some((name, value)) = map.next_entry::<String, T>()? {
433 if values.insert(name.clone(), value).is_some() {
434 return Err(serde::de::Error::custom(
435 QueryError::DuplicateParameterName(name),
436 ));
437 }
438 }
439 Ok(UniqueMap(values))
440 }
441 }
442
443 deserializer.deserialize_map(UniqueMapVisitor(std::marker::PhantomData))
444 }
445}
446
447impl<'de> Deserialize<'de> for QueryRequest {
448 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
449 where
450 D: Deserializer<'de>,
451 {
452 let raw = RawQueryRequest::deserialize(deserializer)?;
453 if !matches!(
454 (&raw.request_type, &raw.query),
455 (QueryRequestType::Read, BatchQuery::Read(_))
456 | (QueryRequestType::Write, BatchQuery::Write(_))
457 ) {
458 return Err(serde::de::Error::custom(
459 "request_type must match the query batch variant",
460 ));
461 }
462
463 let values = raw.parameters.map_or_else(BTreeMap::new, |values| values.0);
464 let parameters = match raw.parameter_types.map(|types| types.0) {
465 None => {
466 for (name, value) in &values {
467 validate_parameter_name(name).map_err(serde::de::Error::custom)?;
468 validate_json_value(value, name).map_err(serde::de::Error::custom)?;
469 }
470 QueryParameters::Untyped(values)
471 }
472 Some(types) => {
473 let missing_values = types
474 .keys()
475 .filter(|name| !values.contains_key(*name))
476 .cloned()
477 .collect::<Vec<_>>();
478 let extra_values = values
479 .keys()
480 .filter(|name| !types.contains_key(*name))
481 .cloned()
482 .collect::<Vec<_>>();
483 if !missing_values.is_empty() || !extra_values.is_empty() {
484 return Err(serde::de::Error::custom(
485 QueryError::ParameterNameMismatch {
486 missing_values,
487 extra_values,
488 },
489 ));
490 }
491 let values = values
492 .into_iter()
493 .map(|(name, value)| {
494 validate_parameter_name(&name).map_err(serde::de::Error::custom)?;
495 let ty = types
496 .get(&name)
497 .expect("schema and value names were proven equal");
498 normalize_typed_value(ty, value, &name)
499 .map(|value| (name, value))
500 .map_err(serde::de::Error::custom)
501 })
502 .collect::<Result<BTreeMap<_, _>, _>>()?;
503 QueryParameters::Typed { values, types }
504 }
505 };
506
507 Ok(Self {
508 query_name: raw.query_name,
509 query: raw.query,
510 parameters,
511 })
512 }
513}
514
515fn validate_parameter_name(name: &str) -> Result<(), QueryError> {
516 if name.is_empty() {
517 Err(QueryError::InvalidParameterName)
518 } else {
519 Ok(())
520 }
521}
522
523fn validate_json_value(value: &QueryValue, path: &str) -> Result<(), QueryError> {
524 match value {
525 QueryValue::F64(value) if !value.is_finite() => Err(QueryError::ParameterTypeMismatch {
526 path: path.to_owned(),
527 expected: QueryParamType::Value,
528 actual: "non-finite f64",
529 }),
530 QueryValue::F32(value) if !value.is_finite() => Err(QueryError::ParameterTypeMismatch {
531 path: path.to_owned(),
532 expected: QueryParamType::Value,
533 actual: "non-finite f32",
534 }),
535 QueryValue::Array(values) => values
536 .iter()
537 .enumerate()
538 .try_for_each(|(index, value)| validate_json_value(value, &format!("{path}[{index}]"))),
539 QueryValue::Object(values) => values
540 .iter()
541 .try_for_each(|(name, value)| validate_json_value(value, &format!("{path}.{name}"))),
542 QueryValue::Null
543 | QueryValue::Bool(_)
544 | QueryValue::I64(_)
545 | QueryValue::F64(_)
546 | QueryValue::F32(_)
547 | QueryValue::String(_) => Ok(()),
548 }
549}
550
551fn normalize_typed_value(
552 ty: &QueryParamType,
553 value: QueryValue,
554 path: &str,
555) -> Result<QueryValue, QueryError> {
556 let actual = query_value_kind(&value);
557 match (ty, value) {
558 (QueryParamType::Bool, value @ QueryValue::Bool(_))
559 | (QueryParamType::I64, value @ QueryValue::I64(_))
560 | (QueryParamType::String, value @ QueryValue::String(_)) => Ok(value),
561 (QueryParamType::F64, QueryValue::F64(value)) if value.is_finite() => {
562 Ok(QueryValue::F64(value))
563 }
564 (QueryParamType::F64, QueryValue::F32(value)) if value.is_finite() => {
565 Ok(QueryValue::F64(value.into()))
566 }
567 (QueryParamType::F32, QueryValue::F32(value)) if value.is_finite() => {
568 Ok(QueryValue::F32(value))
569 }
570 (QueryParamType::F32, QueryValue::F64(value))
571 if value.is_finite()
572 && value >= f64::from(f32::MIN)
573 && value <= f64::from(f32::MAX) =>
574 {
575 Ok(QueryValue::F32(value as f32))
576 }
577 (QueryParamType::DateTime, QueryValue::String(datetime))
578 if chrono::DateTime::parse_from_rfc3339(&datetime).is_ok() =>
579 {
580 Ok(QueryValue::String(datetime))
581 }
582 (QueryParamType::Value, value) => {
583 validate_json_value(&value, path)?;
584 Ok(value)
585 }
586 (QueryParamType::Object, value @ QueryValue::Object(_)) => {
587 validate_json_value(&value, path)?;
588 Ok(value)
589 }
590 (QueryParamType::Array(inner), QueryValue::Array(values)) => values
591 .into_iter()
592 .enumerate()
593 .map(|(index, value)| normalize_typed_value(inner, value, &format!("{path}[{index}]")))
594 .collect::<Result<Vec<_>, _>>()
595 .map(QueryValue::Array),
596 (QueryParamType::Bytes, _) => Err(QueryError::unsupported_bytes(path)),
597 (expected, _) => Err(QueryError::ParameterTypeMismatch {
598 path: path.to_owned(),
599 expected: expected.clone(),
600 actual,
601 }),
602 }
603}
604
605fn query_value_kind(value: &QueryValue) -> &'static str {
606 match value {
607 QueryValue::Null => "null",
608 QueryValue::Bool(_) => "bool",
609 QueryValue::I64(_) => "i64",
610 QueryValue::F64(_) => "f64",
611 QueryValue::F32(_) => "f32",
612 QueryValue::String(_) => "string",
613 QueryValue::Array(_) => "array",
614 QueryValue::Object(_) => "object",
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use crate::batch::{read_batch, write_batch};
622
623 fn typed(ty: QueryParamType, value: QueryValue) -> Result<QueryRequest, QueryError> {
624 QueryRequest::read(read_batch()).with_typed_parameter("value", ty, value)
625 }
626
627 fn read_wire(parameters: &str, parameter_types: Option<&str>) -> String {
628 let parameter_types = parameter_types
629 .map(|types| format!(r#","parameter_types":{types}"#))
630 .unwrap_or_default();
631 format!(
632 r#"{{"request_type":"read","query_name":null,"query":{{"read":{{"entries":[],"returns":[]}}}},"parameters":{parameters}{parameter_types}}}"#
633 )
634 }
635
636 #[test]
637 fn request_serde_accepts_matching_tags_and_rejects_both_disagreements() {
638 let read = QueryRequest::read(read_batch())
639 .to_json_string()
640 .expect("read request should serialize");
641 let write = QueryRequest::write(write_batch())
642 .to_json_string()
643 .expect("write request should serialize");
644
645 let parsed_read =
646 sonic_rs::from_str::<QueryRequest>(&read).expect("read/read should deserialize");
647 let parsed_write =
648 sonic_rs::from_str::<QueryRequest>(&write).expect("write/write should deserialize");
649 assert_eq!(parsed_read.request_type(), QueryRequestType::Read);
650 assert_eq!(parsed_write.request_type(), QueryRequestType::Write);
651
652 let read_tagged_write =
653 write.replacen(r#""request_type":"write""#, r#""request_type":"read""#, 1);
654 let write_tagged_read =
655 read.replacen(r#""request_type":"read""#, r#""request_type":"write""#, 1);
656 assert!(sonic_rs::from_str::<QueryRequest>(&read_tagged_write).is_err());
657 assert!(sonic_rs::from_str::<QueryRequest>(&write_tagged_read).is_err());
658 }
659
660 #[test]
661 fn typed_parameter_schema_matrix_accepts_only_valid_shapes() {
662 assert!(typed(QueryParamType::Bool, QueryValue::Bool(true)).is_ok());
663 assert!(typed(QueryParamType::Bool, QueryValue::I64(1)).is_err());
664
665 assert!(typed(QueryParamType::I64, QueryValue::I64(i64::MAX)).is_ok());
666 assert!(typed(QueryParamType::I64, QueryValue::F64(1.0)).is_err());
667
668 assert!(typed(QueryParamType::F64, QueryValue::F64(1.25)).is_ok());
669 let f64_from_f32 = typed(QueryParamType::F64, QueryValue::F32(1.25)).unwrap();
670 assert!(matches!(
671 f64_from_f32.parameters().unwrap().get("value"),
672 Some(QueryValue::F64(value)) if *value == 1.25
673 ));
674 assert!(typed(QueryParamType::F64, QueryValue::F64(f64::NAN)).is_err());
675
676 let f32_from_json = typed(QueryParamType::F32, QueryValue::F64(1.25)).unwrap();
677 assert!(matches!(
678 f32_from_json.parameters().unwrap().get("value"),
679 Some(QueryValue::F32(value)) if *value == 1.25
680 ));
681 assert!(typed(QueryParamType::F32, QueryValue::F64(f64::MAX)).is_err());
682 assert!(typed(QueryParamType::F32, QueryValue::F32(f32::INFINITY)).is_err());
683
684 assert!(typed(QueryParamType::String, QueryValue::String("x".to_owned())).is_ok());
685 assert!(typed(QueryParamType::String, QueryValue::Null).is_err());
686
687 assert!(typed(
688 QueryParamType::DateTime,
689 QueryValue::String("2026-07-28T12:34:56Z".to_owned()),
690 )
691 .is_ok());
692 assert!(typed(
693 QueryParamType::DateTime,
694 QueryValue::String("28 July 2026".to_owned()),
695 )
696 .is_err());
697
698 assert!(matches!(
699 typed(QueryParamType::Bytes, QueryValue::String("AQID".to_owned())),
700 Err(QueryError::UnsupportedBytesParameter(path)) if path == "value"
701 ));
702
703 assert!(typed(
704 QueryParamType::Value,
705 QueryValue::Array(vec![QueryValue::Object(BTreeMap::from([(
706 "nested".to_owned(),
707 QueryValue::Null,
708 )]))]),
709 )
710 .is_ok());
711 assert!(typed(
712 QueryParamType::Value,
713 QueryValue::Array(vec![QueryValue::F64(f64::INFINITY)]),
714 )
715 .is_err());
716
717 assert!(typed(QueryParamType::Object, QueryValue::Object(BTreeMap::new())).is_ok());
718 assert!(typed(QueryParamType::Object, QueryValue::Array(Vec::new())).is_err());
719
720 assert!(typed(
721 QueryParamType::Array(Box::new(QueryParamType::Bool)),
722 QueryValue::Array(vec![QueryValue::Bool(true), QueryValue::Bool(false)]),
723 )
724 .is_ok());
725 assert!(typed(
726 QueryParamType::Array(Box::new(QueryParamType::Bool)),
727 QueryValue::Array(vec![QueryValue::Bool(true), QueryValue::I64(0)]),
728 )
729 .is_err());
730 }
731
732 #[test]
733 fn parameter_modes_names_and_duplicate_entries_are_closed() {
734 let mut untyped = QueryRequest::read(read_batch());
735 untyped
736 .try_insert_untyped_parameter("value", QueryValue::Bool(true))
737 .unwrap();
738 assert!(matches!(
739 untyped.try_insert_untyped_parameter("value", QueryValue::Bool(false)),
740 Err(QueryError::DuplicateParameterName(name)) if name == "value"
741 ));
742 assert!(matches!(
743 untyped.try_insert_typed_parameter(
744 "typed",
745 QueryParamType::Bool,
746 QueryValue::Bool(true),
747 ),
748 Err(QueryError::MixedParameterModes)
749 ));
750
751 let mut typed = QueryRequest::read(read_batch());
752 typed
753 .try_insert_typed_parameter("value", QueryParamType::Bool, QueryValue::Bool(true))
754 .unwrap();
755 assert!(matches!(
756 typed.try_insert_typed_parameter(
757 "value",
758 QueryParamType::Bool,
759 QueryValue::Bool(false),
760 ),
761 Err(QueryError::DuplicateParameterName(name)) if name == "value"
762 ));
763 assert!(matches!(
764 typed.try_insert_untyped_parameter("untyped", QueryValue::Bool(true)),
765 Err(QueryError::MixedParameterModes)
766 ));
767
768 assert!(matches!(
769 QueryRequest::read(read_batch()).with_typed_parameter(
770 "",
771 QueryParamType::Bool,
772 QueryValue::Bool(true),
773 ),
774 Err(QueryError::InvalidParameterName)
775 ));
776 }
777
778 #[test]
779 fn raw_parameter_dto_rejects_mismatched_empty_and_duplicate_names() {
780 let missing_value = read_wire(r#"{}"#, Some(r#"{"value":"bool"}"#));
781 let extra_value = read_wire(r#"{"value":true}"#, Some(r#"{}"#));
782 let empty_name = read_wire(r#"{"":true}"#, Some(r#"{"":"bool"}"#));
783 let duplicate_value = read_wire(
784 r#"{"value":true,"value":false}"#,
785 Some(r#"{"value":"bool"}"#),
786 );
787 let duplicate_type = read_wire(
788 r#"{"value":true}"#,
789 Some(r#"{"value":"bool","value":"bool"}"#),
790 );
791
792 for invalid in [
793 missing_value,
794 extra_value,
795 empty_name,
796 duplicate_value,
797 duplicate_type,
798 ] {
799 assert!(
800 sonic_rs::from_str::<QueryRequest>(&invalid).is_err(),
801 "invalid DTO should be rejected: {invalid}"
802 );
803 }
804 }
805
806 #[test]
807 fn raw_f32_is_normalized_and_untyped_parameters_remain_explicit() {
808 let raw = read_wire(r#"{"value":1.25}"#, Some(r#"{"value":"f32"}"#));
809 let typed = sonic_rs::from_str::<QueryRequest>(&raw).expect("valid typed f32 request");
810 assert!(matches!(
811 typed.parameters().unwrap().get("value"),
812 Some(QueryValue::F32(value)) if *value == 1.25
813 ));
814
815 let raw = read_wire(r#"{"value":{"nested":[true,1,"x"]}}"#, None);
816 let untyped = sonic_rs::from_str::<QueryRequest>(&raw).expect("valid untyped JSON request");
817 assert!(untyped.parameter_types().is_none());
818 assert!(matches!(
819 untyped.parameters().unwrap().get("value"),
820 Some(QueryValue::Object(_))
821 ));
822 }
823}