Skip to content

Commit 6192fbe

Browse files
committed
doc mapper: convert number handling to deserialization
change number deserialization in docmapper from json to generic deserialization. This improves codes reuse between different code paths, e.g. serialization and validation.
1 parent a91d2e7 commit 6192fbe

File tree

6 files changed

+235
-123
lines changed

6 files changed

+235
-123
lines changed

quickwit/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ sea-query-binder = { version = "0.5", features = [
212212
# ^1.0.184 due to serde-rs/serde#2538
213213
serde = { version = "1.0.184", features = ["derive", "rc"] }
214214
serde_json = "1.0"
215-
serde_json_borrow = "0.5"
215+
serde_json_borrow = "0.7"
216216
serde_qs = { version = "0.12", features = ["warp"] }
217217
serde_with = "3.9.0"
218218
serde_yaml = "0.9"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Copyright (C) 2024 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at [email protected].
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
use std::fmt;
21+
22+
use serde::de::{self, Deserializer, IntoDeserializer, Visitor};
23+
use serde::Deserialize;
24+
use serde_json::Value;
25+
26+
/// Deserialize a number.
27+
///
28+
/// If the value is a string, it can be optionally coerced to a number.
29+
fn deserialize_num_with_coerce<'de, T, D>(deserializer: D, coerce: bool) -> Result<T, String>
30+
where
31+
T: std::str::FromStr + Deserialize<'de>,
32+
T::Err: fmt::Display,
33+
D: Deserializer<'de>,
34+
{
35+
struct CoerceVisitor<T> {
36+
coerce: bool,
37+
marker: std::marker::PhantomData<T>,
38+
}
39+
40+
impl<'de, T> Visitor<'de> for CoerceVisitor<T>
41+
where
42+
T: std::str::FromStr + Deserialize<'de>,
43+
T::Err: fmt::Display,
44+
{
45+
type Value = T;
46+
47+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
48+
formatter.write_str("a number (i64, u64, or f64) or a string that can be coerced")
49+
}
50+
51+
fn visit_str<E>(self, v: &str) -> Result<T, E>
52+
where E: de::Error {
53+
if self.coerce {
54+
v.parse::<T>().map_err(|_e| {
55+
de::Error::custom(format!(
56+
"failed to coerce JSON string `\"{}\"` to {}",
57+
v,
58+
std::any::type_name::<T>(),
59+
))
60+
})
61+
} else {
62+
Err(de::Error::custom(format!(
63+
"expected JSON number, got string `\"{}\"`. enable coercion to {} with the \
64+
`coerce` parameter in the field mapping",
65+
v,
66+
std::any::type_name::<T>()
67+
)))
68+
}
69+
}
70+
71+
fn visit_i64<E>(self, v: i64) -> Result<T, E>
72+
where E: de::Error {
73+
T::deserialize(v.into_deserializer()).map_err(|_: E| {
74+
de::Error::custom(format!(
75+
"expected {}, got inconvertible JSON number `{}`",
76+
std::any::type_name::<T>(),
77+
v
78+
))
79+
})
80+
}
81+
82+
fn visit_u64<E>(self, v: u64) -> Result<T, E>
83+
where E: de::Error {
84+
T::deserialize(v.into_deserializer()).map_err(|_: E| {
85+
de::Error::custom(format!(
86+
"expected {}, got inconvertible JSON number `{}`",
87+
std::any::type_name::<T>(),
88+
v
89+
))
90+
})
91+
}
92+
93+
fn visit_f64<E>(self, v: f64) -> Result<T, E>
94+
where E: de::Error {
95+
T::deserialize(v.into_deserializer()).map_err(|_: E| {
96+
de::Error::custom(format!(
97+
"expected {}, got inconvertible JSON number `{}`",
98+
std::any::type_name::<T>(),
99+
v
100+
))
101+
})
102+
}
103+
104+
fn visit_map<M>(self, mut map: M) -> Result<T, M::Error>
105+
where M: de::MapAccess<'de> {
106+
let json_value: Value =
107+
Deserialize::deserialize(de::value::MapAccessDeserializer::new(&mut map))?;
108+
Err(de::Error::custom(format!(
109+
"expected JSON number or string, got `{}`",
110+
json_value
111+
)))
112+
}
113+
114+
fn visit_seq<S>(self, mut seq: S) -> Result<T, S::Error>
115+
where S: de::SeqAccess<'de> {
116+
let json_value: Value =
117+
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(&mut seq))?;
118+
Err(de::Error::custom(format!(
119+
"expected JSON number or string, got `{}`",
120+
json_value
121+
)))
122+
}
123+
}
124+
125+
deserializer
126+
.deserialize_any(CoerceVisitor {
127+
coerce,
128+
marker: std::marker::PhantomData,
129+
})
130+
.map_err(|err| err.to_string())
131+
}
132+
133+
pub fn deserialize_i64<'de, D>(deserializer: D, coerce: bool) -> Result<i64, String>
134+
where D: Deserializer<'de> {
135+
deserialize_num_with_coerce(deserializer, coerce)
136+
}
137+
138+
pub fn deserialize_u64<'de, D>(deserializer: D, coerce: bool) -> Result<u64, String>
139+
where D: Deserializer<'de> {
140+
deserialize_num_with_coerce(deserializer, coerce)
141+
}
142+
143+
pub fn deserialize_f64<'de, D>(deserializer: D, coerce: bool) -> Result<f64, String>
144+
where D: Deserializer<'de> {
145+
deserialize_num_with_coerce(deserializer, coerce)
146+
}
147+
148+
#[cfg(test)]
149+
mod tests {
150+
use serde_json::json;
151+
152+
use super::*;
153+
154+
#[test]
155+
fn test_deserialize_i64_with_coercion() {
156+
let json_data = json!("-123");
157+
let result: i64 = deserialize_i64(json_data.into_deserializer(), true).unwrap();
158+
assert_eq!(result, -123);
159+
160+
let json_data = json!("456");
161+
let result: i64 = deserialize_i64(json_data.into_deserializer(), true).unwrap();
162+
assert_eq!(result, 456);
163+
}
164+
165+
#[test]
166+
fn test_deserialize_u64_with_coercion() {
167+
let json_data = json!("789");
168+
let result: u64 = deserialize_u64(json_data.into_deserializer(), true).unwrap();
169+
assert_eq!(result, 789);
170+
171+
let json_data = json!(123);
172+
let result: u64 = deserialize_u64(json_data.into_deserializer(), false).unwrap();
173+
assert_eq!(result, 123);
174+
}
175+
176+
#[test]
177+
fn test_deserialize_f64_with_coercion() {
178+
let json_data = json!("78.9");
179+
let result: f64 = deserialize_f64(json_data.into_deserializer(), true).unwrap();
180+
assert_eq!(result, 78.9);
181+
182+
let json_data = json!(45.6);
183+
let result: f64 = deserialize_f64(json_data.into_deserializer(), false).unwrap();
184+
assert_eq!(result, 45.6);
185+
}
186+
187+
#[test]
188+
fn test_deserialize_invalid_string_coercion() {
189+
let json_data = json!("abc");
190+
let result: Result<i64, _> = deserialize_i64(json_data.into_deserializer(), true);
191+
assert!(result.is_err());
192+
193+
let err_msg = result.unwrap_err().to_string();
194+
assert_eq!(err_msg, "failed to coerce JSON string `\"abc\"` to i64");
195+
}
196+
197+
#[test]
198+
fn test_deserialize_json_object() {
199+
let json_data = json!({ "key": "value" });
200+
let result: Result<i64, _> = deserialize_i64(json_data.into_deserializer(), true);
201+
assert!(result.is_err());
202+
203+
let err_msg = result.unwrap_err().to_string();
204+
assert_eq!(
205+
err_msg,
206+
"expected JSON number or string, got `{\"key\":\"value\"}`"
207+
);
208+
}
209+
}

quickwit/quickwit-doc-mapper/src/doc_mapper/doc_mapper_impl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1846,7 +1846,7 @@ mod tests {
18461846
}"#,
18471847
"concat",
18481848
r#"{"some_int": 25}"#,
1849-
vec![25_u64.into()],
1849+
vec![25_i64.into()],
18501850
);
18511851
}
18521852

0 commit comments

Comments
 (0)