use crate::prelude::*;
pub mod hardware;
pub use hardware::memory::{Memory, MemoryUnit};
pub use hardware::{
AcceleratorArchitecture, Architecture, Backend, CpuArchitecture, DspArchitecture, FpgaArchitecture, GpuArchitecture, Model, Paradigm, Regime,
Resource, SensorModality, Topology, Vendor, Vendored,
};
#[cfg(feature = "std")]
use crate::prelude::PathBuf;
#[cfg(feature = "std")]
use crate::schema::validate::format_phone_number;
#[cfg(feature = "std")]
use crate::util::constants::app::DEFAULT_AFFILIATION;
use crate::util::constants::MAX_LENGTH_IMAGE_CAPTION;
#[cfg(feature = "std")]
use crate::util::Label;
use crate::util::{Constant, LinkedData, MimeType, ToMarkdown};
use bon::Builder;
#[cfg(feature = "std")]
use convert_case::{Case, Casing};
#[cfg(test)]
use core::fmt;
use core::hash::Hash;
use core::iter::once;
use core::num::NonZeroU64;
use derive_more::Display;
#[cfg(feature = "std")]
use fancy_regex::Regex;
#[cfg(feature = "std")]
use nucleo_matcher::{
pattern::{CaseMatching, Normalization, Pattern},
Config, Matcher,
};
#[cfg(feature = "std")]
use percy_dom::prelude::{html, IterableNodes, View, VirtualNode};
use petgraph::graph::Graph;
use schemars::JsonSchema;
#[cfg(test)]
use serde::de::value::SeqAccessDeserializer;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_trim::{option_string_trim, string_trim};
use serde_with::skip_serializing_none;
#[cfg(feature = "std")]
use tracing::{debug, error, trace};
use validator::Validate;
#[cfg(feature = "std")]
pub mod agent;
pub mod geonames;
pub mod graph;
pub mod namespaces;
pub mod pid;
pub mod research_activity;
pub mod standard;
pub mod validate;
use graph::{node_from_label, node_name, node_parent};
use namespaces::{bibo, foaf, schema_org};
use validate::{has_image_extension, is_date, is_orcid, is_phone_number};
pub type Keyword = String;
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(untagged)]
pub enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, PartialEq, PartialOrd, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ClassificationLevel {
#[default]
#[display("UNCLASSIFIED")]
Unclassified,
#[display("CONFIDENTIAL")]
Confidential,
#[display("SECRET")]
Secret,
#[display("TOP SECRET")]
#[serde(alias = "top secret")]
TopSecret,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MediaObject {
Image(ImageObject),
Video(VideoObject),
}
#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum OrganizationType {
#[display("agency")]
Agency,
#[display("center")]
Center,
#[display("consortium")]
Consortium,
#[display("directorate")]
Directorate,
#[display("division")]
Division,
#[display("facility")]
Facility,
#[display("FFRDC")]
Ffrdc,
#[display("group")]
Group,
#[display("institute")]
Institute,
#[display("office")]
Office,
#[display("program")]
Program,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Other {
Unformatted(String),
Formatted(Notes),
}
#[derive(Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ProgrammingLanguage {
#[display("html")]
Html,
#[display("javascript")]
JavaScript,
#[display("julia")]
Julia,
#[display("markdown")]
Markdown,
#[display("json")]
Json,
#[display("rust")]
Rust,
#[display("shell")]
#[serde(alias = "bash", alias = "zsh", alias = "fish", alias = "powershell")]
Shell,
#[display("yaml")]
Yaml,
}
#[derive(Clone, Debug, Default, Deserialize, Display, PartialEq, PartialOrd, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
#[display("canceled")]
#[serde(alias = "cancelled")]
Canceled,
#[display("completed")]
Completed,
#[display("paused")]
#[serde(alias = "on-hold", alias = "postponed", alias = "rescheduled")]
Paused,
#[default]
#[display("active")]
Active,
}
#[derive(Clone, Debug, Default, Deserialize_repr, Display, Serialize_repr, PartialEq, PartialOrd, JsonSchema)]
#[repr(u8)]
#[serde(deny_unknown_fields)]
pub enum TechnologyReadinessLevel {
#[default]
#[display("Greenfield Research")]
Principles = 0,
#[display("Basic Research")]
Research = 1,
#[display("Technology Concept")]
Concept = 2,
#[display("Feasible")]
Feasible = 3,
#[display("Developing")]
Developing = 4,
#[display("Developed")]
Developed = 5,
#[display("Prototype")]
Prototype = 6,
#[display("Operational")]
Operational = 7,
#[display("Mission Ready")]
MissionReady = 8,
#[display("Mission Capable")]
MissionCapable = 9,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
#[builder(start_fn = init)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ContactPoint {
#[serde(rename = "@context")]
pub context: Option<ContactPointContext>,
#[serde(rename = "@type")]
pub contact_point_type: Option<String>,
#[builder(default = "Researcher".to_string())]
#[serde(alias = "title", deserialize_with = "string_trim")]
pub job_title: String,
#[builder(default = "First".to_string())]
#[serde(alias = "first", deserialize_with = "string_trim")]
pub given_name: String,
#[builder(default = "Last".to_string())]
#[serde(alias = "last", deserialize_with = "string_trim")]
pub family_name: String,
#[validate(custom(function = "is_orcid"))]
#[serde(alias = "orcid")]
pub identifier: Option<String>,
#[validate(email(message = "Email address must be in the format name@example.com"))]
#[builder(default = "first_last@example.com".to_string())]
#[serde(deserialize_with = "string_trim")]
pub email: String,
#[validate(custom(function = "is_phone_number"))]
#[builder(default = "123-456-7890".to_string())]
#[serde(alias = "phone", deserialize_with = "string_trim")]
pub telephone: String,
#[validate(url(message = "Profile URL must be in the format https://example.com"))]
#[builder(default = "https://example.com".to_string())]
#[serde(alias = "profile", deserialize_with = "string_trim")]
pub url: String,
#[builder(default = "Some Organization".to_string())]
#[serde(deserialize_with = "string_trim")]
pub organization: String,
pub affiliation: Option<String>,
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[builder(start_fn = init, on(String, into))]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ContactPointContext {
pub job_title: String,
pub given_name: String,
pub family_name: String,
pub identifier: String,
pub email: String,
pub telephone: String,
pub url: String,
pub organization: String,
pub affiliation: String,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
#[serde(rename_all = "camelCase")]
pub struct Date {
#[validate(custom(function = "is_date"))]
pub start_date: Option<String>,
#[validate(custom(function = "is_date"))]
pub end_date: Option<String>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[builder(start_fn = init)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ImageObject {
#[validate(length(
max = "MAX_LENGTH_IMAGE_CAPTION",
message = "Caption is too long, reduce the length below 100 characters."
))]
#[serde(deserialize_with = "string_trim")]
pub caption: String,
#[serde(alias = "size")]
pub content_size: Option<NonZeroU64>,
#[validate(custom(function = "has_image_extension"))]
#[serde(alias = "url", alias = "href")]
pub content_url: Option<String>,
pub height: Option<NonZeroU64>,
pub width: Option<NonZeroU64>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields)]
pub struct Notes {
pub managers: Option<Vec<String>>,
pub programs: Option<Vec<String>>,
#[serde(default, deserialize_with = "option_string_trim")]
pub presentation: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, Display, Hash, PartialEq, PartialOrd)]
#[display("Organization ({additional_type}) - {name})")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Organization {
#[serde(deserialize_with = "string_trim")]
pub name: String,
#[serde(default, deserialize_with = "option_string_trim")]
pub ror: Option<String>,
#[serde(default, deserialize_with = "option_string_trim")]
pub alternative_name: Option<String>,
pub additional_type: OrganizationType,
pub keywords: Option<Vec<Keyword>>,
pub member: Vec<Organization>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct VideoObject {
#[serde(alias = "size")]
pub content_size: Option<NonZeroU64>,
#[validate(url)]
#[serde(alias = "url", alias = "href")]
pub content_url: Option<String>,
#[serde(deserialize_with = "string_trim")]
pub description: String,
pub duration: Option<String>,
pub height: Option<NonZeroU64>,
pub width: Option<NonZeroU64>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Validate, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Website {
#[serde(alias = "title", deserialize_with = "string_trim")]
pub description: String,
#[validate(url(message = "Provide valid URL"))]
#[serde(deserialize_with = "string_trim")]
pub url: String,
}
impl ContactPoint {
#[cfg(feature = "std")]
pub fn format(self) -> Self {
let ContactPoint {
affiliation,
context,
contact_point_type,
email,
family_name,
given_name,
identifier,
job_title,
organization,
telephone,
url,
..
} = self.clone();
let updated_organization = match resolve_from_organization_json(organization) {
| Some(value) => value,
| None => "".to_string(),
};
let updated_affiliation = match affiliation {
| Some(ref affiliation) => match resolve_from_organization_json(affiliation.to_string()) {
| Some(resolved) => Some(resolved),
| None => {
error!(affiliation, "=> {} Affiliation", Label::not_found());
Some(DEFAULT_AFFILIATION.to_string())
}
},
| None => match Organization::load().into_iter().next() {
| Some(ornl) => match ornl.member(&updated_organization) {
| Some(organization) => match organization.nearest(OrganizationType::Directorate) {
| Some(Organization { name, .. }) => Some(name),
| None => Some(DEFAULT_AFFILIATION.to_string()),
},
| None => {
error!("=> {} Nearest directorate", Label::not_found());
Some(DEFAULT_AFFILIATION.to_string())
}
},
| None => {
error!("=> {} Organization hierarchy", Label::not_found());
Some(DEFAULT_AFFILIATION.to_string())
}
},
};
let updated_telephone = match format_phone_number(&telephone) {
| Ok(value) => value,
| Err(_) => {
error!(value = telephone, "=> {} Phone number", Label::invalid());
telephone.to_string()
}
};
Self::init()
.maybe_affiliation(updated_affiliation)
.maybe_context(context)
.maybe_contact_point_type(contact_point_type)
.email(email)
.family_name(family_name)
.given_name(given_name)
.maybe_identifier(identifier)
.job_title(job_title)
.organization(updated_organization)
.telephone(updated_telephone)
.url(url)
.build()
}
#[cfg(not(feature = "std"))]
pub fn format(self) -> Self {
self
}
#[cfg(feature = "std")]
pub fn format_with(self, _context: Option<PathBuf>) -> Self {
self.format()
}
}
impl Default for ContactPoint {
fn default() -> Self {
Self::init().build()
}
}
impl Default for ContactPointContext {
fn default() -> Self {
Self::init()
.job_title(schema_org("jobTitle"))
.given_name(foaf("givenName"))
.family_name(foaf("familyName"))
.identifier(bibo("identifier"))
.email(foaf("mbox"))
.telephone(schema_org("telephone"))
.url(foaf("workInfoHomepage"))
.organization(schema_org("worksFor"))
.affiliation(schema_org("affiliation"))
.build()
}
}
impl LinkedData for ContactPoint {
fn with_context(&self) -> Self {
let mut clone = self.clone();
clone.context = Some(ContactPointContext::default());
clone.contact_point_type = Some(schema_org("person"));
clone
}
}
impl MediaObject {
pub fn content_url(self) -> Option<String> {
match self {
| MediaObject::Image(ImageObject { content_url, .. }) => content_url,
| MediaObject::Video(VideoObject { content_url, .. }) => content_url,
}
}
pub fn description(self) -> String {
match self {
| MediaObject::Image(ImageObject { caption, .. }) => caption,
| MediaObject::Video(VideoObject { description, .. }) => description,
}
}
pub fn is_image(self) -> bool {
match self {
| MediaObject::Image(_) => true,
| _ => false,
}
}
}
impl<T> OneOrMany<T> {
pub fn as_slice(&self) -> &[T] {
match self {
| Self::One(value) => core::slice::from_ref(value),
| Self::Many(values) => values.as_slice(),
}
}
pub fn first(&self) -> Option<&T> {
self.as_slice().first()
}
pub fn is_empty(&self) -> bool {
self.as_slice().is_empty()
}
pub fn into_vec(self) -> Vec<T> {
match self {
| Self::One(value) => vec![value],
| Self::Many(values) => values,
}
}
pub fn iter(&self) -> core::slice::Iter<'_, T> {
self.as_slice().iter()
}
pub fn len(&self) -> usize {
self.as_slice().len()
}
pub fn map<U, F>(self, f: F) -> Result<OneOrMany<U>, standard::crosswalk::CrosswalkError>
where
F: Fn(T) -> Result<U, standard::crosswalk::CrosswalkError>,
{
match self {
| Self::One(value) => f(value).map(OneOrMany::One),
| Self::Many(values) => values
.into_iter()
.enumerate()
.map(|(index, value)| {
f(value).map_err(|e| standard::crosswalk::CrosswalkError::BuildFailed(format!("Failed to convert record at index {index} — {e}")))
})
.collect::<Result<Vec<_>, _>>()
.map(OneOrMany::Many),
}
}
pub fn parse(content: &str, mime: MimeType) -> Result<OneOrMany<T>, standard::crosswalk::CrosswalkError>
where
T: DeserializeOwned,
{
match mime {
| MimeType::Json => serde_json::from_str(content).map_err(|e| standard::crosswalk::CrosswalkError::ParseFailed(e.to_string())),
| MimeType::Yaml => serde_norway::from_str(content).map_err(|e| standard::crosswalk::CrosswalkError::ParseFailed(e.to_string())),
| _ => Err(standard::crosswalk::CrosswalkError::ParseFailed("Content must be JSON or YAML".into())),
}
}
pub fn serialize(&self, mime: MimeType) -> Result<String, standard::crosswalk::CrosswalkError>
where
T: Serialize,
{
match mime {
| MimeType::Json => serde_json::to_string_pretty(self).map_err(|e| standard::crosswalk::CrosswalkError::SerializeFailed(e.to_string())),
| MimeType::Yaml => serde_norway::to_string(self).map_err(|e| standard::crosswalk::CrosswalkError::SerializeFailed(e.to_string())),
| _ => Err(standard::crosswalk::CrosswalkError::SerializeFailed("Output must be JSON or YAML".into())),
}
}
}
impl<'a, T> IntoIterator for &'a OneOrMany<T> {
type Item = &'a T;
type IntoIter = core::slice::Iter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<T> Validate for OneOrMany<T>
where
T: Validate,
{
fn validate(&self) -> Result<(), validator::ValidationErrors> {
self.iter().find_map(|value| value.validate().err()).map_or(Ok(()), Err)
}
}
impl Organization {
pub fn alternative_names() -> Vec<String> {
match Organization::load().into_iter().next() {
| Some(organization) => organization
.members()
.into_iter()
.flat_map(|Organization { alternative_name, .. }| alternative_name)
.collect::<Vec<String>>(),
| None => vec![],
}
}
pub fn load() -> Vec<Organization> {
Constant::from_asset("organization.json")
.and_then(|content| serde_json::from_str(&content).ok())
.unwrap_or_default()
}
pub fn member(self, label: &str) -> Option<Organization> {
self.members().into_iter().find(|Organization { name, .. }| name == label)
}
pub fn members(self) -> Vec<Organization> {
let organization = self.clone();
once(organization.clone())
.chain(organization.member.iter().flat_map(|directorate| {
once(directorate.clone()).chain(
directorate
.member
.iter()
.flat_map(|division| once(division.clone()).chain(division.member.iter().cloned())),
)
}))
.collect()
}
pub fn nearest(self, organization_type: OrganizationType) -> Option<Organization> {
let a = self.clone().additional_type.order();
let b = organization_type.order();
if a > b {
None
} else {
let ornl = Organization::load().into_iter().next()?;
let graph = ornl.clone().to_graph();
let name = match b.saturating_sub(a) {
| 3 => Some(ornl.clone().name),
| 2 => node_from_label(&graph, &self.name)
.and_then(|node| node_parent(&graph, node))
.and_then(|parent| node_parent(&graph, parent))
.and_then(|grandparent| node_name(&graph, grandparent)),
| 1 => node_from_label(&graph, &self.name)
.and_then(|node| node_parent(&graph, node))
.and_then(|parent| node_name(&graph, parent)),
| 0 => Some(self.name),
| _ => None,
};
name.and_then(|value| ornl.member(&value))
}
}
pub fn to_graph(self) -> Graph<String, u8> {
let mut graph: Graph<String, u8, petgraph::Directed> = Graph::new();
let organization = &self;
let root = graph.add_node(organization.name.clone());
let edges: Vec<(String, String)> = organization
.member
.iter()
.flat_map(|directorate| {
once((organization.name.clone(), directorate.name.clone())).chain(directorate.member.iter().flat_map(|division| {
once((directorate.name.clone(), division.name.clone()))
.chain(division.member.iter().map(|group| (division.name.clone(), group.name.clone())))
}))
})
.collect();
edges.into_iter().for_each(|(parent_name, child_name)| {
let parent = node_from_label(&graph, &parent_name).unwrap_or(root);
let child = node_from_label(&graph, &child_name).unwrap_or_else(|| graph.add_node(child_name.clone()));
graph.add_edge(parent, child, 0);
});
graph
}
}
impl OrganizationType {
pub fn from_string(value: String) -> OrganizationType {
match value.to_lowercase().as_str() {
| "agency" => OrganizationType::Agency,
| "center" => OrganizationType::Center,
| "consortium" => OrganizationType::Consortium,
| "division" => OrganizationType::Division,
| "directorate" => OrganizationType::Directorate,
| "group" => OrganizationType::Group,
| "institute" => OrganizationType::Institute,
| "office" => OrganizationType::Office,
| "program" => OrganizationType::Program,
| "facility" => OrganizationType::Facility,
| "ffrdc" => OrganizationType::Ffrdc,
| _ => OrganizationType::Institute,
}
}
pub fn order(self) -> u8 {
match self {
| OrganizationType::Ffrdc
| OrganizationType::Agency
| OrganizationType::Consortium
| OrganizationType::Institute
| OrganizationType::Office => 4,
| OrganizationType::Directorate => 3,
| OrganizationType::Division | OrganizationType::Center | OrganizationType::Program | OrganizationType::Facility => 2,
| OrganizationType::Group => 1,
}
}
}
impl ToMarkdown for ContactPoint {
fn to_markdown(&self) -> String {
let ContactPoint {
given_name,
family_name,
job_title: role,
email,
telephone,
..
} = self;
format!(
r#"## Contact
- Role: {role}
- Name: {given_name} {family_name}
- email: [{email}](mailto:{email})
- Telephone: {telephone}
"#
)
}
}
impl ToMarkdown for Website {
fn to_markdown(&self) -> String {
let Website { description, url } = self;
format!("[{}]({})", description, url)
}
}
impl Validate for MediaObject {
fn validate(&self) -> Result<(), validator::ValidationErrors> {
match self {
| Self::Image(img) => img.validate(),
| Self::Video(vid) => vid.validate(),
}
}
}
#[cfg(feature = "std")]
impl View for ContactPoint {
fn render(&self) -> VirtualNode {
let ContactPoint {
given_name,
family_name,
job_title: role,
email,
telephone,
..
} = self;
html! {
<section id="contact">
<div>
<span class="label">Contact</span>
<span class="spacer"> </span>
<span class="name">{ format!("{} {}", given_name, family_name) }</span>
<span class="spacer">|</span>
<span class="title">{ role }</span>
<span class="spacer">|</span>
<span class="email">{ email }</span>
<span class="spacer">|</span>
<span class="phone">{ telephone }</span>
</div>
</section>
}
}
}
#[cfg(feature = "std")]
pub(crate) fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, data: I, name: String) -> Option<String> {
fn match_list<I: IntoIterator<Item = String> + Clone>(value: String, values: I) -> Vec<(String, u32)> {
let pattern = Pattern::parse(&value, CaseMatching::Ignore, Normalization::Smart);
let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
pattern.match_list(values.clone(), &mut matcher)
}
fn print_resolution(output: Option<String>, value: String, name: String) {
let label = name.to_case(Case::Title);
match output {
| Some(resolved) => {
if resolved.eq(&value.to_string()) {
trace!("=> {} {} = \"{}\"", Label::using(), label, value.clone());
} else {
debug!(input = value.clone(), resolved, "=> {} {}", Label::found(), label);
}
}
| None => {
debug!(value = value.clone(), "=> {} {}", Label::not_found(), label);
}
};
}
fn sanitize(value: String) -> String {
match Regex::new(r"[-_.,]") {
| Ok(re) => re.replace_all(&value, "").replace("&", "and").trim().to_string(),
| Err(err) => err.to_string(),
}
}
let output = data
.into_iter()
.flat_map(|values| {
let sanitized = sanitize(value.clone());
let matched = match_list(sanitized, values.clone().into_iter().take(4));
trace!("{} => {:?}", value.clone(), matched.clone());
if matched.clone().is_empty() {
None
} else {
match values.first() {
| Some(x) => {
if value.eq(x) {
Some((x.into(), 10000))
} else {
let score = matched.into_iter().map(|(_, score)| score).max();
match score {
| Some(value) if value > 0 => Some((x.to_string(), value)),
| Some(_) | None => None,
}
}
}
| None => None,
}
}
})
.max_by_key(|(_, score)| *score)
.map(|(x, _)| x.to_string());
print_resolution(output.clone(), value, name);
output
}
#[cfg(not(feature = "std"))]
#[allow(dead_code)]
pub(crate) fn resolve_from_list_of_lists<I: IntoIterator<Item = Vec<String>>>(value: String, _data: I, _name: String) -> Option<String> {
Some(value)
}
#[cfg(feature = "std")]
pub(crate) fn resolve_from_organization_json(value: String) -> Option<String> {
let organization = Organization::load().into_iter().next()?;
let items: Vec<Organization> = once(organization.clone())
.chain(
organization
.member
.iter()
.flat_map(|directorate| once(directorate.clone()).chain(directorate.member.iter().cloned())),
)
.collect();
let data = items
.into_iter()
.map(|x| (x.name.clone(), x.alternative_name.clone()))
.filter(|(name, alias)| !(name.is_empty() && alias.is_none()))
.map(|(name, alias)| {
let alternative_name = alias.as_ref().map_or(name.clone(), |x| x.to_string());
vec![name, alternative_name]
})
.collect::<Vec<Vec<String>>>();
resolve_from_list_of_lists(value, data, "organization".to_string())
}
#[cfg(test)]
pub(crate) fn optional_string_or_seq<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptStringOrSeq;
impl<'de> serde::de::Visitor<'de> for OptStringOrSeq {
type Value = Option<Vec<String>>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("string, list of strings, or null")
}
fn visit_none<E: serde::de::Error>(self) -> Result<Option<Vec<String>>, E> {
Ok(None)
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Option<Vec<String>>, E> {
Ok(None)
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Option<Vec<String>>, E> {
Ok(Some(vec![value.to_owned()]))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, seq: A) -> Result<Option<Vec<String>>, A::Error> {
Deserialize::deserialize(SeqAccessDeserializer::new(seq)).map(Some)
}
}
deserializer.deserialize_any(OptStringOrSeq)
}
#[cfg(not(feature = "std"))]
#[allow(dead_code)]
pub(crate) fn resolve_from_organization_json(value: String) -> Option<String> {
Some(value)
}
#[cfg(test)]
mod tests;