Skip to content

Commit 9b232c6

Browse files
committed
feat(cli): When invoked with only a schema file (no instances), validates the schema against its meta-schema
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent 8b7491e commit 9b232c6

File tree

5 files changed

+368
-3
lines changed

5 files changed

+368
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
- **CLI**: Structured `--output flag|list|hierarchical` modes now stream newline-delimited JSON records with schema/instance metadata plus JSON Schema Output v1 payloads (default `text` output remains human-readable).
88
- **CLI**: `--errors-only` flag to suppress successful validation output and only show failures.
9+
- **CLI**: When invoked with only a schema file (no instances), validates the schema against its meta-schema. [#804](https://github.com/Stranger6667/jsonschema/issues/804)
910
- New `Validator::evaluate()` API exposes JSON Schema Output v1 (flag/list/hierarchical) reports along with iterator helpers for annotations and errors.
11+
- `meta::validator_for()` function to build validators for meta-schema validation with full `Validator` API access.
1012
- `Validator` now implements `Clone`. [#809](https://github.com/Stranger6667/jsonschema/issues/809)
1113

1214
### Removed

crates/jsonschema-cli/src/main.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,65 @@ fn path_to_uri(path: &std::path::Path) -> String {
181181
result
182182
}
183183

184+
fn output_schema_validation(
185+
schema_path: &Path,
186+
schema_json: &serde_json::Value,
187+
output: Output,
188+
errors_only: bool,
189+
) -> Result<bool, Box<dyn std::error::Error>> {
190+
let meta_validator = jsonschema::meta::validator_for(schema_json)?;
191+
let evaluation = meta_validator.evaluate(schema_json);
192+
let flag_output = evaluation.flag();
193+
194+
// Skip valid schemas if errors_only is enabled
195+
if !(errors_only && flag_output.valid) {
196+
let schema_display = schema_path.to_string_lossy().to_string();
197+
let output_format = output.as_str();
198+
let payload = match output {
199+
Output::Text => unreachable!("text mode should not call this function"),
200+
Output::Flag => serde_json::to_value(flag_output)?,
201+
Output::List => serde_json::to_value(evaluation.list())?,
202+
Output::Hierarchical => serde_json::to_value(evaluation.hierarchical())?,
203+
};
204+
205+
let record = json!({
206+
"output": output_format,
207+
"schema": &schema_display,
208+
"payload": payload,
209+
});
210+
println!("{}", serde_json::to_string(&record)?);
211+
}
212+
213+
Ok(flag_output.valid)
214+
}
215+
216+
fn validate_schema_meta(
217+
schema_path: &Path,
218+
output: Output,
219+
errors_only: bool,
220+
) -> Result<bool, Box<dyn std::error::Error>> {
221+
let schema_json = read_json(schema_path)??;
222+
223+
if matches!(output, Output::Text) {
224+
// Text output mode
225+
match jsonschema::meta::validate(&schema_json) {
226+
Ok(()) => {
227+
if !errors_only {
228+
println!("Schema is valid");
229+
}
230+
Ok(true)
231+
}
232+
Err(error) => {
233+
println!("Schema is invalid. Error: {error}");
234+
Ok(false)
235+
}
236+
}
237+
} else {
238+
// Structured output modes using evaluate API
239+
output_schema_validation(schema_path, &schema_json, output, errors_only)
240+
}
241+
}
242+
184243
fn validate_instances(
185244
instances: &[PathBuf],
186245
schema_path: &Path,
@@ -255,7 +314,12 @@ fn validate_instances(
255314
}
256315
}
257316
Err(error) => {
258-
println!("Schema is invalid. Error: {error}");
317+
if matches!(output, Output::Text) {
318+
println!("Schema is invalid. Error: {error}");
319+
} else {
320+
// Schema compilation failed - validate the schema itself to get structured output
321+
output_schema_validation(schema_path, &schema_json, output, errors_only)?;
322+
}
259323
success = false;
260324
}
261325
}
@@ -274,7 +338,7 @@ fn main() -> ExitCode {
274338
if let Some(instances) = config.instances {
275339
// - Some(true) if --assert-format
276340
// - Some(false) if --no-assert-format
277-
// - None if neither (use builders default)
341+
// - None if neither (use builder's default)
278342
let assert_format = config.assert_format.or(config.no_assert_format);
279343
return match validate_instances(
280344
&instances,
@@ -292,6 +356,15 @@ fn main() -> ExitCode {
292356
}
293357
};
294358
}
359+
// No instances provided - validate the schema itself
360+
return match validate_schema_meta(&schema, config.output, config.errors_only) {
361+
Ok(true) => ExitCode::SUCCESS,
362+
Ok(false) => ExitCode::FAILURE,
363+
Err(error) => {
364+
println!("Error: {error}");
365+
ExitCode::FAILURE
366+
}
367+
};
295368
}
296369
ExitCode::SUCCESS
297370
}

