1use crate::prelude::*;
6pub mod hardware;
7pub use hardware::memory::{Memory, MemoryUnit};
8pub use hardware::{
9 AcceleratorArchitecture, Architecture, Backend, CpuArchitecture, DspArchitecture, FpgaArchitecture, GpuArchitecture, Model, Paradigm, Regime,
10 Resource, SensorModality, Topology, Vendor, Vendored,
11};
12
13#[cfg(feature = "std")]
14use crate::prelude::PathBuf;
15#[cfg(feature = "std")]
16use crate::schema::validate::format_phone_number;
17#[cfg(feature = "std")]
18use crate::util::constants::app::DEFAULT_AFFILIATION;
19use crate::util::constants::MAX_LENGTH_IMAGE_CAPTION;
20#[cfg(feature = "std")]
21use crate::util::Label;
22use crate::util::{Constant, LinkedData, MimeType, ToMarkdown};
23use bon::Builder;
24#[cfg(feature = "std")]
25use convert_case::{Case, Casing};
26#[cfg(test)]
27use core::fmt;
28use core::hash::Hash;
29use core::iter::once;
30use core::num::NonZeroU64;
31use derive_more::Display;
32#[cfg(feature = "std")]
33use fancy_regex::Regex;
34#[cfg(feature = "std")]
35use nucleo_matcher::{
36 pattern::{CaseMatching, Normalization, Pattern},
37 Config, Matcher,
38};
39#[cfg(feature = "std")]
40use percy_dom::prelude::{html, IterableNodes, View, VirtualNode};
41use petgraph::graph::Graph;
42use schemars::JsonSchema;
43#[cfg(test)]
44use serde::de::value::SeqAccessDeserializer;
45use serde::de::DeserializeOwned;
46use serde::{Deserialize, Serialize};
47use serde_repr::{Deserialize_repr, Serialize_repr};
48use serde_trim::{option_string_trim, string_trim};
49use serde_with::skip_serializing_none;
50#[cfg(feature = "std")]
51use tracing::{debug, error, trace};
52use validator::Validate;
53
54#[cfg(feature = "std")]
55pub mod agent;
56pub mod geonames;
57pub mod graph;
58pub mod namespaces;
59pub mod pid;
60pub mod research_activity;
61pub mod standard;
62pub mod validate;
63
64use graph::{node_from_label, node_name, node_parent};
65use namespaces::{bibo, foaf, schema_org};
66use validate::{has_image_extension, is_date, is_orcid, is_phone_number};
67
68pub type Keyword = String;
85#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
90#[serde(untagged)]
91pub enum OneOrMany<T> {
92 One(T),
94 Many(Vec<T>),
96}
97#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, PartialEq, PartialOrd, JsonSchema)]
101#[serde(rename_all = "lowercase")]
102pub enum ClassificationLevel {
103 #[default]
105 #[display("UNCLASSIFIED")]
106 Unclassified,
107 #[display("CONFIDENTIAL")]
111 Confidential,
112 #[display("SECRET")]
116 Secret,
117 #[display("TOP SECRET")]
121 #[serde(alias = "top secret")]
122 TopSecret,
123}
124#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
129#[serde(untagged)]
130pub enum MediaObject {
131 Image(ImageObject),
133 Video(VideoObject),
135}
136#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd, JsonSchema)]
138#[serde(rename_all = "lowercase")]
139pub enum OrganizationType {
140 #[display("agency")]
142 Agency,
143 #[display("center")]
147 Center,
148 #[display("consortium")]
150 Consortium,
151 #[display("directorate")]
153 Directorate,
154 #[display("division")]
156 Division,
157 #[display("facility")]
161 Facility,
162 #[display("FFRDC")]
164 Ffrdc,
165 #[display("group")]
167 Group,
168 #[display("institute")]
170 Institute,
171 #[display("office")]
173 Office,
174 #[display("program")]
176 Program,
177}
178#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
180#[serde(untagged)]
181pub enum Other {
182 Unformatted(String),
184 Formatted(Notes),
186}
187#[derive(Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
189#[serde(rename_all = "lowercase")]
190pub enum ProgrammingLanguage {
191 #[display("html")]
193 Html,
194 #[display("javascript")]
198 JavaScript,
199 #[display("julia")]
203 Julia,
204 #[display("markdown")]
208 Markdown,
209 #[display("json")]
213 Json,
214 #[display("rust")]
218 Rust,
219 #[display("shell")]
223 #[serde(alias = "bash", alias = "zsh", alias = "fish", alias = "powershell")]
224 Shell,
225 #[display("yaml")]
229 Yaml,
230}
231#[derive(Clone, Debug, Default, Deserialize, Display, PartialEq, PartialOrd, Serialize, JsonSchema)]
237#[serde(deny_unknown_fields)]
238#[serde(rename_all = "kebab-case")]
239pub enum Status {
240 #[display("canceled")]
242 #[serde(alias = "cancelled")]
243 Canceled,
244 #[display("completed")]
246 Completed,
247 #[display("paused")]
249 #[serde(alias = "on-hold", alias = "postponed", alias = "rescheduled")]
250 Paused,
251 #[default]
253 #[display("active")]
254 Active,
255}
256#[derive(Clone, Debug, Default, Deserialize_repr, Display, Serialize_repr, PartialEq, PartialOrd, JsonSchema)]
262#[repr(u8)]
263#[serde(deny_unknown_fields)]
264pub enum TechnologyReadinessLevel {
265 #[default]
266 #[display("Greenfield Research")]
270 Principles = 0,
271 #[display("Basic Research")]
275 Research = 1,
276 #[display("Technology Concept")]
280 Concept = 2,
281 #[display("Feasible")]
285 Feasible = 3,
286 #[display("Developing")]
290 Developing = 4,
291 #[display("Developed")]
295 Developed = 5,
296 #[display("Prototype")]
300 Prototype = 6,
301 #[display("Operational")]
305 Operational = 7,
306 #[display("Mission Ready")]
310 MissionReady = 8,
311 #[display("Mission Capable")]
315 MissionCapable = 9,
316}
317#[skip_serializing_none]
319#[derive(Builder, Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
320#[builder(start_fn = init)]
321#[serde(deny_unknown_fields, rename_all = "camelCase")]
322pub struct ContactPoint {
323 #[serde(rename = "@context")]
325 pub context: Option<ContactPointContext>,
326 #[serde(rename = "@type")]
328 pub contact_point_type: Option<String>,
329 #[builder(default = "Researcher".to_string())]
340 #[serde(alias = "title", deserialize_with = "string_trim")]
341 pub job_title: String,
342 #[builder(default = "First".to_string())]
346 #[serde(alias = "first", deserialize_with = "string_trim")]
347 pub given_name: String,
348 #[builder(default = "Last".to_string())]
352 #[serde(alias = "last", deserialize_with = "string_trim")]
353 pub family_name: String,
354 #[validate(custom(function = "is_orcid"))]
358 #[serde(alias = "orcid")]
359 pub identifier: Option<String>,
360 #[validate(email(message = "Email address must be in the format name@example.com"))]
364 #[builder(default = "first_last@example.com".to_string())]
365 #[serde(deserialize_with = "string_trim")]
366 pub email: String,
367 #[validate(custom(function = "is_phone_number"))]
371 #[builder(default = "123-456-7890".to_string())]
372 #[serde(alias = "phone", deserialize_with = "string_trim")]
373 pub telephone: String,
374 #[validate(url(message = "Profile URL must be in the format https://example.com"))]
378 #[builder(default = "https://example.com".to_string())]
379 #[serde(alias = "profile", deserialize_with = "string_trim")]
380 pub url: String,
381 #[builder(default = "Some Organization".to_string())]
385 #[serde(deserialize_with = "string_trim")]
386 pub organization: String,
387 pub affiliation: Option<String>,
393}
394#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema)]
398#[builder(start_fn = init, on(String, into))]
399#[serde(deny_unknown_fields, rename_all = "camelCase")]
400pub struct ContactPointContext {
401 pub job_title: String,
403 pub given_name: String,
405 pub family_name: String,
407 pub identifier: String,
409 pub email: String,
411 pub telephone: String,
413 pub url: String,
415 pub organization: String,
417 pub affiliation: String,
419}
420#[skip_serializing_none]
422#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
423#[builder(start_fn = init, on(String, into))]
424#[serde(rename_all = "camelCase")]
425pub struct Date {
426 #[validate(custom(function = "is_date"))]
428 pub start_date: Option<String>,
429 #[validate(custom(function = "is_date"))]
431 pub end_date: Option<String>,
432}
433#[skip_serializing_none]
437#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
438#[builder(start_fn = init)]
439#[serde(deny_unknown_fields, rename_all = "camelCase")]
440pub struct ImageObject {
441 #[validate(length(
443 max = "MAX_LENGTH_IMAGE_CAPTION",
444 message = "Caption is too long, reduce the length below 100 characters."
445 ))]
446 #[serde(deserialize_with = "string_trim")]
447 pub caption: String,
448 #[serde(alias = "size")]
454 pub content_size: Option<NonZeroU64>,
455 #[validate(custom(function = "has_image_extension"))]
457 #[serde(alias = "url", alias = "href")]
458 pub content_url: Option<String>,
459 pub height: Option<NonZeroU64>,
465 pub width: Option<NonZeroU64>,
471}
472#[skip_serializing_none]
476#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Validate)]
477#[serde(deny_unknown_fields)]
478pub struct Notes {
479 pub managers: Option<Vec<String>>,
481 pub programs: Option<Vec<String>>,
483 #[serde(default, deserialize_with = "option_string_trim")]
485 pub presentation: Option<String>,
486}
487#[skip_serializing_none]
491#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd)]
492#[display("Organization ({additional_type}) - {name})")]
493#[serde(deny_unknown_fields, rename_all = "camelCase")]
494pub struct Organization {
495 #[serde(deserialize_with = "string_trim")]
499 pub name: String,
500 #[serde(default, deserialize_with = "option_string_trim")]
504 pub ror: Option<String>,
505 #[serde(default, deserialize_with = "option_string_trim")]
509 pub alternative_name: Option<String>,
510 pub additional_type: OrganizationType,
514 pub keywords: Option<Vec<Keyword>>,
516 pub member: Vec<Organization>,
520}
521#[skip_serializing_none]
525#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
526#[serde(deny_unknown_fields, rename_all = "camelCase")]
527pub struct VideoObject {
528 #[serde(alias = "size")]
532 pub content_size: Option<NonZeroU64>,
533 #[validate(url)]
535 #[serde(alias = "url", alias = "href")]
536 pub content_url: Option<String>,
537 #[serde(deserialize_with = "string_trim")]
541 pub description: String,
542 pub duration: Option<String>,
546 pub height: Option<NonZeroU64>,
550 pub width: Option<NonZeroU64>,
554}
555#[derive(Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
574#[serde(deny_unknown_fields)]
575pub struct Website {
576 #[serde(alias = "title", deserialize_with = "string_trim")]
580 pub description: String,
581 #[validate(url(message = "Provide valid URL"))]
583 #[serde(deserialize_with = "string_trim")]
584 pub url: String,
585}
586impl ContactPoint {
587 #[cfg(feature = "std")]
589 pub fn format(self) -> Self {
590 let ContactPoint {
591 affiliation,
592 context,
593 contact_point_type,
594 email,
595 family_name,
596 given_name,
597 identifier,
598 job_title,
599 organization,
600 telephone,
601 url,
602 ..
603 } = self.clone();
604 let updated_organization = match resolve_from_organization_json(organization) {
605 | Some(value) => value,
606 | None => "".to_string(),
607 };
608 let updated_affiliation = match affiliation {
609 | Some(ref affiliation) => match resolve_from_organization_json(affiliation.to_string()) {
610 | Some(resolved) => Some(resolved),
611 | None => {
612 error!(affiliation, "=> {} Affiliation", Label::not_found());
613 Some(DEFAULT_AFFILIATION.to_string())
614 }
615 },
616 | None => match Organization::load().into_iter().next() {
617 | Some(ornl) => match ornl.member(&updated_organization) {
618 | Some(organization) => match organization.nearest(OrganizationType::Directorate) {
619 | Some(Organization { name, .. }) => Some(name),
620 | None => Some(DEFAULT_AFFILIATION.to_string()),
621 },
622 | None => {
623 error!("=> {} Nearest directorate", Label::not_found());
624 Some(DEFAULT_AFFILIATION.to_string())
625 }
626 },
627 | None => {
628 error!("=> {} Organization hierarchy", Label::not_found());
629 Some(DEFAULT_AFFILIATION.to_string())
630 }
631 },
632 };
633 let updated_telephone = match format_phone_number(&telephone) {
634 | Ok(value) => value,
635 | Err(_) => {
636 error!(value = telephone, "=> {} Phone number", Label::invalid());
637 telephone.to_string()
638 }
639 };
640 Self::init()
641 .maybe_affiliation(updated_affiliation)
642 .maybe_context(context)
643 .maybe_contact_point_type(contact_point_type)
644 .email(email)
645 .family_name(family_name)
646 .given_name(given_name)
647 .maybe_identifier(identifier)
648 .job_title(job_title)
649 .organization(updated_organization)
650 .telephone(updated_telephone)
651 .url(url)
652 .build()
653 }
654 #[cfg(not(feature = "std"))]
655 pub fn format(self) -> Self {
657 self
658 }
659 #[cfg(feature = "std")]
661 pub fn format_with(self, _context: Option<PathBuf>) -> Self {
662 self.format()
664 }
665}
666impl Default for ContactPoint {
667 fn default() -> Self {
668 Self::init().build()
669 }
670}
671impl Default for ContactPointContext {
672 fn default() -> Self {
673 Self::init()
674 .job_title(schema_org("jobTitle"))
675 .given_name(foaf("givenName"))
676 .family_name(foaf("familyName"))
677 .identifier(bibo("identifier"))
678 .email(foaf("mbox"))
679 .telephone(schema_org("telephone"))
680 .url(foaf("workInfoHomepage"))
681 .organization(schema_org("worksFor"))
682 .affiliation(schema_org("affiliation"))
683 .build()
684 }
685}
686impl LinkedData for ContactPoint {
687 fn with_context(&self) -> Self {
688 let mut clone = self.clone();
689 clone.context = Some(ContactPointContext::default());
690 clone.contact_point_type = Some(schema_org("person"));
691 clone
692 }
693}
694impl MediaObject {
695 pub fn content_url(self) -> Option<String> {
697 match self {
698 | MediaObject::Image(ImageObject { content_url, .. }) => content_url,
699 | MediaObject::Video(VideoObject { content_url, .. }) => content_url,
700 }
701 }
702 pub fn description(self) -> String {
704 match self {
705 | MediaObject::Image(ImageObject { caption, .. }) => caption,
706 | MediaObject::Video(VideoObject { description, .. }) => description,
707 }
708 }
709 pub fn is_image(self) -> bool {
711 match self {
712 | MediaObject::Image(_) => true,
713 | _ => false,
714 }
715 }
716}
717impl<T> OneOrMany<T> {
718 pub fn as_slice(&self) -> &[T] {
720 match self {
721 | Self::One(value) => core::slice::from_ref(value),
722 | Self::Many(values) => values.as_slice(),
723 }
724 }
725 pub fn first(&self) -> Option<&T> {
727 self.as_slice().first()
728 }
729 pub fn is_empty(&self) -> bool {
731 self.as_slice().is_empty()
732 }
733 pub fn into_vec(self) -> Vec<T> {
735 match self {
736 | Self::One(value) => vec![value],
737 | Self::Many(values) => values,
738 }
739 }
740 pub fn iter(&self) -> core::slice::Iter<'_, T> {
742 self.as_slice().iter()
743 }
744 pub fn len(&self) -> usize {
746 self.as_slice().len()
747 }
748 pub fn map<U, F>(self, f: F) -> Result<OneOrMany<U>, standard::crosswalk::CrosswalkError>
750 where
751 F: Fn(T) -> Result<U, standard::crosswalk::CrosswalkError>,
752 {
753 match self {
754 | Self::One(value) => f(value).map(OneOrMany::One),
755 | Self::Many(values) => values
756 .into_iter()
757 .enumerate()
758 .map(|(index, value)| {
759 f(value).map_err(|e| standard::crosswalk::CrosswalkError::BuildFailed(format!("Failed to convert record at index {index} — {e}")))
760 })
761 .collect::<Result<Vec<_>, _>>()
762 .map(OneOrMany::Many),
763 }
764 }
765 pub fn parse(content: &str, mime: MimeType) -> Result<OneOrMany<T>, standard::crosswalk::CrosswalkError>
767 where
768 T: DeserializeOwned,
769 {
770 match mime {
771 | MimeType::Json => serde_json::from_str(content).map_err(|e| standard::crosswalk::CrosswalkError::ParseFailed(e.to_string())),
772 | MimeType::Yaml => serde_norway::from_str(content).map_err(|e| standard::crosswalk::CrosswalkError::ParseFailed(e.to_string())),
773 | _ => Err(standard::crosswalk::CrosswalkError::ParseFailed("Content must be JSON or YAML".into())),
774 }
775 }
776 pub fn serialize(&self, mime: MimeType) -> Result<String, standard::crosswalk::CrosswalkError>
778 where
779 T: Serialize,
780 {
781 match mime {
782 | MimeType::Json => serde_json::to_string_pretty(self).map_err(|e| standard::crosswalk::CrosswalkError::SerializeFailed(e.to_string())),
783 | MimeType::Yaml => serde_norway::to_string(self).map_err(|e| standard::crosswalk::CrosswalkError::SerializeFailed(e.to_string())),
784 | _ => Err(standard::crosswalk::CrosswalkError::SerializeFailed("Output must be JSON or YAML".into())),
785 }
786 }
787}
788impl<'a, T> IntoIterator for &'a OneOrMany<T> {
789 type Item = &'a T;
790 type IntoIter = core::slice::Iter<'a, T>;
791
792 fn into_iter(self) -> Self::IntoIter {
793 self.iter()
794 }
795}
796impl<T> Validate for OneOrMany<T>
797where
798 T: Validate,
799{
800 fn validate(&self) -> Result<(), validator::ValidationErrors> {
801 self.iter().find_map(|value| value.validate().err()).map_or(Ok(()), Err)
802 }
803}
804impl Organization {
805 pub fn alternative_names() -> Vec<String> {
807 match Organization::load().into_iter().next() {
808 | Some(organization) => organization
809 .members()
810 .into_iter()
811 .flat_map(|Organization { alternative_name, .. }| alternative_name)
812 .collect::<Vec<String>>(),
813 | None => vec![],
814 }
815 }
816 pub fn load() -> Vec<Organization> {
818 Constant::from_asset("organization.json")
819 .and_then(|content| serde_json::from_str(&content).ok())
820 .unwrap_or_default()
821 }
822 pub fn member(self, label: &str) -> Option<Organization> {
824 self.members().into_iter().find(|Organization { name, .. }| name == label)
825 }
826 pub fn members(self) -> Vec<Organization> {
831 let organization = self.clone();
832 once(organization.clone())
833 .chain(organization.member.iter().flat_map(|directorate| {
834 once(directorate.clone()).chain(
835 directorate
836 .member
837 .iter()
838 .flat_map(|division| once(division.clone()).chain(division.member.iter().cloned())),
839 )
840 }))
841 .collect()
842 }
843 pub fn nearest(self, organization_type: OrganizationType) -> Option<Organization> {
845 let a = self.clone().additional_type.order();
846 let b = organization_type.order();
847 if a > b {
848 None
849 } else {
850 let ornl = Organization::load().into_iter().next()?;
851 let graph = ornl.clone().to_graph();
852 let name = match b.saturating_sub(a) {
853 | 3 => Some(ornl.clone().name),
854 | 2 => node_from_label(&graph, &self.name)
855 .and_then(|node| node_parent(&graph, node))
856 .and_then(|parent| node_parent(&graph, parent))
857 .and_then(|grandparent| node_name(&graph, grandparent)),
858 | 1 => node_from_label(&graph, &self.name)
859 .and_then(|node| node_parent(&graph, node))
860 .and_then(|parent| node_name(&graph, parent)),
861 | 0 => Some(self.name),
862 | _ => None,
863 };
864 name.and_then(|value| ornl.member(&value))
865 }
866 }
867 pub fn to_graph(self) -> Graph<String, u8> {
869 let mut graph: Graph<String, u8, petgraph::Directed> = Graph::new();
870 let organization = &self;
871 let root = graph.add_node(organization.name.clone());
872 let edges: Vec<(String, String)> = organization
873 .member
874 .iter()
875 .flat_map(|directorate| {
876 once((organization.name.clone(), directorate.name.clone())).chain(directorate.member.iter().flat_map(|division| {
877 once((directorate.name.clone(), division.name.clone()))
878 .chain(division.member.iter().map(|group| (division.name.clone(), group.name.clone())))
879 }))
880 })
881 .collect();
882
883 edges.into_iter().for_each(|(parent_name, child_name)| {
884 let parent = node_from_label(&graph, &parent_name).unwrap_or(root);
885 let child = node_from_label(&graph, &child_name).unwrap_or_else(|| graph.add_node(child_name.clone()));
886 graph.add_edge(parent, child, 0);
887 });
888 graph
889 }
890}
891impl OrganizationType {
892 pub fn from_string(value: String) -> OrganizationType {
894 match value.to_lowercase().as_str() {
895 | "agency" => OrganizationType::Agency,
896 | "center" => OrganizationType::Center,
897 | "consortium" => OrganizationType::Consortium,
898 | "division" => OrganizationType::Division,
899 | "directorate" => OrganizationType::Directorate,
900 | "group" => OrganizationType::Group,
901 | "institute" => OrganizationType::Institute,
902 | "office" => OrganizationType::Office,
903 | "program" => OrganizationType::Program,
904 | "facility" => OrganizationType::Facility,
905 | "ffrdc" => OrganizationType::Ffrdc,
906 | _ => OrganizationType::Institute,
907 }
908 }
909 pub fn order(self) -> u8 {
911 match self {
912 | OrganizationType::Ffrdc
913 | OrganizationType::Agency
914 | OrganizationType::Consortium
915 | OrganizationType::Institute
916 | OrganizationType::Office => 4,
917 | OrganizationType::Directorate => 3,
918 | OrganizationType::Division | OrganizationType::Center | OrganizationType::Program | OrganizationType::Facility => 2,
919 | OrganizationType::Group => 1,
920 }
921 }
922}
923impl ToMarkdown for ContactPoint {
924 fn to_markdown(&self) -> String {
925 let ContactPoint {
926 given_name,
927 family_name,
928 job_title: role,
929 email,
930 telephone,
931 ..
932 } = self;
933 format!(
934 r#"## Contact
935- Role: {role}
936- Name: {given_name} {family_name}
937- email: [{email}](mailto:{email})
938- Telephone: {telephone}
939"#
940 )
941 }
942}
943impl ToMarkdown for Website {
944 fn to_markdown(&self) -> String {
945 let Website { description, url } = self;
946 format!("[{}]({})", description, url)
947 }
948}
949impl Validate for MediaObject {
950 fn validate(&self) -> Result<(), validator::ValidationErrors> {
951 match self {
952 | Self::Image(img) => img.validate(),
953 | Self::Video(vid) => vid.validate(),
954 }
955 }
956}
957#[cfg(feature = "std")]
958impl View for ContactPoint {
959 fn render(&self) -> VirtualNode {
960 let ContactPoint {
961 given_name,
962 family_name,
963 job_title: role,
964 email,
965 telephone,
966 ..
967 } = self;
968 html! {
969 <section id="contact">
970 <div>
971 <span class="label">Contact</span>
972 <span class="spacer"> </span>
973 <span class="name">{ format!("{} {}", given_name, family_name) }</span>
974 <span class="spacer">|</span>
975 <span class="title">{ role }</span>
976 <span class="spacer">|</span>
977 <span class="email">{ email }</span>
978 <span class="spacer">|</span>
979 <span class="phone">{ telephone }</span>
980 </div>
981 </section>
982 }
983 }
984}
985#[cfg(feature = "std")]
986pub(crate) fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, data: I, name: String) -> Option<String> {
987 fn match_list<I: IntoIterator<Item = String> + Clone>(value: String, values: I) -> Vec<(String, u32)> {
988 let pattern = Pattern::parse(&value, CaseMatching::Ignore, Normalization::Smart);
989 let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
990 pattern.match_list(values.clone(), &mut matcher)
991 }
992 fn print_resolution(output: Option<String>, value: String, name: String) {
993 let label = name.to_case(Case::Title);
994 match output {
995 | Some(resolved) => {
996 if resolved.eq(&value.to_string()) {
997 trace!("=> {} {} = \"{}\"", Label::using(), label, value.clone());
998 } else {
999 debug!(input = value.clone(), resolved, "=> {} {}", Label::found(), label);
1000 }
1001 }
1002 | None => {
1003 debug!(value = value.clone(), "=> {} {}", Label::not_found(), label);
1004 }
1005 };
1006 }
1007 fn sanitize(value: String) -> String {
1008 match Regex::new(r"[-_.,]") {
1009 | Ok(re) => re.replace_all(&value, "").replace("&", "and").trim().to_string(),
1010 | Err(err) => err.to_string(),
1011 }
1012 }
1013 let output = data
1014 .into_iter()
1015 .flat_map(|values| {
1016 let sanitized = sanitize(value.clone());
1017 let matched = match_list(sanitized, values.clone().into_iter().take(4));
1018 trace!("{} => {:?}", value.clone(), matched.clone());
1019 if matched.clone().is_empty() {
1020 None
1021 } else {
1022 match values.first() {
1023 | Some(x) => {
1024 if value.eq(x) {
1025 Some((x.into(), 10000))
1026 } else {
1027 let score = matched.into_iter().map(|(_, score)| score).max();
1028 match score {
1029 | Some(value) if value > 0 => Some((x.to_string(), value)),
1030 | Some(_) | None => None,
1031 }
1032 }
1033 }
1034 | None => None,
1035 }
1036 }
1037 })
1038 .max_by_key(|(_, score)| *score)
1039 .map(|(x, _)| x.to_string());
1040 print_resolution(output.clone(), value, name);
1041 output
1042}
1043#[cfg(not(feature = "std"))]
1044#[allow(dead_code)]
1045pub(crate) fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, _data: I, _name: String) -> Option<String> {
1046 Some(value)
1047}
1048
1049#[cfg(feature = "std")]
1050pub(crate) fn resolve_from_organization_json(value: String) -> Option<String> {
1051 let organization = Organization::load().into_iter().next()?;
1052 let items: Vec<Organization> = once(organization.clone())
1053 .chain(
1054 organization
1055 .member
1056 .iter()
1057 .flat_map(|directorate| once(directorate.clone()).chain(directorate.member.iter().cloned())),
1058 )
1059 .collect();
1060 let data = items
1061 .into_iter()
1062 .map(|x| (x.name.clone(), x.alternative_name.clone()))
1063 .filter(|(name, alias)| !(name.is_empty() && alias.is_none()))
1064 .map(|(name, alias)| {
1065 let alternative_name = alias.as_ref().map_or(name.clone(), |x| x.to_string());
1066 vec![name, alternative_name]
1067 })
1068 .collect::<Vec<Vec<String>>>();
1069 resolve_from_list_of_lists(value, data, "organization".to_string())
1070}
1071#[cfg(test)]
1073pub(crate) fn optional_string_or_seq<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
1074where
1075 D: serde::Deserializer<'de>,
1076{
1077 struct OptStringOrSeq;
1078 impl<'de> serde::de::Visitor<'de> for OptStringOrSeq {
1079 type Value = Option<Vec<String>>;
1080 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1081 formatter.write_str("string, list of strings, or null")
1082 }
1083 fn visit_none<E: serde::de::Error>(self) -> Result<Option<Vec<String>>, E> {
1084 Ok(None)
1085 }
1086 fn visit_unit<E: serde::de::Error>(self) -> Result<Option<Vec<String>>, E> {
1087 Ok(None)
1088 }
1089 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Option<Vec<String>>, E> {
1090 Ok(Some(vec![value.to_owned()]))
1091 }
1092 fn visit_seq<A: serde::de::SeqAccess<'de>>(self, seq: A) -> Result<Option<Vec<String>>, A::Error> {
1093 Deserialize::deserialize(SeqAccessDeserializer::new(seq)).map(Some)
1094 }
1095 }
1096 deserializer.deserialize_any(OptStringOrSeq)
1097}
1098
1099#[cfg(not(feature = "std"))]
1100#[allow(dead_code)]
1101pub(crate) fn resolve_from_organization_json(value: String) -> Option<String> {
1102 Some(value)
1103}
1104
1105#[cfg(test)]
1106mod tests;