rustyphoenixcodemeta 1.1.0

This project aims to generate a `codemeta.json` file used by zenodo from the `pixi.toml` of the current project
/***************************************
	Auteur : Pierre Aubert
	Mail : pierre.aubert@lapp.in2p3.fr
	Licence : CeCILL-C
****************************************/

use std::{fs, io};
use std::path::Path;
// use std::time::

use serde::Serialize;
use json;
// use std::process::Command;
use chrono::Datelike;

//We have to find how to make the json serialized work
// use json::{self, ser::Error};

use crate::pixiconfig::PixiConfig;

///Get a name value in json
/// # Parameters
/// - `name` : name of the field
/// - `value` : value of the field
/// # Returns
/// Corresponding JSON name value
fn phoenix_save_json_value(name: &String, value: &String) -> String{
	format!("\t{}: {},\n", json::stringify(name.as_str()), json::stringify(value.as_str()))
}
///Get a name value in json with comma
/// # Parameters
/// - `name` : name of the field
/// - `value` : value of the field
/// # Returns
/// Corresponding JSON name value
fn phoenix_save_json_value_end(name: &String, value: &String) -> String{
	format!("\t{}: {}\n", json::stringify(name.as_str()), json::stringify(value.as_str()))
}

///Affiliation to an organisation
#[derive(Debug, Serialize)]
pub struct AffiliationConfig{
	///Type of affiliation
	#[serde(rename = "@type")]
	p_type: String,
	///Name of the affiliation
	#[serde(rename = "name")]
	p_name: String,
}

impl Default for AffiliationConfig {
	///Default constructor of AffiliationConfig
	/// # Returns
	/// Default AffiliationConfig
	fn default() -> Self {
		AffiliationConfig{
			p_type: String::from("Organization"),
			p_name: String::from("LAPP, Univ. Savoie Mont-Blanc, CNRS")
		}
	}
}

impl AffiliationConfig{
	///Convert the AffiliationConfig into a string
	/// # Returns
	/// AffiliationConfig in json
	fn to_json_string(&self) -> String{
		let mut body: String = String::from("\t\t\t\"affiliation\": {\n");
		body += &format!("\t\t\t{}", phoenix_save_json_value(&String::from("@type"), &self.p_type));
		body += &format!("\t\t\t{}", phoenix_save_json_value_end(&String::from("name"), &self.p_name));
		body += &String::from("\t\t\t}\n");
		return body;
	}
}

///Person who develop, maintain or contribute to a software
#[derive(Debug, Serialize)]
pub struct PersonConfig{
	///Type of person
	#[serde(rename = "@type")]
	p_type: String,
	///Name of the person
	#[serde(rename = "givenName")]
	p_name: String,
	///Familly Name of the person (surname)
	#[serde(rename = "familyName")]
	p_familly_name: String,
	///Affiliation of the person
	#[serde(rename = "affiliation")]
	p_affiliation: AffiliationConfig,
}

impl Default for PersonConfig {
	///Default constructor of PersonConfig
	/// # Returns
	/// Default PersonConfig
	fn default() -> Self {
		PersonConfig{
			p_type: String::from("Person"),
			p_name: String::from("Pierre"),
			p_familly_name: String::from("Aubert"),
			p_affiliation: Default::default()
		}
	}
}

impl PersonConfig {
	///Default constructor of PersonConfig
	/// # Parameters
	/// - `name` : name of the Person
	/// - `familly_name` : familly name of the Person
	/// # Returns
	/// Initialised PersonConfig
	pub fn new(name: &String, familly_name: &String) -> Self {
		PersonConfig{
			p_type: String::from("Person"),
			p_name: name.to_owned(),
			p_familly_name: familly_name.to_owned(),
			p_affiliation: Default::default()
		}
	}
	///Convert the PersonConfig into a string
	/// # Returns
	/// PersonConfig in json
	fn to_json_string(&self) -> String{
		let mut body: String = String::from("\t\t{\n");
		body += &format!("\t\t{}", phoenix_save_json_value(&String::from("@type"), &self.p_type));
		body += &format!("\t\t{}", phoenix_save_json_value(&String::from("givenName"), &self.p_name));
		body += &format!("\t\t{}", phoenix_save_json_value(&String::from("familyName"), &self.p_familly_name));
		body += &self.p_affiliation.to_json_string();
		body += &String::from("\t\t}");
		return body;
	}
}

