acorn_lib/schema/
mod.rs

1//! ## Research activity schema
2//!
3//! Here you'll find everything needed to build and use the research activity data schema, including metadata fields, section information, media objects, formats, and functions that power ACORN CLI commands.
4//!
5use crate::analyzer::readability::ReadabilityType;
6use crate::analyzer::vale::{Vale, ValeConfig};
7use crate::analyzer::{StaticAnalyzer, StaticAnalyzerConfig};
8use crate::constants::*;
9use crate::util::*;
10use bon::{builder, Builder};
11use derive_more::Display;
12use fancy_regex::Regex;
13use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
14use nucleo_matcher::{Config, Matcher};
15use owo_colors::OwoColorize;
16use percy_dom::prelude::{html, IterableNodes, View, VirtualNode};
17use petgraph::graph::Graph;
18use rayon::prelude::*;
19use schemars::{schema_for, JsonSchema};
20use serde::{Deserialize, Serialize};
21use serde_repr::*;
22use serde_trim::*;
23use serde_with::skip_serializing_none;
24use std::hash::{Hash, Hasher};
25use std::num::NonZeroU64;
26use std::path::PathBuf;
27use titlecase::Titlecase;
28use tracing::{debug, error, info, trace};
29use validator::Validate;
30
31pub mod graph;
32pub mod raid;
33pub mod validate;
34use graph::*;
35use validate::*;
36
37/// ## Keywords
38/// > Core concepts related to the associated research activity
39///
40/// Could be used to filter research activity data and/or power data analytics through concept composition
41///
42/// ### Guidelines for creating keywords
43/// - **Shall**
44///     - Be officially sanctioned by responsible parties
45///     - Be in lower-kebab-case
46///     - Be unique relative to other keywords
47///     - Contain three or more characters
48/// - **Should**
49///     - Not be too specific
50///     - Be one or two words (ex. `foo` or `foo-bar`)
51///
52/// <div class="warning"><a href="https://code.ornl.gov/research-enablement/acorn/-/blob/main/acorn-lib/assets/constants/keywords.csv">Full list of keywords</a></div>
53pub type Keyword = String;
54/// U.S. Classified National Security Information Level
55///
56/// See [President Executive Order 13526](https://www.archives.gov/isoo/policy-documents/cnsi-eo.html)
57#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, PartialEq, PartialOrd, JsonSchema)]
58#[serde(rename_all = "lowercase")]
59pub enum ClassificationLevel {
60    /// ### Unclassified (U)
61    #[default]
62    #[display("UNCLASSIFIED")]
63    Unclassified,
64    /// ### Confidential (C)
65    ///
66    /// Shall be applied to information, the unauthorized disclosure of which reasonably could be expected to cause ***damage*** to the national security that the original classification authority is able to identify or describe.
67    #[display("CONFIDENTIAL")]
68    Confidential,
69    /// ### Secret (S)
70    ///
71    /// Shall be applied to information, the unauthorized disclosure of which reasonably could be expected to cause ***serious damage*** to the national security that the original classification authority is able to identify or describe.
72    #[display("SECRET")]
73    Secret,
74    /// ### Top Secret (TS)
75    ///
76    /// Shall be applied to information, the unauthorized disclosure of which reasonably could be expected to cause ***exceptionally grave damage*** to the national security that the original classification authority is able to identify or describe.
77    #[display("TOP SECRET")]
78    #[serde(alias = "top secret")]
79    TopSecret,
80}
81#[derive(Clone, Debug, Serialize, Deserialize, Display)]
82enum FuzzyValue {
83    #[display("partners")]
84    Partner,
85    /// See [Keyword]
86    #[display("keywords")]
87    Keyword,
88    #[display("sponsors")]
89    Sponsor,
90    #[display("technology")]
91    Technology,
92}
93/// Media object such as image or video
94///
95/// See <https://schema.org/MediaObject>
96#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
97#[serde(untagged)]
98pub enum MediaObject {
99    /// Image format media
100    Image(ImageObject),
101    /// Video format media
102    Video(VideoObject),
103}
104/// Organization sub type
105#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd, JsonSchema)]
106#[serde(rename_all = "lowercase")]
107pub enum OrganizationType {
108    /// Agency
109    #[display("agency")]
110    Agency,
111    /// Initiative that involves multiple DOE laboratories partnering together for a shared purpose
112    #[display("center")]
113    Center,
114    /// Laboratory, public, and private partners
115    #[display("consortium")]
116    Consortium,
117    /// Top-level organizational unit that contains one or more divisions
118    #[display("directorate")]
119    Directorate,
120    /// Mid-level organizational unit that contains one or more sections and groups
121    #[display("division")]
122    Division,
123    /// Building, room, array of equipment, or a number of such things, designed to serve a particular function
124    ///
125    /// Includes DOE-designated user facilities
126    #[display("facility")]
127    Facility,
128    /// Federally Funded Research and Development Center
129    #[display("FFRDC")]
130    Ffrdc,
131    /// Low-level organizational unit that contains a small number of people that function as a team
132    #[display("group")]
133    Group,
134    /// Office
135    #[display("office")]
136    Office,
137    /// Program
138    #[display("program")]
139    Program,
140}
141/// "Other" content not easily placed into the schema
142#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
143#[serde(untagged)]
144pub enum Other {
145    /// Free-form test
146    Unformatted(String),
147    /// Structured container for miscellaneaous things
148    Formatted(Notes),
149}
150/// ## TRL
151/// > Technology readiness levels (TRLs) are a method for estimating the maturity of technologies during the acquisition phase of a program.
152///
153/// The "optimal point" to introduce technology depends on technology maturity (TRL) and program requirements. That point can be virtually anywhere in the acquisition process.
154///
155/// See [Technology Readiness for Machine Learning Systems](https://doi.org/10.1038/s41467-022-33128-9) for applying TRLs to machine learning (ML) systems
156#[derive(Clone, Debug, Default, Display, Serialize_repr, Deserialize_repr, PartialEq, PartialOrd, JsonSchema)]
157#[repr(u8)]
158#[serde(deny_unknown_fields)]
159pub enum TechnologyReadinessLevel {
160    /// A stage for greenfield research
161    ///
162    /// Not a standard TRL
163    #[display("Greenfield Research")]
164    Principles = 0,
165    /// Basic principles observed and reported
166    ///
167    /// ML: Goal-oriented research
168    #[default]
169    #[display("Basic Research")]
170    Research = 1,
171    /// Technology concept and/or application formulated
172    ///
173    /// ML: Proof of principle development
174    #[display("Technology Concept")]
175    Concept = 2,
176    /// Analytical and experimental critical function and/or characteristic proof-of-concept
177    ///
178    /// ML: Systems development
179    #[display("Feasible")]
180    Feasible = 3,
181    /// Component and/or breadboard validation in laboratory environment (low fidelity)
182    ///
183    /// ML: Proof of concept development
184    #[display("Developing")]
185    Developing = 4,
186    /// Component and/or breadboard validation in relevant environment (high fidelity)
187    ///
188    /// ML: Machine learning "capability"
189    #[display("Developed")]
190    Developed = 5,
191    /// System/subsystem model or prototype demonstration in a relevant environment (high fidelity)
192    ///
193    /// ML: Application development
194    #[display("Prototype")]
195    Prototype = 6,
196    /// System prototype demonstration in an operational environment
197    ///
198    /// ML: Integrations
199    #[display("Operational")]
200    Operational = 7,
201    /// Actual system completed and qualified through test and demonstration
202    ///
203    /// ML: Mission-ready
204    #[display("Mission Ready")]
205    MissionReady = 8,
206    /// Actual system proven through successful mission operation
207    ///
208    /// ML: Deployment
209    #[display("Mission Capable")]
210    MissionCapable = 9,
211}
212/// Contact point (i.e. "point of contact") for research activity
213///
214/// See <https://schema.org/ContactPoint>
215#[derive(Builder, Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
216#[builder(start_fn = init)]
217#[serde(deny_unknown_fields, rename_all = "camelCase")]
218pub struct ContactPoint {
219    /// Job title (e.g., "Group Lead") of role that the contact fills related to the asscociated research activity.
220    /// ### Example
221    /// > Ideal contact title for a project would be "Primary Investigator"
222    ///
223    /// ### Example
224    /// > Ideal contact title for a group organization would be "Group Lead"
225    ///
226    /// <div class="warning">When the nearest associated title is unclear, job role of the contact can be used (e.g., "Senior Scientist").</div>
227    ///
228    /// See <https://schema.org/jobTitle> for more information
229    #[builder(default = "Researcher".to_string())]
230    #[serde(alias = "title", deserialize_with = "string_trim")]
231    pub job_title: String,
232    /// First (given) name of contact
233    ///
234    /// See <https://schema.org/givenName> for more information
235    #[builder(default = "First".to_string())]
236    #[serde(alias = "first", deserialize_with = "string_trim")]
237    pub given_name: String,
238    /// Last (family) name of contact
239    ///
240    /// See <https://schema.org/familyName> for more information
241    #[builder(default = "Last".to_string())]
242    #[serde(alias = "last", deserialize_with = "string_trim")]
243    pub family_name: String,
244    /// Email address of contact point
245    ///
246    /// See <https://schema.org/email> for more information
247    #[validate(email(message = "Please provide a valid email"))]
248    #[builder(default = "first_last@example.com".to_string())]
249    #[serde(deserialize_with = "string_trim")]
250    pub email: String,
251    /// Phone number of contact point
252    ///
253    /// See <https://schema.org/telephone> for more information
254    #[validate(custom(function = "is_phone_number"))]
255    #[builder(default = "123-456-7890".to_string())]
256    #[serde(alias = "phone", deserialize_with = "string_trim")]
257    pub telephone: String,
258    /// Profile URL of contact point
259    /// ### Example
260    /// > Profile URL for "Jason Wohlgemuth" could be <https://impact.ornl.gov/en/persons/jason-wohlgemuth>
261    #[validate(url(message = "Please provide a valid profile URL"))]
262    #[builder(default = "https://example.com".to_string())]
263    #[serde(alias = "profile", deserialize_with = "string_trim")]
264    pub url: String,
265    /// Organization of contact point
266    ///
267    /// See [Organization]
268    #[builder(default = "Some Organization".to_string())]
269    #[serde(deserialize_with = "string_trim")]
270    pub organization: String,
271    /// Affiliation of associated research activity data
272    ///
273    /// <div class="warning">Where organization applies to the contact point, affiliation applies to the research activity the contact point is associated with</div>
274    ///
275    /// See <https://schema.org/affiliation> for more information
276    pub affiliation: Option<String>,
277}
278/// Image format media (e.g., PNG, JPEG, SVG, etc.)
279///
280/// See <https://schema.org/ImageObject>
281#[skip_serializing_none]
282#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
283#[builder(start_fn = init)]
284#[serde(deny_unknown_fields, rename_all = "camelCase")]
285pub struct ImageObject {
286    /// Image caption
287    #[validate(length(max = "MAX_LENGTH_IMAGE_CAPTION", message = "Caption is too long, please reduce the length below 100."))]
288    #[serde(deserialize_with = "string_trim")]
289    pub caption: String,
290    /// File size (in kilobytes)
291    ///
292    /// <div class="warning">Will be overwritten by running <pre>acorn format</pre></div>
293    ///
294    /// See <https://schema.org/contentSize> for more information
295    #[serde(alias = "size")]
296    pub content_size: Option<NonZeroU64>,
297    /// Content URL
298    #[validate(custom(function = "has_image_extension"))]
299    #[serde(alias = "url", alias = "href")]
300    pub content_url: Option<String>,
301    /// Image height (in pixels)
302    ///
303    /// <div class="warning">Will be overwritten by running <pre>acorn format</pre></div>
304    ///
305    /// See <https://schema.org/height> for more information
306    pub height: Option<NonZeroU64>,
307    /// Image width (in pixels)
308    ///
309    /// <div class="warning">Will be overwritten by running <pre>acorn format</pre></div>
310    ///
311    /// See <https://schema.org/width> for more information
312    pub width: Option<NonZeroU64>,
313}
314/// ## Research Activity Metadata
315#[skip_serializing_none]
316#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
317#[builder(start_fn = init)]
318#[serde(deny_unknown_fields, rename_all = "camelCase")]
319pub struct Metadata {
320    /// Classification level of associated research activity data
321    pub classification: Option<ClassificationLevel>,
322    /// <abbr title="Technology Readiness Level">TRL</abbr> is applicable to acquisition, machine learning, and more
323    pub trl: Option<TechnologyReadinessLevel>,
324    /// Describes the active status of the associated research activity data
325    ///
326    /// <div class="warning">Archived content typically will be omitted from public artifacts such as <a href="https://research.ornl.gov">the ORNL research activity index</a></div>
327    #[builder(default = false)]
328    pub archive: bool,
329    /// Describes the draft status of the associated research activity data
330    ///
331    /// <div class="warning">Draft content typically will be omitted from public artifacts such as <a href="https://research.ornl.gov">the ORNL research activity index</a></div>
332    #[builder(default = true)]
333    pub draft: bool,
334    /// Identifier for associated research activity data
335    /// ### Example
336    /// > `my-research-project`
337    ///
338    /// <div class="warning">Should be <a href="https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case">lower-kebab-case</a></div>
339    ///
340    #[validate(custom(function = "is_kebabcase"))]
341    #[builder(default = "some-research-project".to_string())]
342    #[serde(alias = "id", rename = "identifier", deserialize_with = "string_trim")]
343    pub identifier: String,
344    /// Digital Object Identifier(s) related to the associated research activity data
345    ///
346    /// See <https://www.doi.org/> for more information
347    #[validate(custom(function = "validate_attribute_doi"))]
348    #[serde(default)]
349    pub doi: Option<Vec<String>>,
350    /// URL(s) of internet location where associated publication(s) can be found
351    #[validate(custom(function = "is_list_url"))]
352    #[serde(default)]
353    pub publications: Option<Vec<String>>,
354    /// Research Activity Identifier
355    ///
356    /// See <https://www.raid.org/> for more information
357    #[validate(nested)]
358    #[serde(default)]
359    pub raid: Option<raid::Metadata>,
360    /// Research Organization Registry
361    ///
362    /// See <https://www.ror.org/> for more information
363    #[validate(custom(function = "validate_attribute_ror"))]
364    #[serde(default)]
365    pub ror: Option<Vec<String>>,
366    /// Additional type
367    ///
368    /// Type of associated research activity data when directly associated with an organization
369    pub additional_type: Option<OrganizationType>,
370    /// Images, videos, and other media related to the associated research activity data
371    #[serde(alias = "graphics")]
372    pub media: Option<Vec<MediaObject>>,
373    /// Websites related to the associated research activity data
374    #[validate(nested)]
375    pub websites: Option<Vec<Website>>,
376    /// See [Keyword]
377    #[builder(default = Vec::<String>::new())]
378    pub keywords: Vec<Keyword>,
379    /// Software, programmings languages, and digital resources (e.g., tools, libraries, frameworks, data) related to the associated research activity data
380    /// ### Examples
381    /// - Rust
382    /// - Polars
383    /// - gdal
384    /// - matplotlib
385    /// - LaTeX
386    ///
387    /// <div class="warning"><a href="https://code.ornl.gov/research-enablement/acorn/-/blob/main/acorn-lib/assets/constants/technology.csv">Full list of technologies</a></div>
388    #[builder(default = Vec::<String>::new())]
389    #[serde(deserialize_with = "vec_string_trim")]
390    pub technology: Vec<String>,
391    /// Organization(s) responsible for funding associated research activity data
392    ///
393    /// Includes any office within a US cabinet-level department that has leadership appointed by the president and confirmed by the Senate, e.g., NNSA or Office of Science.
394    ///
395    /// <div class="warning"><a href="https://code.ornl.gov/research-enablement/acorn/-/blob/main/acorn-lib/assets/constants/sponsors.csv">Full list of sponsors</a></div>
396    pub sponsors: Option<Vec<String>>,
397    /// Organization(s) related to the associated research activity data
398    /// ### Examples
399    /// - Los Alamos National Laboratory
400    /// - University of Tennessee
401    /// - IBM
402    /// <div class="warning"><a href="https://code.ornl.gov/research-enablement/acorn/-/blob/main/acorn-lib/assets/constants/partners.csv">Full list of partners</a></div>
403    pub partners: Option<Vec<String>>,
404    /// Related resarch activity data identifiers of related research activity data
405    ///
406    /// <div class="warning">WIP</div>
407    pub related: Option<Vec<String>>,
408}
409/// Notes
410///
411/// Structured container for information not easily captured in other fields
412#[skip_serializing_none]
413#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Validate)]
414#[serde(deny_unknown_fields)]
415pub struct Notes {
416    /// [ASCR](https://www.energy.gov/science/ascr/advanced-scientific-computing-research) highlight attribute
417    pub managers: Option<Vec<String>>,
418    /// Collection of capabilities aimed at achieving a specific cross-cutting research outcome
419    pub programs: Option<Vec<String>>,
420    /// (PowerPoint) presentation notes
421    #[serde(default, deserialize_with = "option_string_trim")]
422    pub presentation: Option<String>,
423}
424/// ### Organization
425///
426/// Structured container for information about an organization
427///
428/// See also [OrganizationType]
429#[skip_serializing_none]
430#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd)]
431#[display("Organization ({additional_type}) - {name})")]
432#[serde(deny_unknown_fields, rename_all = "camelCase")]
433pub struct Organization {
434    /// Full name of the organization
435    ///
436    /// See <https://schema.org/name> for more information
437    #[serde(deserialize_with = "string_trim")]
438    pub name: String,
439    /// Research Organization Registry
440    ///
441    /// See <https://www.ror.org/> for more information
442    #[serde(default, deserialize_with = "option_string_trim")]
443    pub ror: Option<String>,
444    /// Organization alias (e.g., acronym or nickname)
445    ///
446    /// See <https://schema.org/alternateName> for more information
447    #[serde(default, deserialize_with = "option_string_trim")]
448    pub alternative_name: Option<String>,
449    /// Organization sub-type
450    ///
451    /// See <https://schema.org/additionalType> for more information
452    pub additional_type: OrganizationType,
453    /// See [Keyword]
454    pub keywords: Option<Vec<Keyword>>,
455    /// Distinct part(s) of the associated containing organization
456    ///
457    /// See <https://schema.org/member> for more information
458    pub member: Vec<Organization>,
459}
460/// ## Research Activity
461/// > Research activity is an identifiable package of work involving organized, systematic investigation.
462///
463/// See <https://www.raid.org/> for more information
464#[skip_serializing_none]
465#[derive(Builder, Clone, Debug, Display, Deserialize, Serialize, JsonSchema, Validate)]
466#[builder(start_fn = init)]
467#[display("Research Activity ({title})")]
468#[serde(deny_unknown_fields)]
469pub struct ResearchActivity {
470    /// Associated metadata
471    #[validate(nested)]
472    #[builder(default)]
473    pub meta: Metadata,
474    /// Heading that identifies and describes the associated research activity
475    #[validate(length(min = 4, max = "MAX_LENGTH_TITLE"))]
476    #[builder(default = "Research Activity Title".to_string())]
477    #[serde(deserialize_with = "string_trim")]
478    pub title: String,
479    /// Short description that augments the title of the associated research activity
480    #[validate(length(max = "MAX_LENGTH_SUBTITLE", message = "Subtitle is too long, please reduce the length below 75."))]
481    #[serde(default, deserialize_with = "option_string_trim")]
482    pub subtitle: Option<String>,
483    /// Prose components of associated research activity
484    #[validate(nested)]
485    #[builder(default)]
486    pub sections: Sections,
487    /// Contact point (i.e. "point of contact") for research activity
488    #[validate(nested)]
489    #[builder(default)]
490    pub contact: ContactPoint,
491    /// Other information related to the associated research activity not easily captured in structured areas of the schema
492    pub notes: Option<Other>,
493}
494/// Video format media (e.g., MP4, AVI, MOV, GIF, etc.)
495///
496/// See <https://schema.org/VideoObject> for more information
497#[skip_serializing_none]
498#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
499#[serde(deny_unknown_fields, rename_all = "camelCase")]
500pub struct VideoObject {
501    /// File size (in kilobytes)
502    ///
503    /// See <https://schema.org/contentSize> for more information
504    #[serde(alias = "size")]
505    pub content_size: Option<NonZeroU64>,
506    /// Video URL
507    #[validate(url)]
508    #[serde(alias = "url", alias = "href")]
509    pub content_url: Option<String>,
510    /// Video description
511    ///
512    /// See <https://schema.org/description> for more information
513    #[serde(deserialize_with = "string_trim")]
514    pub description: String,
515    // TODO: Create ISO 8601 struct and/or validator
516    /// Duration of video in [ISO 8601 format](https://en.wikipedia.org/wiki/ISO_8601)
517    ///
518    /// See <https://schema.org/duration> for more information
519    pub duration: Option<String>,
520    /// Video height (in pixels)
521    ///
522    /// See <https://schema.org/height> for more information
523    pub height: Option<NonZeroU64>,
524    /// Video width (in pixels)
525    ///
526    /// See <https://schema.org/width> for more information
527    pub width: Option<NonZeroU64>,
528}
529/// ## Website
530/// > Website link and title description
531/// ### Example
532/// When deserializing research activity data, websites can be provided as a list of JSON objects.
533/// ```json
534/// {
535///     "websites": [
536///       {
537///         "title": "Home Page",
538///         "url": "https://example.com"
539///       },
540///       {
541///         "title": "Job Listing",
542///         "url": "https://www.example.com/jobs"
543///       }
544///     ]
545/// }
546/// ```
547///
548#[derive(Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
549#[serde(deny_unknown_fields)]
550pub struct Website {
551    /// Brief description of webpage content
552    ///
553    /// See <https://schema.org/description> for more information
554    #[serde(alias = "title", deserialize_with = "string_trim")]
555    pub description: String,
556    /// Associated website URL
557    #[validate(url(message = "Please provide a valid URL"))]
558    #[serde(deserialize_with = "string_trim")]
559    pub url: String,
560}
561/// Research activity prose components that describe the activity using natural language
562#[skip_serializing_none]
563#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
564#[builder(start_fn = init)]
565#[serde(deny_unknown_fields)]
566pub struct Sections {
567    /// The reason for the research or research organization to exist
568    /// ### Example
569    /// > "Develop the first atomic bombs in the world to assist the Allied forces and bring an end to WWII"
570    #[validate(length(
571        min = 10,
572        max = "MAX_LENGTH_SECTION_MISSION",
573        message = "Mission is too long, please reduce the length below 250."
574    ))]
575    #[builder(default = "Purpose of the research".to_string())]
576    #[serde(alias = "introduction", deserialize_with = "string_trim")]
577    pub mission: String,
578    /// A problem or situation within a research field requiring scientific effort, resources, and/or innovation to overcome
579    /// ### Example
580    /// > "During WWII, there was a fear that Germany was researching and developing nuclear weapons, giving them a decisive advantage over Allied forces, including the United States, Great Britain, and Canada."
581    #[validate(length(
582        min = 10,
583        max = "MAX_LENGTH_SECTION_CHALLENGE",
584        message = "Challenge is too long, please reduce the length below 500."
585    ))]
586    #[builder(default = "Reason for the research".to_string())]
587    #[serde(deserialize_with = "string_trim")]
588    pub challenge: String,
589    /// The plan, resources and actions taken to perform the research in a given project or organization
590    /// ### Examples
591    /// - "Production across four different sites in the United States, each with a different focus, for security and safety purposes"
592    /// - "Research into new fields including nuclear fission, isotope separation methods, uranium enrichment, plutonium development, and weapons design"
593    /// - "Military coordination for project construction and security management as well as defense communications to national leaders"
594    #[validate(
595        length(min = 1, max = "MAX_COUNT_APPROACH", message = "Please limit the number of approaches to 6"),
596        custom(function = "validate_attribute_approach")
597    )]
598    #[builder(default = vec!["List of actions taken to perform the research".to_string()])]
599    #[serde(deserialize_with = "vec_string_trim")]
600    pub approach: Vec<String>,
601    /// Tangible effects the research approach has on areas outside academia, such as industry, society, the surrounding environment, or culture
602    /// ### Examples
603    /// - "Development of the world's first atomic weapons"
604    /// - "Introduction of the nuclear age, including advancements in nuclear science, engineering and a new source of energy"
605    /// - "The end of WWII, along with many ethical and moral considerations related to use of atomic weapons"
606    #[validate(length(min = 1, max = "MAX_COUNT_IMPACT"), custom(function = "validate_attribute_impact"))]
607    #[builder(default = vec!["List of tangible proof that validates the research approach".to_string()])]
608    #[serde(alias = "outcomes", deserialize_with = "vec_string_trim")]
609    pub impact: Vec<String>,
610    /// Notable recognition or awards given to the research team, organization, or research products
611    /// ### Examples
612    /// - "At least six Nobel Prizes awarded to Manhattan Project researchers in the years following the end of the project"
613    /// - "Creation of the Atomic Energy Commission in 1946, later becoming the Department of Energy and Nuclear Regulatory Commission"
614    #[validate(length(min = 1, max = 4, message = "Please limit the number of achievements to 4"))]
615    pub achievement: Option<Vec<String>>,
616    /// Expertise as applied to technology in a given mission space
617    /// ### Examples
618    /// - "Gaseous diffusion and electromagnetic separation to create fissionable materials"
619    /// - "Mechanisms for achieving supercritical mass for nuclear detonation"
620    /// - "Nuclear reactor development, which paved the way for nuclear power"
621    /// - "Radiochemistry for nuclear detonation analysis and advanced medical research with radioisotopes"
622    /// - "Large-scale multidisciplinary scientific collaboration"
623    #[validate(length(min = 1, max = "MAX_COUNT_CAPABILITIES"), custom(function = "validate_attribute_capabilities"))]
624    pub capabilities: Option<Vec<String>>,
625    /// Overview of research focus and areas
626    /// ### Example Focus
627    /// > "Developing fissionable materials for nuclear reactions to develop the world's first atomic weapons"
628    /// ### Example Areas
629    /// - "Nuclear fission"
630    /// - "Radiochemistry"
631    /// - "Uranium enrichment"
632    /// - "Electromagnetic separation"
633    /// - "Weapon design"
634    #[validate(nested)]
635    #[builder(default = Research::init().build())]
636    pub research: Research,
637}
638/// Overview of research focus and areas
639#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
640#[builder(start_fn = init)]
641#[serde(deny_unknown_fields)]
642pub struct Research {
643    /// Brief overview of the project or organization's research
644    #[validate(length(
645        min = 10,
646        max = "MAX_LENGTH_RESEARCH_FOCUS",
647        message = "Focus is too long, please reduce the length below 150."
648    ))]
649    #[builder(default = "Focus of the research".to_string())]
650    #[serde(deserialize_with = "string_trim")]
651    pub focus: String,
652    /// Topics related to and encapsulated within the project or organization
653    #[validate(length(min = 1, max = "MAX_COUNT_RESEARCH_AREAS"), custom(function = "validate_attribute_areas"))]
654    #[builder(default = vec!["Areas of research".to_string()])]
655    #[serde(deserialize_with = "vec_string_trim")]
656    pub areas: Vec<String>,
657}
658impl Default for ContactPoint {
659    fn default() -> Self {
660        Self::init().build()
661    }
662}
663impl Default for Metadata {
664    fn default() -> Self {
665        Metadata::init().build()
666    }
667}
668impl Default for ResearchActivity {
669    fn default() -> Self {
670        ResearchActivity::init().build()
671    }
672}
673impl Default for Sections {
674    fn default() -> Self {
675        Sections::init().build()
676    }
677}
678impl Hash for ResearchActivity {
679    fn hash<H: Hasher>(&self, state: &mut H) {
680        self.meta.identifier.hash(state);
681    }
682}
683impl MediaObject {
684    /// Returns the content URL of the media object
685    pub fn content_url(self) -> Option<String> {
686        match self {
687            | MediaObject::Image(ImageObject { content_url, .. }) => content_url,
688            | MediaObject::Video(VideoObject { content_url, .. }) => content_url,
689        }
690    }
691    /// Returns the description of the media object
692    pub fn description(self) -> String {
693        match self {
694            | MediaObject::Image(ImageObject { caption, .. }) => caption,
695            | MediaObject::Video(VideoObject { description, .. }) => description,
696        }
697    }
698    /// Returns true if the media object is an image, false otherwise
699    pub fn is_image(self) -> bool {
700        match self {
701            | MediaObject::Image(_) => true,
702            | _ => false,
703        }
704    }
705}
706impl Metadata {
707    fn first_image(self) -> Option<MediaObject> {
708        match self.media {
709            | Some(values) => values.into_iter().filter(|x| x.clone().is_image()).collect::<Vec<_>>().first().cloned(),
710            | None => None,
711        }
712    }
713    /// Returns the content URL of the first image in the list of media objects, or a default value if none are present.
714    pub fn first_image_content_url(self) -> String {
715        match self.first_image() {
716            | Some(media) => match media {
717                | MediaObject::Image(ImageObject { content_url, .. }) => match content_url {
718                    | Some(value) if !value.is_empty() => value.clone().trim().to_string(),
719                    | Some(_) | None => DEFAULT_GRAPHIC_HREF.to_string(),
720                },
721                | _ => DEFAULT_GRAPHIC_HREF.to_string(),
722            },
723            | None => DEFAULT_GRAPHIC_HREF.to_string(),
724        }
725    }
726    /// Returns the caption of the first image in the list of media objects, or a default value if none are present.
727    pub fn first_image_caption(self) -> String {
728        match self.first_image() {
729            | Some(MediaObject::Image(ImageObject { caption, .. })) => match caption.clone() {
730                | value if !value.is_empty() => value.clone(),
731                | _ => DEFAULT_GRAPHIC_CAPTION.to_string(),
732            },
733            | Some(_) | None => DEFAULT_GRAPHIC_CAPTION.to_string(),
734        }
735    }
736}
737impl Organization {
738    /// Returns a list of all organizations, loaded from the organization.json asset file
739    pub fn load() -> Vec<Organization> {
740        serde_json::from_str(&Constant::from_asset("organization.json")).unwrap()
741    }
742    /// Finds the first organization in the hierarchy with the given label.
743    pub fn member(self, label: &str) -> Option<Organization> {
744        self.members().into_iter().find(|Organization { name, .. }| name == label)
745    }
746    /// Returns a flattened vector of the organization hierarchy.
747    ///
748    /// This function collects the organization, its directorates, divisions, and groups
749    /// into a single vector, maintaining their hierarchical order.
750    pub fn members(self) -> Vec<Organization> {
751        let organization = self;
752        let mut items = vec![organization.clone()];
753        let directorates = organization.member.clone();
754        for directorate in &directorates {
755            items.push(directorate.clone());
756            let divisions = directorate.member.clone();
757            for division in &divisions {
758                items.push(division.clone());
759                let groups = division.member.clone();
760                for group in &groups {
761                    items.push(group.clone());
762                }
763            }
764        }
765        items
766    }
767    /// Returns the nearest organization of the given type in the organization hierarchy.
768    pub fn nearest(self, organization_type: OrganizationType) -> Option<Organization> {
769        let a = self.clone().additional_type.order();
770        let b = organization_type.order();
771        if a > b {
772            None
773        } else {
774            let ornl = Organization::load()[0].clone();
775            let graph = ornl.clone().to_graph();
776            let name = match b - a {
777                | 3 => Some(ornl.clone().name),
778                | 2 => match node_from_label(&graph, &self.name) {
779                    | Some(node) => match node_parent(&graph, node) {
780                        | Some(parent) => match node_parent(&graph, parent) {
781                            | Some(grandparent) => node_name(&graph, grandparent),
782                            | None => None,
783                        },
784                        | None => None,
785                    },
786                    | None => None,
787                },
788                | 1 => match node_from_label(&graph, &self.name) {
789                    | Some(node) => match node_parent(&graph, node) {
790                        | Some(parent) => node_name(&graph, parent),
791                        | None => None,
792                    },
793                    | None => None,
794                },
795                | 0 => Some(self.name),
796                | _ => None,
797            };
798            match name {
799                | Some(value) => match ornl.member(&value) {
800                    | Some(organization) => Some(organization),
801                    | None => None,
802                },
803                | None => None,
804            }
805        }
806    }
807    /// Returns a graph representation of the organization hierarchy.
808    pub fn to_graph(self) -> Graph<String, u8> {
809        let mut graph: Graph<String, u8, petgraph::Directed> = Graph::new();
810        let organization = &self;
811        let root = graph.add_node(organization.name.clone());
812        for directorate in organization.member.iter() {
813            let a = graph.add_node(directorate.name.clone());
814            graph.add_edge(root, a, 0);
815            for division in directorate.member.iter() {
816                let b = graph.add_node(division.name.clone());
817                graph.add_edge(a, b, 0);
818                for group in division.member.iter() {
819                    let c = graph.add_node(group.name.clone());
820                    graph.add_edge(b, c, 0);
821                }
822            }
823        }
824        graph
825    }
826}
827impl OrganizationType {
828    /// Parses a string into an `OrganizationType` value
829    pub fn from_string(value: String) -> OrganizationType {
830        match value.to_lowercase().as_str() {
831            | "agency" => OrganizationType::Agency,
832            | "center" => OrganizationType::Center,
833            | "consortium" => OrganizationType::Consortium,
834            | "division" => OrganizationType::Division,
835            | "directorate" => OrganizationType::Directorate,
836            | "group" => OrganizationType::Group,
837            | "office" => OrganizationType::Office,
838            | "program" => OrganizationType::Program,
839            | "facility" => OrganizationType::Facility,
840            | "ffrdc" => OrganizationType::Ffrdc,
841            | _ => unreachable!(),
842        }
843    }
844    /// Returns the order of an `OrganizationType` value
845    pub fn order(self) -> u8 {
846        match self {
847            | OrganizationType::Ffrdc | OrganizationType::Agency | OrganizationType::Consortium | OrganizationType::Office => 4,
848            | OrganizationType::Directorate => 3,
849            | OrganizationType::Division | OrganizationType::Center | OrganizationType::Program | OrganizationType::Facility => 2,
850            | OrganizationType::Group => 1,
851        }
852    }
853}
854impl ResearchActivity {
855    /// Creates a new `ResearchActivity`
856    pub fn new() -> Self {
857        ResearchActivity::default()
858    }
859    /// Print research activity schema as JSON schema
860    pub fn to_schema() {
861        let schema = schema_for!(ResearchActivity);
862        println!("{}", serde_json::to_string_pretty(&schema).unwrap());
863    }
864    /// Analyzes a list of research activity files
865    pub fn analyze(paths: Vec<PathBuf>, is_offline: bool) -> usize {
866        let config = ValeConfig::default().save();
867        let init = Vale::init().build();
868        let vale = if command_exists("vale") {
869            init.with_config(config).with_system_command()
870        } else if is_offline {
871            todo!("Support pointing to local vale executable");
872        } else {
873            init.download(Some(config))
874        };
875        match vale.clone().sync(is_offline) {
876            | Ok(_) => {
877                let results = paths.iter().map(|path| match ResearchActivity::read(path.into()) {
878                    | Some(data) => vale
879                        .clone()
880                        .analyze(data.clone().meta.identifier, data.extract_prose(), Some("JSON".into())),
881                    | None => {
882                        error!("=> {} Read research activity data", Label::fail());
883                        1
884                    }
885                });
886                let output = results.collect::<Vec<usize>>();
887                output.into_iter().sum()
888            }
889            | Err(err) => {
890                error!("=> {} Vale sync - {}", Label::fail(), err);
891                1
892            }
893        }
894    }
895    /// Calculate readability based on passed options for a list of research activity files
896    pub fn calculate_readability(paths: Vec<PathBuf>, readability_type: ReadabilityType) -> usize {
897        paths
898            .par_iter()
899            .map(|path| match ResearchActivity::read(path.into()) {
900                | Some(data) => {
901                    let index = readability_type.calculate(&data.extract_prose());
902                    let maximum = readability_type.maximum_allowed();
903                    debug!("{} Readability index = {index}", Label::using());
904                    if index > maximum {
905                        error!(
906                            "=> {} {} has a readability index of {} (less than {} is recommended for {} metric)",
907                            Label::fail(),
908                            path.display(),
909                            index.red().bold(),
910                            maximum.cyan(),
911                            readability_type.to_string().to_uppercase()
912                        );
913                        1
914                    } else {
915                        let score = format!("({} = {}/{})", readability_type.to_string().to_uppercase(), index, maximum);
916                        info!(
917                            "=> {} {} has {} {}",
918                            Label::pass(),
919                            path.display(),
920                            "no readability issues".green().bold(),
921                            score.dimmed()
922                        );
923                        0
924                    }
925                }
926                | None => {
927                    error!("=> {} Read research activity data", Label::fail());
928                    1
929                }
930            })
931            .sum()
932    }
933    /// Checks a list of research activity files
934    pub fn check(paths: Vec<PathBuf>, is_offline: bool) -> usize {
935        let runtime = tokio_runtime();
936        paths
937            .par_iter()
938            .map(|path| match ResearchActivity::read(path.into()) {
939                | Some(data) => {
940                    let offline_issues = data
941                        .clone()
942                        .validation_issues()
943                        .into_iter()
944                        .map(|issue| issue.with_path(path.clone()))
945                        .collect::<Vec<_>>();
946                    let online_issues = runtime.block_on(async {
947                        let mut issues: Vec<LinkCheck> = vec![];
948                        if !is_offline {
949                            let dois = match data.clone().meta.doi {
950                                | Some(values) => values.into_iter().map(|doi| format!("https://doi.org/{doi}")).collect(),
951                                | None => vec![],
952                            };
953                            let websites = match data.clone().meta.websites {
954                                | Some(values) => values.into_iter().map(|Website { url, .. }| url).collect(),
955                                | None => vec![],
956                            };
957                            for url in dois.into_iter().chain(websites.into_iter()) {
958                                let result = LinkCheck::run(Some(url)).await;
959                                issues.push(result);
960                            }
961                        }
962                        issues
963                    });
964                    let offline_issue_count: usize = offline_issues
965                        .into_iter()
966                        .inspect(|issue| issue.clone().print())
967                        .map(|issue| issue.issue_count())
968                        .sum();
969                    let online_issue_count: usize = online_issues
970                        .into_iter()
971                        .inspect(|issue| issue.clone().print())
972                        .filter(|issue| !issue.success)
973                        .count();
974                    offline_issue_count + online_issue_count
975                }
976                | None => {
977                    error!("=> {} Read research activity data at {}", Label::fail(), path.display());
978                    0
979                }
980            })
981            .sum()
982    }
983    /// Creates a copy of a `ResearchActivity`
984    pub fn copy(self) -> ResearchActivity {
985        let ResearchActivity {
986            meta,
987            title,
988            subtitle,
989            sections,
990            contact,
991            notes,
992        } = self.clone();
993        ResearchActivity::init()
994            .meta(meta)
995            .title(title)
996            .maybe_subtitle(subtitle)
997            .sections(sections)
998            .contact(contact)
999            .maybe_notes(notes)
1000            .build()
1001    }
1002    /// Extracts prose from a `ResearchActivity`
1003    pub fn extract_prose(self) -> String {
1004        let Sections {
1005            mission,
1006            challenge,
1007            approach,
1008            impact,
1009            research,
1010            ..
1011        } = self.sections;
1012        let Research { focus, areas } = research;
1013        let sections = format!(
1014            r#"
1015<!-- Introduction -->
1016{}
1017
1018<!-- Challenge -->
1019{}
1020
1021<!-- Approach -->
1022{}
1023
1024<!-- Impact -->
1025{}
1026
1027<!-- Focus -->
1028{}
1029
1030<!-- Areas -->
1031{}"#,
1032            mission,
1033            challenge,
1034            approach.into_iter().map(|x| format!("- {x}")).collect::<Vec<String>>().join("\n"),
1035            impact.into_iter().map(|x| format!("- {x}")).collect::<Vec<String>>().join("\n"),
1036            focus,
1037            areas.into_iter().map(|x| format!("- {x}")).collect::<Vec<String>>().join("\n")
1038        );
1039        match self.subtitle {
1040            | Some(subtitle) => format!(
1041                r#"# {}
1042> {}
1043{}"#,
1044                self.title, subtitle, sections
1045            ),
1046            | None => sections.to_string(),
1047        }
1048    }
1049    /// Formats research activity data
1050    /// ### Actions
1051    /// - Resolves URL of first media object (if found) and add empty caption
1052    /// - Resolves keywords, technology, organization, partners, sponsors, and affiliation using fuzzy matching against controlled vocabularies
1053    /// - Formats telephone number
1054    pub fn format(self, path: Option<PathBuf>) -> ResearchActivity {
1055        let mut clone = self.clone().copy();
1056        let path_parent = match path {
1057            | Some(value) => parent(value),
1058            | None => PathBuf::from("."),
1059        };
1060        let name = match image_paths(&path_parent) {
1061            | value if !value.is_empty() => Some(value[0].file_name().unwrap().to_string_lossy().to_string()),
1062            | _ => None,
1063        };
1064        debug!(path = path_to_string(path_parent), "=> {} Parent directory", Label::using());
1065        if let Some(value) = name {
1066            debug!(value, "=> {} First image", Label::using());
1067            // Make sure first graphic is well formed with a resolved image URL and caption
1068            let first_graphic = match self.meta.clone().media {
1069                | Some(values) if !values.is_empty() => {
1070                    let caption = self.meta.clone().first_image_caption();
1071                    let image_data = ImageObject::init().caption(caption.to_string()).content_url(value.clone()).build();
1072                    MediaObject::Image(image_data)
1073                }
1074                | Some(_) | None => {
1075                    let image_data = ImageObject::init().caption("".to_string()).content_url(value.clone()).build();
1076                    MediaObject::Image(image_data)
1077                }
1078            };
1079            // Get the rest of the media objects
1080            let rest = match self.clone().meta.media {
1081                | Some(values) if !values.is_empty() => values.into_iter().skip(1).collect::<Vec<_>>(),
1082                | Some(_) | None => vec![],
1083            };
1084            clone.meta.media = Some([vec![first_graphic], rest].concat());
1085        };
1086        clone.meta.keywords = self.clone().resolve(FuzzyValue::Keyword);
1087        clone.meta.technology = self.clone().resolve(FuzzyValue::Technology);
1088        clone.contact.telephone = match format_phone_number(&self.contact.telephone) {
1089            | Ok(value) => value,
1090            | Err(_) => {
1091                error!(value = self.contact.telephone, "=> {} Phone number", Label::invalid());
1092                self.contact.telephone.to_string()
1093            }
1094        };
1095        clone.contact.organization = match resolve_from_organization_json(self.clone().contact.organization) {
1096            | Some(value) => value,
1097            | None => "".to_string(),
1098        };
1099        clone.contact.affiliation = match self.clone().contact.affiliation {
1100            | Some(ref affiliation) => match resolve_from_organization_json(affiliation.to_string()) {
1101                | Some(resolved) => Some(resolved),
1102                | None => {
1103                    error!(affiliation, "=> {} Affiliation", Label::not_found());
1104                    Some(DEFAULT_AFFILIATION.to_string())
1105                }
1106            },
1107            | None => {
1108                let ornl = &Organization::load()[0];
1109                match ornl.clone().member(&clone.contact.organization) {
1110                    | Some(organization) => match organization.nearest(OrganizationType::Directorate) {
1111                        | Some(Organization { name, .. }) => Some(name),
1112                        | None => Some(DEFAULT_AFFILIATION.to_string()),
1113                    },
1114                    | None => {
1115                        error!("=> {} Nearest directorate", Label::not_found());
1116                        Some(DEFAULT_AFFILIATION.to_string())
1117                    }
1118                }
1119            }
1120        };
1121        clone.meta.partners = match self.clone().resolve(FuzzyValue::Partner) {
1122            | values if !values.is_empty() => Some(values),
1123            | _ => None,
1124        };
1125        clone.meta.sponsors = match self.clone().resolve(FuzzyValue::Sponsor) {
1126            | values if !values.is_empty() => Some(values),
1127            | _ => None,
1128        };
1129        clone
1130    }
1131    /// Read and parse research activity data (JSON or YAML)
1132    pub fn read(path: PathBuf) -> Option<ResearchActivity> {
1133        let content = match MimeType::from_path(path.clone()) {
1134            | MimeType::Json => match ResearchActivity::read_json(path.clone()) {
1135                | Ok(value) => Some(value),
1136                | Err(_) => None,
1137            },
1138            | MimeType::Yaml => match ResearchActivity::read_yaml(path.clone()) {
1139                | Ok(value) => Some(value),
1140                | Err(_) => None,
1141            },
1142            | _ => unimplemented!("Unsupported research activity data file extension"),
1143        };
1144        let label = match content {
1145            | Some(_) => Label::using(),
1146            | _ => Label::invalid(),
1147        };
1148        match content {
1149            | Some(data) => {
1150                debug!(path = path.to_str().unwrap(), "=> {}", label);
1151                trace!("=> {} Research activity data = {:#?}", label, data.dimmed().cyan());
1152                Some(data)
1153            }
1154            | None => {
1155                error!(path = path.to_str().unwrap(), "=> {}", label);
1156                None
1157            }
1158        }
1159    }
1160    /// Read research activity data using Serde and [`ResearchActivity`] struct
1161    fn read_json(path: PathBuf) -> serde_json::Result<ResearchActivity> {
1162        let content = match read_file(path.clone()) {
1163            | Ok(value) if !value.is_empty() => value,
1164            | Ok(_) | Err(_) => {
1165                error!(path = path.to_str().unwrap(), "=> {} RAD content is not valid", Label::fail());
1166                "{}".to_owned()
1167            }
1168        };
1169        let data: serde_json::Result<ResearchActivity> = serde_json::from_str(&content);
1170        let label = match data {
1171            | Ok(_) => Label::using(),
1172            | Err(_) => Label::invalid(),
1173        };
1174        trace!("=> {} RAD content = {:#?}", label, data.dimmed());
1175        data
1176    }
1177    /// Read research activity data (e.g., `buckets.yaml`) using Serde and [`ResearchActivity`] struct
1178    fn read_yaml(path: PathBuf) -> serde_yml::Result<ResearchActivity> {
1179        let content = match read_file(path.clone()) {
1180            | Ok(value) => value,
1181            | Err(_) => {
1182                error!(path = path.to_str().unwrap(), "=> {} RAD content is not valid", Label::fail());
1183                "".to_owned()
1184            }
1185        };
1186        let data: serde_yml::Result<ResearchActivity> = serde_yml::from_str(&content);
1187        let label = match data {
1188            | Ok(_) => Label::output(),
1189            | Err(_) => Label::fail(),
1190        };
1191        debug!("=> {} RAD content = {:#?}", label, data.dimmed());
1192        data
1193    }
1194    /// Resolve values to intended values according to controlled vocabularies and conventions
1195    fn resolve(self, value_type: FuzzyValue) -> Vec<String> {
1196        let values: Vec<_> = match value_type {
1197            | FuzzyValue::Keyword => self.meta.keywords,
1198            | FuzzyValue::Partner => match self.meta.partners {
1199                | Some(values) => values,
1200                | None => vec![],
1201            },
1202            | FuzzyValue::Sponsor => match self.meta.sponsors {
1203                | Some(values) => values,
1204                | None => vec![],
1205            },
1206            | FuzzyValue::Technology => self.meta.technology,
1207        };
1208        let mut data: Vec<_> = values
1209            .into_iter()
1210            .flat_map(|x| resolve_from_csv_asset(format!("{value_type}"), x))
1211            .collect();
1212        data.sort();
1213        data.dedup();
1214        data
1215    }
1216    /// Export to markdown
1217    pub fn to_markdown(self) -> String {
1218        let ResearchActivity { title, .. } = self.clone();
1219        format!("# {title}")
1220    }
1221    fn validation_issues(self) -> Vec<SchemaCheck> {
1222        fn errors_collect<T: Validate>(attribute: T) -> Option<Vec<SchemaCheck>> {
1223            match attribute.validate() {
1224                | Ok(_) => None,
1225                | Err(err) => Some(
1226                    err.into_errors()
1227                        .into_iter()
1228                        .map(|(key, value)| SchemaCheck::init().success(false).errors(value).message(key.to_string()).build())
1229                        .collect::<Vec<SchemaCheck>>(),
1230                ),
1231            }
1232        }
1233        let mut found = vec![errors_collect::<ResearchActivity>(self.clone())];
1234        match self.meta.media {
1235            | Some(values) => values.iter().for_each(|media| match media {
1236                | MediaObject::Image(x) => found.push(errors_collect::<ImageObject>(x.clone())),
1237                | MediaObject::Video(x) => found.push(errors_collect::<VideoObject>(x.clone())),
1238            }),
1239            | None => {}
1240        }
1241        found.into_iter().flatten().flatten().collect::<Vec<_>>()
1242    }
1243}
1244impl View for ContactPoint {
1245    fn render(&self) -> VirtualNode {
1246        let ContactPoint {
1247            given_name,
1248            family_name,
1249            job_title: role,
1250            email,
1251            telephone,
1252            ..
1253        } = self;
1254        html! {
1255            <section id="contact">
1256                <div>
1257                    <span class="label">Contact</span>
1258                    <span class="spacer"> </span>
1259                    <span class="name">{ format!("{} {}", given_name, family_name) }</span>
1260                    <span class="spacer">|</span>
1261                    <span class="title">{ role }</span>
1262                    <span class="spacer">|</span>
1263                    <span class="email">{ email }</span>
1264                    <span class="spacer">|</span>
1265                    <span class="phone">{ telephone }</span>
1266                </div>
1267            </section>
1268        }
1269    }
1270}
1271fn match_list<I: IntoIterator<Item = String> + Clone>(value: String, values: I) -> Vec<(String, u32)> {
1272    let pattern = Pattern::parse(&value, CaseMatching::Ignore, Normalization::Smart);
1273    let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
1274    pattern.match_list(values.clone(), &mut matcher)
1275}
1276fn print_resolution(output: Option<String>, value: String, name: String) {
1277    let label = name.titlecase();
1278    match output {
1279        | Some(resolved) => {
1280            if resolved.eq(&value.to_string()) {
1281                trace!("=> {} {} = \"{}\"", Label::using(), label, value.clone());
1282            } else {
1283                debug!(input = value.clone(), resolved, "=> {} {}", Label::found(), label);
1284            }
1285        }
1286        | None => {
1287            debug!(value = value.clone(), "=> {} {}", Label::not_found(), label);
1288        }
1289    };
1290}
1291fn resolve_from_csv_asset(name: String, value: String) -> Option<String> {
1292    let data = Constant::csv(&name);
1293    resolve_from_list_of_lists(value, data, name)
1294}
1295fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, data: I, name: String) -> Option<String> {
1296    let output = data
1297        .into_iter()
1298        .flat_map(|values| {
1299            let sanitized = sanitize(value.clone());
1300            let matched = match_list(sanitized, values.clone());
1301            trace!("{} => {:?}", value.clone(), matched.clone());
1302            if matched.clone().is_empty() {
1303                None
1304            } else {
1305                match values.first() {
1306                    | Some(x) => {
1307                        if value.eq(x) {
1308                            Some((x.into(), 10000))
1309                        } else {
1310                            let score = matched.into_iter().map(|(_, score)| score).max();
1311                            match score {
1312                                | Some(value) if value > 0 => Some((x.to_string(), value)),
1313                                | Some(_) | None => None,
1314                            }
1315                        }
1316                    }
1317                    | None => None,
1318                }
1319            }
1320        })
1321        .max_by_key(|(_, score)| *score)
1322        .map(|(x, _)| x.to_string());
1323    print_resolution(output.clone(), value, name);
1324    output
1325}
1326fn resolve_from_organization_json(value: String) -> Option<String> {
1327    let organization = &Organization::load()[0];
1328    let mut items = vec![organization.clone()];
1329    let directorates = organization.member.clone();
1330    for directorate in &directorates {
1331        items.push(directorate.clone());
1332        let divisions = directorate.member.clone();
1333        for division in &divisions {
1334            items.push(division.clone());
1335        }
1336    }
1337    let data = items
1338        .into_iter()
1339        .map(|x| (x.name.clone(), x.alternative_name.clone()))
1340        .filter(|(name, alias)| !(name.is_empty() && alias.is_none()))
1341        .map(|(name, alias)| {
1342            let alternative_name = match alias {
1343                | Some(x) => x.to_string(),
1344                | None => name.clone(),
1345            };
1346            vec![name, alternative_name]
1347        })
1348        .collect::<Vec<Vec<String>>>();
1349    resolve_from_list_of_lists(value, data, "organization".to_string())
1350}
1351fn sanitize(value: String) -> String {
1352    match Regex::new(r"[-_.,]") {
1353        | Ok(re) => re.replace_all(&value, "").replace("&", "and").trim().to_string(),
1354        | Err(err) => err.to_string(),
1355    }
1356}
1357
1358#[cfg(test)]
1359mod tests;