1use std::fmt;
5
6use jiff::Timestamp;
7use serde::{Deserialize, Serialize};
8
9use crate::id::WorkId;
10use crate::provider::ProviderId;
11use crate::work::Work;
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[non_exhaustive]
18pub struct SourceRecord {
19 pub source: ProviderId,
20 pub ids: Vec<WorkId>,
23 pub work: Work,
24 pub fields: FieldSet,
28 pub raw: Raw,
29 pub fetched_at: Timestamp,
30}
31
32impl SourceRecord {
33 pub fn new(
36 source: ProviderId,
37 ids: Vec<WorkId>,
38 work: Work,
39 fields: FieldSet,
40 raw: Raw,
41 fetched_at: Timestamp,
42 ) -> Self {
43 SourceRecord {
44 source,
45 ids,
46 work,
47 fields,
48 raw,
49 fetched_at,
50 }
51 }
52
53 pub fn native_id(&self) -> Option<&WorkId> {
55 self.ids.first()
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct Raw {
63 pub media_type: String,
65 pub body: String,
66}
67
68impl Raw {
69 pub fn json(value: &serde_json::Value) -> Self {
70 Raw {
71 media_type: "application/json".into(),
72 body: value.to_string(),
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80#[non_exhaustive]
81pub enum Field {
82 Title,
83 Authors,
84 Issued,
85 Type,
86 Container,
88 Publication,
90 Abstract,
91 Url,
92 CitedByCount,
93 Updates,
95 OpenAccess,
97}
98
99impl Field {
100 pub const ALL: &[Field] = &[
102 Field::Title,
103 Field::Authors,
104 Field::Issued,
105 Field::Type,
106 Field::Container,
107 Field::Publication,
108 Field::Abstract,
109 Field::Url,
110 Field::CitedByCount,
111 Field::Updates,
112 Field::OpenAccess,
113 ];
114
115 const fn bit(self) -> u16 {
116 1 << self as u16
117 }
118}
119
120#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
122pub struct FieldSet(u16);
123
124impl FieldSet {
125 pub const EMPTY: FieldSet = FieldSet(0);
126 pub const ALL: FieldSet = FieldSet::of(Field::ALL);
127 pub const DEFAULT: FieldSet = FieldSet::of(&[
129 Field::Title,
130 Field::Authors,
131 Field::Issued,
132 Field::Type,
133 Field::Container,
134 ]);
135
136 pub const fn of(fields: &[Field]) -> FieldSet {
137 let mut bits = 0;
138 let mut i = 0;
139 while i < fields.len() {
140 bits |= fields[i].bit();
141 i += 1;
142 }
143 FieldSet(bits)
144 }
145
146 pub const fn contains(self, field: Field) -> bool {
147 self.0 & field.bit() != 0
148 }
149
150 pub const fn is_subset(self, other: FieldSet) -> bool {
151 self.0 & !other.0 == 0
152 }
153
154 pub const fn union(self, other: FieldSet) -> FieldSet {
155 FieldSet(self.0 | other.0)
156 }
157
158 pub const fn difference(self, other: FieldSet) -> FieldSet {
159 FieldSet(self.0 & !other.0)
160 }
161
162 pub const fn intersection(self, other: FieldSet) -> FieldSet {
163 FieldSet(self.0 & other.0)
164 }
165
166 pub const fn with(self, field: Field) -> FieldSet {
167 FieldSet(self.0 | field.bit())
168 }
169
170 pub const fn is_empty(self) -> bool {
171 self.0 == 0
172 }
173
174 pub fn iter(self) -> impl Iterator<Item = Field> {
175 Field::ALL
176 .iter()
177 .copied()
178 .filter(move |f| self.contains(*f))
179 }
180}
181
182impl FromIterator<Field> for FieldSet {
183 fn from_iter<I: IntoIterator<Item = Field>>(iter: I) -> Self {
184 iter.into_iter().fold(FieldSet::EMPTY, FieldSet::with)
185 }
186}
187
188impl fmt::Debug for FieldSet {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 f.debug_set().entries(self.iter()).finish()
191 }
192}
193
194impl Serialize for FieldSet {
195 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
196 s.collect_seq(self.iter())
197 }
198}
199
200impl<'de> Deserialize<'de> for FieldSet {
201 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
202 Ok(Vec::<Field>::deserialize(d)?.into_iter().collect())
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn subset_logic_drives_cache_hits() {
212 let cached = FieldSet::DEFAULT;
213 assert!(FieldSet::of(&[Field::Title, Field::Issued]).is_subset(cached));
214 assert!(!FieldSet::DEFAULT.with(Field::Abstract).is_subset(cached));
215 assert_eq!(
216 FieldSet::DEFAULT.with(Field::Abstract).difference(cached),
217 FieldSet::of(&[Field::Abstract])
218 );
219 assert!(FieldSet::EMPTY.is_subset(FieldSet::EMPTY));
220 assert!(FieldSet::ALL.iter().eq(Field::ALL.iter().copied()));
221 }
222
223 #[test]
224 fn field_set_serialises_as_names() {
225 let set = FieldSet::of(&[Field::Title, Field::CitedByCount]);
226 let json = serde_json::to_string(&set).unwrap();
227 assert_eq!(json, r#"["title","cited_by_count"]"#);
228 assert_eq!(serde_json::from_str::<FieldSet>(&json).unwrap(), set);
229 }
230}