///CodeMeta configuration
#[derive(Debug, Serialize)]
pub struct CodeMetaConfig{
	///Context of codemeta
	#[serde(rename = "@context")]
	p_context: String,
	///Type of codemeta
	#[serde(rename = "@type")]
	p_type: String,
	///License of the project
	#[serde(rename = "license")]
	p_license: String,
	///Url to the code of the project
	#[serde(rename = "codeRepository")]
	p_code_repository: String,
	///Url to the CI of the project
	#[serde(rename = "contIntegration")]
	p_continuous_integration: String,
	///Creation date of the project
	#[serde(rename = "dateCreated")]
	p_date_created: String,
	///Publication date of the project
	#[serde(rename = "datePublished")]
	p_date_published: String,
	///Modification date of the project
	#[serde(rename = "dateModified")]
	p_date_modified: String,
	///Url to download archive of the project's sources
	#[serde(rename = "downloadUrl")]
	p_download_url: String,
	///Issues of the project
	#[serde(rename = "issueTracker")]
	p_issue_tracker: String,
	///Name of the project
	#[serde(rename = "name")]
	p_name: String,
	///Version of the project
	#[serde(rename = "version")]
	p_version: String,
	///Software Version of the project (the same as version because why not)
	#[serde(rename = "softwareVersion")]
	p_software_version: String,
	///Readme of the project
	#[serde(rename = "readme")]
	p_readme: String,
	///Description of the project
	#[serde(rename = "description")]
	p_description: String,
	///Keywords to describe the project
	#[serde(rename = "keywords")]
	p_vec_keyword: Vec<String>,
	///Vector of authors of the project
	#[serde(rename = "author")]
	p_vec_author: Vec<PersonConfig>,
	///Vector of contributors of the project
	#[serde(rename = "contributor")]
	p_vec_contributor: Vec<PersonConfig>,
	///Maintainer of the project
	#[serde(rename = "maintainer")]
	p_maintainer: PersonConfig,
}

impl CodeMetaConfig{
	///Initialise a CodeMetaConfig from a PixiConfig
	/// # Parameters
	/// - `config` : pixi configuration
	/// # Returns
	/// Initalised CodeMetaConfig
	/// - `vec_keyword` : vector of keywords to describe the current project
	/// - `creation_date` : date of hte creation of the current project
	pub fn from(config: &PixiConfig, vec_keyword: &Vec<String>, creation_date: &String) -> Self{
		//Let's be honest, this is really ugly, but the std seems not to have date
		// let cmd = Command::new("date").arg("+%Y-%m-%d").output().expect("Cannot call date");
		// let date: String = String::from_utf8(cmd.stdout).unwrap().replace("\n", "");
		//This is better to get date like this
		let current_date = chrono::Utc::now();
		let year = current_date.year();
		let month = current_date.month();
		let day = current_date.day();
		let date: String = String::from(format!("{}-{:02}-{:02}", year, month, day));
		
		CodeMetaConfig {
			p_context: String::from("https://doi.org/10.5063/schema/codemeta-2.0"),
			p_type: String::from("SoftwareSourceCode"),
			p_license: String::from("https://spdx.org/licenses/CECILL-C"),
			p_code_repository: config.get_repository(),
			p_continuous_integration: config.get_continuous_integration(),
			p_date_created: creation_date.to_owned(),
			p_date_published: date.clone(),
			p_date_modified: date.clone(),
			p_download_url: config.get_download_url(),
			p_issue_tracker: config.get_issue_tracker(),
			p_name: config.get_name().to_owned(),
			p_version: config.get_version().to_owned(),
			p_software_version: config.get_version().to_owned(),
			p_readme: config.get_readme(),
			p_description: config.get_description().to_owned(),
			p_vec_keyword: vec_keyword.to_owned(),
			p_vec_author: vec![
				PersonConfig::new(&String::from("Pierre"), &String::from("Aubert")),
				PersonConfig::new(&String::from("Vincent"), &String::from("Pollet")),
				PersonConfig::new(&String::from("Thibaut"), &String::from("Oprinsen"))
			],
			p_vec_contributor: vec![
				PersonConfig::new(&String::from("Pierre"), &String::from("Aubert")),
				PersonConfig::new(&String::from("Vincent"), &String::from("Pollet")),
				PersonConfig::new(&String::from("Thibaut"), &String::from("Oprinsen"))
			],
			p_maintainer: Default::default()
		}
	}
}

