Skip to main content

mdmodels_core/json/
validation.rs

1/*
2 * Copyright (c) 2025 Jan Range
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 *
22 */
23
24use std::error::Error;
25use std::path::PathBuf;
26
27use colored::Colorize;
28use jsonschema::error::ValidationErrorKind;
29use serde_json::Value;
30use std::convert::TryFrom;
31
32use crate::datamodel::DataModel;
33use jsonschema::validator_for;
34
35/// Represents a validation error that occurs during dataset validation.
36#[derive(Debug)]
37pub struct ValidationError {
38    pub instance_path: String,
39    pub schema_path: String,
40    pub message: String,
41    pub kind: ValidationErrorKind,
42}
43
44impl std::fmt::Display for ValidationError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(
47            f,
48            "Validation Error: Instance {} violates schema at {}: {}",
49            self.instance_path.red().bold(),
50            self.schema_path.green().bold(),
51            self.message.yellow().bold()
52        )
53    }
54}
55
56impl From<jsonschema::ValidationError<'_>> for ValidationError {
57    fn from(err: jsonschema::ValidationError) -> Self {
58        ValidationError {
59            instance_path: err.instance_path.to_string(),
60            schema_path: err.schema_path.to_string(),
61            message: err.to_string(),
62            kind: err.kind,
63        }
64    }
65}
66
67/// Validates a dataset against a given DataModel.
68///
69/// # Arguments
70///
71/// * `dataset` - The dataset to validate, which can be provided in various forms.
72/// * `model` - A reference to the DataModel against which the dataset will be validated.
73/// * `root` - An optional root path for the schema.
74///
75/// # Returns
76///
77/// A Result containing a vector of ValidationErrors if validation fails, or an empty vector if successful.
78pub fn validate_json<T: Into<DatasetInput>>(
79    dataset: T,
80    model: &DataModel,
81    root: Option<String>,
82) -> Result<Vec<ValidationError>, Box<dyn Error>> {
83    // Convert the dataset input to a Value
84    let dataset_input: DatasetInput = dataset.into();
85    let value: Value = dataset_input.try_into()?;
86
87    // Get the JSON Schema from the model
88    let schema = model.json_schema(root, false)?;
89    let schema_value: Value = serde_json::from_str(&schema)?;
90
91    // Create a validator for the schema
92    let validator = validator_for(&schema_value)?;
93
94    // Validate the dataset against the schema
95    let result = validator.iter_errors(&value);
96    let mut errors: Vec<ValidationError> = Vec::new();
97
98    for err in result {
99        errors.push(ValidationError::from(err));
100    }
101
102    Ok(errors)
103}
104
105/// Enum representing the different types of dataset inputs.
106pub enum DatasetInput {
107    Path(PathBuf),
108    Value(Value),
109    String(String),
110}
111
112impl From<PathBuf> for DatasetInput {
113    /// Converts a PathBuf into a DatasetInput.
114    fn from(path: PathBuf) -> Self {
115        DatasetInput::Path(path)
116    }
117}
118
119impl From<Value> for DatasetInput {
120    /// Converts a Value into a DatasetInput.
121    fn from(value: Value) -> Self {
122        DatasetInput::Value(value)
123    }
124}
125
126impl From<&mut Value> for DatasetInput {
127    /// Converts a Value into a DatasetInput.
128    fn from(value: &mut Value) -> Self {
129        DatasetInput::Value(value.clone())
130    }
131}
132
133impl From<String> for DatasetInput {
134    /// Converts a String into a DatasetInput.
135    fn from(string: String) -> Self {
136        DatasetInput::String(string)
137    }
138}
139
140impl TryFrom<DatasetInput> for Value {
141    type Error = Box<dyn Error>;
142
143    fn try_from(input: DatasetInput) -> Result<Self, Self::Error> {
144        match input {
145            DatasetInput::Path(path) => {
146                // Logic to read from the path and convert to Value
147                let content = std::fs::read_to_string(path)?;
148                let value: Value = serde_json::from_str(&content)?;
149                Ok(value)
150            }
151            DatasetInput::Value(value) => Ok(value),
152            DatasetInput::String(string) => {
153                let value: Value = serde_json::from_str(&string)?;
154                Ok(value)
155            }
156        }
157    }
158}