crates/jsonschema-cli/tests/cli.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,174 @@ fn test_errors_only_structured_output() {
713713
assert_eq!(records[0]["instance"], invalid);
714714
assert_eq!(records[0]["payload"]["valid"], false);
715715
}
716+
717+
#[test]
718+
fn test_validate_valid_schema() {
719+
let dir = tempdir().unwrap();
720+
let schema = create_temp_file(&dir, "schema.json", r#"{"type": "string"}"#);
721+
722+
let mut cmd = cli();
723+
cmd.arg(&schema);
724+
let output = cmd.output().unwrap();
725+
assert!(output.status.success());
726+
727+
let stdout = String::from_utf8_lossy(&output.stdout);
728+
assert!(stdout.contains("Schema is valid"));
729+
}
730+
731+
#[test]
732+
fn test_validate_invalid_schema() {
733+
let dir = tempdir().unwrap();
734+
let schema = create_temp_file(
735+
&dir,
736+
"schema.json",
737+
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
738+
);
739+
740+
let mut cmd = cli();
741+
cmd.arg(&schema);
742+
let output = cmd.output().unwrap();
743+
assert!(!output.status.success());
744+
745+
let stdout = String::from_utf8_lossy(&output.stdout);
746+
assert!(stdout.contains("Schema is invalid"));
747+
}
748+
749+
#[test]
750+
fn test_instance_validation_with_invalid_schema_structured_output() {
751+
let dir = tempdir().unwrap();
752+
let schema = create_temp_file(
753+
&dir,
754+
"schema.json",
755+
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
756+
);
757+
let instance = create_temp_file(&dir, "instance.json", "42");
758+
759+
let mut cmd = cli();
760+
cmd.arg(&schema)
761+
.arg("--instance")
762+
.arg(&instance)
763+
.arg("--output")
764+
.arg("flag");
765+
let output = cmd.output().unwrap();
766+
assert!(!output.status.success());
767+
768+
let stdout = String::from_utf8_lossy(&output.stdout);
769+
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");
770+
771+
assert_eq!(json["output"], "flag");
772+
assert_eq!(json["payload"]["valid"], false);
773+
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
774+
}
775+
776+
#[test]
777+
fn test_instance_validation_with_invalid_schema_list_output() {
778+
let dir = tempdir().unwrap();
779+
let schema = create_temp_file(
780+
&dir,
781+
"schema.json",
782+
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
783+
);
784+
let instance = create_temp_file(&dir, "instance.json", "42");
785+
786+
let mut cmd = cli();
787+
cmd.arg(&schema)
788+
.arg("--instance")
789+
.arg(&instance)
790+
.arg("--output")
791+
.arg("list");
792+
let output = cmd.output().unwrap();
793+
assert!(!output.status.success());
794+
795+
let stdout = String::from_utf8_lossy(&output.stdout);
796+
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");
797+
798+
assert_eq!(json["output"], "list");
799+
assert_eq!(json["payload"]["valid"], false);
800+
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
801+
}
802+
803+
#[test]
804+
fn test_instance_validation_with_invalid_schema_hierarchical_output() {
805+
let dir = tempdir().unwrap();
806+
let schema = create_temp_file(
807+
&dir,
808+
"schema.json",
809+
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
810+
);
811+
let instance = create_temp_file(&dir, "instance.json", "42");
812+
813+
let mut cmd = cli();
814+
cmd.arg(&schema)
815+
.arg("--instance")
816+
.arg(&instance)
817+
.arg("--output")
818+
.arg("hierarchical");
819+
let output = cmd.output().unwrap();
820+
assert!(!output.status.success());
821+
822+
let stdout = String::from_utf8_lossy(&output.stdout);
823+
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");
824+
825+
assert_eq!(json["output"], "hierarchical");
826+
assert_eq!(json["payload"]["valid"], false);
827+
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
828+
}
829+
830+
#[test]
831+
fn test_validate_invalid_schema_list_output() {
832+
let dir = tempdir().unwrap();
833+
let schema = create_temp_file(
834+
&dir,
835+
"schema.json",
836+
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
837+
);
838+
839+
let mut cmd = cli();
840+
cmd.arg(&schema).arg("--output").arg("list");
841+
let output = cmd.output().unwrap();
842+
assert!(!output.status.success());
843+
844+
let stdout = String::from_utf8_lossy(&output.stdout);
845+
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");
846+
847+
assert_eq!(json["output"], "list");
848+
assert_eq!(json["payload"]["valid"], false);
849+
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
850+
}
851+
852+
#[test]
853+
fn test_validate_invalid_schema_hierarchical_output() {
854+
let dir = tempdir().unwrap();
855+
let schema = create_temp_file(
856+
&dir,
857+
"schema.json",
858+
r#"{"type": "invalid_type", "minimum": "not a number"}"#,
859+
);
860+
861+
let mut cmd = cli();
862+
cmd.arg(&schema).arg("--output").arg("hierarchical");
863+
let output = cmd.output().unwrap();
864+
assert!(!output.status.success());
865+
866+
let stdout = String::from_utf8_lossy(&output.stdout);
867+
let json: serde_json::Value = serde_json::from_str(&stdout).expect("Should be valid JSON");
868+
869+
assert_eq!(json["output"], "hierarchical");
870+
assert_eq!(json["payload"]["valid"], false);
871+
assert!(json["schema"].as_str().unwrap().ends_with("schema.json"));
872+
}
873+
874+
#[test]
875+
fn test_validate_schema_with_json_parse_error() {
876+
let dir = tempdir().unwrap();
877+
let schema = create_temp_file(&dir, "schema.json", r#"{"type": "string"#);
878+
879+
let mut cmd = cli();
880+
cmd.arg(&schema).arg("--output").arg("flag");
881+
let output = cmd.output().unwrap();
882+
assert!(!output.status.success());
883+
884+
let stdout = String::from_utf8_lossy(&output.stdout);
885+
assert!(stdout.contains("Error:"));
886+
}

crates/jsonschema-cli/tests/snapshots/cli__no_instances.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
source: crates/jsonschema-cli/tests/cli.rs
33
expression: "String::from_utf8_lossy(&output.stdout)"
44
---
5-
5+
Schema is valid

0 commit comments

Comments
 (0)