1mod 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#[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
58pub 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(format!(
102 "`{}` {}",
103 error.instance_path,
104 error.kind.headline()
105 ));
106
107 diagnostic.context = context;
108 diagnostic.file_path = source_path.map(|path| path.display().to_string());
109
110 if let Some(parent) = error.schema_path.parent()
111 && let Some(node) = schema_node.pointer(parent.join("description").as_str())
112 && let Some(contents) = node.as_str()
113 {
114 for line in contents.lines() {
115 diagnostic.notes.push(normalize_message(line));
116 }
117 }
118
119 diagnostics.push(diagnostic);
120 }
121 }
122
123 Ok(diagnostics)
124}
125
126#[cfg(test)]
127mod test {
128 use std::path::Path;
129
130 const SOURCE: &str = include_str!("../tests/sample.json");
131 const SCHEMA: &str = include_str!("../tests/sample.schema.json");
132
133 #[test]
134 fn validates_sample_correctly() {
135 let diagnostics = crate::validate(
136 SOURCE,
137 SCHEMA,
138 Some(Path::new("crates/ts-json/tests/sample.json")),
139 )
140 .expect("validation to succeed");
141 assert!(!diagnostics.is_empty());
142 assert_eq!(4, diagnostics.errors().count());
143 eprintln!("{diagnostics}");
144 }
145}