From f2973247125df2332e1a513b44f882d9e549da43 Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Tue, 23 Mar 2021 10:33:10 +0100 Subject: [PATCH] feat: add some basic validation rules --- pkg-upd/src/bin/pkg-validate.rs | 125 ++++++++++++++++++ pkg-upd/src/lib.rs | 1 + pkg-upd/src/rules.rs | 92 +++++++++++++ pkg-upd/src/rules/metadata.rs | 25 ++++ pkg-upd/src/rules/metadata/chocolatey.rs | 13 ++ .../metadata/chocolatey/id_is_lowercase.rs | 74 +++++++++++ pkg-upd/src/rules/metadata/id_not_empty.rs | 65 +++++++++ .../rules/metadata/maintainers_not_empty.rs | 86 ++++++++++++ .../metadata/project_url_not_local_path.rs | 81 ++++++++++++ 9 files changed, 562 insertions(+) create mode 100644 pkg-upd/src/bin/pkg-validate.rs create mode 100644 pkg-upd/src/rules.rs create mode 100644 pkg-upd/src/rules/metadata.rs create mode 100644 pkg-upd/src/rules/metadata/chocolatey.rs create mode 100644 pkg-upd/src/rules/metadata/chocolatey/id_is_lowercase.rs create mode 100644 pkg-upd/src/rules/metadata/id_not_empty.rs create mode 100644 pkg-upd/src/rules/metadata/maintainers_not_empty.rs create mode 100644 pkg-upd/src/rules/metadata/project_url_not_local_path.rs diff --git a/pkg-upd/src/bin/pkg-validate.rs b/pkg-upd/src/bin/pkg-validate.rs new file mode 100644 index 0000000..8cd504b --- /dev/null +++ b/pkg-upd/src/bin/pkg-validate.rs @@ -0,0 +1,125 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project +#![windows_subsystem = "console"] + +extern crate pkg_upd; + +use std::path::PathBuf; + +use human_panic::setup_panic; +use log::{error, info}; +use pkg_upd::logging::setup_logging; +use pkg_upd::rules::{MessageType, RuleKind, RuleMessage}; +use structopt::StructOpt; +use yansi::{Color, Style}; + +/// Validates that the specified meta file is using valid structer, can use the +/// download locations and that the specified metadata conforms to the wanted +/// rules. +#[derive(StructOpt)] +#[structopt(author = "AdmiringWorm ")] +struct Arguments { + /// The path to the meta file that should be validated. + #[structopt(parse(from_os_str))] + file: PathBuf, + + /// The rule that the metadata should confirm to. + /// + /// By using the default or explicitly specifying the `core` rule, only + /// metadata that would prevent the creation of a package would be + /// validated. + /// + /// Specifying `communty` validates all implemented metadata rules against + /// best practices when pushing to a community repository. Requirements + /// would be reported as errors and prevent further processing after the + /// metadata, while Guidelines and suggestions would be reported as + /// Warnings. + #[structopt(long = "rule", default_value, env = "PKG_VALIDATE_RULE", possible_values = &["core", "community"])] + rule: RuleKind, + + #[structopt(flatten)] + log: pkg_upd::logging::LogData, +} + +fn main() { + setup_panic!(); + + run().unwrap(); // We do unwrap here, and rely on human_panic to display any errors to the user in case of failure. +} + +fn run() -> Result<(), Box> { + let arguments = Arguments::from_args(); + setup_logging(&arguments.log)?; + + info!("Loading metadata file from '{}'", arguments.file.display()); + + let data = match pkg_upd::parsers::read_file(&arguments.file) { + Ok(data) => { + info!("Loaded metadata file successfully!"); + data + } + Err(err) => { + error!("Failed to load metadata file. Failure message: \n\t{}", err); + return Ok(()); + } + }; + + validate_metadata(&data, &arguments.rule); + + Ok(()) +} + +fn validate_metadata(data: &pkg_data::PackageData, rule_kind: &RuleKind) { + let metadata = data.metadata(); + + if let Err(rules) = pkg_upd::rules::validate_metadata(metadata, rule_kind) { + info!( + "{}", + Style::new(Color::Yellow) + .paint("The following issues was found during validation of the package data!") + ); + + let types = &[ + MessageType::Requirement, + MessageType::Guideline, + MessageType::Suggestion, + MessageType::Note, + ]; + + for t in types { + write_rule_messages(*t, rules.iter().filter(|r| &r.message_type == t)); + } + } else { + println!( + "{}", + Style::new(Color::Green).paint("No issues was found during validation!") + ); + } +} + +fn write_rule_messages<'a>( + message_type: MessageType, + rules: impl Iterator, +) { + let mut write_header = true; + + for rule in rules { + if write_header { + let (msg, color) = match message_type { + MessageType::Requirement => ("REQUIREMENTS", Color::Red), + MessageType::Guideline => ("GUIDELINES", Color::Yellow), + MessageType::Suggestion => ("SUGGESTIONS", Color::Cyan), + MessageType::Note => ("NOTES", Color::Magenta), + }; + + println!("\n{}", color.style().bold().paint(msg)); + write_header = false; + } + + if rule.package_manager.is_empty() { + println!("- {}", rule.message); + } else { + println!("- {}: {}", rule.package_manager, rule.message); + } + } +} diff --git a/pkg-upd/src/lib.rs b/pkg-upd/src/lib.rs index e683c35..7bbcaeb 100644 --- a/pkg-upd/src/lib.rs +++ b/pkg-upd/src/lib.rs @@ -14,4 +14,5 @@ pub mod logging; pub mod parsers; +pub mod rules; pub mod runners; diff --git a/pkg-upd/src/rules.rs b/pkg-upd/src/rules.rs new file mode 100644 index 0000000..b5cabde --- /dev/null +++ b/pkg-upd/src/rules.rs @@ -0,0 +1,92 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project + +mod metadata; + +use std::fmt::Display; +use std::str::FromStr; + +use pkg_data::metadata::PackageMetadata; + +#[macro_export(local_inner_macros)] +macro_rules! call_rules { + ($msgs:expr,$rule_kind:expr,$data:expr,$($rule:path),*) => { + use crate::rules::RuleHandler; + + $( + if <$rule>::should_validate($rule_kind) { + if let Err(msg) = <$rule>::validate($data) { + $msgs.push(msg) + } + } + )* + }; +} + +pub fn validate_metadata( + data: &PackageMetadata, + rule_kind: &RuleKind, +) -> Result<(), Vec> { + let mut msgs = vec![]; + + metadata::run_validation(&mut msgs, data, rule_kind); + + if msgs.is_empty() { Ok(()) } else { Err(msgs) } +} + +pub trait RuleHandler { + fn should_validate(rule_type: &RuleKind) -> bool; + fn validate(data: &T) -> Result<(), RuleMessage>; +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum MessageType { + Requirement, + Guideline, + Suggestion, + Note, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct RuleMessage { + pub message_type: MessageType, + pub package_manager: &'static str, + pub message: String, +} + +#[derive(Debug, PartialEq)] +pub enum RuleKind { + Core, + Community, +} + +impl FromStr for RuleKind { + type Err = String; + + fn from_str(value: &str) -> std::result::Result::Err> { + let value_lower = value.to_lowercase(); + + if value_lower == "core" { + Ok(RuleKind::Core) + } else if value_lower == "community" { + Ok(RuleKind::Community) + } else { + Err(format!("{} is not a valid rule!", value)) + } + } +} + +impl Default for RuleKind { + fn default() -> Self { + RuleKind::Core + } +} + +impl Display for RuleKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + RuleKind::Core => f.write_str("core"), + RuleKind::Community => f.write_str("community"), + } + } +} diff --git a/pkg-upd/src/rules/metadata.rs b/pkg-upd/src/rules/metadata.rs new file mode 100644 index 0000000..b02664e --- /dev/null +++ b/pkg-upd/src/rules/metadata.rs @@ -0,0 +1,25 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project + +mod chocolatey; +mod id_not_empty; +mod maintainers_not_empty; +mod project_url_not_local_path; + +use pkg_data::metadata::PackageMetadata; + +use crate::call_rules; +use crate::rules::{RuleKind, RuleMessage}; + +pub fn run_validation(msgs: &mut Vec, data: &PackageMetadata, rule_kind: &RuleKind) { + call_rules!( + msgs, + rule_kind, + data, + id_not_empty::IdNotEmptyRequirement, + maintainers_not_empty::MaintainersNotEmptyRequirement, + project_url_not_local_path::ProjectUrlNotLocalPathRequirement + ); + + chocolatey::run_validation(msgs, data, rule_kind); +} diff --git a/pkg-upd/src/rules/metadata/chocolatey.rs b/pkg-upd/src/rules/metadata/chocolatey.rs new file mode 100644 index 0000000..26c545f --- /dev/null +++ b/pkg-upd/src/rules/metadata/chocolatey.rs @@ -0,0 +1,13 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project + +mod id_is_lowercase; + +use pkg_data::metadata::PackageMetadata; + +use crate::call_rules; +use crate::rules::{RuleKind, RuleMessage}; + +pub fn run_validation(msgs: &mut Vec, data: &PackageMetadata, rule_kind: &RuleKind) { + call_rules!(msgs, rule_kind, data, id_is_lowercase::IdIsLowercaseNote); +} diff --git a/pkg-upd/src/rules/metadata/chocolatey/id_is_lowercase.rs b/pkg-upd/src/rules/metadata/chocolatey/id_is_lowercase.rs new file mode 100644 index 0000000..93fe26a --- /dev/null +++ b/pkg-upd/src/rules/metadata/chocolatey/id_is_lowercase.rs @@ -0,0 +1,74 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project + +use pkg_data::prelude::PackageMetadata; + +use crate::rules::{MessageType, RuleHandler, RuleKind, RuleMessage}; + +pub struct IdIsLowercaseNote; + +impl RuleHandler for IdIsLowercaseNote { + fn should_validate(rule_kind: &RuleKind) -> bool { + rule_kind == &RuleKind::Community + } + + fn validate(data: &PackageMetadata) -> std::result::Result<(), RuleMessage> { + let id = data.id(); + + if id.chars().any(|ch| ch.is_uppercase()) { + Err(RuleMessage { + message_type: MessageType::Note, + message: "The identifier contains upper case characters. If this is a new \ + package, it should only contain characters in lower case!" + .into(), + package_manager: "choco", + }) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[test] + fn should_validate_should_be_true_for_community() { + assert!(IdIsLowercaseNote::should_validate(&RuleKind::Community)) + } + + #[rstest(kind, case(RuleKind::Core))] + fn should_validate_should_be_false(kind: RuleKind) { + assert!(!IdIsLowercaseNote::should_validate(&kind)) + } + + #[test] + fn validate_should_return_rule_message_on_uppercase_letter() { + let data = PackageMetadata::new("test-PackAGE"); + + let result = IdIsLowercaseNote::validate(&data); + + assert_eq!( + result, + Err(RuleMessage { + message_type: MessageType::Note, + message: "The identifier contains upper case characters. If this is a new \ + package, it should only contain characters in lower case!" + .into(), + package_manager: "choco", + }) + ) + } + + #[test] + fn validate_should_not_return_message_on_all_lowercase_letters() { + let data = PackageMetadata::new("test-package"); + + let result = IdIsLowercaseNote::validate(&data); + + assert_eq!(result, Ok(())) + } +} diff --git a/pkg-upd/src/rules/metadata/id_not_empty.rs b/pkg-upd/src/rules/metadata/id_not_empty.rs new file mode 100644 index 0000000..cad1906 --- /dev/null +++ b/pkg-upd/src/rules/metadata/id_not_empty.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project + +use pkg_data::metadata::PackageMetadata; + +use crate::rules::{MessageType, RuleHandler, RuleKind, RuleMessage}; + +pub struct IdNotEmptyRequirement; + +impl RuleHandler for IdNotEmptyRequirement { + #[inline(always)] + fn should_validate(_: &RuleKind) -> bool { + true + } + + fn validate(data: &PackageMetadata) -> Result<(), RuleMessage> { + if data.id().trim().is_empty() { + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "A identifier can not be empty!".into(), + package_manager: "", + }) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[test] + fn should_validate_should_always_be_true() { + assert!(IdNotEmptyRequirement::should_validate(&RuleKind::Core)); + assert!(IdNotEmptyRequirement::should_validate(&RuleKind::Community)); + } + + #[rstest(id, case(""), case(" "), case(" \n"), case("\r "), case("\r\n"))] + fn validate_should_return_rule_message_on_empty_id(id: &'static str) { + let data = PackageMetadata::new(id); + + let result = IdNotEmptyRequirement::validate(&data); + + assert_eq!( + result, + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "A identifier can not be empty!".into(), + package_manager: "" + }) + ); + } + + #[test] + fn validate_should_not_return_message_on_non_empty_id() { + let data = PackageMetadata::new("test-id"); + + let result = IdNotEmptyRequirement::validate(&data); + + assert_eq!(result, Ok(())); + } +} diff --git a/pkg-upd/src/rules/metadata/maintainers_not_empty.rs b/pkg-upd/src/rules/metadata/maintainers_not_empty.rs new file mode 100644 index 0000000..ad37267 --- /dev/null +++ b/pkg-upd/src/rules/metadata/maintainers_not_empty.rs @@ -0,0 +1,86 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project + +use pkg_data::prelude::PackageMetadata; + +use crate::rules::{MessageType, RuleHandler, RuleKind, RuleMessage}; + +pub struct MaintainersNotEmptyRequirement; + +impl RuleHandler for MaintainersNotEmptyRequirement { + #[inline(always)] + fn should_validate(_: &RuleKind) -> bool { + true + } + + fn validate(metadata: &PackageMetadata) -> Result<(), RuleMessage> { + let maintainers = metadata.maintainers(); + + if maintainers.iter().all(|m| m.is_empty()) { + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "At least 1 maintainer must be specified for the package!".into(), + package_manager: "", + }) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest(kind, case(RuleKind::Community), case(RuleKind::Core))] + fn should_validate_should_always_be_true(kind: RuleKind) { + assert!(MaintainersNotEmptyRequirement::should_validate(&kind)); + } + + #[test] + fn validate_should_return_rule_message_on_empty_maintainers() { + let mut data = PackageMetadata::default(); + let maintainers: [&str; 0] = []; + data.set_maintainers(&maintainers); + + let result = MaintainersNotEmptyRequirement::validate(&data); + + assert_eq!( + result, + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "At least 1 maintainer must be specified for the package!".into(), + package_manager: "" + }) + ) + } + + #[test] + fn validate_should_return_rule_message_when_all_items_is_empty() { + let mut data = PackageMetadata::default(); + data.set_maintainers(&["", "", ""]); + + let result = MaintainersNotEmptyRequirement::validate(&data); + + assert_eq!( + result, + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "At least 1 maintainer must be specified for the package!".into(), + package_manager: "" + }) + ) + } + + #[test] + fn validate_should_not_return_message_on_non_empty_array() { + let mut data = PackageMetadata::default(); + data.set_maintainers(&["AdmiringWorm", "Chocolatey"]); + + let result = MaintainersNotEmptyRequirement::validate(&data); + + assert_eq!(result, Ok(())) + } +} diff --git a/pkg-upd/src/rules/metadata/project_url_not_local_path.rs b/pkg-upd/src/rules/metadata/project_url_not_local_path.rs new file mode 100644 index 0000000..e557cdb --- /dev/null +++ b/pkg-upd/src/rules/metadata/project_url_not_local_path.rs @@ -0,0 +1,81 @@ +// Copyright (c) 2021 Kim J. Nordmo and WormieCorp. +// Licensed under the MIT license. See LICENSE.txt file in the project +use pkg_data::prelude::PackageMetadata; + +use crate::rules::{MessageType, RuleHandler, RuleKind, RuleMessage}; + +pub struct ProjectUrlNotLocalPathRequirement; + +impl RuleHandler for ProjectUrlNotLocalPathRequirement { + #[inline(always)] + fn should_validate(_: &RuleKind) -> bool { + true + } + + fn validate(data: &PackageMetadata) -> std::result::Result<(), RuleMessage> { + let project_url = data.project_url(); + if !project_url.has_host() + || project_url.to_file_path().is_ok() + || project_url.scheme().to_lowercase() == "file" + { + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "The project url can not be a local path!".into(), + package_manager: "", + }) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[test] + fn should_validate_should_always_be_true() { + assert!(ProjectUrlNotLocalPathRequirement::should_validate( + &RuleKind::Core + )); + assert!(ProjectUrlNotLocalPathRequirement::should_validate( + &RuleKind::Community + )); + } + + #[rstest( + url, + case("file:///home/test/test-path"), + case("file://C:/test-path"), + case("file://localhost/etc/fstab"), + case("file:///c:/WINDOWS/clock.avi"), + case("file://localhost/c$/WINDOWS/clock.avi"), + case("file://./sharename/path/to/the%20file.txt") + )] + fn validate_should_return_rule_message_on_local_paths(url: &'static str) { + let mut data = PackageMetadata::new("valid-id"); + data.set_project_url(url); + + let result = ProjectUrlNotLocalPathRequirement::validate(&data); + + assert_eq!( + result, + Err(RuleMessage { + message_type: MessageType::Requirement, + message: "The project url can not be a local path!".into(), + package_manager: "" + }) + ); + } + + #[test] + fn validate_should_not_return_any_messages_on_valid_url() { + let mut data = PackageMetadata::new("valid-id"); + data.set_project_url("https://github.com"); + let result = ProjectUrlNotLocalPathRequirement::validate(&data); + + assert_eq!(result, Ok(())) + } +}