use std::fmt;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use crate::id::WorkId;
use crate::provider::ProviderId;
use crate::work::Work;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SourceRecord {
pub source: ProviderId,
pub ids: Vec<WorkId>,
pub work: Work,
pub fields: FieldSet,
pub raw: Raw,
pub fetched_at: Timestamp,
}
impl SourceRecord {
pub fn new(
source: ProviderId,
ids: Vec<WorkId>,
work: Work,
fields: FieldSet,
raw: Raw,
fetched_at: Timestamp,
) -> Self {
SourceRecord {
source,
ids,
work,
fields,
raw,
fetched_at,
}
}
pub fn native_id(&self) -> Option<&WorkId> {
self.ids.first()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Raw {
pub media_type: String,
pub body: String,
}
impl Raw {
pub fn json(value: &serde_json::Value) -> Self {
Raw {
media_type: "application/json".into(),
body: value.to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Field {
Title,
Authors,
Issued,
Type,
Container,
Publication,
Abstract,
Url,
CitedByCount,
Updates,
OpenAccess,
}
impl Field {
pub const ALL: &[Field] = &[
Field::Title,
Field::Authors,
Field::Issued,
Field::Type,
Field::Container,
Field::Publication,
Field::Abstract,
Field::Url,
Field::CitedByCount,
Field::Updates,
Field::OpenAccess,
];
const fn bit(self) -> u16 {
1 << self as u16
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct FieldSet(u16);
impl FieldSet {
pub const EMPTY: FieldSet = FieldSet(0);
pub const ALL: FieldSet = FieldSet::of(Field::ALL);
pub const DEFAULT: FieldSet = FieldSet::of(&[
Field::Title,
Field::Authors,
Field::Issued,
Field::Type,
Field::Container,
]);
pub const fn of(fields: &[Field]) -> FieldSet {
let mut bits = 0;
let mut i = 0;
while i < fields.len() {
bits |= fields[i].bit();
i += 1;
}
FieldSet(bits)
}
pub const fn contains(self, field: Field) -> bool {
self.0 & field.bit() != 0
}
pub const fn is_subset(self, other: FieldSet) -> bool {
self.0 & !other.0 == 0
}
pub const fn union(self, other: FieldSet) -> FieldSet {
FieldSet(self.0 | other.0)
}
pub const fn difference(self, other: FieldSet) -> FieldSet {
FieldSet(self.0 & !other.0)
}
pub const fn intersection(self, other: FieldSet) -> FieldSet {
FieldSet(self.0 & other.0)
}
pub const fn with(self, field: Field) -> FieldSet {
FieldSet(self.0 | field.bit())
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub fn iter(self) -> impl Iterator<Item = Field> {
Field::ALL
.iter()
.copied()
.filter(move |f| self.contains(*f))
}
}
impl FromIterator<Field> for FieldSet {
fn from_iter<I: IntoIterator<Item = Field>>(iter: I) -> Self {
iter.into_iter().fold(FieldSet::EMPTY, FieldSet::with)
}
}
impl fmt::Debug for FieldSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_set().entries(self.iter()).finish()
}
}
impl Serialize for FieldSet {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.iter())
}
}
impl<'de> Deserialize<'de> for FieldSet {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Ok(Vec::<Field>::deserialize(d)?.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subset_logic_drives_cache_hits() {
let cached = FieldSet::DEFAULT;
assert!(FieldSet::of(&[Field::Title, Field::Issued]).is_subset(cached));
assert!(!FieldSet::DEFAULT.with(Field::Abstract).is_subset(cached));
assert_eq!(
FieldSet::DEFAULT.with(Field::Abstract).difference(cached),
FieldSet::of(&[Field::Abstract])
);
assert!(FieldSet::EMPTY.is_subset(FieldSet::EMPTY));
assert!(FieldSet::ALL.iter().eq(Field::ALL.iter().copied()));
}
#[test]
fn field_set_serialises_as_names() {
let set = FieldSet::of(&[Field::Title, Field::CitedByCount]);
let json = serde_json::to_string(&set).unwrap();
assert_eq!(json, r#"["title","cited_by_count"]"#);
assert_eq!(serde_json::from_str::<FieldSet>(&json).unwrap(), set);
}
}