Skip to content

Commit 396d80a

Browse files
authored
impl(wkt): support custom u32 encoding (#2342)
1 parent ac608bf commit 396d80a

File tree

4 files changed

+180
-66
lines changed

4 files changed

+180
-66
lines changed

src/wkt/src/internal.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
//! These types are intended for developers of the Google Cloud client libraries
1818
//! for Rust. They are undocumented and may change at any time.
1919
20+
#[macro_use]
21+
mod visitor_32;
2022
mod int32;
2123
pub use int32::I32;
24+
mod uint32;
25+
pub use uint32::U32;
2226

2327
pub struct F32;
2428
pub struct F64;

src/wkt/src/internal/int32.rs

Lines changed: 1 addition & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -29,72 +29,7 @@ impl<'de> serde_with::DeserializeAs<'de, i32> for I32 {
2929
}
3030
}
3131

32-
const ERRMSG: &str = "a 32-bit signed integer";
33-
34-
struct I32Visitor;
35-
36-
impl serde::de::Visitor<'_> for I32Visitor {
37-
type Value = i32;
38-
39-
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
40-
where
41-
E: serde::de::Error,
42-
{
43-
// ProtoJSON says that both strings and numbers are accepted. Parse the
44-
// string as a `f64` number (all JSON numbers are `f64`) and then try to
45-
// parse that as an `i32`.
46-
let number = value.parse::<f64>().map_err(E::custom)?;
47-
self.visit_f64(number)
48-
}
49-
50-
fn visit_i64<E>(self, value: i64) -> std::result::Result<Self::Value, E>
51-
where
52-
E: serde::de::Error,
53-
{
54-
match value {
55-
_ if value < i32::MIN as i64 => Err(self::value_error(value)),
56-
_ if value > i32::MAX as i64 => Err(self::value_error(value)),
57-
_ => Ok(value as i32),
58-
}
59-
}
60-
61-
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
62-
where
63-
E: serde::de::Error,
64-
{
65-
match value {
66-
_ if value > i32::MAX as u64 => Err(self::value_error(value)),
67-
_ => Ok(value as i32),
68-
}
69-
}
70-
71-
fn visit_f64<E>(self, value: f64) -> std::result::Result<Self::Value, E>
72-
where
73-
E: serde::de::Error,
74-
{
75-
match value {
76-
_ if value < i32::MIN as f64 => Err(self::value_error(value)),
77-
_ if value > i32::MAX as f64 => Err(self::value_error(value)),
78-
_ if value.fract().abs() > 0.0 => Err(self::value_error(value)),
79-
// The number is "rounded towards zero". Because we are in range,
80-
// and the fractional part is 0, this conversion should be safe.
81-
// See https://doc.rust-lang.org/reference/expressions/operator-expr.html#r-expr.as.numeric.float-as-int
82-
_ => Ok(value as i32),
83-
}
84-
}
85-
86-
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
87-
formatter.write_str("a 32-bit integer in ProtoJSON format")
88-
}
89-
}
90-
91-
fn value_error<T, E>(value: T) -> E
92-
where
93-
T: std::fmt::Display,
94-
E: serde::de::Error,
95-
{
96-
E::invalid_value(Other(&format!("{value}")), &ERRMSG)
97-
}
32+
visitor_32!(I32Visitor, i32, "a 32-bit signed integer");
9833

