Skip to content

Commit

Permalink
Integer deserialization for page ranges (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
reknih authored Oct 10, 2024
1 parent 471280b commit 9e30220
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 53 deletions.
11 changes: 9 additions & 2 deletions src/csl/taxonomy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,15 @@ impl EntryLike for citationberg::json::Item {
) -> Option<MaybeTyped<PageRanges>> {
match variable {
PageVariable::Page => match self.0.get("page")? {
csl_json::Value::Number(n) => {
Some(MaybeTyped::Typed(PageRanges::from(*n as u64)))
&csl_json::Value::Number(n) => {
// Page ranges use i32 internally, so we check whether the
// number is in range.
Some(match i32::try_from(n) {
Ok(n) => MaybeTyped::Typed(PageRanges::from(n)),
// If the number is not in range, we degrade to a
// string, which disables some CSL features.
Err(_) => MaybeTyped::String(n.to_string()),
})
}
csl_json::Value::String(s) => {
let res = MaybeTyped::<PageRanges>::infallible_from_str(s);
Expand Down
115 changes: 74 additions & 41 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,41 @@ macro_rules! deserialize_from_str {
D: serde::Deserializer<'de>,
{
let s = <&'de str>::deserialize(deserializer)?;
FromStr::from_str(s).map_err(de::Error::custom)
FromStr::from_str(s).map_err(serde::de::Error::custom)
}
}
};
}

macro_rules! custom_deserialize {
($type_name:ident where $expect:literal $($additional_visitors:item)+) => {
impl<'de> Deserialize<'de> for $type_name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::fmt;
use serde::de::{Visitor};
struct OurVisitor;

impl<'de> Visitor<'de> for OurVisitor {
type Value = $type_name;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str($expect)
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Self::Value::from_str(value).map_err(|e| E::custom(e.to_string()))
}

$($additional_visitors)*
}

deserializer.deserialize_any(OurVisitor)
}
}
};
Expand Down Expand Up @@ -75,53 +109,29 @@ macro_rules! derive_or_from_str {
}


impl<'de> Deserialize<'de> for $s {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::fmt;
use serde::de::{self, Visitor};
struct OurVisitor;

impl<'de> Visitor<'de> for OurVisitor {
type Value = $s;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str($expect)
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Self::Value::from_str(value).map_err(|e| E::custom(e.to_string()))
}
crate::types::custom_deserialize!(
$s where $expect
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where A: serde::de::MapAccess<'de>, {
use serde::{de, Deserialize};

fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct Inner {
$(
$(#[serde $serde])*
$i: $t,
)*
}

Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
.map(|inner: Inner| $s { $($i: inner.$i),* })
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct Inner {
$(
$(#[serde $serde])*
$i: $t,
)*
}

deserializer.deserialize_any(OurVisitor)
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
.map(|inner: Inner| $s { $($i: inner.$i),* })
}
}
);
};
}

use custom_deserialize;
use derive_or_from_str;
use deserialize_from_str;
use serialize_display;
Expand Down Expand Up @@ -595,4 +605,27 @@ mod tests {
assert!(Numeric::from_str("second").is_err());
assert!(Numeric::from_str("2nd edition").is_err());
}

#[test]
#[cfg(feature = "biblatex")]
fn test_issue_227() {
let yaml = r#"
AAAnonymous_AventureMortevielle_1987:
type: Book
page-range: 100"#;

let library = crate::io::from_yaml_str(yaml).unwrap();
let entry = library.get("AAAnonymous_AventureMortevielle_1987").unwrap();
assert_eq!(
entry
.page_range
.as_ref()
.unwrap()
.as_typed()
.unwrap()
.first()
.unwrap(),
&Numeric::new(100)
);
}
}
4 changes: 3 additions & 1 deletion src/types/numeric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ impl<'de> Deserialize<'de> for Numeric {

/// A default serde fallthrough handler for signed integers.
fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(Numeric::new(v.try_into().map_err(|_| E::custom("value too large"))?))
Ok(Numeric::new(
v.try_into().map_err(|_| E::custom("value out of bounds"))?,
))
}