///Save the code meta in json
/// # Parameters
/// - `filename` : name of the json file to write
/// - `config` : config to save
/// # Error
/// I/O error
pub fn save_codemeta_json(filename: &Path, config: &CodeMetaConfig) -> io::Result<()>{
	let mut body = String::from("{\n");
	body += &phoenix_save_json_value(&String::from("@context"), &config.p_context);
	body += &phoenix_save_json_value(&String::from("@type"), &config.p_type);
	body += &phoenix_save_json_value(&String::from("license"), &config.p_license);
	body += &phoenix_save_json_value(&String::from("codeRepository"), &config.p_code_repository);
	body += &phoenix_save_json_value(&String::from("contIntegration"), &config.p_continuous_integration);
	body += &phoenix_save_json_value(&String::from("dateCreated"), &config.p_date_created);
	body += &phoenix_save_json_value(&String::from("dateModified"), &config.p_date_modified);
	body += &phoenix_save_json_value(&String::from("datePublished"), &config.p_date_published);
	body += &phoenix_save_json_value(&String::from("downloadUrl"), &config.p_download_url);
	body += &phoenix_save_json_value(&String::from("issueTracker"), &config.p_issue_tracker);
	body += &phoenix_save_json_value(&String::from("name"), &config.p_name);
	body += &phoenix_save_json_value(&String::from("version"), &config.p_version);
	body += &phoenix_save_json_value(&String::from("softwareVersion"), &config.p_software_version);
	body += &phoenix_save_json_value(&String::from("readme"), &config.p_readme);
	body += &phoenix_save_json_value(&String::from("description"), &config.p_description);
	body += &String::from("\t\"keywords\": [");
	
	// let date: Date = Date::
	let mut comma_keyword = String::from("");
	for key in config.p_vec_keyword.iter(){
		body += &comma_keyword;
		body += &json::stringify(key.to_owned());
		comma_keyword = String::from(", ");
	}
	
	body += &String::from("],\n");
	
	body += &String::from(r#"	"relatedLink": [
		"https://gitlab.in2p3.fr/CTA-LAPP/PHOENIX_LIBS2/PHOENIX2"
	],
"#);
	body += &String::from("\t\"author\": [\n");
	let mut author_comma = String::from("");
	for person in config.p_vec_author.iter(){
		body += &author_comma;
		body += &person.to_json_string();
		author_comma = String::from(",\n");
	}
	body += &String::from("\n\t],\n");
	body += &String::from("\t\"contributor\": [\n");
	let mut contributor_comma = String::from("");
	for person in config.p_vec_contributor.iter(){
		body += &contributor_comma;
		body += &person.to_json_string();
		contributor_comma = String::from(",\n");
	}
	body += &String::from("\n\t],\n");
	body += &String::from("\t\"maintainer\":\n");
	body += &config.p_maintainer.to_json_string();
	
	body += &String::from("\n}\n");
	fs::write(filename, body)?;
	// fs::write(filename, json::stringify(config))?;
	Ok(())
}

#[cfg(test)]
mod tests {
	use std::path::PathBuf;
	use super::*;
	
	///Test save of a CodeMeta to JSON
	#[test]
	fn test_codemeta_to_json(){
		fs::create_dir_all("generated/").unwrap();
		let config: CodeMetaConfig = CodeMetaConfig {
			p_context: String::from("https://doi.org/10.5063/schema/codemeta-2.0"),
			p_type: String::from("SoftwareSourceCode"),
			p_license: String::from("https://spdx.org/licenses/CECILL-C"),
			p_code_repository: String::from(""),
			p_continuous_integration: String::from(""),
			p_date_created: String::from("2026-03-02"),
			p_date_published: String::from(""),
			p_date_modified: String::from(""),
			p_download_url: String::from(""),
			p_issue_tracker: String::from(""),
			p_name: String::from(""),
			p_version: String::from("0.42.0"),
			p_software_version: String::from("0.42.0"),
			p_readme: String::from(""),
			p_description: String::from("Very important project"),
			p_vec_keyword: vec![String::from("Phoenix2")],
			p_vec_author: vec![
				PersonConfig::new(&String::from("Pierre"), &String::from("Aubert")),
				PersonConfig::new(&String::from("Vincent"), &String::from("Pollet")),
				PersonConfig::new(&String::from("Thibaut"), &String::from("Oprinsen"))
			],
			p_vec_contributor: vec![
				PersonConfig::new(&String::from("Pierre"), &String::from("Aubert")),
				PersonConfig::new(&String::from("Vincent"), &String::from("Pollet")),
				PersonConfig::new(&String::from("Thibaut"), &String::from("Oprinsen"))
			],
			p_maintainer: Default::default()
		};
		let filename: PathBuf = PathBuf::from("generated/codemeta.json");
		save_codemeta_json(&filename, &config).unwrap();
	}
}