Skip to content

Commit

Permalink
Add JsonSchemaAs impls for all Duration and Timestamp adaptors (#685)
Browse files Browse the repository at this point in the history
jonasbb authored Jan 30, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 00382b9 + cf29573 commit dca18c4
Showing 3 changed files with 429 additions and 2 deletions.
236 changes: 235 additions & 1 deletion serde_with/src/schemars_0_8.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
//! see [`JsonSchemaAs`].
use crate::{
formats::{Flexible, Separator, Strict},
formats::{Flexible, Format, Separator, Strict},
prelude::{Schema as WrapSchema, *},
};
use ::schemars_0_8::{
@@ -609,3 +609,237 @@ where
{
forward_schema!(Vec<WrapSchema<T, TA>>);
}

mod timespan {
use super::*;

// #[non_exhaustive] is not actually necessary here but it should
// help avoid warnings about semver breakage if this ever changes.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TimespanTargetType {
String,
F64,
U64,
I64,
}

impl TimespanTargetType {
pub const fn is_signed(self) -> bool {
!matches!(self, Self::U64)
}
}

/// Internal helper trait used to constrain which types we implement
/// `JsonSchemaAs<T>` for.
pub trait TimespanSchemaTarget<F> {
/// The underlying type.
///
/// This is mainly used to decide which variant of the resulting schema
/// should be marked as `write_only: true`.
const TYPE: TimespanTargetType;

/// Whether the target type is signed.
///
/// This is only true for `std::time::Duration`.
const SIGNED: bool = true;
}

macro_rules! timespan_type_of {
(String) => {
TimespanTargetType::String
};
(f64) => {
TimespanTargetType::F64
};
(i64) => {
TimespanTargetType::I64
};
(u64) => {
TimespanTargetType::U64
};
}

macro_rules! declare_timespan_target {
( $target:ty { $($format:ident),* $(,)? } ) => {
$(
impl TimespanSchemaTarget<$format> for $target {
const TYPE: TimespanTargetType = timespan_type_of!($format);
}
)*
}
}

impl TimespanSchemaTarget<u64> for Duration {
const TYPE: TimespanTargetType = TimespanTargetType::U64;
const SIGNED: bool = false;
}

impl TimespanSchemaTarget<f64> for Duration {
const TYPE: TimespanTargetType = TimespanTargetType::F64;
const SIGNED: bool = false;
}

impl TimespanSchemaTarget<String> for Duration {
const TYPE: TimespanTargetType = TimespanTargetType::String;
const SIGNED: bool = false;
}

declare_timespan_target!(SystemTime { i64, f64, String });

#[cfg(feature = "chrono_0_4")]
declare_timespan_target!(::chrono_0_4::Duration { i64, f64, String });
#[cfg(feature = "chrono_0_4")]
declare_timespan_target!(::chrono_0_4::DateTime<::chrono_0_4::Utc> { i64, f64, String });
#[cfg(feature = "chrono_0_4")]
declare_timespan_target!(::chrono_0_4::DateTime<::chrono_0_4::Local> { i64, f64, String });
#[cfg(feature = "chrono_0_4")]
declare_timespan_target!(::chrono_0_4::NaiveDateTime { i64, f64, String });

#[cfg(feature = "time_0_3")]
declare_timespan_target!(::time_0_3::Duration { i64, f64, String });
#[cfg(feature = "time_0_3")]
declare_timespan_target!(::time_0_3::OffsetDateTime { i64, f64, String });
#[cfg(feature = "time_0_3")]
declare_timespan_target!(::time_0_3::PrimitiveDateTime { i64, f64, String });
}

use self::timespan::{TimespanSchemaTarget, TimespanTargetType};

/// Internal type used for the base impls on DurationXXX and TimestampYYY types.
///
/// This allows the JsonSchema impls that are Strict to be generic without
/// committing to it as part of the public API.
struct Timespan<Format, Strictness>(PhantomData<(Format, Strictness)>);

impl<T, F> JsonSchemaAs<T> for Timespan<F, Strict>
where
T: TimespanSchemaTarget<F>,
F: Format + JsonSchema,
{
forward_schema!(F);
}

impl TimespanTargetType {
pub(crate) fn to_flexible_schema(self, signed: bool) -> Schema {
use ::schemars_0_8::schema::StringValidation;

let mut number = SchemaObject {
instance_type: Some(InstanceType::Number.into()),
number: (!signed).then(|| {
Box::new(NumberValidation {
minimum: Some(0.0),
..Default::default()
})
}),
..Default::default()
};

// This is a more lenient version of the regex used to determine
// whether JSON numbers are valid. Specifically, it allows multiple
// leading zeroes whereas that is illegal in JSON.
let regex = r#"[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?"#;
let mut string = SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
pattern: Some(match signed {
true => std::format!("^-?{regex}$"),
false => std::format!("^{regex}$"),
}),
..Default::default()
})),
..Default::default()
};

