use std::{fs, path::Path};
use regex::Regex;
use relative_path::RelativePathBuf;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::macros::rgx;
use super::{Locator, ParseError, SerError};
type DSString = String;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Document {
#[serde(default)]
pub title: Option<DSString>,
#[serde(default)]
pub path: Option<Locator>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct License {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub hardware: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub software: Option<DSString>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Person {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub affiliation: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<DSString>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub social: Vec<Social>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Standard {
#[serde(default)]
pub reference: DSString,
#[serde(default)]
pub standard_title: DSString,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct OtherThing {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest: Option<Url>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub web: Option<Url>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct OtherThingWithLang {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest: Option<Url>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub web: Option<Url>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<DSString>,
}
#[allow(clippy::unnecessary_wraps)]
fn default_okh_manifest_version() -> Option<DSString> {
Some("1.0.0".to_owned())
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Okh {
pub title: DSString,
pub description: DSString,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub intended_use: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<DSString>,
pub project_link: Option<Url>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<RelativePathBuf>,
#[serde(default)] pub made: bool,
#[serde(default)] pub made_independently: bool,
pub license: License,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub licensor: Option<Person>,
#[serde(default = "default_okh_manifest_version")]
#[serde(rename = "okh-manifest-version")]
pub manifest_version: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub date_created: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub date_updated: Option<DSString>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_author: Option<Person>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_language: Option<DSString>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_is_translation: Option<OtherThingWithLang>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<PersonSocial>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub contributors: Vec<Person>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub development_stage: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_home: Option<Url>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub archive_download: Option<Url>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub design_files: Vec<RelativePathBuf>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_language: Option<DSString>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_is_translation: Option<OtherThingWithLang>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub schematics: Vec<Document>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub bom: Option<RelativePathBuf>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tools_list: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub making_instructions: Vec<Document>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub manufacturing_files: Vec<Document>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub risk_assessment: Vec<Document>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tool_settings: Vec<Document>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub quality_instructions: Vec<Document>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub operating_instructions: Vec<Document>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub maintenance_instructions: Vec<Document>,
#[cfg(feature = "v1_non_losh")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub disposal_instructions: Vec<Document>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub software: Vec<Document>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub health_safety_notice: Option<DSString>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub standards_used: Vec<Standard>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub derivative_of: Option<OtherThing>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub variant_of: Option<OtherThing>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(alias = "sub")] pub sub_parts: Vec<OtherThing>,
}
impl Okh {
pub fn main_url(&self) -> Option<&Url> {
self.project_link
.as_ref()
.or(self.documentation_home.as_ref())
}
}
impl Okh {
pub fn from_yaml(yaml_str: &str) -> Result<Self, ParseError> {
log::debug!("Parsing YAML to v1 ...");
let parsed = serde_yaml::from_str::<Self>(yaml_str)?;
if (
&parsed.license.documentation,
&parsed.license.hardware,
&parsed.license.software,
) == (&None, &None, &None)
{
panic!(
"at least one of the three licenses (documentation, hardware or software) has to be set"
);
}
Ok(parsed)
}
pub fn from_yaml_file<IP>(yaml_file: IP) -> Result<Self, ParseError>
where
IP: AsRef<Path>,
{
log::debug!("Reading YAML file to string ...");
let yaml_str = fs::read_to_string(yaml_file)?;
Self::from_yaml(&yaml_str)
}
pub fn to_yaml(&self) -> Result<String, SerError> {
log::debug!("Serializing v1 to YAML ...");
Ok(serde_yaml::to_string(self)?)
}
pub fn to_yaml_file<OP>(&self, yaml_file: OP) -> Result<(), SerError>
where
OP: AsRef<Path>,
{
let serialized = self.to_yaml()?;
log::debug!("Writing v1 to YAML file ...");
fs::write(yaml_file, serialized)?;
Ok(())
}
pub fn ext_matcher() -> &'static Regex {
rgx!(r"(^|\.)[yY][aA]?[mM][lL]$")
}
pub fn file_matcher() -> &'static Regex {
rgx!(r"okh\.[yY][aA]?[mM][lL]$")
}
}