fn visit_i32<E: Error>(self, v: i32) -> Result<Self::Value, E> {
Expand Down
105 changes: 96 additions & 9 deletions src/types/page.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use std::{cmp::Ordering, fmt::Display, num::NonZeroUsize, str::FromStr};
use std::{
cmp::Ordering,
fmt::Display,
num::{NonZeroUsize, TryFromIntError},
str::FromStr,
};

use crate::{MaybeTyped, Numeric, NumericError};

use super::{deserialize_from_str, serialize_display};
use serde::{de, Deserialize, Serialize};
use super::{custom_deserialize, serialize_display};
use serde::{Deserialize, Serialize};
use thiserror::Error;

impl MaybeTyped<PageRanges> {
Expand All @@ -23,6 +28,22 @@ pub struct PageRanges {
pub ranges: Vec<PageRangesPart>,
}

custom_deserialize!(
PageRanges where "pages, page ranges, ampersands, and commas"
fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
Ok(PageRanges::from(v))
}
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
PageRanges::try_from(v).map_err(|_| E::custom("value too large"))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
PageRanges::try_from(v).map_err(|_| E::custom("value out of bounds"))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
PageRanges::try_from(v).map_err(|_| E::custom("value too large"))
}
);

impl PageRanges {
/// Create a new `PageRanges` struct.
pub fn new(ranges: Vec<PageRangesPart>) -> Self {
Expand Down Expand Up @@ -76,12 +97,36 @@ impl PageRanges {
}
}

impl From<u64> for PageRanges {
fn from(value: u64) -> Self {
impl From<i32> for PageRanges {
fn from(value: i32) -> Self {
Self { ranges: vec![value.into()] }
}
}

impl TryFrom<u32> for PageRanges {
type Error = TryFromIntError;

fn try_from(value: u32) -> Result<Self, Self::Error> {
Ok(Self { ranges: vec![value.try_into()?] })
}
}

impl TryFrom<i64> for PageRanges {
type Error = TryFromIntError;

fn try_from(value: i64) -> Result<Self, Self::Error> {
Ok(Self { ranges: vec![value.try_into()?] })
}
}

impl TryFrom<u64> for PageRanges {
type Error = TryFromIntError;

fn try_from(value: u64) -> Result<Self, Self::Error> {
Ok(Self { ranges: vec![value.try_into()?] })
}
}

impl Display for PageRanges {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.ranges.iter().try_for_each(|r| r.fmt(f))
Expand Down Expand Up @@ -116,6 +161,22 @@ pub enum PageRangesPart {
Range(Numeric, Numeric),
}

custom_deserialize!(
PageRangesPart where "a page, a page range, or a separator"
fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
Ok(PageRangesPart::from(v))
}
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
PageRangesPart::try_from(v).map_err(|_| E::custom("value too large"))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
PageRangesPart::try_from(v).map_err(|_| E::custom("value out of bounds"))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
PageRangesPart::try_from(v).map_err(|_| E::custom("value too large"))
}
);

impl PageRangesPart {
/// The start of a range, if any.
pub fn start(&self) -> Option<&Numeric> {
Expand Down Expand Up @@ -169,9 +230,36 @@ impl PageRangesPart {
}
}

impl From<u64> for PageRangesPart {
fn from(value: u64) -> Self {
Self::SinglePage((value as u32).into())
impl From<i32> for PageRangesPart {
fn from(value: i32) -> Self {
Self::SinglePage(value.into())
}
}

impl TryFrom<u32> for PageRangesPart {
type Error = TryFromIntError;

fn try_from(value: u32) -> Result<Self, Self::Error> {
let value: i32 = value.try_into()?;
Ok(Self::SinglePage(value.into()))
}
}

impl TryFrom<i64> for PageRangesPart {
type Error = TryFromIntError;

fn try_from(value: i64) -> Result<Self, Self::Error> {
let value: i32 = value.try_into()?;
Ok(Self::SinglePage(value.into()))
}
}

impl TryFrom<u64> for PageRangesPart {
type Error = TryFromIntError;

fn try_from(value: u64) -> Result<Self, Self::Error> {
let value: i32 = value.try_into()?;
Ok(Self::SinglePage(value.into()))
}
}

Expand Down Expand Up @@ -246,7 +334,6 @@ impl FromStr for PageRangesPart {
}
}

deserialize_from_str!(PageRanges);
serialize_display!(PageRanges);

fn parse_number(s: &str) -> Result<Numeric, NumericError> {
Expand Down

0 comments on commit 9e30220

Please sign in to comment.