prdoclib/
schema.rs

1//! Schema used by [prdoc](/prdoc) for its validation
2//!
3//! [prdoc](/prdoc) does not really care about the schema itself and the data is not used or loaded.
4//! The schema is stored in the repository and embedded into the cli for convenience. The various
5//! commands do check that files comply with the schema but nothing more. That also means that the
6//! schema can be adjusted at any time without impact on the code.
7
8use crate::error::PRdocLibError;
9use regex::Regex;
10use serde_yaml::Value;
11use std::{
12	fs::{self, File},
13	path::{Path, PathBuf},
14};
15use valico::json_schema;
16
17/// Default file extension
18pub const EXTENSION: &str = "prdoc";
19
20/// Default location where prdoc are stored
21pub const PRDOC_DEFAULT_DIR: &str = "prdoc";
22
23/// The schema embedded in [prdoc](/prdoc).
24#[derive(Debug, Clone)]
25pub struct Schema {
26	schema: PathBuf,
27}
28
29impl Schema {
30	/// Create a new instance of the schema
31	pub fn new(schema: PathBuf) -> Self {
32		Self { schema }
33	}
34
35	/// JSON Schema sometimes do contain comments. This function strips them to allow
36	/// proper deserialization.
37	pub fn get(s: String, strip_comments: bool) -> String {
38		if !strip_comments {
39			s
40		} else {
41			let re = Regex::new(r"(?m)^//(.*)$").unwrap();
42			let result = re.replace_all(&s, "");
43			result.to_string().trim().to_string()
44		}
45	}
46
47	/// Check the validity of a file by attempting to load it
48	pub fn check_file(&self, file: &PathBuf) -> bool {
49		self.load(file).is_ok()
50	}
51
52	/// Load the content of a file. The name does not matter here.
53	pub fn load<P: AsRef<Path>>(&self, file: &P) -> crate::error::Result<Value> {
54		log::trace!("Loading schema file");
55		let content = fs::read_to_string(self.schema.clone())?.parse()?;
56		let schema_str = Self::get(content, true);
57
58		log::trace!("Parsing schema");
59		let json_schema: serde_json::Value = serde_json::from_str(&schema_str)?;
60
61		let reader = File::open(file)?;
62		let mut doc_as_yaml: serde_yaml::Value = serde_yaml::from_reader(reader)?;
63		doc_as_yaml.apply_merge()?;
64
65		let doc_as_json: serde_json::Value =
66			serde_yaml::from_value(serde_yaml::to_value(&doc_as_yaml)?)?;
67
68		let mut scope = json_schema::Scope::new();
69		let schema = scope.compile_and_return(json_schema, false)?;
70
71		log::trace!("Validate file with schema");
72		let validation = schema.validate(&doc_as_json);
73		let validation_result = validation.is_valid();
74		let validation_result_strict = validation.is_strictly_valid();
75
76		if !(validation_result && validation_result_strict) {
77			log::debug!("validation_result: {validation_result}");
78			log::debug!("validation_result_strict: {validation_result_strict}");
79			return Err(PRdocLibError::ValidationErrors(validation));
80		}
81
82		Ok(doc_as_yaml)
83	}
84}
85
86#[cfg(test)]
87mod test_schema_validation {
88	use super::*;
89	use std::path::PathBuf;
90
91	#[test]
92	fn test_load_valid_1234() {
93		let schema = Schema::new("./tests/data/sample_schema.json".into());
94		let file = PathBuf::from("./tests/data/some/pr_1234_some_test_minimal.prdoc");
95		assert!(schema.load(&file).is_ok());
96	}
97
98	#[test]
99	fn test_check_valid_1234() {
100		let schema = Schema::new("./tests/data/sample_schema.json".into());
101
102		let file = PathBuf::from("./tests/data/some/pr_1234_some_test_minimal.prdoc");
103		assert!(schema.check_file(&file));
104	}
105}