1use crate::prelude::*;
6use alloc::collections::BTreeSet;
7pub mod hardware;
8pub use hardware::memory::{Memory, MemoryUnit};
9pub use hardware::{
10 AcceleratorArchitecture, Architecture, Backend, CpuArchitecture, DspArchitecture, FpgaArchitecture, GpuArchitecture, Model, Paradigm, Regime,
11 Resource, SensorModality, Topology, Vendor, Vendored,
12};
13
14#[cfg(feature = "std")]
15use crate::prelude::PathBuf;
16#[cfg(feature = "std")]
17use crate::schema::validate::format_phone_number;
18#[cfg(feature = "std")]
19use crate::util::constants::app::DEFAULT_AFFILIATION;
20use crate::util::constants::MAX_LENGTH_IMAGE_CAPTION;
21#[cfg(feature = "std")]
22use crate::util::Label;
23use crate::util::{Constant, LinkedData, MarkdownSupport, MimeType};
24use bon::Builder;
25#[cfg(feature = "std")]
26use convert_case::{Case, Casing};
27use core::hash::Hash;
28use core::iter::once;
29use core::num::NonZeroU64;
30use derive_more::Display;
31#[cfg(feature = "std")]
32use nucleo_matcher::{
33 pattern::{CaseMatching, Normalization, Pattern},
34 Config, Matcher,
35};
36#[cfg(feature = "std")]
37use percy_dom::prelude::{html, IterableNodes, View, VirtualNode};
38use petgraph::graph::Graph;
39use schemars::JsonSchema;
40use serde::de::DeserializeOwned;
41use serde::{Deserialize, Serialize};
42use serde_repr::{Deserialize_repr, Serialize_repr};
43use serde_trim::{option_string_trim, string_trim};
44use serde_with::skip_serializing_none;
45#[cfg(feature = "std")]
46use tracing::{debug, error, trace};
47use validator::Validate;
48
49#[cfg(feature = "std")]
50pub mod agent;
51pub mod discovery;
52pub mod geonames;
53pub mod graph;
54pub mod namespaces;
55pub mod pid;
56pub mod research_activity;
57pub mod standard;
58pub mod validate;
59
60use graph::{node_from_label, node_name, node_parent};
61use namespaces::{bibo, foaf, schema_org};
62use validate::{has_image_extension, is_date, is_orcid, is_phone_number};
63
64pub type Keyword = String;
81#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
86#[serde(untagged)]
87pub enum OneOrMany<T> {
88 One(T),
90 Many(Vec<T>),
92}
93#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, PartialEq, PartialOrd, JsonSchema)]
97#[serde(rename_all = "lowercase")]
98pub enum ClassificationLevel {
99 #[default]
101 #[display("UNCLASSIFIED")]
102 Unclassified,
103 #[display("CONFIDENTIAL")]
107 Confidential,
108 #[display("SECRET")]
112 Secret,
113 #[display("TOP SECRET")]
117 #[serde(alias = "top secret")]
118 TopSecret,
119}
120#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
125#[serde(untagged)]
126pub enum MediaObject {
127 Image(ImageObject),
129 Video(VideoObject),
131}
132#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd, JsonSchema)]
134#[serde(rename_all = "lowercase")]
135pub enum OrganizationType {
136 #[display("agency")]
138 Agency,
139 #[display("center")]
143 Center,
144 #[display("consortium")]
146 Consortium,
147 #[display("directorate")]
149 Directorate,
150 #[display("division")]
152 Division,
153 #[display("facility")]
157 Facility,
158 #[display("FFRDC")]
160 Ffrdc,
161 #[display("group")]
163 Group,
164 #[display("institute")]
166 Institute,
167 #[display("office")]
169 Office,
170 #[display("program")]
172 Program,
173}
174#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
176#[serde(untagged)]
177pub enum Other {
178 Unformatted(String),
180 Formatted(Notes),
182}
183#[derive(Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
185#[serde(rename_all = "lowercase")]
186pub enum ProgrammingLanguage {
187 #[display("html")]
189 Html,
190 #[display("javascript")]
194 JavaScript,
195 #[display("julia")]
199 Julia,
200 #[display("markdown")]
204 Markdown,
205 #[display("json")]
209 Json,
210 #[display("rust")]
214 Rust,
215 #[display("shell")]
219 #[serde(alias = "bash", alias = "zsh", alias = "fish", alias = "powershell")]
220 Shell,
221 #[display("yaml")]
225 Yaml,
226}
227#[derive(Clone, Debug, Default, Deserialize, Display, PartialEq, PartialOrd, Serialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234#[serde(rename_all = "kebab-case")]
235pub enum Status {
236 #[display("canceled")]
238 #[serde(alias = "cancelled")]
239 Canceled,
240 #[display("completed")]
242 Completed,
243 #[display("paused")]
245 #[serde(alias = "on-hold", alias = "postponed", alias = "rescheduled")]
246 Paused,
247 #[default]
249 #[display("active")]
250 Active,
251}
252#[derive(Clone, Debug, Default, Deserialize_repr, Display, Serialize_repr, PartialEq, PartialOrd, JsonSchema)]
258#[repr(u8)]
259#[serde(deny_unknown_fields)]
260pub enum TechnologyReadinessLevel {
261 #[default]
262 #[display("Greenfield Research")]
266 Principles = 0,
267 #[display("Basic Research")]
271 Research = 1,
272 #[display("Technology Concept")]
276 Concept = 2,
277 #[display("Feasible")]
281 Feasible = 3,
282 #[display("Developing")]
286 Developing = 4,
287 #[display("Developed")]
291 Developed = 5,
292 #[display("Prototype")]
296 Prototype = 6,
297 #[display("Operational")]
301 Operational = 7,
302 #[display("Mission Ready")]
306 MissionReady = 8,
307 #[display("Mission Capable")]
311 MissionCapable = 9,
312}
313#[skip_serializing_none]
315#[derive(Builder, Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
316#[builder(start_fn = init)]
317#[serde(deny_unknown_fields, rename_all = "camelCase")]
318pub struct ContactPoint {
319 #[serde(rename = "@context")]
321 pub context: Option<ContactPointContext>,
322 #[serde(rename = "@type")]
324 pub contact_point_type: Option<String>,
325 #[builder(default = "Researcher".to_string())]
336 #[serde(alias = "title", deserialize_with = "string_trim")]
337 pub job_title: String,
338 #[builder(default = "First".to_string())]
342 #[serde(alias = "first", deserialize_with = "string_trim")]
343 pub given_name: String,
344 #[builder(default = "Last".to_string())]
348 #[serde(alias = "last", deserialize_with = "string_trim")]
349 pub family_name: String,
350 #[validate(custom(function = "is_orcid"))]
354 #[serde(alias = "orcid")]
355 pub identifier: Option<String>,
356 #[validate(email(message = "Email address must be in the format name@example.com"))]
360 #[builder(default = "first_last@example.com".to_string())]
361 #[serde(deserialize_with = "string_trim")]
362 pub email: String,
363 #[validate(custom(function = "is_phone_number"))]
367 #[builder(default = "123-456-7890".to_string())]
368 #[serde(alias = "phone", deserialize_with = "string_trim")]
369 pub telephone: String,
370 #[validate(url(message = "Profile URL must be in the format https://example.com"))]
374 #[builder(default = "https://example.com".to_string())]
375 #[serde(deserialize_with = "string_trim")]
376 pub url: String,
377 #[builder(default = "Some Organization".to_string())]
381 #[serde(deserialize_with = "string_trim")]
382 pub organization: String,
383 pub affiliation: Option<String>,
389}
390#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema)]
394#[builder(start_fn = init, on(String, into))]
395#[serde(deny_unknown_fields, rename_all = "camelCase")]
396pub struct ContactPointContext {
397 pub job_title: String,
399 pub given_name: String,
401 pub family_name: String,
403 pub identifier: String,
405 pub email: String,
407 pub telephone: String,
409 pub url: String,
411 pub organization: String,
413 pub affiliation: String,
415}
416#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
418#[serde(transparent)]
419pub struct ControlledVocabulary(Vec<Keyword>);
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 pub content_url: Option<String>,
458 pub height: Option<NonZeroU64>,
464 pub width: Option<NonZeroU64>,
470}
471#[skip_serializing_none]
475#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Validate)]
476#[serde(deny_unknown_fields)]
477pub struct Notes {
478 pub managers: Option<Vec<String>>,
480 pub programs: Option<Vec<String>>,
482 #[serde(default, deserialize_with = "option_string_trim")]
484 pub presentation: Option<String>,
485}
486#[skip_serializing_none]
490#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd)]
491#[display("Organization ({additional_type}) - {name})")]
492#[serde(deny_unknown_fields, rename_all = "camelCase")]
493pub struct Organization {
494 #[serde(deserialize_with = "string_trim")]
498 pub name: String,
499 #[serde(default, deserialize_with = "option_string_trim")]
503 pub ror: Option<String>,
504 #[serde(default, deserialize_with = "option_string_trim")]
508 pub alternative_name: Option<String>,
509 pub additional_type: OrganizationType,
513 pub keywords: Option<Vec<Keyword>>,
515 pub member: Vec<Organization>,
519}
520#[skip_serializing_none]
524#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
525#[serde(deny_unknown_fields, rename_all = "camelCase")]
526pub struct VideoObject {
527 #[serde(alias = "size")]
531 pub content_size: Option<NonZeroU64>,
532 #[validate(url)]
534 pub content_url: Option<String>,
535 #[serde(deserialize_with = "string_trim")]
539 pub description: String,
540 pub duration: Option<String>,
544 pub height: Option<NonZeroU64>,
548 pub width: Option<NonZeroU64>,
552}
553#[derive(Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
572#[serde(deny_unknown_fields)]
573pub struct Website {
574 #[serde(alias = "title", deserialize_with = "string_trim")]
578 pub description: String,
579 #[validate(url(message = "Provide valid URL"))]
581 #[serde(deserialize_with = "string_trim")]
582 pub url: String,
583}
584impl ContactPoint {
585 #[cfg(feature = "std")]
587 pub fn format(self) -> Self {
588 let ContactPoint {
589 affiliation,
590 context,
591 contact_point_type,
592 email,
593 family_name,
594 given_name,
595 identifier,
596 job_title,
597 organization,
598 telephone,
599 url,
600 ..
601 } = self.clone();
602 let updated_organization = match resolve_from_organization_json(organization) {
603 | Some(value) => value,
604 | None => "".to_string(),
605 };
606 let updated_affiliation = match affiliation {
607 | Some(ref affiliation) => match resolve_from_organization_json(affiliation.to_string()) {
608 | Some(resolved) => Some(resolved),
609 | None => {
610 error!(affiliation, "=> {} Affiliation", Label::not_found());
611 Some(DEFAULT_AFFILIATION.to_string())
612 }
613 },
614 | None => match Organization::load().into_iter().next() {
615 | Some(ornl) => match ornl.member(&updated_organization) {
616 | Some(organization) => match organization.nearest(OrganizationType::Directorate) {
617 | Some(Organization { name, .. }) => Some(name),
618 | None => Some(DEFAULT_AFFILIATION.to_string()),
619 },
620 | None => {
621 error!("=> {} Nearest directorate", Label::not_found());
622 Some(DEFAULT_AFFILIATION.to_string())
623 }
624 },
625 | None => {
626 error!("=> {} Organization hierarchy", Label::not_found());
627 Some(DEFAULT_AFFILIATION.to_string())
628 }
629 },
630 };
631 let updated_telephone = match format_phone_number(&telephone) {
632 | Ok(value) => value,
633 | Err(_) => {
634 error!(value = telephone, "=> {} Phone number", Label::invalid());
635 telephone.to_string()
636 }
637 };
638 Self::init()
639 .maybe_affiliation(updated_affiliation)
640 .maybe_context(context)
641 .maybe_contact_point_type(contact_point_type)
642 .email(email)
643 .family_name(family_name)
644 .given_name(given_name)
645 .maybe_identifier(identifier)
646 .job_title(job_title)
647 .organization(updated_organization)
648 .telephone(updated_telephone)
649 .url(url)
650 .build()
651 }
652 #[cfg(not(feature = "std"))]
653 pub fn format(self) -> Self {
655 self
656 }
657 #[cfg(feature = "std")]
659 pub fn format_with(self, _context: Option<PathBuf>) -> Self {
660 self.format()
662 }
663}
664impl ControlledVocabulary {
665 pub fn normalize<T: Into<String>>(name: &str, values: impl IntoIterator<Item = T>) -> Self {
667 Self(
668 values
669 .into_iter()
670 .filter_map(|value| research_activity::resolve_from_csv_asset(name.to_string(), value.into()))
671 .collect::<BTreeSet<_>>()
672 .into_iter()
673 .collect(),
674 )
675 }
676 pub fn into_values(self) -> Vec<Keyword> {
678 self.0
679 }
680}
681impl Default for ContactPoint {
682 fn default() -> Self {
683 Self::init().build()
684 }
685}
686impl Default for ContactPointContext {
687 fn default() -> Self {
688 Self::init()
689 .job_title(schema_org("jobTitle"))
690 .given_name(foaf("givenName"))
691 .family_name(foaf("familyName"))
692 .identifier(bibo("identifier"))
693 .email(foaf("mbox"))
694 .telephone(schema_org("telephone"))
695 .url(foaf("workInfoHomepage"))
696 .organization(schema_org("worksFor"))
697 .affiliation(schema_org("affiliation"))
698 .build()
699 }
700}
701impl LinkedData for ContactPoint {
702 fn with_context(&self) -> Self {
703 let mut clone = self.clone();
704 clone.context = Some(ContactPointContext::default());
705 clone.contact_point_type = Some(schema_org("person"));
706 clone
707 }
708}
709impl MediaObject {
710 pub fn content_url(self) -> Option<String> {
712 match self {
713 | MediaObject::Image(ImageObject { content_url, .. }) | MediaObject::Video(VideoObject { content_url, .. }) => content_url,
714 }
715 }
716 pub fn description(self) -> String {
718 match self {
719 | MediaObject::Image(ImageObject { caption, .. }) => caption,
720 | MediaObject::Video(VideoObject { description, .. }) => description,
721 }
722 }
723 pub fn is_image(self) -> bool {
725 match self {
726 | MediaObject::Image(_) => true,
727 | _ => false,
728 }
729 }
730}
731impl<T> OneOrMany<T> {
732 pub fn as_slice(&self) -> &[T] {
734 match self {
735 | Self::One(value) => core::slice::from_ref(value),
736 | Self::Many(values) => values.as_slice(),
737 }
738 }
739 pub fn first(&self) -> Option<&T> {
741 self.as_slice().first()
742 }
743 pub fn is_empty(&self) -> bool {
745 self.as_slice().is_empty()
746 }
747 pub fn into_vec(self) -> Vec<T> {
749 match self {
750 | Self::One(value) => vec![value],
751 | Self::Many(values) => values,
752 }
753 }
754 pub fn iter(&self) -> core::slice::Iter<'_, T> {
756 self.as_slice().iter()
757 }
758 pub fn len(&self) -> usize {
760 self.as_slice().len()
761 }
762 pub fn map<U, F>(self, f: F) -> Result<OneOrMany<U>, standard::crosswalk::CrosswalkError>
764 where
765 F: Fn(T) -> Result<U, standard::crosswalk::CrosswalkError>,
766 {
767 match self {
768 | Self::One(value) => f(value).map(OneOrMany::One),
769 | Self::Many(values) => values
770 .into_iter()
771 .enumerate()
772 .map(|(index, value)| {
773 f(value).map_err(|e| standard::crosswalk::CrosswalkError::BuildFailed(format!("Failed to convert record at index {index} — {e}")))
774 })
775 .collect::<Result<Vec<_>, _>>()
776 .map(OneOrMany::Many),
777 }
778 }
779 pub fn parse(content: &str, mime: MimeType) -> Result<OneOrMany<T>, standard::crosswalk::CrosswalkError>
781 where
782 T: DeserializeOwned,
783 {
784 match mime {
785 | MimeType::Json => serde_json::from_str(content).map_err(|e| standard::crosswalk::CrosswalkError::ParseFailed(e.to_string())),
786 | MimeType::Yaml => serde_norway::from_str(content).map_err(|e| standard::crosswalk::CrosswalkError::ParseFailed(e.to_string())),
787 | _ => Err(standard::crosswalk::CrosswalkError::ParseFailed("Content must be JSON or YAML".into())),
788 }
789 }
790 pub fn serialize(&self, mime: MimeType) -> Result<String, standard::crosswalk::CrosswalkError>
792 where
793 T: Serialize,
794 {
795 match mime {
796 | MimeType::Json => serde_json::to_string_pretty(self).map_err(|e| standard::crosswalk::CrosswalkError::SerializeFailed(e.to_string())),
797 | MimeType::Yaml => serde_norway::to_string(self).map_err(|e| standard::crosswalk::CrosswalkError::SerializeFailed(e.to_string())),
798 | _ => Err(standard::crosswalk::CrosswalkError::SerializeFailed("Output must be JSON or YAML".into())),
799 }
800 }
801}
802impl<'a, T> IntoIterator for &'a OneOrMany<T> {
803 type Item = &'a T;
804 type IntoIter = core::slice::Iter<'a, T>;
805
806 fn into_iter(self) -> Self::IntoIter {
807 self.iter()
808 }
809}
810impl<T> Validate for OneOrMany<T>
811where
812 T: Validate,
813{
814 fn validate(&self) -> Result<(), validator::ValidationErrors> {
815 self.iter().find_map(|value| value.validate().err()).map_or(Ok(()), Err)
816 }
817}
818impl Organization {
819 pub fn alternative_names() -> Vec<String> {
821 match Organization::load().into_iter().next() {
822 | Some(organization) => organization
823 .members()
824 .into_iter()
825 .flat_map(|Organization { alternative_name, .. }| alternative_name)
826 .collect::<Vec<String>>(),
827 | None => vec![],
828 }
829 }
830 pub fn load() -> Vec<Organization> {
832 Constant::from_asset("organization.json")
833 .and_then(|content| serde_json::from_str(&content).ok())
834 .unwrap_or_default()
835 }
836 pub fn member(self, label: &str) -> Option<Organization> {
838 self.members().into_iter().find(|Organization { name, .. }| name == label)
839 }
840 pub fn members(self) -> Vec<Organization> {
845 let organization = self.clone();
846 once(organization.clone())
847 .chain(organization.member.iter().flat_map(|directorate| {
848 once(directorate.clone()).chain(
849 directorate
850 .member
851 .iter()
852 .flat_map(|division| once(division.clone()).chain(division.member.iter().cloned())),
853 )
854 }))
855 .collect()
856 }
857 pub fn nearest(self, organization_type: OrganizationType) -> Option<Organization> {
859 let a = self.clone().additional_type.order();
860 let b = organization_type.order();
861 if a > b {
862 None
863 } else {
864 let ornl = Organization::load().into_iter().next()?;
865 let graph = ornl.clone().to_graph();
866 let name = match b.saturating_sub(a) {
867 | 3 => Some(ornl.clone().name),
868 | 2 => node_from_label(&graph, &self.name)
869 .and_then(|node| node_parent(&graph, node))
870 .and_then(|parent| node_parent(&graph, parent))
871 .and_then(|grandparent| node_name(&graph, grandparent)),
872 | 1 => node_from_label(&graph, &self.name)
873 .and_then(|node| node_parent(&graph, node))
874 .and_then(|parent| node_name(&graph, parent)),
875 | 0 => Some(self.name),
876 | _ => None,
877 };
878 name.and_then(|value| ornl.member(&value))
879 }
880 }
881 pub fn to_graph(self) -> Graph<String, u8> {
883 let mut graph: Graph<String, u8, petgraph::Directed> = Graph::new();
884 let organization = &self;
885 let root = graph.add_node(organization.name.clone());
886 let edges: Vec<(String, String)> = organization
887 .member
888 .iter()
889 .flat_map(|directorate| {
890 once((organization.name.clone(), directorate.name.clone())).chain(directorate.member.iter().flat_map(|division| {
891 once((directorate.name.clone(), division.name.clone()))
892 .chain(division.member.iter().map(|group| (division.name.clone(), group.name.clone())))
893 }))
894 })
895 .collect();
896
897 edges.into_iter().for_each(|(parent_name, child_name)| {
898 let parent = node_from_label(&graph, &parent_name).unwrap_or(root);
899 let child = node_from_label(&graph, &child_name).unwrap_or_else(|| graph.add_node(child_name.clone()));
900 graph.add_edge(parent, child, 0);
901 });
902 graph
903 }
904}
905impl OrganizationType {
906 pub fn from_string(value: String) -> OrganizationType {
908 match value.to_lowercase().as_str() {
909 | "agency" => OrganizationType::Agency,
910 | "center" => OrganizationType::Center,
911 | "consortium" => OrganizationType::Consortium,
912 | "division" => OrganizationType::Division,
913 | "directorate" => OrganizationType::Directorate,
914 | "group" => OrganizationType::Group,
915 | "office" => OrganizationType::Office,
916 | "program" => OrganizationType::Program,
917 | "facility" => OrganizationType::Facility,
918 | "ffrdc" => OrganizationType::Ffrdc,
919 | _ => OrganizationType::Institute,
920 }
921 }
922 pub fn order(self) -> u8 {
924 match self {
925 | OrganizationType::Ffrdc
926 | OrganizationType::Agency
927 | OrganizationType::Consortium
928 | OrganizationType::Institute
929 | OrganizationType::Office => 4,
930 | OrganizationType::Directorate => 3,
931 | OrganizationType::Division | OrganizationType::Center | OrganizationType::Program | OrganizationType::Facility => 2,
932 | OrganizationType::Group => 1,
933 }
934 }
935}
936impl MarkdownSupport for ContactPoint {
937 fn to_markdown(&self) -> String {
938 let ContactPoint {
939 given_name,
940 family_name,
941 job_title,
942 identifier,
943 email,
944 telephone,
945 url,
946 organization,
947 affiliation,
948 ..
949 } = self;
950 let job_title = job_title.to_markdown_text();
951 let given_name = given_name.to_markdown_text();
952 let family_name = family_name.to_markdown_text();
953 let identifier = identifier.as_ref().map(MarkdownSupport::to_markdown_text);
954 let email = email.to_markdown_text();
955 let telephone = telephone.to_markdown_text();
956 let url = url.to_markdown_text();
957 let organization = organization.to_markdown_text();
958 let affiliation = affiliation.as_ref().map(MarkdownSupport::to_markdown_text);
959 [
960 Some("## Contact".to_string()),
961 Some(format!("- Job Title: {job_title}")),
962 Some(format!("- Given Name: {given_name}")),
963 Some(format!("- Family Name: {family_name}")),
964 identifier.as_ref().map(|value| format!("- Identifier: {value}")),
965 Some(format!("- Email: [{email}](mailto:{email})")),
966 Some(format!("- Telephone: {telephone}")),
967 Some(format!("- URL: {url}")),
968 Some(format!("- Organization: {organization}")),
969 affiliation.as_ref().map(|value| format!("- Affiliation: {value}")),
970 ]
971 .into_iter()
972 .flatten()
973 .collect::<Vec<_>>()
974 .join("\n")
975 }
976}
977impl MarkdownSupport for Website {
978 fn to_markdown(&self) -> String {
979 let Website { description, url } = self;
980 format!("[{}]({})", description, url)
981 }
982}
983impl Validate for MediaObject {
984 fn validate(&self) -> Result<(), validator::ValidationErrors> {
985 match self {
986 | Self::Image(img) => img.validate(),
987 | Self::Video(vid) => vid.validate(),
988 }
989 }
990}
991#[cfg(feature = "std")]
992impl View for ContactPoint {
993 fn render(&self) -> VirtualNode {
994 let ContactPoint {
995 given_name,
996 family_name,
997 job_title: role,
998 email,
999 telephone,
1000 ..
1001 } = self;
1002 html! {
1003 <section id="contact">
1004 <div>
1005 <span class="label">Contact</span>
1006 <span class="spacer"> </span>
1007 <span class="name">{ format!("{} {}", given_name, family_name) }</span>
1008 <span class="spacer">|</span>
1009 <span class="title">{ role }</span>
1010 <span class="spacer">|</span>
1011 <span class="email">{ email }</span>
1012 <span class="spacer">|</span>
1013 <span class="phone">{ telephone }</span>
1014 </div>
1015 </section>
1016 }
1017 }
1018}
1019#[cfg(feature = "std")]
1020pub(crate) fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, data: I, name: String) -> Option<String> {
1021 fn match_list<I: IntoIterator<Item = String> + Clone>(value: String, values: I) -> Vec<(String, u32)> {
1022 let pattern = Pattern::parse(&value, CaseMatching::Ignore, Normalization::Smart);
1023 let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
1024 pattern.match_list(values.clone(), &mut matcher)
1025 }
1026 fn print_resolution(output: Option<String>, value: String, name: String) {
1027 let label = name.to_case(Case::Title);
1028 match output {
1029 | Some(resolved) => {
1030 if resolved.eq(&value.to_string()) {
1031 trace!("=> {} {} = \"{}\"", Label::using(), label, value.clone());
1032 } else {
1033 debug!(input = value.clone(), resolved, "=> {} {}", Label::found(), label);
1034 }
1035 }
1036 | None => {
1037 debug!(value = value.clone(), "=> {} {}", Label::not_found(), label);
1038 }
1039 };
1040 }
1041 let output = data
1042 .into_iter()
1043 .flat_map(|values| {
1044 let sanitized = value.normalize();
1045 let matched = match_list(sanitized, values.clone().into_iter().take(4));
1046 trace!("{} => {:?}", value.clone(), matched.clone());
1047 if matched.clone().is_empty() {
1048 None
1049 } else {
1050 match values.first() {
1051 | Some(x) => {
1052 if value.eq(x) {
1053 Some((x.into(), 10000))
1054 } else {
1055 let score = matched.into_iter().map(|(_, score)| score).max();
1056 match score {
1057 | Some(value) if value > 0 => Some((x.to_string(), value)),
1058 | Some(_) | None => None,
1059 }
1060 }
1061 }
1062 | None => None,
1063 }
1064 }
1065 })
1066 .max_by_key(|(_, score)| *score)
1067 .map(|(x, _)| x.to_string());
1068 print_resolution(output.clone(), value, name);
1069 output
1070}
1071#[cfg(not(feature = "std"))]
1072#[allow(dead_code)]
1073pub(crate) fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, _data: I, _name: String) -> Option<String> {
1074 Some(value)
1075}
1076#[cfg(feature = "std")]
1077pub(crate) fn resolve_from_organization_json(value: String) -> Option<String> {
1078 let organization = Organization::load().into_iter().next()?;
1079 let items: Vec<Organization> = once(organization.clone())
1080 .chain(
1081 organization
1082 .member
1083 .iter()
1084 .flat_map(|directorate| once(directorate.clone()).chain(directorate.member.iter().cloned())),
1085 )
1086 .collect();
1087 let data = items
1088 .into_iter()
1089 .map(|x| (x.name.clone(), x.alternative_name.clone()))
1090 .filter(|(name, alias)| !(name.is_empty() && alias.is_none()))
1091 .map(|(name, alias)| {
1092 let alternative_name = alias.as_ref().map_or(name.clone(), |x| x.to_string());
1093 vec![name, alternative_name]
1094 })
1095 .collect::<Vec<Vec<String>>>();
1096 resolve_from_list_of_lists(value, data, "organization".to_string())
1097}
1098#[cfg(not(feature = "std"))]
1099#[allow(dead_code)]
1100pub(crate) fn resolve_from_organization_json(value: String) -> Option<String> {
1101 Some(value)
1102}
1103
1104#[cfg(test)]
1105mod tests;