1use crate::analyzer::vale::{Vale, ValeConfig};
2use crate::analyzer::{StaticAnalyzer, StaticAnalyzerConfig};
3use crate::constants::*;
4use crate::util::*;
5use bon::Builder;
6use derive_more::Display;
7use fancy_regex::Regex;
8use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
9use nucleo_matcher::{Config, Matcher};
10use owo_colors::OwoColorize;
11use percy_dom::prelude::{html, IterableNodes, View, VirtualNode};
12use petgraph::graph::Graph;
13use rayon::prelude::*;
14use schemars::{schema_for, JsonSchema};
15use serde::{Deserialize, Serialize};
16use serde_repr::*;
17use serde_trim::*;
18use serde_with::skip_serializing_none;
19use std::collections::HashMap;
20use std::path::PathBuf;
21use std::result::Result;
22use titlecase::Titlecase;
23use tracing::{debug, error, info, trace};
24use validator::{Validate, ValidationErrorsKind};
25
26pub mod graph;
27pub mod validate;
28use graph::*;
29use validate::*;
30
31pub type Keyword = String;
47#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, PartialEq, PartialOrd, JsonSchema)]
51#[serde(rename_all = "lowercase")]
52pub enum ClassificationLevel {
53 #[default]
55 #[display("UNCLASSIFIED")]
56 Unclassified,
57 #[display("CONFIDENTIAL")]
61 Confidential,
62 #[display("SECRET")]
66 Secret,
67 #[display("TOP SECRET")]
71 #[serde(alias = "top secret")]
72 TopSecret,
73}
74#[derive(Clone, Debug, Serialize, Deserialize, Display)]
75pub enum FuzzyValue {
76 #[display("partners")]
77 Partner,
78 #[display("keywords")]
80 Keyword,
81 #[display("sponsors")]
82 Sponsor,
83 #[display("technology")]
84 Technology,
85}
86#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
87#[serde(untagged)]
88pub enum Other {
89 Unformatted(String),
90 Formatted(Notes),
91}
92#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
93#[serde(untagged)]
94pub enum Sections {
95 Highlight(HighlightSections),
96 Project(ProjectSections),
97 Organization(OrganizationSections),
98}
99#[derive(Clone, Debug, Default, Serialize, Deserialize, Display, PartialEq, JsonSchema)]
100#[serde(rename_all = "lowercase")]
101pub enum SchemaType {
103 #[display("highlight")]
106 Highlight,
107 #[default]
109 #[display("project")]
110 Project,
111 #[display("organization")]
113 Organization,
114}
115#[derive(Clone, Debug, Default, Display, Serialize_repr, Deserialize_repr, PartialEq, PartialOrd, JsonSchema)]
121#[repr(u8)]
122pub enum TechnologyReadinessLevel {
123 #[display("Greenfield Research")]
127 Principles = 0,
128 #[default]
132 #[display("Basic Research")]
133 Research = 1,
134 #[display("Technology Concept")]
138 Concept = 2,
139 #[display("Feasible")]
143 Feasible = 3,
144 #[display("Developing")]
148 Developing = 4,
149 #[display("Developed")]
153 Developed = 5,
154 #[display("Prototype")]
158 Prototype = 6,
159 #[display("Operational")]
163 Operational = 7,
164 #[display("Mission Ready")]
168 MissionReady = 8,
169 #[display("Mission Capable")]
173 MissionCapable = 9,
174}
175#[derive(Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
179#[serde(rename_all = "camelCase")]
180pub struct ContactPoint {
181 #[serde(alias = "title", deserialize_with = "string_trim")]
191 pub job_title: String,
192 #[serde(alias = "first", deserialize_with = "string_trim")]
196 pub given_name: String,
197 #[serde(alias = "last", deserialize_with = "string_trim")]
201 pub family_name: String,
202 #[validate(email(message = "Please provide a valid email"))]
206 #[serde(deserialize_with = "string_trim")]
207 pub email: String,
208 #[validate(custom(function = "is_phone_number"))]
212 #[serde(alias = "phone", deserialize_with = "string_trim")]
213 pub telephone: String,
214 #[validate(url(message = "Please provide a valid profile URL"))]
218 #[serde(alias = "profile", deserialize_with = "string_trim")]
219 pub url: String,
220 #[serde(deserialize_with = "string_trim")]
224 pub organization: String,
225 pub affiliation: Option<String>,
231}
232#[skip_serializing_none]
233#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
234pub struct Graphic {
235 #[validate(length(max = "MAX_LENGTH_GRAPHIC_CAPTION", message = "Caption is too long, please reduce the length below 100."))]
236 #[serde(deserialize_with = "string_trim")]
237 pub caption: String,
238 #[validate(custom(function = "has_image_extension"))]
239 #[serde(default, deserialize_with = "option_string_trim")]
240 pub href: Option<String>,
241}
242#[skip_serializing_none]
243#[derive(Clone, Builder, Debug, Serialize, Deserialize, JsonSchema, Validate)]
244#[builder(start_fn = init)]
245#[serde(rename_all = "camelCase")]
246pub struct Metadata {
247 pub classification: Option<ClassificationLevel>,
248 #[validate(custom(function = "is_kebabcase"))]
254 #[serde(alias = "id", rename = "identifier", deserialize_with = "string_trim")]
255 pub identifier: String,
256 #[builder(default = SchemaType::default())]
257 #[serde(alias = "type", rename = "type")]
258 pub schema_type: SchemaType,
259 pub additional_type: Option<OrganizationType>,
260 #[validate(custom(function = "is_doi"))]
264 #[serde(default, deserialize_with = "option_string_trim")]
265 pub doi: Option<String>,
266 #[validate(custom(function = "is_raid"))]
270 #[serde(default, deserialize_with = "option_string_trim")]
271 pub raid: Option<String>,
272 #[validate(custom(function = "is_ror"))]
276 #[serde(default, deserialize_with = "option_string_trim")]
277 pub ror: Option<String>,
278 #[validate(url(message = "Please provide a valid URL"))]
280 #[serde(default, deserialize_with = "option_string_trim")]
281 pub publication: Option<String>,
282 #[builder(default = false)]
286 pub archive: bool,
287 #[builder(default = true)]
291 pub draft: bool,
292 pub trl: Option<TechnologyReadinessLevel>,
294 #[validate(nested)]
295 pub websites: Option<Vec<Website>>,
296 #[validate(nested)]
297 pub graphics: Option<Vec<Graphic>>,
298 #[builder(default = Vec::<String>::new())]
299 pub keywords: Vec<Keyword>,
300 #[builder(default = Vec::<String>::new())]
311 #[serde(deserialize_with = "vec_string_trim")]
312 pub technology: Vec<String>,
313 pub sponsors: Option<Vec<String>>,
319 pub partners: Option<Vec<String>>,
327 pub related: Option<Vec<String>>,
331}
332#[skip_serializing_none]
333#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
334pub struct Notes {
335 pub managers: Option<Vec<String>>,
337 pub programs: Option<Vec<String>>,
339 #[serde(default, deserialize_with = "option_string_trim")]
340 pub presentation: Option<String>,
341}
342#[skip_serializing_none]
343#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd)]
344#[display("Organization ({additional_type}) - {name})")]
345#[serde(rename_all = "camelCase")]
346pub struct Organization {
347 #[serde(deserialize_with = "string_trim")]
351 pub name: String,
352 #[serde(default, deserialize_with = "option_string_trim")]
356 pub ror: Option<String>,
357 #[serde(default, deserialize_with = "option_string_trim")]
361 pub alternative_name: Option<String>,
362 pub additional_type: OrganizationType,
366 pub keywords: Option<Vec<Keyword>>,
367 pub member: Vec<Organization>,
371}
372#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd, JsonSchema)]
373#[serde(rename_all = "lowercase")]
374pub enum OrganizationType {
375 #[display("agency")]
376 Agency,
377 #[display("center")]
378 Center,
379 #[display("directorate")]
380 Directorate,
381 #[display("division")]
382 Division,
383 #[display("facility")]
384 Facility,
385 #[display("FFRDC")]
387 Ffrdc,
388 #[display("group")]
389 Group,
390 #[display("office")]
391 Office,
392 #[display("program")]
393 Program,
394}
395#[skip_serializing_none]
399#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
400#[builder(start_fn = init)]
401pub struct ResearchActivity {
402 #[validate(nested)]
403 pub meta: Metadata,
404 #[validate(length(min = 4, max = "MAX_LENGTH_TITLE"))]
405 #[serde(deserialize_with = "string_trim")]
406 pub title: String,
407 #[validate(length(max = "MAX_LENGTH_SUBTITLE", message = "Subtitle is too long, please reduce the length below 75."))]
408 #[serde(default, deserialize_with = "option_string_trim")]
409 pub subtitle: Option<String>,
410 pub sections: Sections,
411 #[validate(nested)]
412 pub contact: ContactPoint,
413 pub notes: Option<Other>,
414}
415#[derive(Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
434pub struct Website {
435 #[serde(alias = "title", deserialize_with = "string_trim")]
439 pub description: String,
440 #[validate(url(message = "Please provide a valid URL"))]
441 #[serde(deserialize_with = "string_trim")]
442 pub url: String,
443}
444#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
445pub struct HighlightSections {
446 #[serde(alias = "scientific achievement", deserialize_with = "string_trim")]
447 pub achievement: String,
448 #[serde(alias = "significance & impact", deserialize_with = "string_trim")]
449 pub impact: String,
450 #[validate(
451 length(max = 6, message = "Please limit the number of methods to 6"),
452 custom(function = "validate_attribute_technical")
453 )]
454 #[serde(alias = "technical approach")]
455 #[serde(deserialize_with = "vec_string_trim")]
456 pub technical: Vec<String>,
457}
458#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
459pub struct ProjectSections {
460 #[validate(length(
461 min = 10,
462 max = "MAX_LENGTH_SECTION_INTRODUCTION",
463 message = "Introduction is too long, please reduce the length below 250."
464 ))]
465 #[serde(deserialize_with = "string_trim")]
466 pub introduction: String,
467 #[validate(length(
468 min = 10,
469 max = "MAX_LENGTH_SECTION_CHALLENGE",
470 message = "Challenge is too long, please reduce the length below 500."
471 ))]
472 #[serde(deserialize_with = "string_trim")]
473 pub challenge: String,
474 #[validate(length(
475 min = 10,
476 max = "MAX_LENGTH_SECTION_APPROACH",
477 message = "Approach is too long, please reduce the length below 500."
478 ))]
479 #[serde(deserialize_with = "string_trim")]
480 pub approach: String,
481 #[validate(length(min = 1, max = "MAX_COUNT_OUTCOMES"), custom(function = "validate_attribute_outcomes"))]
482 #[serde(deserialize_with = "vec_string_trim")]
483 pub outcomes: Vec<String>,
484 #[validate(nested)]
485 pub research: Research,
486}
487#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
488pub struct OrganizationSections {
489 #[validate(length(
490 min = 10,
491 max = "MAX_LENGTH_SECTION_INTRODUCTION",
492 message = "Mission is too long, please reduce the length below 250."
493 ))]
494 #[serde(deserialize_with = "string_trim")]
495 pub mission: String,
496 #[validate(length(min = 1, max = "MAX_COUNT_CAPABILITIES"), custom(function = "validate_attribute_capabilities"))]
497 #[serde(deserialize_with = "vec_string_trim")]
498 pub capabilities: Vec<String>,
499 #[validate(length(min = 1), custom(function = "validate_attribute_impact"))]
500 #[serde(deserialize_with = "vec_string_trim")]
501 pub impact: Vec<String>,
502 #[validate(custom(function = "validate_attribute_facilities"))]
503 pub facilities: Option<Vec<String>>,
504}
505#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
506pub struct Research {
507 #[validate(length(
508 min = 10,
509 max = "MAX_LENGTH_RESEARCH_FOCUS",
510 message = "Focus is too long, please reduce the length below 150."
511 ))]
512 #[serde(deserialize_with = "string_trim")]
513 pub focus: String,
514 #[validate(length(min = 1, max = "MAX_COUNT_RESEARCH_AREAS"), custom(function = "validate_attribute_areas"))]
515 #[serde(deserialize_with = "vec_string_trim")]
516 pub areas: Vec<String>,
517}
518impl View for ContactPoint {
519 fn render(&self) -> VirtualNode {
520 let ContactPoint {
521 given_name,
522 family_name,
523 job_title: role,
524 email,
525 telephone,
526 ..
527 } = self;
528 html! {
529 <section id="contact">
530 <div>
531 <span class="label">Contact</span>
532 <span class="spacer"> </span>
533 <span class="name">{ format!("{} {}", given_name, family_name) }</span>
534 <span class="spacer">|</span>
535 <span class="title">{ role }</span>
536 <span class="spacer">|</span>
537 <span class="email">{ email }</span>
538 <span class="spacer">|</span>
539 <span class="phone">{ telephone }</span>
540 </div>
541 </section>
542 }
543 }
544}
545impl Metadata {
546 fn get_first_graphic(self) -> Option<Graphic> {
547 match self.graphics {
548 | Some(values) => values.first().cloned(),
549 | None => None,
550 }
551 }
552 pub fn get_first_graphic_href(self) -> String {
553 match self.get_first_graphic() {
554 | Some(Graphic { href, .. }) => match href.clone() {
555 | Some(href) => href.clone(),
556 | None => DEFAULT_GRAPHIC_HREF.to_string(),
557 },
558 | None => DEFAULT_GRAPHIC_HREF.to_string(),
559 }
560 }
561 pub fn get_first_graphic_caption(self) -> String {
562 match self.get_first_graphic() {
563 | Some(Graphic { caption, .. }) => match caption.clone() {
564 | value if !value.is_empty() => value.clone(),
565 | _ => DEFAULT_GRAPHIC_CAPTION.to_string(),
566 },
567 | None => DEFAULT_GRAPHIC_CAPTION.to_string(),
568 }
569 }
570}
571impl Organization {
572 pub fn load() -> Vec<Organization> {
573 serde_json::from_str(&Constant::from_asset("organization.json")).unwrap()
574 }
575 pub fn visit<F: Copy + for<'a> Fn(&'a mut Organization)>(&mut self, f: F) {
576 f(self);
577 for child in self.member.iter_mut() {
578 child.visit(f);
579 }
580 }
581 pub fn get_member(self, label: &str) -> Option<Organization> {
582 self.get_members().into_iter().find(|Organization { name, .. }| name == label)
583 }
584 pub fn get_members(self) -> Vec<Organization> {
585 let organization = self;
586 let mut items = vec![organization.clone()];
587 let directorates = organization.member.clone();
588 for directorate in &directorates {
589 items.push(directorate.clone());
590 let divisions = directorate.member.clone();
591 for division in &divisions {
592 items.push(division.clone());
593 let groups = division.member.clone();
594 for group in &groups {
595 items.push(group.clone());
596 }
597 }
598 }
599 items
600 }
601 pub fn get_nearest(self, organization_type: OrganizationType) -> Option<Organization> {
602 let a = self.clone().additional_type.order();
603 let b = organization_type.order();
604 if a > b {
605 None
606 } else {
607 let ornl = Organization::load()[0].clone();
608 let graph = ornl.clone().to_graph();
609 let name = match b - a {
610 | 3 => Some(ornl.clone().name),
611 | 2 => match get_node_from_label(&graph, &self.name) {
612 | Some(node) => match get_node_parent(&graph, node) {
613 | Some(parent) => match get_node_parent(&graph, parent) {
614 | Some(grandparent) => get_node_name(&graph, grandparent),
615 | None => None,
616 },
617 | None => None,
618 },
619 | None => None,
620 },
621 | 1 => match get_node_from_label(&graph, &self.name) {
622 | Some(node) => match get_node_parent(&graph, node) {
623 | Some(parent) => get_node_name(&graph, parent),
624 | None => None,
625 },
626 | None => None,
627 },
628 | 0 => Some(self.name),
629 | _ => None,
630 };
631 match name {
632 | Some(value) => match ornl.get_member(&value) {
633 | Some(organization) => Some(organization),
634 | None => None,
635 },
636 | None => None,
637 }
638 }
639 }
640 pub fn to_graph(self) -> Graph<String, u8> {
641 let mut graph: Graph<String, u8, petgraph::Directed> = Graph::new();
642 let organization = &self;
643 let root = graph.add_node(organization.name.clone());
644 for directorate in organization.member.iter() {
645 let a = graph.add_node(directorate.name.clone());
646 graph.add_edge(root, a, 0);
647 for division in directorate.member.iter() {
648 let b = graph.add_node(division.name.clone());
649 graph.add_edge(a, b, 0);
650 for group in division.member.iter() {
651 let c = graph.add_node(group.name.clone());
652 graph.add_edge(b, c, 0);
653 }
654 }
655 }
656 graph
657 }
658}
659impl OrganizationType {
660 pub fn from_string(value: String) -> OrganizationType {
661 match value.to_lowercase().as_str() {
662 | "agency" => OrganizationType::Agency,
663 | "center" => OrganizationType::Center,
664 | "division" => OrganizationType::Division,
665 | "directorate" => OrganizationType::Directorate,
666 | "group" => OrganizationType::Group,
667 | "office" => OrganizationType::Office,
668 | "program" => OrganizationType::Program,
669 | "facility" => OrganizationType::Facility,
670 | "ffrdc" => OrganizationType::Ffrdc,
671 | _ => unreachable!(),
672 }
673 }
674 pub fn order(self) -> u8 {
675 match self {
676 | OrganizationType::Ffrdc | OrganizationType::Agency | OrganizationType::Office => 4,
677 | OrganizationType::Directorate => 3,
678 | OrganizationType::Division | OrganizationType::Center | OrganizationType::Program | OrganizationType::Facility => 2,
679 | OrganizationType::Group => 1,
680 }
681 }
682}
683impl ResearchActivity {
684 pub fn to_schema() {
685 let schema = schema_for!(ResearchActivity);
686 println!("{}", serde_json::to_string_pretty(&schema).unwrap());
687 }
688 pub fn analyze(paths: Vec<PathBuf>) -> usize {
689 let config = ValeConfig::default().save();
690 let init = Vale::init().build();
691 let vale = if test_command("vale".into()) {
692 init.with_config(config).with_system_command()
693 } else {
694 init.download(Some(config))
695 };
696 match vale.clone().sync() {
697 | Ok(_) => {
698 let results = paths.iter().map(|path| match ResearchActivity::read(path.into()) {
699 | Ok(data) => vale
700 .clone()
701 .analyze(data.clone().meta.identifier, data.extract_prose(), Some("JSON".into())),
702 | Err(err) => {
703 error!("=> {} Read research activity data at - {}", Label::fail(), err);
704 1
705 }
706 });
707 let output = results.collect::<Vec<usize>>();
708 output.into_iter().sum()
709 }
710 | Err(err) => {
711 error!("=> {} Vale sync - {}", Label::fail(), err);
712 1
713 }
714 }
715 }
716 pub fn check(paths: Vec<PathBuf>) -> usize {
717 paths
718 .par_iter()
719 .map(|path| match ResearchActivity::read(path.into()) {
720 | Ok(data) => match data.clone().get_errors() {
721 | Ok(_) => {
722 info!("=> {} {} has {}", Label::pass(), path.display(), "no schema errors".green().bold());
723 0
724 }
725 | Err(found) => {
726 let count = get_error_count(found.clone());
727 error!("=> {} Found {} errors in {}: {:#?}", Label::fail(), count, path.display(), found,);
728 count
729 }
730 },
731 | Err(err) => {
732 error!("=> {} Read research activity data at {} - {}", Label::fail(), path.display(), err);
733 0
734 }
735 })
736 .sum()
737 }
738 pub fn copy(self) -> ResearchActivity {
739 let ResearchActivity {
740 meta,
741 title,
742 subtitle,
743 sections,
744 contact,
745 notes,
746 } = self.clone();
747 ResearchActivity::init()
748 .meta(meta)
749 .title(title)
750 .maybe_subtitle(subtitle)
751 .sections(sections)
752 .contact(contact)
753 .maybe_notes(notes)
754 .build()
755 }
756 pub fn extract_prose(self) -> String {
757 let sections = match self.sections {
758 | Sections::Highlight(HighlightSections {
759 achievement,
760 impact,
761 technical,
762 }) => {
763 format!(
764 r#"
765<!-- Achievement -->
766{}
767
768<!-- Impact -->
769{}
770
771<!-- Technical Approach -->
772{}"#,
773 achievement,
774 impact,
775 technical.into_iter().map(|x| format!("- {}", x)).collect::<Vec<String>>().join("\n")
776 )
777 }
778 | Sections::Project(ProjectSections {
779 introduction,
780 challenge,
781 approach,
782 outcomes,
783 research,
784 ..
785 }) => {
786 let Research { focus, areas } = research;
787 format!(
788 r#"
789<!-- Introduction -->
790{}
791
792<!-- Challenge -->
793{}
794
795<!-- Approach -->
796{}
797
798<!-- Outcomes -->
799{}
800
801<!-- Focus -->
802{}
803
804<!-- Areas -->
805{}"#,
806 introduction,
807 challenge,
808 approach,
809 outcomes.into_iter().map(|x| format!("- {}", x)).collect::<Vec<String>>().join("\n"),
810 focus,
811 areas.into_iter().map(|x| format!("- {}", x)).collect::<Vec<String>>().join("\n")
812 )
813 }
814 | Sections::Organization(OrganizationSections {
815 mission,
816 capabilities,
817 impact,
818 facilities,
819 }) => {
820 format!(
821 r#"
822<!-- Mission -->
823{}
824
825<!-- Capabilities -->
826{}
827
828<!-- Impact -->
829{}
830
831<!-- Facilities -->
832{}"#,
833 mission,
834 capabilities.into_iter().map(|x| format!("- {}", x)).collect::<Vec<String>>().join("\n"),
835 impact.into_iter().map(|x| format!("- {}", x)).collect::<Vec<String>>().join("\n"),
836 facilities
837 .unwrap_or_default()
838 .into_iter()
839 .map(|x| format!("- {}", x))
840 .collect::<Vec<String>>()
841 .join("\n")
842 )
843 }
844 };
845 match self.subtitle {
846 | Some(subtitle) => format!(
847 r#"# {}
848> {}
849{}"#,
850 self.title, subtitle, sections
851 ),
852 | None => sections.to_string(),
853 }
854 }
855 pub fn format(self, path: Option<PathBuf>) -> ResearchActivity {
856 let parent = match path {
857 | Some(value) => value.parent().unwrap().to_path_buf(),
858 | None => PathBuf::from("."),
859 };
860 let name = match get_image_paths(parent.clone()) {
861 | value if !value.is_empty() => value[0].file_name().unwrap().to_string_lossy().to_string(),
862 | _ => DEFAULT_GRAPHIC_HREF.to_string(),
863 };
864 debug!(name, "=> {} First image", Label::using());
865 let first_graphic = match self.meta.clone().graphics {
866 | Some(values) if !values.is_empty() => {
867 let caption = match values.first() {
868 | Some(Graphic { caption, .. }) if !caption.is_empty() => {
869 let trimmed = caption.trim();
870 trace!(caption = trimmed, "=> {}", Label::using());
871 trimmed
872 }
873 | Some(_) | None => {
874 error!(path = parent.to_str().unwrap(), "=> {} Caption", Label::not_found());
875 &"".to_string()
876 }
877 };
878 Graphic {
879 href: Some(name.clone()),
880 caption: caption.to_string(),
881 }
882 }
883 | Some(_) | None => Graphic {
884 href: Some(name.clone()),
885 caption: "".to_string(),
886 },
887 };
888 let mut clone = self.clone().copy();
889 clone.meta.graphics = Some(vec![first_graphic]);
890 clone.meta.keywords = self.clone().resolve(FuzzyValue::Keyword);
891 clone.meta.technology = self.clone().resolve(FuzzyValue::Technology);
892 clone.contact.organization = match resolve_from_organization_json(self.clone().contact.organization) {
893 | Some(value) => value,
894 | None => "".to_string(),
895 };
896 clone.contact.affiliation = match self.clone().contact.affiliation {
897 | Some(ref affiliation) => match resolve_from_organization_json(affiliation.to_string()) {
898 | Some(resolved) => Some(resolved),
899 | None => {
900 error!(affiliation, "=> {} Affiliation", Label::not_found());
901 Some(DEFAULT_AFFILIATION.to_string())
902 }
903 },
904 | None => {
905 let ornl = &Organization::load()[0];
906 match ornl.clone().get_member(&clone.contact.organization) {
907 | Some(organization) => match organization.get_nearest(OrganizationType::Directorate) {
908 | Some(Organization { name, .. }) => Some(name),
909 | None => Some(DEFAULT_AFFILIATION.to_string()),
910 },
911 | None => {
912 error!("=> {} Nearest directorate", Label::not_found());
913 Some(DEFAULT_AFFILIATION.to_string())
914 }
915 }
916 }
917 };
918 clone.meta.partners = match self.clone().resolve(FuzzyValue::Partner) {
919 | values if !values.is_empty() => Some(values),
920 | _ => None,
921 };
922 clone.meta.sponsors = match self.clone().resolve(FuzzyValue::Sponsor) {
923 | values if !values.is_empty() => Some(values),
924 | _ => None,
925 };
926 clone
927 }
928 pub fn get_errors(self) -> Result<(), HashMap<String, ValidationErrorsKind>> {
929 let mut found: Vec<Option<HashMap<String, ValidationErrorsKind>>> = vec![];
930 found.push(get_validation_errors::<ResearchActivity>(self.clone()));
931 match self.clone().sections {
932 | Sections::Highlight(sections) => {
933 found.push(get_validation_errors::<HighlightSections>(sections));
934 }
935 | Sections::Project(sections) => {
936 found.push(get_validation_errors::<ProjectSections>(sections));
937 }
938 | Sections::Organization(sections) => {
939 found.push(get_validation_errors::<OrganizationSections>(sections));
940 }
941 };
942 let errors = format_errors(found.clone());
943 if !errors.is_empty() {
944 Err(errors)
945 } else {
946 Ok(())
947 }
948 }
949 pub fn read(path: PathBuf) -> serde_json::Result<ResearchActivity> {
950 let content = match read_file(path.clone()) {
951 | Ok(value) if !value.is_empty() => value,
952 | Ok(_) | Err(_) => {
953 error!(path = path.to_str().unwrap(), "=> {} Project content is not valid", Label::fail());
954 "{}".to_owned()
955 }
956 };
957 let parsed: serde_json::Result<ResearchActivity> = serde_json::from_str(&content);
958 let label = match parsed {
959 | Ok(_) => Label::using(),
960 | Err(_) => Label::invalid(),
961 };
962 match parsed {
963 | Ok(data) => {
964 debug!(path = path.to_str().unwrap(), "=> {}", label);
965 trace!("=> {} Research activity data = {:#?}", label, data.dimmed().cyan());
966 Ok(data)
967 }
968 | Err(err) => {
969 error!(path = path.to_str().unwrap(), "=> {}", label);
970 Err(err)
971 }
972 }
973 }
974 fn resolve(self, value_type: FuzzyValue) -> Vec<String> {
975 let values: Vec<_> = match value_type {
976 | FuzzyValue::Keyword => self.meta.keywords,
977 | FuzzyValue::Partner => match self.meta.partners {
978 | Some(values) => values,
979 | None => vec![],
980 },
981 | FuzzyValue::Sponsor => match self.meta.sponsors {
982 | Some(values) => values,
983 | None => vec![],
984 },
985 | FuzzyValue::Technology => self.meta.technology,
986 };
987 let mut data: Vec<_> = values
988 .into_iter()
989 .flat_map(|x| resolve_from_csv_asset(format!("{}", value_type), x))
990 .collect();
991 data.sort();
992 data.dedup();
993 data
994 }
995 pub fn to_markdown(self) -> String {
996 let ResearchActivity { title, .. } = self.clone();
997 format!("# {}", title)
998 }
999}
1000fn get_match_list(value: String, values: Vec<String>) -> Vec<(String, u32)> {
1001 let pattern = Pattern::parse(&value, CaseMatching::Ignore, Normalization::Smart);
1002 let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
1003 pattern.match_list(values.clone(), &mut matcher)
1004}
1005fn print_resolution(output: Option<String>, value: String, name: String) {
1006 let label = name.titlecase();
1007 match output {
1008 | Some(resolved) => {
1009 if resolved.eq(&value.to_string()) {
1010 trace!("=> {} {} = \"{}\"", Label::using(), label, value.clone());
1011 } else {
1012 debug!(input = value.clone(), resolved, "=> {} {}", Label::found(), label);
1013 }
1014 }
1015 | None => {
1016 debug!(value = value.clone(), "=> {} {}", Label::not_found(), label);
1017 }
1018 };
1019}
1020fn resolve_from_csv_asset(name: String, value: String) -> Option<String> {
1021 let data = Constant::csv(&name);
1022 resolve_from_list_of_lists(value, data, name)
1023}
1024fn resolve_from_list_of_lists(value: String, data: Vec<Vec<String>>, name: String) -> Option<String> {
1025 let output = data
1026 .into_iter()
1027 .flat_map(|values| {
1028 let sanitized = sanitize(value.clone());
1029 let matched = get_match_list(sanitized, values.clone());
1030 trace!("{} => {:?}", value.clone(), matched.clone());
1031 if matched.clone().is_empty() {
1032 None
1033 } else {
1034 match values.first() {
1035 | Some(x) => {
1036 if value.eq(x) {
1037 Some((x.into(), 10000))
1038 } else {
1039 let score = matched.into_iter().map(|(_, score)| score).max();
1040 match score {
1041 | Some(value) if value > 0 => Some((x.to_string(), value)),
1042 | Some(_) | None => None,
1043 }
1044 }
1045 }
1046 | None => None,
1047 }
1048 }
1049 })
1050 .max_by_key(|(_, score)| *score)
1051 .map(|(x, _)| x.to_string());
1052 print_resolution(output.clone(), value, name);
1053 output
1054}
1055fn resolve_from_organization_json(value: String) -> Option<String> {
1056 let organization = &Organization::load()[0];
1057 let mut items = vec![organization.clone()];
1058 let directorates = organization.member.clone();
1059 for directorate in &directorates {
1060 items.push(directorate.clone());
1061 let divisions = directorate.member.clone();
1062 for division in &divisions {
1063 items.push(division.clone());
1064 }
1065 }
1066 let data = items
1067 .into_iter()
1068 .map(|x| (x.name.clone(), x.alternative_name.clone()))
1069 .filter(|(name, alias)| !(name.is_empty() || alias.is_none()))
1070 .map(|(name, alias)| {
1071 let alternative_name = match alias {
1072 | Some(x) => x.to_string(),
1073 | None => name.clone(),
1074 };
1075 vec![name, alternative_name]
1076 })
1077 .collect::<Vec<Vec<String>>>();
1078 resolve_from_list_of_lists(value, data, "organization".to_string())
1079}
1080fn sanitize(value: String) -> String {
1081 match Regex::new(r"[-_.,]") {
1082 | Ok(re) => re.replace_all(&value, "").replace("&", "and").trim().to_string(),
1083 | Err(err) => err.to_string(),
1084 }
1085}
1086
1087#[cfg(test)]
1088mod tests;