use core::fmt;
use std::borrow::Cow;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::ser::SerializeStruct as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _, ser::Error as _};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Range {
pub min: f64,
pub max: f64,
}
impl Range {
#[must_use]
pub const fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
#[must_use]
pub fn contains(&self, value: f64) -> bool {
value.is_finite() && value >= self.min && value <= self.max
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Reported<T> {
value: T,
out_of_range: bool,
}
impl Reported<f64> {
#[must_use]
pub fn new(value: f64, range: Range) -> Self {
Self {
value,
out_of_range: !range.contains(value),
}
}
#[must_use]
pub const fn value(&self) -> f64 {
self.value
}
#[must_use]
pub const fn out_of_range(&self) -> bool {
self.out_of_range
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ReportedWire {
value: f64,
out_of_range: bool,
}
impl Serialize for Reported<f64> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if !self.value.is_finite() {
return Err(S::Error::custom(format!(
"a non-finite reported value ({}) has no JSON number spelling, and emitting it \
as `null` would parse back as a field the source did not report",
self.value
)));
}
let mut state = serializer.serialize_struct("Reported", 2)?;
state.serialize_field("value", &self.value)?;
state.serialize_field("out_of_range", &self.out_of_range)?;
state.end()
}
}
impl<'de> Deserialize<'de> for Reported<f64> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = ReportedWire::deserialize(deserializer)?;
Ok(Self {
value: wire.value,
out_of_range: wire.out_of_range,
})
}
}
impl JsonSchema for Reported<f64> {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Reported")
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "object",
"title": "Reported",
"description": "A value as a source reported it, flagged when it is outside the \
plausibility range this crate declares for its field.",
"properties": {
"value": { "type": "number" },
"out_of_range": { "type": "boolean" }
},
"required": ["value", "out_of_range"],
"additionalProperties": false
})
}
}
impl fmt::Display for Reported<f64> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.out_of_range {
write!(formatter, "{} (out of range)", self.value)
} else {
write!(formatter, "{}", self.value)
}
}
}
pub fn deserialize_ranged<'de, D: Deserializer<'de>>(
deserializer: D,
field: &str,
range: Range,
) -> Result<Option<Reported<f64>>, D::Error> {
let Some(reported) = Option::<Reported<f64>>::deserialize(deserializer)? else {
return Ok(None);
};
let expected = !range.contains(reported.value);
if reported.out_of_range != expected {
return Err(D::Error::custom(format!(
"`{field}` reported value {} with out_of_range {}, but the declared range \
{} to {} makes it {expected}",
reported.value, reported.out_of_range, range.min, range.max
)));
}
Ok(Some(reported))
}