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