evergreen/
idl.rs

1//! Creates an in-memory representation of the fieldmapper IDL.
2use crate as eg;
3use crate::EgResult;
4use crate::EgValue;
5use roxmltree;
6use std::collections::HashMap;
7use std::fmt;
8use std::fs;
9use std::sync::Arc;
10use std::sync::OnceLock;
11
12/// Parse the IDL once and store it here, making it accessible to all
13/// threads as a read-only value.
14static GLOBAL_IDL: OnceLock<Parser> = OnceLock::new();
15
16const _OILS_NS_BASE: &str = "http://opensrf.org/spec/IDL/base/v1";
17const OILS_NS_OBJ: &str = "http://open-ils.org/spec/opensrf/IDL/objects/v1";
18const OILS_NS_PERSIST: &str = "http://open-ils.org/spec/opensrf/IDL/persistence/v1";
19const OILS_NS_REPORTER: &str = "http://open-ils.org/spec/opensrf/IDL/reporter/v1";
20const AUTO_FIELDS: [&str; 3] = ["isnew", "ischanged", "isdeleted"];
21
22/// Returns a ref to the global IDL parser instance
23pub fn parser() -> &'static Parser {
24    if let Some(idl) = GLOBAL_IDL.get() {
25        idl
26    } else {
27        log::error!("IDL Required");
28        panic!("IDL Required")
29    }
30}
31
32/// Returns a ref to an IDL class by classname.
33///
34/// Err is returned if no such classes exists.
35pub fn get_class(classname: &str) -> EgResult<&Arc<Class>> {
36    parser()
37        .classes
38        .get(classname)
39        .ok_or_else(|| format!("No such IDL class: {classname}").into())
40}
41
42/// Various forms an IDL-classed object can take internally and on
43/// the wire.
44#[derive(Debug, Clone, PartialEq)]
45pub enum DataFormat {
46    /// Traditional hash with a class key an array payload.
47    Fieldmapper,
48    /// IDL objects modeled as key/value pairs in a flat hash, with one
49    /// reserved hash key of "_classname" to contain the short IDL class
50    /// key.  No NULL values are included.
51    Hash,
52    /// Same as 'Hash' with NULL values included.  Useful for seeing
53    /// all of the key names for an IDL object, regardless of
54    /// whether a value is present for every key.
55    HashFull,
56}
57
58impl From<&str> for DataFormat {
59    fn from(s: &str) -> DataFormat {
60        match s {
61            "hash" => Self::Hash,
62            "hashfull" => Self::HashFull,
63            _ => Self::Fieldmapper,
64        }
65    }
66}
67
68impl DataFormat {
69    pub fn is_hash(&self) -> bool {
70        self == &Self::Hash || self == &Self::HashFull
71    }
72}
73
74/// Key where IDL class name/hint value is stored on unpacked JSON objects.
75/// OpenSRF has its own class key used for storing class names on
76/// packed (array-based) JSON objects, which is separate.
77//pub const CLASSNAME_KEY: &str = "_classname";
78
79#[derive(Debug, Clone, PartialEq)]
80pub enum DataType {
81    Id,
82    Int,
83    Float,
84    Text,
85    Bool,
86    Link,
87    Money,
88    OrgUnit,
89    Timestamp,
90}
91
92impl DataType {
93    pub fn is_numeric(&self) -> bool {
94        matches!(
95            *self,
96            Self::Id | Self::Int | Self::Float | Self::Money | Self::OrgUnit
97        )
98    }
99}
100
101impl From<&str> for DataType {
102    fn from(s: &str) -> Self {
103        match s {
104            "id" => Self::Id,
105            "int" => Self::Int,
106            "float" => Self::Float,
107            "text" => Self::Text,
108            "bool" => Self::Bool,
109            "timestamp" => Self::Timestamp,
110            "money" => Self::Money,
111            "org_unit" => Self::OrgUnit,
112            "link" => Self::Link,
113            _ => Self::Text,
114        }
115    }
116}
117
118impl From<&DataType> for &'static str {
119    fn from(d: &DataType) -> Self {
120        match *d {
121            DataType::Id => "id",
122            DataType::Int => "int",
123            DataType::Float => "float",
124            DataType::Text => "text",
125            DataType::Bool => "bool",
126            DataType::Timestamp => "timestamp",
127            DataType::Money => "money",
128            DataType::OrgUnit => "org_unit",
129            DataType::Link => "link",
130        }
131    }
132}
133
134impl fmt::Display for DataType {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        let s: &str = self.into();
137        write!(f, "{s}")
138    }
139}
140
141#[derive(Debug, Clone, PartialEq)]
142pub struct Field {
143    name: String,
144    label: String,
145    datatype: DataType,
146    i18n: bool,
147    array_pos: usize,
148    is_virtual: bool,
149    suppress_controller: Option<String>,
150}
151
152impl fmt::Display for Field {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(
155            f,
156            "Field: name={} datatype={} virtual={} label={}",
157            self.name, self.datatype, self.is_virtual, self.label
158        )
159    }
160}
161
162impl Field {
163    pub fn name(&self) -> &str {
164        &self.name
165    }
166    pub fn label(&self) -> &str {
167        &self.label
168    }
169    pub fn datatype(&self) -> &DataType {
170        &self.datatype
171    }
172    pub fn i18n(&self) -> bool {
173        self.i18n
174    }
175    pub fn array_pos(&self) -> usize {
176        self.array_pos
177    }
178    pub fn is_virtual(&self) -> bool {
179        self.is_virtual
180    }
181    pub fn suppress_controller(&self) -> Option<&str> {
182        self.suppress_controller.as_deref()
183    }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
187pub enum RelType {
188    HasA,
189    HasMany,
190    MightHave,
191    Unset,
192}
193
194impl From<&RelType> for &str {
195    fn from(rt: &RelType) -> &'static str {
196        match *rt {
197            RelType::HasA => "has_a",
198            RelType::HasMany => "has_many",
199            RelType::MightHave => "might_have",
200            RelType::Unset => "unset",
201        }
202    }
203}
204
205impl From<&str> for RelType {
206    fn from(s: &str) -> Self {
207        match s {
208            "has_a" => Self::HasA,
209            "has_many" => Self::HasMany,
210            "might_have" => Self::MightHave,
211            _ => Self::Unset,
212        }
213    }
214}
215
216impl fmt::Display for RelType {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        let s: &str = self.into();
219        write!(f, "{s}")
220    }
221}
222
223#[derive(Debug, Clone, PartialEq)]
224pub struct Link {
225    field: String,
226    reltype: RelType,
227    key: String,
228    map: Option<String>,
229    class: String,
230}
231
232impl Link {
233    pub fn field(&self) -> &str {
234        &self.field
235    }
236    pub fn reltype(&self) -> RelType {
237        self.reltype
238    }
239    pub fn key(&self) -> &str {
240        &self.key
241    }
242    pub fn map(&self) -> Option<&str> {
243        if let Some(map) = self.map.as_ref() {
244            if map.is_empty() || map == " " {
245                None
246            } else {
247                self.map.as_deref()
248            }
249        } else {
250            None
251        }
252    }
253    pub fn class(&self) -> &str {
254        &self.class
255    }
256}
257
258#[derive(Debug, Clone, PartialEq)]
259pub struct Class {
260    classname: String,
261    label: String,
262    field_safe: bool,
263    read_only: bool,
264
265    /// Name of primary key column
266    pkey: Option<String>,
267
268    /// Name of the column to use for the human label value
269    selector: Option<String>,
270
271    fieldmapper: Option<String>,
272    fields: HashMap<String, Field>,
273    links: HashMap<String, Link>,
274    tablename: Option<String>,
275    source_definition: Option<String>,
276    controller: Option<String>,
277    is_virtual: bool,
278}
279
280impl Class {
281    pub fn pkey(&self) -> Option<&str> {
282        self.pkey.as_deref()
283    }
284    pub fn pkey_field(&self) -> Option<&Field> {
285        if let Some(pk) = self.pkey() {
286            self.fields().values().find(|f| f.name().eq(pk))
287        } else {
288            None
289        }
290    }
291
292    pub fn selector(&self) -> Option<&str> {
293        self.selector.as_deref()
294    }
295
296    pub fn source_definition(&self) -> Option<&str> {
297        self.source_definition.as_deref()
298    }
299
300    pub fn classname(&self) -> &str {
301        &self.classname
302    }
303    pub fn label(&self) -> &str {
304        &self.label
305    }
306    pub fn fields(&self) -> &HashMap<String, Field> {
307        &self.fields
308    }
309    pub fn fieldmapper(&self) -> Option<&str> {
310        self.fieldmapper.as_deref()
311    }
312    pub fn links(&self) -> &HashMap<String, Link> {
313        &self.links
314    }
315    pub fn tablename(&self) -> Option<&str> {
316        self.tablename.as_deref()
317    }
318
319    pub fn get_field(&self, name: &str) -> Option<&Field> {
320        self.fields.get(name)
321    }
322
323    pub fn controller(&self) -> Option<&str> {
324        self.controller.as_deref()
325    }
326    pub fn is_virtual(&self) -> bool {
327        self.is_virtual
328    }
329
330    /// Vec of non-virutal fields.
331    pub fn real_fields(&self) -> Vec<&Field> {
332        let mut fields: Vec<&Field> = Vec::new();
333        for (_, field) in self.fields().iter() {
334            if !field.is_virtual() {
335                fields.push(field);
336            }
337        }
338        fields
339    }
340
341    /// Vec of non-virutal fields sorted by name.
342    pub fn real_fields_sorted(&self) -> Vec<&Field> {
343        let mut fields = self.real_fields();
344        fields.sort_by(|a, b| a.name().cmp(b.name()));
345        fields
346    }
347
348    /// Vec of non-virutal field names sorted alphabetically.
349    pub fn real_field_names_sorted(&self) -> Vec<&str> {
350        let mut names: Vec<&str> = self.real_fields().iter().map(|f| f.name()).collect();
351        names.sort();
352        names
353    }
354
355    /// Vec of all field names, unsorted.
356    pub fn field_names(&self) -> Vec<&str> {
357        self.fields().keys().map(|f| f.as_str()).collect()
358    }
359
360    pub fn has_real_field(&self, field: &str) -> bool {
361        self.fields()
362            .values()
363            .any(|f| f.name().eq(field) && !f.is_virtual())
364    }
365
366    pub fn has_field(&self, field: &str) -> bool {
367        self.fields().get(field).is_some()
368    }
369
370    pub fn get_real_field(&self, field: &str) -> Option<&Field> {
371        self.fields()
372            .values()
373            .find(|f| f.name().eq(field) && !f.is_virtual())
374    }
375}
376
377impl fmt::Display for Class {
378    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        write!(
380            f,
381            "Class: class={} fields={} links={} label={} ",
382            self.classname,
383            self.fields.len(),
384            self.links.len(),
385            self.label
386        )
387    }
388}
389
390pub struct Parser {
391    /// Store each class in an Arc so it's easier for components
392    /// to have an owned ref to the Class, which comes in handy quite
393    /// a bit.
394    classes: HashMap<String, Arc<Class>>,
395}
396
397impl fmt::Debug for Parser {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        write!(f, "IDLParser")
400    }
401}
402impl Parser {
403    /// All of our IDL classes keyed on classname/hint (e.g. "aou")
404    pub fn classes(&self) -> &HashMap<String, Arc<Class>> {
405        &self.classes
406    }
407
408    /// Load the IDL from a file.
409    ///
410    /// Returns an Err if the IDL has already been parsed and loaded, in
411    /// part to discourage unnecessary reparsing, which is a heavy job.
412    pub fn load_file(filename: &str) -> EgResult<()> {
413        let xml = match fs::read_to_string(filename) {
414            Ok(x) => x,
415            Err(e) => Err(format!("Cannot parse IDL file '{filename}': {e}"))?,
416        };
417
418        let p = Parser::parse_string(&xml)?;
419
420        if GLOBAL_IDL.set(p).is_err() {
421            return Err("Cannot initialize IDL more than once".into());
422        }
423
424        Ok(())
425    }
426
427    /// Parse the IDL as a string
428    fn parse_string(xml: &str) -> EgResult<Parser> {
429        let doc = match roxmltree::Document::parse(xml) {
430            Ok(d) => d,
431            Err(e) => Err(format!("Error parsing XML string for IDL: {e}"))?,
432        };
433
434        let mut parser = Parser {
435            classes: HashMap::new(),
436        };
437
438        for root_node in doc.root().children() {
439            if root_node.tag_name().name() == "IDL" {
440                for class_node in root_node.children() {
441                    if class_node.node_type() == roxmltree::NodeType::Element
442                        && class_node.tag_name().name() == "class"
443                    {
444                        parser.add_class(&class_node);
445                    }
446                }
447            }
448        }
449
450        Ok(parser)
451    }
452
453    fn add_class(&mut self, node: &roxmltree::Node) {
454        let name = node.attribute("id").unwrap(); // required
455
456        let label = match node.attribute((OILS_NS_REPORTER, "label")) {
457            Some(l) => l.to_string(),
458            None => name.to_string(),
459        };
460
461        let tablename = node
462            .attribute((OILS_NS_PERSIST, "tablename"))
463            .map(|v| v.to_string());
464        let fieldmapper = node
465            .attribute((OILS_NS_OBJ, "fieldmapper"))
466            .map(|v| v.to_string());
467        let controller = node.attribute("controller").map(|v| v.to_string());
468
469        let field_safe = match node.attribute((OILS_NS_PERSIST, "field_safe")) {
470            Some(v) => v.to_lowercase().eq("true"),
471            None => false,
472        };
473
474        let read_only = match node.attribute((OILS_NS_PERSIST, "readonly")) {
475            Some(v) => v.to_lowercase().eq("true"),
476            None => false,
477        };
478
479        let is_virtual: bool = match node.attribute((OILS_NS_PERSIST, "virtual")) {
480            Some(i) => i == "true",
481            None => false,
482        };
483
484        let mut class = Class {
485            tablename,
486            fieldmapper,
487            field_safe,
488            read_only,
489            controller,
490            label,
491            is_virtual,
492            classname: name.to_string(),
493            source_definition: None,
494            fields: HashMap::new(),
495            links: HashMap::new(),
496            selector: None,
497            pkey: None,
498        };
499
500        let mut field_array_pos = 0;
501
502        for child in node
503            .children()
504            .filter(|n| n.node_type() == roxmltree::NodeType::Element)
505        {
506            if child.tag_name().name() == "fields" {
507                class.pkey = child
508                    .attribute((OILS_NS_PERSIST, "primary"))
509                    .map(|v| v.to_string());
510
511                for field_node in child
512                    .children()
513                    .filter(|n| n.node_type() == roxmltree::NodeType::Element)
514                    .filter(|n| n.tag_name().name() == "field")
515                {
516                    self.add_field(&mut class, field_array_pos, &field_node);
517                    field_array_pos += 1;
518                }
519            } else if child.tag_name().name() == "links" {
520                for link_node in child
521                    .children()
522                    .filter(|n| n.node_type() == roxmltree::NodeType::Element)
523                    .filter(|n| n.tag_name().name() == "link")
524                {
525                    self.add_link(&mut class, &link_node);
526                }
527            }
528
529            if child.tag_name().name() == "source_definition" {
530                class.source_definition = child.text().map(|t| t.to_string());
531            }
532        }
533
534        self.add_auto_fields(&mut class, field_array_pos);
535
536        self.classes
537            .insert(class.classname.to_string(), Arc::new(class));
538    }
539
540    fn add_auto_fields(&self, class: &mut Class, mut pos: usize) {
541        for field in AUTO_FIELDS {
542            class.fields.insert(
543                field.to_string(),
544                Field {
545                    name: field.to_string(),
546                    label: field.to_string(),
547                    datatype: DataType::Bool,
548                    i18n: false,
549                    array_pos: pos,
550                    is_virtual: true,
551                    suppress_controller: None,
552                },
553            );
554
555            pos += 1;
556        }
557    }
558
559    fn add_field(&self, class: &mut Class, pos: usize, node: &roxmltree::Node) {
560        let label = match node.attribute((OILS_NS_REPORTER, "label")) {
561            Some(l) => l.to_string(),
562            None => "".to_string(),
563        };
564
565        let datatype: DataType = match node.attribute((OILS_NS_REPORTER, "datatype")) {
566            Some(dt) => dt.into(),
567            None => DataType::Text,
568        };
569
570        if let Some(selector) = node.attribute((OILS_NS_REPORTER, "selector")) {
571            class.selector = Some(selector.to_string());
572        };
573
574        let i18n: bool = match node.attribute((OILS_NS_PERSIST, "i18n")) {
575            Some(i) => i == "true",
576            None => false,
577        };
578
579        let is_virtual: bool = match node.attribute((OILS_NS_PERSIST, "virtual")) {
580            Some(i) => i == "true",
581            None => false,
582        };
583
584        let suppress_controller = node
585            .attribute((OILS_NS_PERSIST, "suppress_controller"))
586            .map(|c| c.to_string());
587
588        let field = Field {
589            name: node.attribute("name").unwrap().to_string(),
590            label,
591            datatype,
592            i18n,
593            array_pos: pos,
594            is_virtual,
595            suppress_controller,
596        };
597
598        class.fields.insert(field.name.to_string(), field);
599    }
600
601    fn add_link(&self, class: &mut Class, node: &roxmltree::Node) {
602        let reltype: RelType = match node.attribute("reltype") {
603            Some(rt) => rt.into(),
604            None => RelType::Unset,
605        };
606
607        let map = node.attribute("map").map(|v| v.to_string());
608
609        let field = match node.attribute("field") {
610            Some(v) => v.to_string(),
611            None => {
612                log::warn!(
613                    "IDL link for class '{}' has no 'field' attr",
614                    class.classname
615                );
616                return;
617            }
618        };
619
620        let key = match node.attribute("key") {
621            Some(v) => v.to_string(),
622            None => {
623                log::warn!("IDL link for class '{}' has no 'key' attr", class.classname);
624                return;
625            }
626        };
627
628        let lclass = match node.attribute("class") {
629            Some(v) => v.to_string(),
630            None => {
631                log::warn!(
632                    "IDL link for class '{}' has no 'class' attr",
633                    class.classname
634                );
635                return;
636            }
637        };
638
639        let link = Link {
640            field,
641            key,
642            map,
643            reltype,
644            class: lclass,
645        };
646
647        class.links.insert(link.field.to_string(), link);
648    }
649
650    /// Translates a set of path-based flesh definition into a flesh
651    /// object that can be used by cstore, etc.
652    ///
653    /// E.g. 'jub', ['lineitems.lineitem_details.owning_lib', 'lineitems.lineitem_details.fund']
654    ///
655    pub fn field_paths_to_flesh(&self, base_class: &str, paths: &[&str]) -> EgResult<EgValue> {
656        let mut flesh = eg::hash! {"flesh_fields": {}};
657        let mut flesh_depth = 1;
658
659        let base_idl_class = self
660            .classes()
661            .get(base_class)
662            .ok_or_else(|| format!("No such IDL class: {base_class}"))?;
663
664        for path in paths {
665            let mut idl_class = base_idl_class;
666
667            for (idx, fieldname) in path.split('.').enumerate() {
668                let cname = idl_class.classname();
669
670                let link_field = idl_class
671                    .links()
672                    .get(fieldname)
673                    .ok_or_else(|| format!("Class '{cname}' cannot flesh '{fieldname}'"))?;
674
675                let flesh_fields = &mut flesh["flesh_fields"];
676
677                if flesh_fields[cname].is_null() {
678                    flesh_fields[cname] = eg::array![];
679                }
680
681                if !flesh_fields[cname].contains(fieldname) {
682                    flesh_fields[cname].push(fieldname).expect("Is Array");
683                }
684
685                if flesh_depth < idx + 1 {
686                    flesh_depth = idx + 1;
687                }
688
689                idl_class = self
690                    .classes()
691                    .get(link_field.class())
692                    .ok_or_else(|| format!("No such IDL class: {}", link_field.class()))?;
693            }
694        }
695
696        flesh["flesh"] = EgValue::from(flesh_depth);
697
698        Ok(flesh)
699    }
700
701    #[deprecated(note = "See EgValue::create()")]
702    pub fn create_from(&self, classname: &str, v: EgValue) -> EgResult<EgValue> {
703        EgValue::create(classname, v)
704    }
705
706    #[deprecated(note = "See EgValue::is_blessed()")]
707    pub fn is_idl_object(&self, v: &EgValue) -> bool {
708        v.is_blessed()
709    }
710
711    #[deprecated(note = "See EgValue::pkey_value()")]
712    pub fn get_pkey_value(&self, v: &EgValue) -> EgResult<EgValue> {
713        if let Some(v) = v.pkey_value() {
714            Ok(v.clone())
715        } else {
716            Err(format!("Cannot determine pkey value: {}", v.dump()).into())
717        }
718    }
719}