Skip to content

Commit

Permalink
feat: add some basic validation rules
Browse files Browse the repository at this point in the history
  • Loading branch information
AdmiringWorm committed Mar 27, 2021
1 parent 7ae0239 commit f297324
Show file tree
Hide file tree
Showing 9 changed files with 562 additions and 0 deletions.
125 changes: 125 additions & 0 deletions pkg-upd/src/bin/pkg-validate.rs
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>")]
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<dyn std::error::Error>> {
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<Item = &'a RuleMessage>,
) {
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);
}
}
}
1 change: 1 addition & 0 deletions pkg-upd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@

pub mod logging;
pub mod parsers;
pub mod rules;
pub mod runners;
92 changes: 92 additions & 0 deletions pkg-upd/src/rules.rs
Original file line number Diff line number Diff line change
@@ -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<RuleMessage>> {
let mut msgs = vec![];

metadata::run_validation(&mut msgs, data, rule_kind);

if msgs.is_empty() { Ok(()) } else { Err(msgs) }
}

pub trait RuleHandler<T> {
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<Self, <Self as std::str::FromStr>::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"),
}
}
}
25 changes: 25 additions & 0 deletions pkg-upd/src/rules/metadata.rs
Original file line number Diff line number Diff line change
@@ -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<RuleMessage>, 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);
}
13 changes: 13 additions & 0 deletions pkg-upd/src/rules/metadata/chocolatey.rs
Original file line number Diff line number Diff line change
@@ -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<RuleMessage>, data: &PackageMetadata, rule_kind: &RuleKind) {
call_rules!(msgs, rule_kind, data, id_is_lowercase::IdIsLowercaseNote);
}
74 changes: 74 additions & 0 deletions pkg-upd/src/rules/metadata/chocolatey/id_is_lowercase.rs
Original file line number Diff line number Diff line change
@@ -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<PackageMetadata> 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(()))
}
}
Loading

0 comments on commit f297324

Please sign in to comment.