use jsonschema::{Draft, Validator};
use std::sync::LazyLock;
use std::{
fmt, fs,
path::{Path, PathBuf},
};
use crate::license;
const SCHEMA_OKH_LOSH: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/resources/okh/src/schema/okh.schema.json"
));
const SCHEMA_OKH_V1: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/resources/okh/src/schema/okh-v1.schema.json"
));
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to find any manifests.")]
NoManifestsFound,
#[error("Failed to read or write, probably from/to the file-system.")]
Io(#[from] std::io::Error),
#[error("Failed to parse the raw content as TOML.")]
TomlParseFailure(#[from] toml::de::Error),
#[error("Failed to parse the raw content as JSON.")]
JsonParseFailure(#[from] serde_json::error::Error),
#[error("Failed to parse the raw content as YAML.")]
YamlParseFailure(#[from] serde_yaml::Error),
#[error("Failed to validate schema content {0:#}.")]
InvalidContent(#[from] JsonSchemaValidationError),
#[error("Failed to validate:\n{0:#}")]
ValidationFailure(#[from] JsonSchemaValidationErrorCollection),
#[error("License issue:\n{0:#}")]
License(#[from] license::Error),
}
#[derive(thiserror::Error, Debug)]
pub struct ErrorCollection {
pub errors: Vec<(PathBuf, Error)>,
}
impl fmt::Display for ErrorCollection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\n# Validation error(s):\n")?;
for (file, err) in &self.errors {
f.write_fmt(format_args!("In file '{}':\n{err}", file.display()))?;
}
Ok(())
}
}
impl From<(PathBuf, Error)> for ErrorCollection {
fn from(file_and_err: (PathBuf, Error)) -> Self {
Self {
errors: vec![file_and_err],
}
}
}
#[derive(thiserror::Error, Debug)]
#[error("Error:\n\tKind: {kind:?}\n\tWhere: {instance_path}\n\tContent: {instance}\n")]
pub struct JsonSchemaValidationError {
pub instance: serde_json::Value,
pub kind: jsonschema::error::ValidationErrorKind,
pub instance_path: jsonschema::paths::Location,
pub schema_path: jsonschema::paths::Location,
}
impl<'a> From<jsonschema::ValidationError<'a>> for JsonSchemaValidationError {
fn from(err: jsonschema::ValidationError<'a>) -> Self {
Self {
instance: err.instance.into_owned(),
kind: err.kind,
instance_path: err.instance_path,
schema_path: err.schema_path,
}
}
}
#[derive(thiserror::Error, Debug)]
pub struct JsonSchemaValidationErrorCollection {
pub failed_reqs: Vec<JsonSchemaValidationError>,
}
impl fmt::Display for JsonSchemaValidationErrorCollection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\n# Validation error(s):\n")?;
for failure in &self.failed_reqs {
f.write_fmt(format_args!("{failure}"))?;
}
Ok(())
}
}
impl<'a> From<jsonschema::ErrorIterator<'a>> for JsonSchemaValidationErrorCollection {
fn from(err: jsonschema::ErrorIterator<'a>) -> Self {
Self {
failed_reqs: err.map(JsonSchemaValidationError::from).collect(),
}
}
}
pub fn with_schema(schema: &Validator, content: &serde_json::Value) -> Result<(), Error> {
schema
.validate(content)
.map_err(JsonSchemaValidationError::from)
.map_err(std::convert::Into::into)
}
pub fn okh_losh_toml<IP>(toml_path: IP) -> Result<(), Error>
where
IP: AsRef<Path>,
{
static RAW_SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
serde_json::from_str::<serde_json::Value>(SCHEMA_OKH_LOSH)
.expect("The OKH-LOSH JSON schema contained within the binary is invalid JSON :/")
});
log::debug!(
"Validating an OKH LOSH file ('{}') ...",
toml_path.as_ref().as_os_str().to_str().unwrap()
);
let toml_str = fs::read_to_string(toml_path)?;
let instance = toml::from_str::<serde_json::Value>(&toml_str)?;
let validator = jsonschema::options()
.with_draft(Draft::Draft7)
.build(&RAW_SCHEMA)
.map_err(JsonSchemaValidationError::from)?;
with_schema(&validator, &instance)?;
if let Some(license_str) = instance.get("license").and_then(|v| v.as_str()) {
license::validate_spdx_expr(license_str, false)?;
}
Ok(())
}
pub fn okh_v1_yaml<IP>(yaml_path: IP) -> Result<(), Error>
where
IP: AsRef<Path>,
{
static RAW_SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
serde_json::from_str::<serde_json::Value>(SCHEMA_OKH_V1)
.expect("The OKH-V1 JSON schema contained within the binary is invalid JSON :/")
});
log::debug!(
"Validating an OKH v1 file ('{}') ...",
yaml_path.as_ref().as_os_str().to_str().unwrap()
);
let yaml_str = fs::read_to_string(yaml_path)?;
let instance = serde_yaml::from_str::<serde_json::Value>(&yaml_str)?;
let validator = jsonschema::options()
.with_draft(Draft::Draft7)
.build(&RAW_SCHEMA)
.map_err(JsonSchemaValidationError::from)?;
with_schema(&validator, &instance)
}