if self == Self::String {
number.metadata().write_only = true;
} else {
string.metadata().write_only = true;
}

SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(std::vec![number.into(), string.into()]),
..Default::default()
})),
..Default::default()
}
.into()
}

pub(crate) fn schema_id(self) -> &'static str {
match self {
Self::String => "serde_with::FlexibleStringTimespan",
Self::F64 => "serde_with::FlexibleF64Timespan",
Self::U64 => "serde_with::FlexibleU64Timespan",
Self::I64 => "serde_with::FlexibleI64Timespan",
}
}
}

impl<T, F> JsonSchemaAs<T> for Timespan<F, Flexible>
where
T: TimespanSchemaTarget<F>,
F: Format + JsonSchema,
{
fn schema_name() -> String {
<T as TimespanSchemaTarget<F>>::TYPE
.schema_id()
.strip_prefix("serde_with::")
.expect("schema id did not start with `serde_with::` - this is a bug")
.into()
}

fn schema_id() -> Cow<'static, str> {
<T as TimespanSchemaTarget<F>>::TYPE.schema_id().into()
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
<T as TimespanSchemaTarget<F>>::TYPE
.to_flexible_schema(<T as TimespanSchemaTarget<F>>::SIGNED)
}

fn is_referenceable() -> bool {
false
}
}

macro_rules! forward_duration_schema {
($ty:ident) => {
impl<T, F> JsonSchemaAs<T> for $ty<F, Strict>
where
T: TimespanSchemaTarget<F>,
F: Format + JsonSchema
{
forward_schema!(WrapSchema<T, Timespan<F, Strict>>);
}

impl<T, F> JsonSchemaAs<T> for $ty<F, Flexible>
where
T: TimespanSchemaTarget<F>,
F: Format + JsonSchema
{
forward_schema!(WrapSchema<T, Timespan<F, Flexible>>);
}
};
}

forward_duration_schema!(DurationSeconds);
forward_duration_schema!(DurationMilliSeconds);
forward_duration_schema!(DurationMicroSeconds);
forward_duration_schema!(DurationNanoSeconds);

forward_duration_schema!(DurationSecondsWithFrac);
forward_duration_schema!(DurationMilliSecondsWithFrac);
forward_duration_schema!(DurationMicroSecondsWithFrac);
forward_duration_schema!(DurationNanoSecondsWithFrac);

forward_duration_schema!(TimestampSeconds);
forward_duration_schema!(TimestampMilliSeconds);
forward_duration_schema!(TimestampMicroSeconds);
forward_duration_schema!(TimestampNanoSeconds);

