ts_json/
lib.rs

1//! # `ts-json`
2//!
3//! JSON schema validation and reporting
4
5mod location;
6mod parser;
7mod problem_message;
8
9use std::path::Path;
10
11use jsonschema::ValidationOptions;
12use serde_json::Value;
13use ts_error::{
14    diagnostic::{Context, Diagnostic, Diagnostics},
15    normalize_message,
16};
17
18use crate::{
19    location::LocationExtensions,
20    parser::{Node, Value as SpannedValue},
21    problem_message::ProblemMessage,
22};
23
24/// Error variants for validating JSON.
25#[derive(Debug)]
26#[non_exhaustive]
27#[allow(missing_docs)]
28pub enum ValidationError {
29    #[non_exhaustive]
30    ParseSource { source: serde_json::Error },
31
32    #[non_exhaustive]
33    ParseSchema { source: serde_json::Error },
34
35    #[non_exhaustive]
36    CreateValidator {
37        source: Box<jsonschema::ValidationError<'static>>,
38    },
39}
40impl core::fmt::Display for ValidationError {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        match &self {
43            Self::ParseSource { .. } => write!(f, "source file is not valid JSON"),
44            Self::ParseSchema { .. } => write!(f, "schema is not valid JSON"),
45            Self::CreateValidator { .. } => write!(f, "could not create validator from schema"),
46        }
47    }
48}
49impl core::error::Error for ValidationError {
50    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
51        match &self {
52            Self::ParseSource { source, .. } | Self::ParseSchema { source, .. } => Some(source),
53            Self::CreateValidator { source, .. } => Some(source),
54        }
55    }
56}
57
58/// Validate some JSON against a JSON schema, returning all problems.
59pub fn validate(
60    source: &str,
61    schema: &str,
62    source_path: Option<&Path>,
63) -> Result<Diagnostics, ValidationError> {
64    let source_node: Value =
65        serde_json::from_str(source).map_err(|source| ValidationError::ParseSource { source })?;
66    let schema_node: Value =
67        serde_json::from_str(schema).map_err(|source| ValidationError::ParseSchema { source })?;
68
69    let validator = ValidationOptions::default()
70        .build(&schema_node)
71        .map_err(|source| ValidationError::CreateValidator {
72            source: Box::new(source),
73        })?;
74
75    let mut diagnostics = Diagnostics::new("validating JSON");
76
77    if !validator.is_valid(&source_node) {
78        let document = Node::parse_document(source);
79        for error in validator.iter_errors(&source_node) {
80            let context = document.as_ref().and_then(|document| {
81                let span = document
82                    .evaluate(&error.instance_path)
83                    .map(|node| match node.value {
84                        SpannedValue::Array(_) | SpannedValue::Object(_) => {
85                            if let Some(tag) = &node.tag {
86                                tag.span
87                            } else {
88                                node.value.span()
89                            }
90                        }
91                        _ => node.value.span(),
92                    });
93
94                span.map(|span| {
95                    let mut context = Context::new(source, span);
96                    context.label = error.kind.message();
97                    context
98                })
99            });
100
101            let mut diagnostic = Diagnostic::error(error.kind.headline());
102            // TODO headline needs the node
103
104            diagnostic.context = context;
105            diagnostic.file_path = source_path.map(|path| path.display().to_string());
106
107            if let Some(parent) = error.schema_path.parent()
108                && let Some(node) = schema_node.pointer(parent.join("description").as_str())
109                && let Some(contents) = node.as_str()
110            {
111                for line in contents.lines() {
112                    diagnostic.notes.push(normalize_message(line));
113                }
114            }
115
116            diagnostics.push(diagnostic);
117        }
118    }
119
120    Ok(diagnostics)
121}
122
123#[cfg(test)]
124mod test {
125    use std::path::Path;
126
127    const SOURCE: &str = include_str!("../tests/sample.json");
128    const SCHEMA: &str = include_str!("../tests/sample.schema.json");
129
130    #[test]
131    fn validates_sample_correctly() {
132        let diagnostics = crate::validate(
133            SOURCE,
134            SCHEMA,
135            Some(Path::new("crates/ts-json/tests/sample.json")),
136        )
137        .expect("validation to succeed");
138        assert!(!diagnostics.is_empty());
139        assert_eq!(4, diagnostics.errors().count());
140        eprintln!("{diagnostics}");
141    }
142}