9934
impl serde_with::SerializeAs<i32> for I32 {
10035
fn serialize_as<S>(source: &i32, serializer: S) -> Result<S::Ok, S::Error>

src/wkt/src/internal/uint32.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Implement custom serializers for `u32`.
16+
//!
17+
//! In ProtoJSON 32-bit integers can be serialized as either strings or numbers.
18+
19+
use serde::de::Unexpected::Other;
20+
21+
pub struct U32;
22+
23+
impl<'de> serde_with::DeserializeAs<'de, u32> for U32 {
24+
fn deserialize_as<D>(deserializer: D) -> Result<u32, D::Error>
25+
where
26+
D: serde::de::Deserializer<'de>,
27+
{
28+
deserializer.deserialize_any(U32Visitor)
29+
}
30+
}
31+
32+
visitor_32!(
33+
U32Visitor,
34+
u32,
35+
"a 32-bit unsigned integer in ProtoJSON format"
36+
);
37+
38+
impl serde_with::SerializeAs<u32> for U32 {
39+
fn serialize_as<S>(source: &u32, serializer: S) -> Result<S::Ok, S::Error>
40+
where
41+
S: serde::Serializer,
42+
{
43+
serializer.serialize_u32(*source)
44+
}
45+
}
46+
47+
#[cfg(test)]
48+
mod test {
49+
use super::*;
50+
use anyhow::Result;
51+
use serde_json::{Value, json};
52+
use serde_with::{DeserializeAs, SerializeAs};
53+
use test_case::test_case;
54+
55+
#[test_case(0, 0)]
56+
#[test_case(0.0, 0; "zero as f64")]
57+
#[test_case("0", 0; "zero as string")]
58+
#[test_case("0.0", 0; "zero as f64 string")]
59+
#[test_case("2.0", 2)]
60+
#[test_case(3e5, 300_000)]
61+
#[test_case("5e4", 50_000)]
62+
#[test_case(84, 84)]
63+
#[test_case(168.0, 168)]
64+
#[test_case("21", 21)]
65+
#[test_case(u32::MAX, u32::MAX; "max")]
66+
#[test_case(u32::MAX as f64, u32::MAX; "max as f64")]
67+
#[test_case(format!("{}", u32::MAX), u32::MAX; "max as string")]
68+
#[test_case(format!("{}.0", u32::MAX), u32::MAX; "max as f64 string")]
69+
// Not quite a roundtrip test because we always serialize as numbers.
70+
fn deser_and_ser<T: serde::Serialize>(input: T, want: u32) -> Result<()> {
71+
let got = U32::deserialize_as(json!(input))?;
72+
assert_eq!(got, want);
73+
74+
let serialized = U32::serialize_as(&got, serde_json::value::Serializer)?;
75+
assert_eq!(serialized, json!(got));
76+
Ok(())
77+
}
78+
79+
#[test_case(json!(u64::MAX))]
80+
#[test_case(json!(u32::MAX as i64 + 2))]
81+
#[test_case(json!(u32::MIN as i64 - 2))]
82+
#[test_case(json!(format!("{}", u64::MAX)))]
83+
#[test_case(json!("abc"))]
84+
#[test_case(json!(123.4))]
85+
#[test_case(json!("234.5"))]
86+
#[test_case(json!({}))]
87+
fn deser_error(input: Value) {
88+
let got = U32::deserialize_as(input).unwrap_err();
89+
assert!(got.is_data(), "{got:?}");
90+
}
91+
}

src/wkt/src/internal/visitor_32.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
macro_rules! visitor_32 {
16+
($name: ident, $t: ty, $msg: literal) => {
17+
struct $name;
18+
19+
impl serde::de::Visitor<'_> for $name {
20+
type Value = $t;
21+
22+
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
23+
where
24+
E: serde::de::Error,
25+
{
26+
// ProtoJSON says that both strings and numbers are accepted.
27+
// Parse the string as a `f64` number (all JSON numbers are
28+
// `f64`) and then try to parse the result as the target type.
29+
let number = value.parse::<f64>().map_err(E::custom)?;
30+
self.visit_f64(number)
31+
}
32+
33+
fn visit_i64<E>(self, value: i64) -> std::result::Result<Self::Value, E>
34+
where
35+
E: serde::de::Error,
36+
{
37+
match value {
38+
_ if value < <$t>::MIN as i64 => Err(self::value_error(value)),
39+
_ if value > <$t>::MAX as i64 => Err(self::value_error(value)),
40+
_ => Ok(value as Self::Value),
41+
}
42+
}
43+
44+
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
45+
where
46+
E: serde::de::Error,
47+
{
48+
match value {
49+
_ if value > <$t>::MAX as u64 => Err(self::value_error(value)),
50+
_ => Ok(value as Self::Value),
51+
}
52+
}
53+
54+
fn visit_f64<E>(self, value: f64) -> std::result::Result<Self::Value, E>
55+
where
56+
E: serde::de::Error,
57+
{
58+
match value {
59+
_ if value < <$t>::MIN as f64 => Err(self::value_error(value)),
60+
_ if value > <$t>::MAX as f64 => Err(self::value_error(value)),
61+
_ if value.fract().abs() > 0.0 => Err(self::value_error(value)),
62+
// In Rust floating point to integer conversions are
63+
// "rounded towards zero":
64+
// https://doc.rust-lang.org/reference/expressions/operator-expr.html#r-expr.as.numeric.float-as-int
65+
// Because we are in range, and the fractional part is 0,
66+
// this conversion is safe.
67+
_ => Ok(value as Self::Value),
68+
}
69+
}
70+
71+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
72+
formatter.write_str($msg)
73+
}
74+
}
75+
76+
fn value_error<T, E>(value: T) -> E
77+
where
78+
T: std::fmt::Display,
79+
E: serde::de::Error,
80+
{
81+
E::invalid_value(Other(&format!("{value}")), &$msg)
82+
}
83+
};
84+
}

0 commit comments

Comments
 (0)