forward_duration_schema!(TimestampSecondsWithFrac);
forward_duration_schema!(TimestampMilliSecondsWithFrac);
forward_duration_schema!(TimestampMicroSecondsWithFrac);
forward_duration_schema!(TimestampNanoSecondsWithFrac);
125 changes: 124 additions & 1 deletion serde_with/tests/schemars_0_8.rs
Original file line number Diff line number Diff line change
@@ -180,7 +180,7 @@ mod test_std {

mod snapshots {
use super::*;
use serde_with::formats::CommaSeparator;
use serde_with::formats::*;
use std::collections::BTreeSet;

declare_snapshot_test! {
@@ -239,6 +239,25 @@ mod snapshots {
data: BTreeSet<u32>,
}
}

duration {
struct Test {
#[serde_as(as = "DurationSeconds<u64, Flexible>")]
seconds: std::time::Duration,

#[serde_as(as = "DurationSecondsWithFrac<f64, Flexible>")]
frac: std::time::Duration,

#[serde_as(as = "DurationSeconds<String, Flexible>")]
flexible_string: std::time::Duration,

#[serde_as(as = "DurationSeconds<u64, Strict>")]
seconds_u64_strict: std::time::Duration,

#[serde_as(as = "TimestampSeconds<i64, Flexible>")]
time_i64: std::time::SystemTime,
}
}
}
}

@@ -439,6 +458,110 @@ mod bytes_or_string {
}
}

mod duration {
use super::*;
use serde_with::formats::{Flexible, Strict};
use std::time::{Duration, SystemTime};

#[serde_as]
#[derive(Serialize, JsonSchema)]
struct DurationTest {
#[serde_as(as = "DurationSeconds<u64, Strict>")]
strict_u64: Duration,

#[serde_as(as = "DurationSeconds<String, Strict>")]
strict_str: Duration,

#[serde_as(as = "DurationSecondsWithFrac<f64, Strict>")]
strict_f64: Duration,

#[serde_as(as = "DurationSeconds<u64, Flexible>")]
flexible_u64: Duration,

#[serde_as(as = "DurationSeconds<f64, Flexible>")]
flexible_f64: Duration,

#[serde_as(as = "DurationSeconds<String, Flexible>")]
flexible_str: Duration,
}

#[test]
fn test_serialized_is_valid() {
check_valid_json_schema(&DurationTest {
strict_u64: Duration::from_millis(2500),
strict_str: Duration::from_millis(2500),
strict_f64: Duration::from_millis(2500),
flexible_u64: Duration::from_millis(2500),
flexible_f64: Duration::from_millis(2500),
flexible_str: Duration::from_millis(2500),
});
}

#[serde_as]
#[derive(Serialize, JsonSchema)]
struct FlexibleU64Duration(#[serde_as(as = "DurationSeconds<u64, Flexible>")] Duration);

#[serde_as]
#[derive(Serialize, JsonSchema)]
struct FlexibleStringDuration(#[serde_as(as = "DurationSeconds<String, Flexible>")] Duration);

#[serde_as]
#[derive(Serialize, JsonSchema)]
struct FlexibleTimestamp(#[serde_as(as = "TimestampSeconds<i64, Flexible>")] SystemTime);

#[test]
fn test_string_as_flexible_u64() {
check_matches_schema::<FlexibleU64Duration>(&json!("32"));
}

#[test]
fn test_integer_as_flexible_u64() {
check_matches_schema::<FlexibleU64Duration>(&json!(16));
}

#[test]
fn test_number_as_flexible_u64() {
check_matches_schema::<FlexibleU64Duration>(&json!(54.1));
}

#[test]
#[should_panic]
fn test_negative_as_flexible_u64() {
check_matches_schema::<FlexibleU64Duration>(&json!(-5));
}

#[test]
fn test_string_as_flexible_string() {
check_matches_schema::<FlexibleStringDuration>(&json!("32"));
}

#[test]
fn test_integer_as_flexible_string() {
check_matches_schema::<FlexibleStringDuration>(&json!(16));
}

#[test]
fn test_number_as_flexible_string() {
check_matches_schema::<FlexibleStringDuration>(&json!(54.1));
}

#[test]
#[should_panic]
fn test_negative_as_flexible_string() {
check_matches_schema::<FlexibleStringDuration>(&json!(-5));
}

#[test]
fn test_negative_as_flexible_timestamp() {
check_matches_schema::<FlexibleTimestamp>(&json!(-50000));
}

#[test]
fn test_negative_string_as_flexible_timestamp() {
check_matches_schema::<FlexibleTimestamp>(&json!("-50000"));
}
}

#[test]
fn test_borrow_cow() {
use std::borrow::Cow;
Loading

0 comments on commit dca18c4

Please sign in to comment.