use std::fmt;
use serde::de::{IntoDeserializer, Visitor};
use serde::Deserializer;
use url::Url;
use crate::ids::Doi;
pub(crate) fn deserialize_u64ish<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
struct U64ishVisitor;
impl Visitor<'_> for U64ishVisitor {
type Value = u64;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a non-negative integer, integer-like float, or numeric string")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
Ok(value)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
u64::try_from(value).map_err(E::custom)
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 {
return Err(E::custom("expected an integer-like numeric value"));
}
value.to_string().parse::<u64>().map_err(E::custom)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
value.parse::<u64>().map_err(E::custom)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_any(U64ishVisitor)
}
pub(crate) fn deserialize_option_u64ish<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
struct OptionalU64ishVisitor;
impl<'de> Visitor<'de> for OptionalU64ishVisitor {
type Value = Option<u64>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"an optional non-negative integer, integer-like float, or numeric string",
)
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserialize_u64ish(deserializer).map(Some)
}
}
deserializer.deserialize_option(OptionalU64ishVisitor)
}
#[allow(dead_code)]
pub(crate) fn deserialize_stringish<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
struct StringishVisitor;
impl Visitor<'_> for StringishVisitor {
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string, integer, or integer-like float")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
Ok(value.to_string())
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
u64::try_from(value)
.map(|value| value.to_string())
.map_err(E::custom)
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
deserialize_u64ish(value.into_deserializer()).map(|value| value.to_string())
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
Ok(value.to_owned())
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
Ok(value)
}
}
deserializer.deserialize_any(StringishVisitor)
}
pub(crate) fn deserialize_boolish<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
struct BoolishVisitor;
impl Visitor<'_> for BoolishVisitor {
type Value = bool;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a bool, 0/1 integer, or boolean-like string")
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
Ok(value)
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(E::custom(
"expected 0 or 1 for a boolean-like numeric value",
)),
}
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(E::custom(
"expected 0 or 1 for a boolean-like numeric value",
)),
}
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value.trim().to_ascii_lowercase().as_str() {
"true" | "1" => Ok(true),
"false" | "0" => Ok(false),
_ => Err(E::custom("expected a boolean-like string")),
}
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_any(BoolishVisitor)
}
pub(crate) fn deserialize_option_boolish<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: Deserializer<'de>,
{
struct OptionalBoolishVisitor;
impl<'de> Visitor<'de> for OptionalBoolishVisitor {
type Value = Option<bool>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an optional boolean-like value")
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserialize_boolish(deserializer).map(Some)
}
}
deserializer.deserialize_option(OptionalBoolishVisitor)
}
pub(crate) fn deserialize_option_urlish<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
where
D: Deserializer<'de>,
{
struct OptionalUrlishVisitor;
impl<'de> Visitor<'de> for OptionalUrlishVisitor {
type Value = Option<Url>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an optional URL or empty string")
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct UrlishVisitor;
impl Visitor<'_> for UrlishVisitor {
type Value = Option<Url>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a URL string or empty string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let value = value.trim();
if value.is_empty() {
return Ok(None);
}
Url::parse(value).map(Some).map_err(E::custom)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_any(UrlishVisitor)
}
}
deserializer.deserialize_option(OptionalUrlishVisitor)
}
pub(crate) fn deserialize_option_doiish<'de, D>(deserializer: D) -> Result<Option<Doi>, D::Error>
where
D: Deserializer<'de>,
{
struct OptionalDoiishVisitor;
impl<'de> Visitor<'de> for OptionalDoiishVisitor {
type Value = Option<Doi>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an optional DOI or empty string")
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct DoiishVisitor;
impl Visitor<'_> for DoiishVisitor {
type Value = Option<Doi>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a DOI string or empty string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let value = value.trim();
if value.is_empty() {
return Ok(None);
}
Doi::new(value).map(Some).map_err(E::custom)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
}
deserializer.deserialize_any(DoiishVisitor)
}
}
deserializer.deserialize_option(OptionalDoiishVisitor)
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use url::Url;
use super::{
deserialize_boolish, deserialize_option_boolish, deserialize_option_doiish,
deserialize_option_u64ish, deserialize_option_urlish, deserialize_stringish,
deserialize_u64ish,
};
use crate::ids::Doi;
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct U64Holder {
#[serde(deserialize_with = "deserialize_u64ish")]
value: u64,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct OptionalU64Holder {
#[serde(default, deserialize_with = "deserialize_option_u64ish")]
value: Option<u64>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct StringHolder {
#[serde(deserialize_with = "deserialize_stringish")]
value: String,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct BoolHolder {
#[serde(deserialize_with = "deserialize_boolish")]
value: bool,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct OptionalBoolHolder {
#[serde(default, deserialize_with = "deserialize_option_boolish")]
value: Option<bool>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct OptionalUrlHolder {
#[serde(default, deserialize_with = "deserialize_option_urlish")]
value: Option<Url>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct OptionalDoiHolder {
#[serde(default, deserialize_with = "deserialize_option_doiish")]
value: Option<Doi>,
}
#[test]
fn u64ish_accepts_integer_like_values() {
assert_eq!(
serde_json::from_value::<U64Holder>(serde_json::json!({ "value": 12 }))
.unwrap()
.value,
12
);
assert_eq!(
serde_json::from_value::<U64Holder>(serde_json::json!({ "value": 13.0 }))
.unwrap()
.value,
13
);
assert_eq!(
serde_json::from_value::<U64Holder>(serde_json::json!({ "value": "14" }))
.unwrap()
.value,
14
);
}
#[test]
fn optional_u64ish_handles_none_and_values() {
assert_eq!(
serde_json::from_value::<OptionalU64Holder>(serde_json::json!({})).unwrap(),
OptionalU64Holder { value: None }
);
assert_eq!(
serde_json::from_value::<OptionalU64Holder>(serde_json::json!({ "value": "15" }))
.unwrap(),
OptionalU64Holder { value: Some(15) }
);
}
#[test]
fn stringish_accepts_strings_and_integer_like_numbers() {
assert_eq!(
serde_json::from_value::<StringHolder>(serde_json::json!({ "value": "abc" })).unwrap(),
StringHolder {
value: "abc".into()
}
);
assert_eq!(
serde_json::from_value::<StringHolder>(serde_json::json!({ "value": 16.0 })).unwrap(),
StringHolder { value: "16".into() }
);
}
#[test]
fn boolish_accepts_bool_numeric_and_string_values() {
assert_eq!(
serde_json::from_value::<BoolHolder>(serde_json::json!({ "value": true })).unwrap(),
BoolHolder { value: true }
);
assert_eq!(
serde_json::from_value::<BoolHolder>(serde_json::json!({ "value": 1 })).unwrap(),
BoolHolder { value: true }
);
assert_eq!(
serde_json::from_value::<BoolHolder>(serde_json::json!({ "value": "false" })).unwrap(),
BoolHolder { value: false }
);
}
#[test]
fn optional_boolish_handles_none_and_values() {
assert_eq!(
serde_json::from_value::<OptionalBoolHolder>(serde_json::json!({})).unwrap(),
OptionalBoolHolder { value: None }
);
assert_eq!(
serde_json::from_value::<OptionalBoolHolder>(serde_json::json!({ "value": "1" }))
.unwrap(),
OptionalBoolHolder { value: Some(true) }
);
}
#[test]
fn u64ish_rejects_invalid_values() {
assert!(serde_json::from_value::<U64Holder>(serde_json::json!({ "value": -1 })).is_err());
assert!(serde_json::from_value::<U64Holder>(serde_json::json!({ "value": 1.25 })).is_err());
assert!(
serde_json::from_value::<U64Holder>(serde_json::json!({ "value": "nope" })).is_err()
);
}
#[test]
fn optional_u64ish_handles_null_and_invalid_values() {
assert_eq!(
serde_json::from_value::<OptionalU64Holder>(serde_json::json!({ "value": null }))
.unwrap(),
OptionalU64Holder { value: None }
);
assert!(
serde_json::from_value::<OptionalU64Holder>(serde_json::json!({ "value": -2 }))
.is_err()
);
}
#[test]
fn stringish_covers_integer_and_invalid_negative_values() {
assert_eq!(
serde_json::from_value::<StringHolder>(serde_json::json!({ "value": 17 })).unwrap(),
StringHolder { value: "17".into() }
);
assert!(
serde_json::from_value::<StringHolder>(serde_json::json!({ "value": -1 })).is_err()
);
}
#[test]
fn boolish_rejects_invalid_numeric_and_string_values() {
assert!(serde_json::from_value::<BoolHolder>(serde_json::json!({ "value": 2 })).is_err());
assert!(
serde_json::from_value::<BoolHolder>(serde_json::json!({ "value": "yes" })).is_err()
);
}
#[test]
fn optional_boolish_handles_null_and_invalid_values() {
assert_eq!(
serde_json::from_value::<OptionalBoolHolder>(serde_json::json!({ "value": null }))
.unwrap(),
OptionalBoolHolder { value: None }
);
assert!(
serde_json::from_value::<OptionalBoolHolder>(serde_json::json!({ "value": 2 }))
.is_err()
);
}
#[test]
fn optional_urlish_handles_empty_null_and_real_urls() {
assert_eq!(
serde_json::from_value::<OptionalUrlHolder>(serde_json::json!({ "value": "" }))
.unwrap(),
OptionalUrlHolder { value: None }
);
assert_eq!(
serde_json::from_value::<OptionalUrlHolder>(serde_json::json!({ "value": null }))
.unwrap(),
OptionalUrlHolder { value: None }
);
assert_eq!(
serde_json::from_value::<OptionalUrlHolder>(
serde_json::json!({ "value": "https://figshare.com/articles/1" })
)
.unwrap(),
OptionalUrlHolder {
value: Some(Url::parse("https://figshare.com/articles/1").unwrap())
}
);
assert!(serde_json::from_value::<OptionalUrlHolder>(
serde_json::json!({ "value": "not a url" })
)
.is_err());
}
#[test]
fn optional_doiish_handles_empty_null_and_real_dois() {
assert_eq!(
serde_json::from_value::<OptionalDoiHolder>(serde_json::json!({ "value": "" }))
.unwrap(),
OptionalDoiHolder { value: None }
);
assert_eq!(
serde_json::from_value::<OptionalDoiHolder>(serde_json::json!({ "value": null }))
.unwrap(),
OptionalDoiHolder { value: None }
);
assert_eq!(
serde_json::from_value::<OptionalDoiHolder>(
serde_json::json!({ "value": "10.6084/m9.figshare.123" })
)
.unwrap(),
OptionalDoiHolder {
value: Some(Doi::new("10.6084/m9.figshare.123").unwrap())
}
);
assert!(serde_json::from_value::<OptionalDoiHolder>(
serde_json::json!({ "value": "not a doi" })
)
.is_err());
}
}