This commit is contained in:
JMARyA 2023-10-30 13:29:13 +01:00
commit 2844e68a88
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
6 changed files with 1805 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1496
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

19
Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "mdlint"
version = "0.1.0"
edition = "2021"
[profile.release]
strip = true
lto = true
codegen-units = 1
panic = "abort"
[dependencies]
clap = { version = "4.4.7", features = ["cargo"] }
console = "0.15.7"
jsonschema = "0.17.1"
regex = "1.10.2"
serde = "1.0.190"
serde_json = "1.0.107"
serde_yaml = "0.9.27"

8
README.md Normal file
View file

@ -0,0 +1,8 @@
# mdlint
Validate markdown frontmatter using [JSON Schemas](https://json-schema.org)
## Usage
```shell
$ mdlint [OPTION] <SchemaFile> <MarkdownFile>
```

10
src/args.rs Normal file
View file

@ -0,0 +1,10 @@
use clap::{arg, command, ArgMatches};
pub fn get_args() -> ArgMatches {
command!()
.about("Lint markdown frontmatter using JSON Schema")
.arg(arg!([schema] "Schema file used for validation").required(true))
.arg(arg!([file] "File to validate").required(true))
.arg(arg!(--greedy "Require all properties to be present even if they are not strictly required in schema").required(false))
.get_matches()
}

271
src/main.rs Normal file
View file

@ -0,0 +1,271 @@
use console::Style;
use jsonschema::{error::ValidationErrorKind, JSONSchema};
mod args;
trait ErrorExit {
type T;
fn quit_on_error(self, msg: &str) -> Self::T;
}
impl<T, E> ErrorExit for Result<T, E> {
type T = T;
fn quit_on_error(self, msg: &str) -> T {
self.map_or_else(
|_| {
eprintln!("{} {msg}", Style::new().red().apply_to("Error:"));
std::process::exit(1);
},
|res| res,
)
}
}
/// get frontmatter from markdown document
#[must_use]
pub fn get_frontmatter(markdown: &str) -> Option<String> {
let frontmatter_regex = regex::Regex::new(r"(?s)^---\s*\n(.*?)\n---").unwrap();
frontmatter_regex.captures(markdown).and_then(|captures| {
let frontmatter = captures.get(1).map(|m| m.as_str().to_string());
frontmatter
})
}
fn require_everything(schema: &mut serde_json::Value) {
if let Some(schema_obj) = schema.as_object_mut() {
if schema_obj.get("type").unwrap().as_str().unwrap() == "object" {
let keys: Vec<_> = schema_obj
.get("properties")
.unwrap()
.as_object()
.unwrap()
.keys()
.cloned()
.collect();
schema_obj.insert("required".to_owned(), keys.into());
for property in schema_obj
.get_mut("properties")
.unwrap()
.as_object_mut()
.unwrap()
{
require_everything(property.1);
}
}
}
}
fn main() {
let args = args::get_args();
let schema = args.get_one::<String>("schema").unwrap();
let file = args.get_one::<String>("file").unwrap();
let mut schema: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(schema).quit_on_error("Could not read schema file"),
)
.quit_on_error("Could not deserialize schema");
if args.get_flag("greedy") {
require_everything(&mut schema);
}
let comp_schema = JSONSchema::options()
.should_ignore_unknown_formats(false)
.compile(&schema)
.quit_on_error("Schema is not valid");
let frontmatter = serde_yaml::from_str(
&get_frontmatter(
&std::fs::read_to_string(file).quit_on_error("Could not read markdown file"),
)
.ok_or(0)
.quit_on_error("Could not parse frontmatter"),
)
.quit_on_error("Frontmatter is no valid yaml");
let result = comp_schema.validate(&frontmatter);
if let Err(errors) = result {
for error in errors {
let instance = error.instance.clone();
let path = error.instance_path.to_string();
let path = if path.is_empty() { "/" } else { &path };
//println!("{file}: {:#?}", error);
let file = Style::new().blue().apply_to(file);
let path = Style::new().bright().red().apply_to(path);
match error.kind {
ValidationErrorKind::AdditionalItems { limit: _ } => todo!(),
ValidationErrorKind::AdditionalProperties { unexpected } => {
let unexpected = Style::new().yellow().apply_to(format!("{unexpected:?}"));
println!("{file}: Found additional properties {unexpected} at {path}");
}
ValidationErrorKind::AnyOf => todo!(),
ValidationErrorKind::BacktrackLimitExceeded { error: _ } => todo!(),
ValidationErrorKind::Constant { expected_value } => {
let instance = Style::new().yellow().apply_to(instance);
let expected_value = Style::new().green().apply_to(expected_value);
println!(
"{file}: Value at {path} should be {expected_value} but is {instance}"
);
}
ValidationErrorKind::Contains => {
println!(
"{file}: Array at {path} does not contain at least one expected element"
);
}
ValidationErrorKind::ContentEncoding { content_encoding } => {
let content_encoding = Style::new().green().apply_to(content_encoding);
println!(
"{file}: Value at {path} should have {content_encoding} content-encoding"
);
}
ValidationErrorKind::ContentMediaType { content_media_type } => {
let content_media_type = Style::new().green().apply_to(content_media_type);
println!("{file}: Value at {path} should have {content_media_type} content-media-type");
}
ValidationErrorKind::Enum { options } => {
let options = Style::new().green().apply_to(options);
let instance = Style::new().yellow().apply_to(instance);
println!(
"{file}: Value at {path} should be one of {options} but is {instance}"
);
}
ValidationErrorKind::ExclusiveMaximum { limit } => {
println!(
"{file}: Value at {path} should be below {} but is {}",
Style::new().green().apply_to(limit),
Style::new().yellow().apply_to(instance)
);
}
ValidationErrorKind::ExclusiveMinimum { limit } => {
println!(
"{file}: Value at {path} should be above {} but is {}",
Style::new().green().apply_to(limit),
Style::new().yellow().apply_to(instance)
);
}
ValidationErrorKind::FalseSchema => todo!(),
ValidationErrorKind::FileNotFound { error: _ } => todo!(),
ValidationErrorKind::Format { format } => {
let format = Style::new().green().apply_to(format);
let instance = Style::new().yellow().apply_to(instance);
println!("{file}: Value at {path} does not comply with format '{format}' (Is {instance})");
}
ValidationErrorKind::FromUtf8 { error: _ } => todo!(),
ValidationErrorKind::Utf8 { error: _ } => todo!(),
ValidationErrorKind::JSONParse { error: _ } => todo!(),
ValidationErrorKind::InvalidReference { reference: _ } => todo!(),
ValidationErrorKind::InvalidURL { error: _ } => todo!(),
ValidationErrorKind::MaxItems { limit } => {
let limit = Style::new().green().apply_to(limit);
println!("{file}: Array at {path} should have a maximum of {limit} elements but has {}", Style::new().yellow().apply_to(instance.as_array().unwrap().len()));
}
ValidationErrorKind::Maximum { limit } => {
println!(
"{file}: Value at {path} should be a maximum of {} but is {}",
Style::new().green().apply_to(limit),
Style::new().yellow().apply_to(instance)
);
}
ValidationErrorKind::MaxLength { limit } => {
let limit = Style::new().green().apply_to(limit);
println!(
"{file}: Value at {path} should have maximum length of {limit} but has {}",
Style::new()
.yellow()
.apply_to(instance.as_str().unwrap().len())
);
}
ValidationErrorKind::MaxProperties { limit } => {
let limit = Style::new().green().apply_to(limit);
println!("{file}: Value at {path} should have a maximum of {limit} properties but has {}", Style::new().yellow().apply_to(instance.as_object().unwrap().len()));
}
ValidationErrorKind::MinItems { limit } => {
let limit = Style::new().green().apply_to(limit);
println!("{file}: Array at {path} should have a minimum of {limit} elements but has {}", Style::new().yellow().apply_to(instance.as_array().unwrap().len()));
}
ValidationErrorKind::Minimum { limit } => {
println!(
"{file}: Value at {path} should be a minimum of {} but is {}",
Style::new().green().apply_to(limit),
Style::new().yellow().apply_to(instance)
);
}
ValidationErrorKind::MinLength { limit } => {
let limit = Style::new().green().apply_to(limit);
println!(
"{file}: Value at {path} should have minimum length of {limit} but has {}",
Style::new()
.yellow()
.apply_to(instance.as_str().unwrap().len())
);
}
ValidationErrorKind::MinProperties { limit } => {
let limit = Style::new().green().apply_to(limit);
println!("{file}: Value at {path} should have a minimum of {limit} properties but has {}", Style::new().yellow().apply_to(instance.as_object().unwrap().len()));
}
ValidationErrorKind::MultipleOf { multiple_of } => {
let multiple_of = Style::new().green().apply_to(multiple_of);
let instance = Style::new().yellow().apply_to(instance);
println!("{file}: Value at {path} should be a multiple of {multiple_of} but is {instance}");
}
ValidationErrorKind::Not { schema: _ } => todo!(),
ValidationErrorKind::OneOfMultipleValid => todo!(),
ValidationErrorKind::OneOfNotValid => todo!(),
ValidationErrorKind::Pattern { pattern } => {
let pattern = Style::new().green().apply_to(format!("'{pattern}'"));
let instance = Style::new().yellow().apply_to(instance);
println!("{file}: Value at {path} does not match {pattern} (Is {instance})'");
}
ValidationErrorKind::PropertyNames { error } => {
if let ValidationErrorKind::Pattern { ref pattern } = error.kind {
let instance = Style::new().yellow().apply_to(error.instance.clone());
let pattern = Style::new().green().apply_to(format!("'{pattern}'"));
println!("{file}: Object at {path} property name {instance} does not match {pattern}");
} else {
unimplemented!()
}
}
ValidationErrorKind::Required { property } => {
println!(
"{file}: Required key {} missing at {}",
Style::new().yellow().apply_to(property),
path
);
}
ValidationErrorKind::Schema => todo!(),
ValidationErrorKind::Type { kind } => {
let expected = Style::new().green().apply_to(match kind {
jsonschema::error::TypeKind::Single(t) => format!("{t:?}").to_lowercase(),
jsonschema::error::TypeKind::Multiple(_) => todo!(),
});
let actual = Style::new().yellow().apply_to(match *instance {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
});
println!("{file}: Value at {path} should be {expected} but is {actual}");
}
ValidationErrorKind::UnevaluatedProperties { unexpected: _ } => todo!(),
ValidationErrorKind::UniqueItems => {
println!("{file}: Array at {path} has values which are not unique");
}
ValidationErrorKind::UnknownReferenceScheme { scheme: _ } => todo!(),
ValidationErrorKind::Resolver { url: _, error: _ } => todo!(),
}
}
std::process::exit(1);
}
}