use std::fmt; use indexmap::{indexmap, IndexMap}; use schemars::JsonSchema; use serde::de::{self, Unexpected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use stringlit::s; pub fn my_trim(v: &str, s: S) -> Result where S: Serializer, { s.serialize_str(v.trim()) } #[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Requirement { pub name: String, #[serde(serialize_with = "my_trim")] pub description: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub requires: Vec, } #[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Topic { pub name: String, #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub requirements: IndexMap, #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub subtopics: IndexMap, } #[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Definition { pub name: String, pub value: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub additional_info: Vec, } #[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct ConfigDefault { pub name: String, #[serde(rename = "type")] pub typ: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub valid_values: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub unit: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_value: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub hint: Option, } #[derive(JsonSchema, Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct Version { major: u64, minor: u64, patch: u64, } impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}.{}.{}", self.major, self.minor, self.patch) } } // Serialization as before fn serialize_version(version: &Version, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&version.to_string()) } // Custom deserialization fn deserialize_version<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { struct VersionVisitor; impl<'de> Visitor<'de> for VersionVisitor { type Value = Version; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a version string in the format 'major.minor.patch'") } fn visit_str(self, value: &str) -> Result where E: de::Error, { let parts: Vec<&str> = value.split('.').collect(); if parts.len() != 3 { return Err(E::invalid_value(Unexpected::Str(value), &self)); } let major = parts[0] .parse::() .map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?; let minor = parts[1] .parse::() .map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?; let patch = parts[2] .parse::() .map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?; Ok(Version { major, minor, patch, }) } } deserializer.deserialize_str(VersionVisitor) } #[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Project { pub name: String, #[serde( serialize_with = "serialize_version", deserialize_with = "deserialize_version" )] pub version: Version, #[serde(serialize_with = "my_trim")] pub description: String, #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub topics: IndexMap, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub definitions: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub config_defaults: Vec, } pub fn demo_project() -> Project { Project { name: s!("journal-uploader"), version: Version { major: 1, minor: 0, patch: 0, }, description: s!(r" The journal-uploader has two main functionalities. - Take a stream of log messages and filter them depending on their severity - Upload journal logs for a specified time when activated through cloud call"), topics: indexmap! { s!("FEAT-1") => Topic { name: s!("Traced Logging"), subtopics: indexmap! { s!("SUB-1") => Topic { name: s!("File Monitoring"), requirements: indexmap! { s!("REQ-1") => Requirement { name: s!("Continuous Monitoring"), description: s!(r"The tool must continuously monitor a designated directory."), requires: vec! [], } }, subtopics: indexmap! {} }, s!("SUB-2") => Topic { name: s!("File Detection"), requirements: indexmap! { s!("REQ-1") => Requirement { name: s!("Detection of New Files"), description: s!(r"The tool must detect the addition of new files in the monitored directory."), requires: vec! [], }, s!("REQ-2") => Requirement { name: s!("Avoid Re-processing"), description: s!(r"The tool must not process files that have already been processed."), requires: vec! [], } }, subtopics: indexmap! {} }, }, requirements: indexmap! {}, } }, definitions: vec![ Definition { name: s!("Default Journal Directory"), value: s!("/run/log/journal/"), additional_info: vec![s!("Machine ID can be found at /etc/machine-id")], }, Definition { name: s!("Default Output Directory"), value: s!("/run/log/filtered-journal"), additional_info: vec![], }, ], config_defaults: vec![ ConfigDefault { name: s!("Journal Directory"), typ: s!("Path"), unit: None, valid_values: None, default_value: None, hint: None, }, ConfigDefault { name: s!("Output Directory"), typ: s!("Path"), unit: None, valid_values: None, default_value: None, hint: None, }, ConfigDefault { name: s!("Trigger Priority"), typ: s!("Enum"), unit: None, valid_values: Some(vec![ s!("Emergency"), s!("Alert"), s!("Critical"), s!("Error"), s!("Warning"), s!("Notice"), s!("Info"), s!("Debug"), ]), default_value: Some(s!("Warning")), hint: None, }, ConfigDefault { name: s!("Journal Context"), typ: s!("Integer"), unit: Some(s!("Seconds")), valid_values: None, default_value: Some(s!("15")), hint: None, }, ConfigDefault { name: s!("Max File Size"), typ: s!("Integer"), unit: Some(s!("Bytes")), valid_values: None, default_value: Some(s!("8388608")), hint: Some(s!("(8 MB)")), }, ConfigDefault { name: s!("Max Directory Size"), typ: s!("Integer"), unit: Some(s!("Bytes")), valid_values: None, default_value: Some(s!("75497472")), hint: Some(s!("(72 MB)")), }, ConfigDefault { name: s!("File Monitoring Interval"), typ: s!("Integer"), unit: Some(s!("Seconds")), valid_values: None, default_value: Some(s!("10")), hint: None, }, ], } }