1use 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
12static 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
22pub 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
32pub 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#[derive(Debug, Clone, PartialEq)]
45pub enum DataFormat {
46 Fieldmapper,
48 Hash,
52 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#[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 pkey: Option<String>,
267
268 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 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 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 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 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 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 pub fn classes(&self) -> &HashMap<String, Arc<Class>> {
405 &self.classes
406 }
407
408 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 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(); 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 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}