This commit is contained in:
Steve Biedermann 2024-05-09 23:14:14 +02:00
parent d9e9ce628c
commit 7c2540a185
5 changed files with 123 additions and 17 deletions

7
Cargo.lock generated
View File

@ -479,6 +479,7 @@ dependencies = [
"stringlit", "stringlit",
"toml", "toml",
"tui", "tui",
"version_operators",
] ]
[[package]] [[package]]
@ -780,6 +781,12 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "version_operators"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "effa0697dca74a72f99a5a3ee248673c19e02a915ce78667768b0d311d468340"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -21,3 +21,4 @@ serde_yaml = "0.9.34"
stringlit = "2.1.0" stringlit = "2.1.0"
toml = { version = "0.8.12", features = ["indexmap", "preserve_order"] } toml = { version = "0.8.12", features = ["indexmap", "preserve_order"] }
tui = "0.19.0" tui = "0.19.0"
version_operators = "0.0.1"

View File

@ -1,4 +1,5 @@
name: journal-uploader name: journal-uploader
version: 1.0.0
description: |- description: |-
The journal-uploader has two main functionalities. The journal-uploader has two main functionalities.
- Take a stream of log messages and filter them depending on their severity - Take a stream of log messages and filter them depending on their severity

View File

@ -1,6 +1,9 @@
use std::fmt;
use indexmap::{indexmap, IndexMap}; use indexmap::{indexmap, IndexMap};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize, Serializer}; use serde::de::{self, Unexpected, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use stringlit::s; use stringlit::s;
pub fn my_trim<S>(v: &str, s: S) -> Result<S::Ok, S::Error> pub fn my_trim<S>(v: &str, s: S) -> Result<S::Ok, S::Error>
@ -51,9 +54,79 @@ pub struct ConfigDefault {
pub hint: Option<String>, pub hint: Option<String>,
} }
#[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<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&version.to_string())
}
// Custom deserialization
fn deserialize_version<'de, D>(deserializer: D) -> Result<Version, D::Error>
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<E>(self, value: &str) -> Result<Version, E>
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::<u64>()
.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?;
let minor = parts[1]
.parse::<u64>()
.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?;
let patch = parts[2]
.parse::<u64>()
.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?;
Ok(Version {
major,
minor,
patch,
})
}
}
deserializer.deserialize_str(VersionVisitor)
}
#[derive(JsonSchema, Debug, Deserialize, Serialize)] #[derive(JsonSchema, Debug, Deserialize, Serialize)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
#[serde(
serialize_with = "serialize_version",
deserialize_with = "deserialize_version"
)]
pub version: Version,
#[serde(serialize_with = "my_trim")] #[serde(serialize_with = "my_trim")]
pub description: String, pub description: String,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")] #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
@ -67,6 +140,11 @@ pub struct Project {
pub fn demo_project() -> Project { pub fn demo_project() -> Project {
Project { Project {
name: s!("journal-uploader"), name: s!("journal-uploader"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
description: s!(r" description: s!(r"
The journal-uploader has two main functionalities. The journal-uploader has two main functionalities.
- Take a stream of log messages and filter them depending on their severity - Take a stream of log messages and filter them depending on their severity

View File

@ -110,21 +110,25 @@ fn add_requirements(output: &mut Vec<String>, requirements: &IndexMap<String, Re
for (id, requirement) in requirements { for (id, requirement) in requirements {
output.push(format!( output.push(format!(
"- **_{id}_ - {}:** {}", "- **_{id}_ - {}:** {}",
requirement.name, requirement.description requirement.name.trim(),
requirement.description.trim()
)); ));
} }
} }
fn add_topics(output: &mut Vec<String>, topics: &IndexMap<String, Topic>, level: usize) { fn add_topics(output: &mut Vec<String>, topics: &IndexMap<String, Topic>, level: usize) {
for (id, topic) in topics { for (id, topic) in topics {
output.push(format!("{} _{id}_ - {}", "#".repeat(level), topic.name)); output.push(format!(
"{} _{id}_ - {}",
"#".repeat(level),
topic.name.trim()
));
if !topic.requirements.is_empty() { if !topic.requirements.is_empty() {
add_requirements(output, &topic.requirements); add_requirements(output, &topic.requirements);
output.push(nl()); output.push(nl());
} }
if !topic.subtopics.is_empty() { if !topic.subtopics.is_empty() {
add_topics(output, &topic.subtopics, level + 1); add_topics(output, &topic.subtopics, level + 1);
output.push(nl());
} }
} }
} }
@ -132,6 +136,7 @@ fn add_topics(output: &mut Vec<String>, topics: &IndexMap<String, Topic>, level:
#[derive(Parser)] #[derive(Parser)]
enum Command { enum Command {
Schema, Schema,
Demo,
#[clap(alias = "md")] #[clap(alias = "md")]
Markdown { Markdown {
requirements: PathBuf, requirements: PathBuf,
@ -153,6 +158,9 @@ struct Args {
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let Args { command } = Args::parse(); let Args { command } = Args::parse();
match command { match command {
Command::Demo => {
println!("{}", serde_yaml::to_string(&demo_project())?);
}
Command::Schema => { Command::Schema => {
let schema = schema_for!(Project); let schema = schema_for!(Project);
println!("{}", serde_json::to_string_pretty(&schema).unwrap()); println!("{}", serde_json::to_string_pretty(&schema).unwrap());
@ -161,29 +169,34 @@ fn main() -> anyhow::Result<()> {
let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?; let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?;
let mut output = vec![ let mut output = vec![
format!("# Requirements for {}", project.name), format!("# Requirements for {}", project.name.trim()),
nl(), nl(),
s!("[[_TOC_]]"), s!("[[_TOC_]]"),
nl(), nl(),
WORD_DESCRIPTION.to_string(), WORD_DESCRIPTION.trim().to_string(),
nl(),
format!("**VERSION: {}**", project.version),
nl(), nl(),
s!("## Description"), s!("## Description"),
project.description, project.description.trim().to_string(),
nl(), nl(),
]; ];
if !project.topics.is_empty() { if !project.topics.is_empty() {
output.push(s!("## Requirements")); output.push(s!("## Requirements"));
add_topics(&mut output, &project.topics, 3); add_topics(&mut output, &project.topics, 3);
output.push(nl());
} }
if !project.definitions.is_empty() { if !project.definitions.is_empty() {
output.push(s!("## Definitions")); output.push(s!("## Definitions"));
for definition in project.definitions { for definition in project.definitions {
output.push(format!("- {}: {}", definition.name, definition.value)); output.push(format!(
"- {}: {}",
definition.name.trim(),
definition.value.trim()
));
for info in definition.additional_info { for info in definition.additional_info {
output.push(format!(" - {info}")) output.push(format!(" - {}", info.trim()))
} }
} }
output.push(nl()); output.push(nl());
@ -192,24 +205,30 @@ fn main() -> anyhow::Result<()> {
if !project.config_defaults.is_empty() { if !project.config_defaults.is_empty() {
output.push(s!("## Config Defaults")); output.push(s!("## Config Defaults"));
for default in project.config_defaults { for default in project.config_defaults {
output.push(format!("- **{}**", default.name)); output.push(format!("- **{}**", default.name.trim()));
output.push(format!(" - Type: {}", default.typ)); output.push(format!(" - Type: {}", default.typ.trim()));
if let Some(unit) = default.unit { if let Some(unit) = default.unit {
output.push(format!(" - Unit: {unit}")); output.push(format!(" - Unit: {}", unit.trim()));
} }
if let Some(valid_values) = default.valid_values { if let Some(valid_values) = default.valid_values {
output.push(format!(" - Valid Values: _{}_", valid_values.join(", "))); output.push(format!(
" - Valid Values: _{}_",
valid_values.join(", ").trim()
));
} }
if let Some(default_value) = default.default_value { if let Some(default_value) = default.default_value {
output.push(format!( output.push(format!(
" - Default Value: _{}_{}", " - Default Value: _{}_{}",
default_value, default_value.trim(),
default.hint.map(|h| format!(" {h}")).unwrap_or_default() default
.hint
.map(|h| format!(" {}", h.trim()))
.unwrap_or_default()
)); ));
} else { } else {
output.push(format!( output.push(format!(
" - **Required**: This value **_MUST_** be provided as a start parameter.{}", " - **Required**: This value **_MUST_** be provided as a start parameter.{}",
default.hint.map(|h| format!(" {h}")).unwrap_or_default() default.hint.map(|h| format!(" {}", h.trim())).unwrap_or_default()
)); ));
} }
output.push(nl()); output.push(nl());