//! `ruled-labels` is a cli helping with Github labels verifications based on a simple rule engine.
//! The rules are defined using a yaml file. `ruled-labels` allows running a single check but also
//! running a set of test cases to validate label set against your rules and ensuring your rules
//! meet all your expectations.
//!
//! You should check the [README](https://github.com/chevdor/ruled_labels/blob/master/README.md)
//! of the project to gain a better understanding of what the functions are.
//!
//! For a deaper understand of the options you have to call `ruled-labels`, you may check out
//! the [Opts](opts::Opts) and especially the list of available [SubCommand](opts::SubCommand)s.
//!
//! If you are interested in write specs or test files, you can find some information below:
//! - [Specs](rllib::specs::Specs)
//! - [Tests](rllib::tests::Tests)
mod opts;
mod rllib;
use crate::rllib::{
parsed_label::LabelId,
rule::Rule,
specs::Specs,
test_result::{ResultPrinter, TestResult},
tests::Tests,
};
use clap::{crate_name, crate_version, Parser};
use env_logger::Env;
use opts::*;
use std::{collections::HashSet, env, error::Error, path::PathBuf};
/// This is the entry point of the `ruled-labels` cli.
fn main() -> Result<(), Box<dyn Error>> {
env_logger::Builder::from_env(Env::default().default_filter_or("none")).init();
log::info!("Running {} v{}", crate_name!(), crate_version!());
let opts: Opts = Opts::parse();
match opts.subcmd {
SubCommand::List(cmd_opts) => {
log::debug!("list: {:#?}", cmd_opts);
let specs = Specs::load(&cmd_opts.spec_file)?;
println!("{}", specs);
Ok(())
},
SubCommand::Lint(cmd_opts) => {
log::debug!("lint: {:#?}", cmd_opts);
let specs: Result<Specs, _> = Specs::load(&cmd_opts.spec_file);
let result = specs.is_ok();
ResultPrinter::new("Lint Result", TestResult::from(result))
.with_message_passed(&format!("The file {} looks OK", cmd_opts.spec_file.display()))
.with_message_failed(&format!(
"The file {} contains errors",
cmd_opts.spec_file.display()
))
.with_color(!opts.no_color)
.print();
if result {
std::process::exit(0)
} else {
std::process::exit(1)
}
},
SubCommand::Check(cmd_opts) => {
log::debug!("check: {:#?}", cmd_opts);
let specs: Specs = Specs::load(&cmd_opts.spec_file)?;
let label_ids: HashSet<LabelId> = cmd_opts.labels.iter().map(|s| s.id).collect();
let res =
specs.run_checks(&label_ids, true, !opts.no_color, opts.dev, cmd_opts.tags, &None);
let aggregated_result = res.iter().fold(true, |acc, x| match x {
Some(v) => acc && *v,
None => acc,
});
if cmd_opts.faulty {
let faulty_rules: Vec<&Rule> = specs.find_faulty(res);
if !faulty_rules.is_empty() {
println!("faulty_rules:");
faulty_rules.iter().for_each(|rule| println!("{:#?}", rule));
}
}
if opts.dev {
let title = format!(
"{} v{} for labels {}",
specs.name,
specs.version,
label_ids.iter().map(|l| l.to_string()).collect::<Vec<String>>().join(", ")
);
ResultPrinter::new(&title, TestResult::from(aggregated_result))
.with_color(!opts.no_color)
.print();
}
if aggregated_result {
std::process::exit(0)
} else {
std::process::exit(1)
}
},
SubCommand::Test(cmd_opts) => {
log::debug!("test: {:#?}", cmd_opts);
let tests = Tests::load(&cmd_opts.test_specs)?;
let specs_path = PathBuf::from(&cmd_opts.test_specs);
let test_file_folder =
specs_path.parent().expect("The test specs should be in a folder");
let spec_file = if let Some(spec_file) = cmd_opts.spec_file {
spec_file
} else {
let t = test_file_folder;
t.join(&tests.spec_file)
};
log::debug!("spec_file: {}", spec_file.display());
let specs = Specs::load(&spec_file)?;
println!("Tests specs: {}", &cmd_opts.test_specs.display());
println!("Specs file : {}", &spec_file.display());
// TODO: The following is unaccurate as it does not consider that some tests are
// included/excluded by `only` and `all` println!(
// "Running {:?} test cases on your {:?} rules",
// tests.specs.specs.len(),
// specs.rules.len()
// );
tests.run(
specs,
cmd_opts.only,
cmd_opts.all,
!opts.no_color,
opts.dev,
&cmd_opts.filter,
);
Ok(())
},
}
}
//! This module defines all the claps (cli) options and flags.
use crate::rllib::{parsed_label::ParsedLabel, rule::Tag};
use clap::{crate_authors, crate_version, Parser, Subcommand};
use regex::Regex;
use std::path::PathBuf;
/// This utility allows checking labels based on rules
#[derive(Parser)]
#[clap(version = crate_version!(), author = crate_authors!())]
pub struct Opts {
// pub json: bool,
#[clap(subcommand)]
pub subcmd: SubCommand,
/// Output without any coloring, this is useful
/// for documentation and CI system where the color code
/// pollute the output.
#[clap(long, global = true)]
pub no_color: bool,
/// The output is more developer oriented
#[clap(short, long, global = true)]
pub dev: bool,
}
/// You can find all available commands below.
#[derive(Debug, Subcommand)]
pub enum SubCommand {
#[clap(version = crate_version!(), author = crate_authors!())]
List(ListOpts),
#[clap(version = crate_version!(), author = crate_authors!())]
Lint(LintOpts),
#[clap(version = crate_version!(), author = crate_authors!())]
Check(CheckOpts),
#[clap(version = crate_version!(), author = crate_authors!())]
Test(TestOpts),
}
/// List all the rules
#[derive(Debug, Parser)]
pub struct ListOpts {
/// The yaml spec file to be used.
#[clap(index = 1, default_value = "specs.yaml", value_hint=clap::ValueHint::FilePath)]
pub spec_file: PathBuf,
}
/// Lint the rules
#[derive(Debug, Parser)]
pub struct LintOpts {
/// Spec file
#[clap(index = 1, default_value = "specs.yaml", value_hint=clap::ValueHint::FilePath)]
pub spec_file: PathBuf,
}
/// Check label set against the rules
#[derive(Debug, Parser)]
pub struct CheckOpts {
/// Spec file
#[clap(index = 1, default_value = "specs.yaml", value_hint=clap::ValueHint::FilePath)]
pub spec_file: PathBuf,
/// The list of labels. You may pass then as `-l A1,B1` or `-l A1 -l B1`.
///
/// NOTE: The following calls are NOT valid: `-l A1, B1` or `-l A1 B1`
#[clap(long, short, required = true, num_args=1.., value_delimiter = ',')]
pub labels: Vec<ParsedLabel>,
/// Show details about the rules of the faulty tests
#[clap(long)]
pub faulty: bool,
/// If you pass optional tags here, only the checks containing
/// **all** those tags will run
#[clap(short, long, num_args=0..)]
pub tags: Option<Vec<Tag>>,
}
/// Run tests using rules and a test set
#[derive(Debug, Parser)]
pub struct TestOpts {
/// The yaml test file
#[clap(index = 1, default_value = "tests.yaml", value_hint=clap::ValueHint::FilePath)]
pub test_specs: PathBuf,
/// The spec is usually defined in the test file but you may override it
#[clap(long, short)]
pub spec_file: Option<PathBuf>,
/// Only run the tests marked with only = true
#[clap(long, conflicts_with = "all")]
pub only: bool,
/// Run ALL tests, even those marked as skip
#[clap(long)]
pub all: bool,
/// By passing an optional filter, you can limit which tests will run.
/// You can pass any valid regexp.
#[clap(short, long)]
pub filter: Option<Regex>,
}
//! Definition of common types and helper functions.
/// This helper function helps converting an interrable object of item convertible to
/// strings, as comma separated string, making debugging easier.
/// ## example:
/// ```
/// let data = vec!["Hello", "World"];
/// println!("{}", set_to_string(data));
/// // Output: "Hello, World"
/// ```
pub fn set_to_string<T: IntoIterator<Item = I>, I: ToString>(c: T) -> String {
c.into_iter().map(|e| e.to_string()).collect::<Vec<String>>().join(", ")
}
pub fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
#![cfg(test)]
use super::parsed_label::LabelId;
use std::collections::HashSet;
/// A convenience struct to create a `Vec<LabelId>` from a comma
/// spearated string.
pub struct LabelIdSet;
impl LabelIdSet {
/// Helper to create a `HashSet<LabelId>` from a string containing
/// comma separated label ids.
/// ## example:
/// ```
/// let set = LabelIdSet::from_str("B0, B1");
/// ```
pub fn from_str(s: &str) -> HashSet<LabelId> {
s.split(',')
.map(|s| {
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
LabelId::from(cleaned.as_str())
})
.collect()
}
}
#[cfg(test)]
mod test_labels_set {
use super::*;
#[test]
fn test_from_str() {
assert_eq!(1, LabelIdSet::from_str("B1").len());
assert_eq!(2, LabelIdSet::from_str("B1,B2").len());
assert_eq!(4, LabelIdSet::from_str("B1, B2 ,B3 ,B4").len());
}
}
//! [LabelMatch] implementation.
use super::parsed_label::LabelId;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
/// A type to describe one or a set of Labels
/// either specifying it or providing a regexp matching several
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
pub struct LabelMatch(String);
impl LabelMatch {
/// Returns true if the passed `LabelId` matches our pattern
pub fn matches(&self, id: &LabelId) -> bool {
let pattern = &self.0;
if pattern.contains('*') {
match pattern.chars().next() {
Some(pat) => pat == id.letter,
_ => false,
}
} else {
pattern == &id.to_string()
}
}
}
impl Display for LabelMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.0))
}
}
impl From<&str> for LabelMatch {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[cfg(test)]
mod test_label_match {
use super::*;
#[test]
fn test_1() {
let m1 = LabelMatch::from("B1");
m1.matches(&LabelId::from("B1"));
}
}
//! [LabelMatchSet] imoplementation.
use super::{label_match::LabelMatch, parsed_label::LabelId, specs::Specs};
use crate::rllib::common::set_to_string;
use serde::{Deserialize, Serialize};
use std::{
collections::{hash_set::Iter, HashSet},
fmt::Display,
};
/// A [HashSet] of [LabelMatch]. It allows describing a list of
/// [LabelId] or patterns that will expand in such a list.
/// ## example:
/// ```
/// let lms = LabelMatchSet::from_str("B1, X*");
/// ```
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LabelMatchSet(HashSet<LabelMatch>);
impl LabelMatchSet {
#[cfg(test)]
pub fn from_str(s: &str) -> Self {
let res: HashSet<LabelMatch> = s
.split(',')
.map(|s| {
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
LabelMatch::from(cleaned.as_str())
})
.collect();
LabelMatchSet::from_vec(res)
}
pub fn iter(&self) -> Iter<LabelMatch> {
self.0.iter()
}
#[cfg(test)]
fn from_vec(label_matches: HashSet<LabelMatch>) -> Self {
Self(label_matches)
}
/// Check whether the passed `LabelId` matches at least one
/// item in the `LabelSet`. If it matches it returns a tupple
/// made of the matching status as boolean as well as the list of
/// matching patterns.
pub fn matches_label(&self, id: &LabelId) -> (bool, Option<Vec<&LabelMatch>>) {
let matches: Vec<&LabelMatch> = self.0.iter().filter(|pat| pat.matches(id)).collect();
let status = !matches.is_empty();
let matches = if !matches.is_empty() { Some(matches) } else { None };
(status, matches)
}
/// Returns true if one of the passed `LabelId` matches items in the set.
pub fn matches_none(&self, _labels: &HashSet<LabelId>, _specs: &Specs) -> bool {
// let augmented_label_set = specs.generate_label_set(self, None);
// println!("augmented_label_set = {:?}", augmented_label_set);
unimplemented!()
}
/// Returns true if one of the passed `LabelId` matches items in the set.
pub fn matches_one(&self, labels: &HashSet<LabelId>, specs: &Specs) -> bool {
let ref_set = specs.generate_reference_set(self, Some(labels));
let hits = labels.iter().filter(|&label| ref_set.contains(label));
hits.count() == 1
}
/// Returns true if one of the passed `LabelId` matches items in the set.
pub fn matches_some(&self, labels: &HashSet<LabelId>, specs: &Specs) -> bool {
let ref_set = specs.generate_reference_set(self, Some(labels));
let hits = labels.iter().filter(|&label| ref_set.contains(label));
hits.count() >= 1
}
/// Returns true if ALL of the passed `LabelId` matches the items in the set.
/// This requires an intermediate step to expand the LabelMatchSet into an actual list
/// according to both the input labels and the specs
pub fn matches_all(&self, labels: &HashSet<LabelId>, specs: &Specs) -> bool {
log::trace!("matches_all");
let ref_set: HashSet<LabelId> =
specs.generate_reference_set(self, Some(labels)).into_iter().collect();
log::debug!("MatchSet : {:?}", self);
log::debug!("new ref_set : {:>3?} => {}", ref_set.len(), set_to_string(&ref_set));
log::debug!("labels : {:>3?} => {}", labels.len(), set_to_string(labels));
// We now iterate the ref_set to ensure that each of the items in the set
// is indeed present in the `labels`.
ref_set.iter().map(|l| labels.contains(l)).all(|r| r)
// self.0.iter().map(|match_set| {
// let labels_under_test = match_set.filter(labels);
// println!("labels_under_test = {:?}", set_to_string(labels_under_test));
// let res = match_set.matches_all(labels_under_test);
// println!("res = {:?}", res);
// res
// }).all(|e| e)
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.0.len()
}
}
impl Display for LabelMatchSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", set_to_string(&self.0)))
}
}
// impl IntoIterator for LabelMatchSet {
// type Item = LabelMatch;
// type IntoIter = LabelMatchIntoIterator;
// fn into_iter(self) -> Self::IntoIter {
// self.0.iter()
// }
// fn next(&mut self) -> Option<Self::Item> {
// let mut iter = self.0.iter();
// println!("next...");
// match iter.next() {
// None => None,
// Some(x) => Some(x.clone()),
// }
// }
// }
// pub struct LabelMatchIntoIterator {
// label_match: LabelMatch,
// index: usize,
// }
// impl From<Vec<LabelMatch>> for LabelMatchSet {
// fn from(lm: Vec<LabelMatch>) -> Self {
// Self(lm)
// }
// }
// /// Conversion from a comma, separated list of `LabelMatch` such as
// /// "A1,B2,C*".
// impl From<&str> for LabelMatchSet {
// fn from(s: &str) -> Self {
// let res: Vec<LabelMatch> = s
// .split(',')
// .map(|s| {
// // println!("s = {:?}", s);
// let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
// LabelMatch::from(cleaned.as_str())
// })
// .collect();
// LabelMatchSet::from_str(res)
// }
// }
#[cfg(test)]
impl Default for LabelMatchSet {
fn default() -> Self {
Self(HashSet::from([LabelMatch::from("B1"), LabelMatch::from("B2")]))
}
}
#[cfg(test)]
mod test_label_set {
use crate::rllib::label_id_set::LabelIdSet;
use super::*;
#[test]
fn test_label_set_from_str_single() {
let set = LabelMatchSet::from_str("B1");
let first = set.0.iter().next().unwrap();
assert_eq!(1, set.len());
assert_eq!(&LabelMatch::from("B1"), first);
}
#[test]
fn test_label_set_from_str_multiple() {
let set = LabelMatchSet::from_str("B1,B2");
assert_eq!(2, set.len());
assert!(set.0.contains(&LabelMatch::from("B1")));
}
#[test]
fn test_label_set_from_str_multiple_spaces() {
let set = LabelMatchSet::from_str(" B1, B2");
assert_eq!(2, set.len());
assert!(set.0.contains(&LabelMatch::from("B1")));
assert!(set.0.contains(&LabelMatch::from("B2")));
}
#[test]
fn test_matches() {
assert!(LabelMatchSet::default().matches_label(&LabelId::from("B1")).0);
}
#[test]
fn test_matches_one() {
let specs_ref = &Specs::load_default().unwrap();
assert!(
LabelMatchSet::default().matches_one(&HashSet::from([LabelId::from("B1")]), specs_ref)
);
}
#[test]
fn test_matches_all() {
let specs_ref = &Specs::load_default().unwrap();
assert!(LabelMatchSet::default().matches_all(&LabelIdSet::from_str("B1,B2"), specs_ref));
assert!(LabelMatchSet::default().matches_all(&LabelIdSet::from_str("B1,B2,B3"), specs_ref));
assert!(LabelMatchSet::default().matches_all(&LabelIdSet::from_str("X0,B1,B2"), specs_ref));
assert!(!LabelMatchSet::default().matches_all(&LabelIdSet::from_str("B1"), specs_ref));
assert!(!LabelMatchSet::default().matches_all(&LabelIdSet::from_str("X0,B1"), specs_ref));
}
}
//! Most of the code for `ruled-labels` is located in this module.
//! You can start looking at [Specs](specs::Specs) and [Tests](tests::Tests).
pub mod common;
pub mod label_id_set;
pub mod label_match;
pub mod label_match_set;
pub mod parsed_label;
pub mod rule;
pub mod rule_filter;
pub mod rule_spec;
pub mod rules;
pub mod specs;
pub mod test_result;
pub mod tests;
pub mod token_rule;
pub use token_rule::*;
//! [ParsedLabel] and [LabelId]
use serde::{Deserialize, Serialize};
use std::fmt::Display;
pub type CodeNumber = u8;
/// The [LabelId] is the initial letter + number from a Label.
/// For instance, the [LabelId] for `B0-silent` is `B0`.
///
/// WARNING: Do not confuse [LabelId] with [LabelMatch](super::label_match::LabelMatch).
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LabelId {
pub letter: char,
pub number: CodeNumber,
}
impl PartialEq<LabelId> for &str {
fn eq(&self, l: &LabelId) -> bool {
let letter = match self.chars().next() {
Some(l) => l,
_ => return false,
};
let number = match self.chars().next() {
Some(l) => match l.to_string().parse::<CodeNumber>() {
Ok(n) => n,
_ => return false,
},
None => return false,
};
l.letter == letter && l.number == number
}
}
impl PartialEq<str> for LabelId {
fn eq(&self, s: &str) -> bool {
let letter = match s.chars().next() {
Some(l) => l,
_ => return false,
};
let number = match s.chars().next() {
Some(l) => match l.to_string().parse::<CodeNumber>() {
Ok(n) => n,
_ => return false,
},
None => return false,
};
self.letter == letter && self.number == number
}
}
impl From<&str> for LabelId {
fn from(s: &str) -> Self {
// TODO: switch to removes_matches once https://github.com/rust-lang/rust/issues/72826 is resolved
let sanitized_str = String::from(s).replace('\"', "");
LabelId::from_str(&sanitized_str).expect("String should be a valid LabelId")
}
}
// error[E0119]: conflicting implementations of trait `std::convert::TryFrom<&str>` for type
// `lib::parsed_label::LabelId` impl TryFrom<&str> for LabelId {
// type Error = String;
// fn try_from(s: &str) -> Result<Self, Self::Error> {
// LabelId::from_str(s)
// }
// }
impl LabelId {
pub fn new(letter: char, number: CodeNumber) -> Self {
Self { letter, number }
}
pub fn from_str(s: &str) -> Result<Self, String> {
let sanitized_str = String::from(s).replace('\"', "");
let mut chars = sanitized_str.chars();
let first = chars.next();
let second = chars.next();
if first.is_none() || second.is_none() {
return Err(format!("Err 001: Invalid label: {} ({:?}{:?})", s, first, second))
}
let first = first.expect("Cannot fail").to_ascii_uppercase();
let second = second.expect("Cannot fail");
if !(first.is_alphabetic() && second.is_numeric()) {
return Err(format!("Err 002: Invalid label: {} ({}{})", s, first, second))
}
let second = second.to_string().parse::<CodeNumber>().expect("Cannot fail");
Ok(LabelId::new(first, second))
}
}
impl Display for LabelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}{}", self.letter, self.number))
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ParsedLabel {
pub id: LabelId,
pub description: Option<String>,
}
impl AsRef<ParsedLabel> for ParsedLabel {
fn as_ref(&self) -> &ParsedLabel {
self
}
}
impl TryFrom<&str> for ParsedLabel {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let id = LabelId::from_str(s)?;
let description = s.to_string().drain(0..2).as_str().to_string();
let description = if description.is_empty() { None } else { Some(description) };
Ok(Self { id, description })
}
}
impl From<String> for ParsedLabel {
fn from(s: String) -> Self {
let id = LabelId::from_str(&s).unwrap();
let mut s = s;
let description = s.drain(0..2).as_str().to_string();
let description = if description.is_empty() { None } else { Some(description) };
Self { id, description }
}
}
#[cfg(test)]
mod test_parsed_label {
use super::*;
#[test]
fn test_parsed_label_from_str_ok() {
const INPUTS: &'static [&'static str] =
&["B0-Silent", "b0-silent", "Z9-foobar", "B0silent", "B00-Silent"];
INPUTS.iter().for_each(|&case| {
let label = ParsedLabel::try_from(case);
println!("{:?}", label);
assert!(label.is_ok());
let label = label.unwrap();
assert!(label.id.letter.is_uppercase());
assert!((0..=9).contains(&label.id.number));
});
}
#[test]
fn test_parsed_label_str_fancy_ok() {
const INPUTS: &'static [&'static str] = &["B0-Foo 🧸", "\"b0-silent\""];
INPUTS.iter().for_each(|&case| {
let label = ParsedLabel::try_from(case);
println!("{:?}", label);
assert!(label.is_ok());
let label = label.unwrap();
assert!(label.id.letter.is_uppercase());
assert!((0..=9).contains(&label.id.number));
});
}
#[test]
fn test_parsed_label_from_str_error() {
const INPUTS: &'static [&'static str] = &["BB-Silent", "B-silent", "99-foobar"];
INPUTS.iter().for_each(|&case| {
let label = ParsedLabel::try_from(case);
println!("{:?}", label);
assert!(label.is_err());
});
}
}
#[cfg(test)]
mod test_label_id {
use super::*;
#[test]
fn test_label_id_ok() {
const INPUTS: &'static [&'static str] = &["B0-Silent", "B1-silent", "X9-foobar", "B0"];
INPUTS.iter().for_each(|&case| {
let id = LabelId::from_str(case);
println!("{:?}", id);
assert!(id.is_ok());
});
}
#[test]
fn test_label_id_err() {
const INPUTS: &'static [&'static str] = &["BB-Silent", "B-silent", "99-foobar"];
INPUTS.iter().for_each(|&case| {
let id = LabelId::from_str(case);
println!("{:?}", id);
assert!(id.is_err());
});
}
#[test]
fn test_label_id_cmp() {
assert_eq!("B0", LabelId::from_str("B0").unwrap().to_string());
}
#[test]
fn test_from_str() {
let id = LabelId::from_str("B1").unwrap();
assert_eq!('B', id.letter);
assert_eq!(1, id.number);
}
}
This diff is collapsed.
use super::rule::RuleId;
use serde::Deserialize;
/// A [RuleFilter] allows a test to specify the list of rules that should be ran
#[derive(Debug, Deserialize)]
pub struct RuleFilter {
pub id: Vec<RuleId>,
}
This diff is collapsed.
//! A `Vec<Rule>`. See [Rule].
use super::rule::Rule;
use serde::{Deserialize, Serialize};
/// Hold a a vector of [Rule]
#[derive(Debug, Serialize, Deserialize)]
pub struct Rules {
pub rules: Vec<Rule>,
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
use crate::rllib::{common::set_to_string, label_match_set::LabelMatchSet};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub enum TokenRuleExclude {
/// All of those in the set
#[serde(rename = "all_of")]
All(LabelMatchSet),
}
impl Display for TokenRuleExclude {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TokenRuleExclude::All(set) => f.write_fmt(format_args!(
"you need to exclude all of the {} label(s)",
set_to_string(set.iter())
)),
}
}
}
//! Definitions of [when::TokenRuleWhen], [require::TokenRuleRequire] and
//! [exclude::TokenRuleExclude].
pub mod exclude;
pub mod require;
pub mod when;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.