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}
96
97impl Field {
98 pub const ALL: [Field; 10] = [
99 Field::Title,
100 Field::Authors,
101 Field::Issued,
102 Field::Type,
103 Field::Container,
104 Field::Publication,
105 Field::Abstract,
106 Field::Url,
107 Field::CitedByCount,
108 Field::Updates,
109 ];
110
111 const fn bit(self) -> u16 {
112 1 << self as u16
113 }
114}
115
116#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
118pub struct FieldSet(u16);
119
120impl FieldSet {
121 pub const EMPTY: FieldSet = FieldSet(0);
122 pub const ALL: FieldSet = FieldSet::of(&Field::ALL);
123 pub const DEFAULT: FieldSet = FieldSet::of(&[
125 Field::Title,
126 Field::Authors,
127 Field::Issued,
128 Field::Type,
129 Field::Container,
130 ]);
131
132 pub const fn of(fields: &[Field]) -> FieldSet {
133 let mut bits = 0;
134 let mut i = 0;
135 while i < fields.len() {
136 bits |= fields[i].bit();
137 i += 1;
138 }
139 FieldSet(bits)
140 }
141
142 pub const fn contains(self, field: Field) -> bool {
143 self.0 & field.bit() != 0
144 }
145
146 pub const fn is_subset(self, other: FieldSet) -> bool {
147 self.0 & !other.0 == 0
148 }
149
150 pub const fn union(self, other: FieldSet) -> FieldSet {
151 FieldSet(self.0 | other.0)
152 }
153
154 pub const fn difference(self, other: FieldSet) -> FieldSet {
155 FieldSet(self.0 & !other.0)
156 }
157
158 pub const fn intersection(self, other: FieldSet) -> FieldSet {
159 FieldSet(self.0 & other.0)
160 }
161
162 pub const fn with(self, field: Field) -> FieldSet {
163 FieldSet(self.0 | field.bit())
164 }
165
166 pub const fn is_empty(self) -> bool {
167 self.0 == 0
168 }
169
170 pub fn iter(self) -> impl Iterator<Item = Field> {
171 Field::ALL.into_iter().filter(move |f| self.contains(*f))
172 }
173}
174
175impl FromIterator<Field> for FieldSet {
176 fn from_iter<I: IntoIterator<Item = Field>>(iter: I) -> Self {
177 iter.into_iter().fold(FieldSet::EMPTY, FieldSet::with)
178 }
179}
180
181impl fmt::Debug for FieldSet {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.debug_set().entries(self.iter()).finish()
184 }
185}
186
187impl Serialize for FieldSet {
188 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
189 s.collect_seq(self.iter())
190 }
191}
192
193impl<'de> Deserialize<'de> for FieldSet {
194 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
195 Ok(Vec::<Field>::deserialize(d)?.into_iter().collect())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn subset_logic_drives_cache_hits() {
205 let cached = FieldSet::DEFAULT;
206 assert!(FieldSet::of(&[Field::Title, Field::Issued]).is_subset(cached));
207 assert!(!FieldSet::DEFAULT.with(Field::Abstract).is_subset(cached));
208 assert_eq!(
209 FieldSet::DEFAULT.with(Field::Abstract).difference(cached),
210 FieldSet::of(&[Field::Abstract])
211 );
212 assert!(FieldSet::EMPTY.is_subset(FieldSet::EMPTY));
213 assert!(FieldSet::ALL.iter().eq(Field::ALL));
214 }
215
216 #[test]
217 fn field_set_serialises_as_names() {
218 let set = FieldSet::of(&[Field::Title, Field::CitedByCount]);
219 let json = serde_json::to_string(&set).unwrap();
220 assert_eq!(json, r#"["title","cited_by_count"]"#);
221 assert_eq!(serde_json::from_str::<FieldSet>(&json).unwrap(), set);
222 }
223}