evergreen/common/
bib.rs

1use crate as eg;
2use eg::common::holds;
3use eg::common::settings::Settings;
4use eg::idl;
5use eg::Editor;
6use eg::EgResult;
7use eg::EgValue;
8use marctk as marc;
9use std::collections::HashMap;
10
11// Bib record display attributes are used widely. May as well flesh them
12// out and provide a bit of structure.
13
14/// Value for a single display attr which may contain one or
15/// multiple values.
16#[derive(Debug, PartialEq)]
17pub enum DisplayAttrValue {
18    Value(Option<String>),
19    List(Vec<String>),
20}
21
22impl DisplayAttrValue {
23    /// Get the first value for this attibute.
24    ///
25    /// If this is a Self::Value, return the value, otherwise return the
26    /// first value in our Self::List, otherwise empty str.
27    pub fn first(&self) -> &str {
28        match self {
29            Self::Value(op) => match op {
30                Some(s) => s.as_str(),
31                None => "",
32            },
33            Self::List(v) => v.first().map(|v| v.as_str()).unwrap_or(""),
34        }
35    }
36
37    pub fn into_value(mut self) -> EgValue {
38        match self {
39            Self::Value(ref mut op) => op.take().map(EgValue::from).unwrap_or(EgValue::Null),
40            Self::List(ref mut l) => EgValue::from(std::mem::take(l)),
41        }
42    }
43}
44
45pub struct DisplayAttr {
46    name: String,
47    label: String,
48    value: DisplayAttrValue,
49}
50
51impl DisplayAttr {
52    pub fn add_value(&mut self, value: String) {
53        match self.value {
54            DisplayAttrValue::Value(ref mut op) => {
55                if let Some(s) = op.take() {
56                    self.value = DisplayAttrValue::List(vec![s, value]);
57                } else {
58                    self.value = DisplayAttrValue::Value(Some(value));
59                }
60            }
61            DisplayAttrValue::List(ref mut l) => {
62                l.push(value);
63            }
64        }
65    }
66    pub fn label(&self) -> &str {
67        &self.label
68    }
69    pub fn name(&self) -> &str {
70        &self.name
71    }
72    pub fn value(&self) -> &DisplayAttrValue {
73        &self.value
74    }
75}
76
77/// Collection of metabib.flat_display_entry data for a given record.
78pub struct DisplayAttrSet {
79    attrs: Vec<DisplayAttr>,
80}
81
82impl DisplayAttrSet {
83    pub fn into_value(mut self) -> EgValue {
84        let mut hash = eg::hash! {};
85        for attr in self.attrs.drain(..) {
86            let name = attr.name().to_string(); // moved below
87            hash[&name] = attr.value.into_value();
88        }
89        hash
90    }
91}
92
93impl DisplayAttrSet {
94    pub fn attrs(&self) -> &Vec<DisplayAttr> {
95        &self.attrs
96    }
97
98    pub fn attr(&self, name: &str) -> Option<&DisplayAttr> {
99        self.attrs.iter().find(|a| a.name.as_str() == name)
100    }
101
102    pub fn attr_mut(&mut self, name: &str) -> Option<&mut DisplayAttr> {
103        self.attrs.iter_mut().find(|a| a.name.as_str() == name)
104    }
105
106    /// Returns the first value for an attribute by name.
107    ///
108    /// For simplicity, returns an empty string if no attribute is found.
109    pub fn first_value(&self, name: &str) -> &str {
110        if let Some(attr) = self.attr(name) {
111            attr.value.first()
112        } else {
113            ""
114        }
115    }
116}
117
118/// Build a virtual mvr from a bib record's display attributes
119pub fn map_to_mvr(editor: &mut Editor, bib_id: i64) -> EgResult<EgValue> {
120    let mut maps = get_display_attrs(editor, &[bib_id])?;
121
122    let mut attr_set = match maps.remove(&bib_id) {
123        Some(m) => m,
124        None => return Err(format!("Bib {bib_id} has no display attributes").into()),
125    };
126
127    let mut mvr = eg::hash! {"doc_id": bib_id};
128
129    let idl_class = idl::get_class("mvr")?;
130
131    // Dynamically copy values from the display attribute set
132    // into an mvr JSON object.
133    let field_names = idl_class.field_names();
134
135    for attr in attr_set.attrs.iter_mut() {
136        if field_names.contains(&attr.name.as_str()) {
137            let value = std::mem::replace(&mut attr.value, DisplayAttrValue::Value(None));
138            mvr[&attr.name] = value.into_value();
139        }
140    }
141
142    EgValue::create("mvr", mvr)
143}
144
145/// Returns a HashMap mapping bib record IDs to a DisplayAttrSet.
146pub fn get_display_attrs(
147    editor: &mut Editor,
148    bib_ids: &[i64],
149) -> EgResult<HashMap<i64, DisplayAttrSet>> {
150    let mut map = HashMap::new();
151    let attrs = editor.search("mfde", eg::hash! {"source": bib_ids})?;
152
153    for attr in attrs {
154        let bib_id = attr["source"].int()?;
155
156        // First time seeing this bib record?
157        let attr_set = map
158            .entry(bib_id)
159            .or_insert_with(|| DisplayAttrSet { attrs: Vec::new() });
160
161        let attr_name = attr["name"].to_string().expect("Required");
162        let attr_label = attr["label"].to_string().expect("Required");
163        let attr_value = attr["value"].to_string().expect("Required");
164
165        if let Some(attr) = attr_set.attr_mut(&attr_name) {
166            attr.add_value(attr_value);
167        } else {
168            let attr = DisplayAttr {
169                name: attr_name,
170                label: attr_label,
171                value: DisplayAttrValue::Value(Some(attr_value)),
172            };
173            attr_set.attrs.push(attr);
174        }
175    }
176
177    Ok(map)
178}
179
180pub struct RecordSummary {
181    id: i64,
182    record: EgValue,
183    display: DisplayAttrSet,
184    attributes: EgValue,
185    urls: Option<Vec<RecordUrl>>,
186    record_note_count: usize,
187    copy_counts: Vec<EgValue>,
188    hold_count: i64,
189    has_holdable_copy: bool,
190}
191
192impl RecordSummary {
193    pub fn into_value(mut self) -> EgValue {
194        let mut urls = EgValue::new_array();
195
196        if let Some(mut list) = self.urls.take() {
197            for v in list.drain(..) {
198                urls.push(v.into_value()).expect("Is Array");
199            }
200        }
201
202        let copy_counts = std::mem::take(&mut self.copy_counts);
203
204        eg::hash! {
205            id: self.id,
206            record: self.record.take(),
207            display: self.display.into_value(),
208            record_note_count: self.record_note_count,
209            attributes: self.attributes.take(),
210            copy_counts: EgValue::from(copy_counts),
211            hold_count: self.hold_count,
212            urls: urls,
213            has_holdable_copy: self.has_holdable_copy,
214            // TODO
215            staff_view_metabib_attributes: eg::hash!{},
216            // TODO
217            staff_view_metabib_records: eg::array! [],
218        }
219    }
220}
221
222pub fn catalog_record_summary(
223    editor: &mut Editor,
224    org_id: i64,
225    rec_id: i64,
226    is_staff: bool,
227    is_meta: bool,
228) -> EgResult<RecordSummary> {
229    let flesh = eg::hash! {
230        "flesh": 1,
231        "flesh_fields": {
232            "bre": ["mattrs", "creator", "editor", "notes"]
233        }
234    };
235
236    let mut record = editor
237        .retrieve_with_ops("bre", rec_id, flesh)?
238        .ok_or_else(|| editor.die_event())?;
239
240    let mut display_map = get_display_attrs(editor, &[rec_id])?;
241
242    let display = display_map
243        .remove(&rec_id)
244        .ok_or_else(|| format!("Cannot load attrs for bib {rec_id}"))?;
245
246    // Create an object of 'mraf' attributes.
247    // Any attribute can be multi so dedupe and array-ify all of them.
248
249    let mut attrs = EgValue::new_object();
250    for attr in record["mattrs"].members_mut() {
251        let name = attr["attr"].take();
252        let val = attr["value"].take();
253
254        if let EgValue::Array(ref mut list) = attrs[name.str()?] {
255            list.push(val);
256        } else {
257            attrs[name.str()?] = vec![val].into();
258        }
259    }
260
261    let urls = record_urls(editor, None, Some(record["marc"].str()?))?;
262
263    let note_count = record["notes"].len();
264    let copy_counts = record_copy_counts(editor, org_id, rec_id, is_staff, is_meta)?;
265    let hold_count = holds::record_hold_counts(editor, rec_id, None)?;
266    let has_holdable_copy = holds::record_has_holdable_copy(editor, rec_id, is_meta)?;
267
268    // Avoid including the actual notes, which may not all be public.
269    record["notes"].take();
270
271    // De-bulk-ify
272    record["marc"].take();
273    record["mattrs"].take();
274
275    Ok(RecordSummary {
276        id: rec_id,
277        record,
278        display,
279        urls,
280        copy_counts,
281        hold_count,
282        attributes: attrs,
283        has_holdable_copy,
284        record_note_count: note_count,
285    })
286}
287
288pub struct RecordUrl {
289    href: String,
290    label: Option<String>,
291    notes: Option<String>,
292    ind2: String,
293}
294
295impl RecordUrl {
296    pub fn into_value(self) -> EgValue {
297        eg::hash! {
298            href: self.href,
299            label: self.label,
300            notes: self.notes,
301            ind2: self.ind2
302        }
303    }
304}
305
306/// Extract/compile 856 URL values from a MARC record.
307pub fn record_urls(
308    editor: &mut Editor,
309    bib_id: Option<i64>,
310    xml: Option<&str>,
311) -> EgResult<Option<Vec<RecordUrl>>> {
312    let rec_binding;
313
314    let xml = match xml.as_ref() {
315        Some(x) => x,
316        None => {
317            if let Some(id) = bib_id {
318                rec_binding = editor
319                    .retrieve("bre", id)?
320                    .ok_or_else(|| editor.die_event())?;
321                rec_binding["marc"].str()?
322            } else {
323                return Err("bib::record_urls requires params".into());
324            }
325        }
326    };
327
328    let record = match marc::Record::from_xml(xml).next() {
329        Some(result) => result?,
330        None => return Err("MARC XML parsing returned no result".into()),
331    };
332
333    let mut urls_maybe = None;
334
335    for field in record.get_fields("856").iter() {
336        if field.ind1() != "4" {
337            continue;
338        }
339
340        // asset.uri's
341        if field.has_subfield("9") || field.has_subfield("w") || field.has_subfield("n") {
342            continue;
343        }
344
345        let label_sf = field.first_subfield("y");
346        let notes_sf = field.first_subfield("z").or(field.first_subfield("3"));
347
348        for href in field.get_subfields("u").iter() {
349            if href.content().trim().is_empty() {
350                continue;
351            }
352
353            // It's possible for multiple $u's to exist within 1 856 tag.
354            // in that case, honor the label/notes data for the first $u, but
355            // leave any subsequent $u's as unadorned href's.
356            // use href/link/note keys to be consistent with args.uri's
357
358            let label = label_sf.map(|l| l.content().to_string());
359            let notes = notes_sf.map(|v| v.content().to_string());
360
361            let url = RecordUrl {
362                label,
363                notes,
364                href: href.content().to_string(),
365                ind2: field.ind2().to_string(),
366            };
367
368            let urls = match urls_maybe.as_mut() {
369                Some(u) => u,
370                None => {
371                    urls_maybe = Some(Vec::new());
372                    urls_maybe.as_mut().unwrap()
373                }
374            };
375
376            urls.push(url);
377        }
378    }
379
380    Ok(urls_maybe)
381}
382
383pub fn record_copy_counts(
384    editor: &mut Editor,
385    org_id: i64,
386    rec_id: i64,
387    is_staff: bool,
388    is_meta: bool,
389) -> EgResult<Vec<EgValue>> {
390    let key = if is_meta { "metarecord" } else { "record" };
391    let func = format!("asset.{key}_copy_count");
392    let query = eg::hash! {"from": [func, org_id, rec_id, is_staff]};
393    let mut data = editor.json_query(query)?;
394
395    for count in data.iter_mut() {
396        // Fix up the key name change; required by stored-proc version.
397        count["count"] = count["visible"].take();
398        count.remove("visible");
399    }
400
401    data.sort_by(|a, b| {
402        let da = a["depth"].int_required();
403        let db = b["depth"].int_required();
404        da.cmp(&db)
405    });
406
407    Ok(data)
408}
409
410#[derive(Debug, Clone)]
411pub struct BibSubfield {
412    pub tag: String,
413    pub subfield: String,
414}
415
416/// Returns bib tag and subfield values where bib-level call number
417/// data may be found.
418pub fn bib_call_number_fields(
419    editor: &mut Editor,
420    org_id: i64,
421) -> EgResult<Option<Vec<BibSubfield>>> {
422    // get the default classification scheme for the specified org unit.
423    let mut settings = Settings::new(editor);
424
425    let class_id = settings.get_value_at_org("cat.default_classification_scheme", org_id)?;
426
427    let Some(class_id) = class_id.as_int() else {
428        log::debug!("No value for cat.default_classification_scheme at org {org_id}");
429        return Ok(None);
430    };
431
432    let classification = editor
433        .retrieve("acnc", class_id)?
434        .ok_or_else(|| editor.die_event())?;
435
436    // Require field; could be string or number.
437    // E.g. "092ab,099ab,086ab"
438    let field_str = classification["field"].string()?;
439
440    let mut subfields = Vec::new();
441
442    for field in field_str.split(',') {
443        if field.len() < 4 {
444            continue;
445        }
446
447        let tag = &field[..3];
448
449        for sf in field[3..].split("") {
450            subfields.push(BibSubfield {
451                tag: tag.to_string(),
452                subfield: sf.to_string(),
453            });
454        }
455    }
456
457    Ok(Some(subfields))
458}