diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 04:57:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-25 04:57:20 +0900 |
| commit | 541a8b4fd2c4b9538e2c88c5a7c4f9ffb5ca25eb (patch) | |
| tree | cbe1e5e9c8edec2a3a875ef0eade99aa4d38e7c4 /crates/shirabe/tests/json | |
| parent | c65cab710bb3d79db20862c9361d4dbbac8ac5d7 (diff) | |
| download | php-shirabe-541a8b4fd2c4b9538e2c88c5a7c4f9ffb5ca25eb.tar.gz php-shirabe-541a8b4fd2c4b9538e2c88c5a7c4f9ffb5ca25eb.tar.zst php-shirabe-541a8b4fd2c4b9538e2c88c5a7c4f9ffb5ca25eb.zip | |
feat(json): validate JSON schema via the jsonschema crate
Replace the todo!() json_schema::Validator stub with the jsonschema crate.
Errors are surfaced as 'property : message'; the message wording follows the
jsonschema crate and is *not* justinrainbow-compatible.
Port the ComposerSchemaTest and JsonFileTest schema cases to the new wording
(noted per test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/json')
| -rw-r--r-- | crates/shirabe/tests/json/composer_schema_test.rs | 135 | ||||
| -rw-r--r-- | crates/shirabe/tests/json/json_file_test.rs | 99 |
2 files changed, 163 insertions, 71 deletions
diff --git a/crates/shirabe/tests/json/composer_schema_test.rs b/crates/shirabe/tests/json/composer_schema_test.rs index 08415f6..9cbea15 100644 --- a/crates/shirabe/tests/json/composer_schema_test.rs +++ b/crates/shirabe/tests/json/composer_schema_test.rs @@ -1,33 +1,144 @@ //! ref: composer/tests/Composer/Test/Json/ComposerSchemaTest.php +//! +//! WORDING NOTE: the upstream PHP tests assert justinrainbow's structured +//! `{property, message, constraint}` error arrays (e.g. "Does not match the regex pattern …", +//! "Array value found, but a string is required", "Does not have a value in the enumeration …"). +//! Shirabe validates with the `jsonschema` crate, whose message wording differs. These ports +//! therefore assert the equivalent `property : message` strings using jsonschema's wording. +//! Only the wording changed — the validation behavior (which inputs are valid/invalid and on +//! which property) is identical to upstream. + +use shirabe::json::{JsonFile, JsonValidationException}; +use shirabe_php_shim::json_decode; + +const NAME_PATTERN: &str = r#"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$"#; +const VERSION_PATTERN: &str = r#"^[vV]?\d+(?:[.-]\d+){0,3}[._-]?(?:(?:[sS][tT][aA][bB][lL][eE]|[bB][eE][tT][aA]|[bB]|[rR][cC]|[aA][lL][pP][hH][aA]|[aA]|[pP][aA][tT][cC][hH]|[pP][lL]|[pP])(?:(?:[.-]?\d+)*)?)?(?:[.-]?[dD][eE][vV]|\.x-dev)?(?:\+.*)?$|^dev-.*$"#; + +/// Ports `ComposerSchemaTest::check`: validate against the bundled `composer-schema.json` +/// (the `{"$ref": "file://…"}` wrapper used by `LAX_SCHEMA`), returning the validation error +/// strings, or an empty vec when the document is valid. +fn check(json: &str) -> Vec<String> { + let data = json_decode(json, false).unwrap(); + match JsonFile::validate_json_schema("test", &data, JsonFile::LAX_SCHEMA, None) { + Ok(_) => Vec::new(), + Err(e) => e + .downcast_ref::<JsonValidationException>() + .unwrap() + .get_errors() + .clone(), + } +} -// These validate documents against the bundled composer-schema.json via JsonFile's -// json-schema validator, which is not ported. -#[ignore = "JsonSchema Validator::get_errors returns Vec<String>; cannot represent structured property/message/constraint error arrays asserted here, and Validator::validate is not ported"] #[test] fn test_name_pattern() { - todo!() + let expected_error = vec![format!( + r#"name : "vendor/-pack__age" does not match "{NAME_PATTERN}""# + )]; + let json = r#"{"name": "vendor/-pack__age", "description": "description"}"#; + assert_eq!(expected_error, check(json)); + + let expected_error = vec![format!( + r#"name : "Vendor/Package" does not match "{NAME_PATTERN}""# + )]; + let json = r#"{"name": "Vendor/Package", "description": "description"}"#; + assert_eq!(expected_error, check(json)); } -#[ignore = "JsonSchema Validator::get_errors returns Vec<String>; cannot represent structured property/message/constraint error arrays asserted here, and Validator::validate is not ported"] #[test] fn test_version_pattern() { - todo!() + let versions: &[(&str, bool)] = &[ + ("1.0.0", true), + ("1.0.2", true), + ("1.1.0", true), + ("1.0.0-dev", true), + ("1.0.0-Alpha", true), + ("1.0.0-ALPHA", true), + ("1.0.0-alphA", true), + ("1.0.0-alpha3", true), + ("1.0.0-Alpha3", true), + ("1.0.0-ALPHA3", true), + ("1.0.0-Beta", true), + ("1.0.0-BETA", true), + ("1.0.0-betA", true), + ("1.0.0-beta232", true), + ("1.0.0-Beta232", true), + ("1.0.0-BETA232", true), + ("10.4.13beta.2", true), + ("1.0.0.RC.15-dev", true), + ("1.0.0-RC", true), + ("v2.0.4-p", true), + ("dev-master", true), + ("0.2.5.4", true), + ("12345678-123456", true), + ("20100102-203040-p1", true), + ("2010-01-02.5", true), + ("0.2.5.4-rc.2", true), + ("dev-feature+issue-1", true), + ("1.0.0-alpha.3.1+foo/-bar", true), + ("00.01.03.04", true), + ("041.x-dev", true), + ("dev-foo bar", true), + ("invalid", false), + ("1.0be", false), + ("1.0.0-meh", false), + ("feature-foo", false), + ("1.0 .2", false), + ]; + + for &(version, is_valid) in versions { + let json = format!( + r#"{{"name": "vendor/package", "description": "description", "version": "{version}"}}"# + ); + if is_valid { + assert!(check(&json).is_empty(), "expected {version} to be valid"); + } else { + let expected_error = vec![format!( + r#"version : "{version}" does not match "{VERSION_PATTERN}""# + )]; + assert_eq!( + expected_error, + check(&json), + "expected {version} to be invalid" + ); + } + } } -#[ignore = "JsonSchema Validator::get_errors returns Vec<String>; cannot represent structured property/message/constraint error arrays asserted here, and Validator::validate is not ported"] #[test] fn test_optional_abandoned_property() { - todo!() + let json = r#"{"name": "vendor/package", "description": "description", "abandoned": true}"#; + assert!(check(json).is_empty()); } -#[ignore = "JsonSchema Validator::get_errors returns Vec<String>; cannot represent structured property/message/constraint error arrays asserted here, and Validator::validate is not ported"] #[test] fn test_require_types() { - todo!() + let json = + r#"{"name": "vendor/package", "description": "description", "require": {"a": ["b"]} }"#; + let expected_error = vec![r#"require.a : ["b"] is not of type "string""#.to_string()]; + assert_eq!(expected_error, check(json)); } -#[ignore = "JsonSchema Validator::get_errors returns Vec<String>; cannot represent structured property/message/constraint error arrays asserted here, and Validator::validate is not ported"] #[test] fn test_minimum_stability_values() { - todo!() + let enum_error = |value: &str| { + vec![format!( + r#"minimum-stability : "{value}" is not one of "dev", "alpha" or 4 other candidates"# + )] + }; + + let json = r#"{ "name": "vendor/package", "description": "generic description", "minimum-stability": "" }"#; + assert_eq!(enum_error(""), check(json), "empty string"); + + let json = r#"{ "name": "vendor/package", "description": "generic description", "minimum-stability": "dummy" }"#; + assert_eq!(enum_error("dummy"), check(json), "dummy"); + + let json = r#"{ "name": "vendor/package", "description": "generic description", "minimum-stability": "devz" }"#; + assert_eq!(enum_error("devz"), check(json), "devz"); + + for stability in ["dev", "alpha", "beta", "rc", "RC", "stable"] { + let json = format!( + r#"{{ "name": "vendor/package", "description": "generic description", "minimum-stability": "{stability}" }}"# + ); + assert!(check(&json).is_empty(), "expected {stability} to be valid"); + } } diff --git a/crates/shirabe/tests/json/json_file_test.rs b/crates/shirabe/tests/json/json_file_test.rs index 032d0fd..d3e1048 100644 --- a/crates/shirabe/tests/json/json_file_test.rs +++ b/crates/shirabe/tests/json/json_file_test.rs @@ -37,20 +37,9 @@ fn fixture_path(name: &str) -> std::path::PathBuf { .join(name) } -/// ref: TestCase::createTempFile (PHP tempnam()) -fn create_temp_file() -> String { - let mut path = std::env::temp_dir(); - let unique = format!( - "shirabe_jsonfiletest_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - ); - path.push(unique); - std::fs::write(&path, b"").unwrap(); - path.to_str().unwrap().to_string() +/// ref: TestCase::createTempFile. +fn create_temp_file() -> tempfile::NamedTempFile { + tempfile::NamedTempFile::new().expect("failed to create a temporary file") } #[test] @@ -124,7 +113,6 @@ fn test_parse_error_detect_missing_colon() { } #[test] -#[ignore] fn test_simple_json_string() { let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); data.insert( @@ -136,7 +124,6 @@ fn test_simple_json_string() { } #[test] -#[ignore] fn test_trailing_backslash() { let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); data.insert( @@ -148,7 +135,6 @@ fn test_trailing_backslash() { } #[test] -#[ignore] fn test_format_empty_array() { let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); data.insert("test".to_string(), PhpMixed::List(vec![])); @@ -158,7 +144,6 @@ fn test_format_empty_array() { } #[test] -#[ignore] fn test_escape() { let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); data.insert( @@ -170,7 +155,6 @@ fn test_escape() { } #[test] -#[ignore] fn test_unicode() { let mut data: IndexMap<String, PhpMixed> = IndexMap::new(); data.insert( @@ -245,7 +229,6 @@ fn test_preserve_indentation_after_read() { } #[test] -#[ignore] fn test_overwrites_indentation_by_default() { let src = fixture_path("tabs.json"); let dst = fixture_path("tabs2.json"); @@ -265,7 +248,6 @@ fn test_overwrites_indentation_by_default() { } #[test] -#[ignore] fn test_schema_validation() { let path = fixture_path("composer.json"); let json = JsonFile::new(path.to_str().unwrap().to_string(), None, None).unwrap(); @@ -274,13 +256,15 @@ fn test_schema_validation() { } #[test] -#[ignore] fn test_schema_validation_error() { - let file = create_temp_file(); + // WORDING NOTE: upstream asserts justinrainbow's "name : NULL value found, but a string is + // required". The jsonschema crate reports the same violation with different wording. + let file_tmp = create_temp_file(); + let file = file_tmp.path().to_str().unwrap().to_string(); std::fs::write(&file, b"{ \"name\": null }").unwrap(); let json = JsonFile::new(file.clone(), None, None).unwrap(); let expected_message = format!("\"{}\" does not match the expected JSON schema", file); - let expected_error = "name : NULL value found, but a string is required".to_string(); + let expected_error = "name : null is not of type \"string\"".to_string(); let err = json .validate_schema(JsonFile::STRICT_SCHEMA, None) @@ -295,14 +279,15 @@ fn test_schema_validation_error() { let e = err.downcast_ref::<JsonValidationException>().unwrap(); assert_eq!(expected_message, e.get_message()); assert!(e.get_errors().contains(&expected_error)); - - std::fs::remove_file(&file).unwrap(); } #[test] -#[ignore] fn test_schema_validation_lax_additional_properties() { - let file = create_temp_file(); + // WORDING NOTE: upstream asserts justinrainbow's "The property foo is not defined and the + // definition does not allow additional properties". The jsonschema crate reports the same + // additionalProperties violation with different wording. + let file_tmp = create_temp_file(); + let file = file_tmp.path().to_str().unwrap().to_string(); std::fs::write( &file, b"{ \"name\": \"vendor/package\", \"description\": \"generic description\", \"foo\": \"bar\" }", @@ -319,21 +304,21 @@ fn test_schema_validation_lax_additional_properties() { e.get_message() ); assert_eq!( - &vec![ - "The property foo is not defined and the definition does not allow additional properties" - .to_string() - ], + &vec!["Additional properties are not allowed ('foo' was unexpected)".to_string()], e.get_errors() ); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); - std::fs::remove_file(&file).unwrap(); } #[test] -#[ignore] fn test_schema_validation_lax_required() { - let file = create_temp_file(); + // WORDING NOTE: upstream asserts justinrainbow's property-prefixed strings such as + // "name : The property name is required". The jsonschema crate reports the same required + // violations as "\"name\" is a required property" (no property prefix; the property name is + // embedded in the message). + let file_tmp = create_temp_file(); + let file = file_tmp.path().to_str().unwrap().to_string(); let json = JsonFile::new(file.clone(), None, None).unwrap(); let expected_message = format!("\"{}\" does not match the expected JSON schema", file); @@ -345,8 +330,8 @@ fn test_schema_validation_lax_required() { let e = err.downcast_ref::<JsonValidationException>().unwrap(); assert_eq!(expected_message, e.get_message()); let errors = e.get_errors(); - assert!(errors.contains(&"name : The property name is required".to_string())); - assert!(errors.contains(&"description : The property description is required".to_string())); + assert!(errors.contains(&"\"name\" is a required property".to_string())); + assert!(errors.contains(&"\"description\" is a required property".to_string())); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); std::fs::write(&file, b"{ \"name\": \"vendor/package\" }").unwrap(); @@ -356,7 +341,7 @@ fn test_schema_validation_lax_required() { let e = err.downcast_ref::<JsonValidationException>().unwrap(); assert_eq!(expected_message, e.get_message()); assert_eq!( - &vec!["description : The property description is required".to_string()], + &vec!["\"description\" is a required property".to_string()], e.get_errors() ); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); @@ -368,7 +353,7 @@ fn test_schema_validation_lax_required() { let e = err.downcast_ref::<JsonValidationException>().unwrap(); assert_eq!(expected_message, e.get_message()); assert_eq!( - &vec!["name : The property name is required".to_string()], + &vec!["\"name\" is a required property".to_string()], e.get_errors() ); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); @@ -380,8 +365,8 @@ fn test_schema_validation_lax_required() { let e = err.downcast_ref::<JsonValidationException>().unwrap(); assert_eq!(expected_message, e.get_message()); let errors = e.get_errors(); - assert!(errors.contains(&"name : The property name is required".to_string())); - assert!(errors.contains(&"description : The property description is required".to_string())); + assert!(errors.contains(&"\"name\" is a required property".to_string())); + assert!(errors.contains(&"\"description\" is a required property".to_string())); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); std::fs::write(&file, b"{ \"type\": \"project\" }").unwrap(); @@ -391,8 +376,8 @@ fn test_schema_validation_lax_required() { let e = err.downcast_ref::<JsonValidationException>().unwrap(); assert_eq!(expected_message, e.get_message()); let errors = e.get_errors(); - assert!(errors.contains(&"name : The property name is required".to_string())); - assert!(errors.contains(&"description : The property description is required".to_string())); + assert!(errors.contains(&"\"name\" is a required property".to_string())); + assert!(errors.contains(&"\"description\" is a required property".to_string())); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); std::fs::write( @@ -402,21 +387,20 @@ fn test_schema_validation_lax_required() { .unwrap(); json.validate_schema(JsonFile::STRICT_SCHEMA, None).unwrap(); json.validate_schema(JsonFile::LAX_SCHEMA, None).unwrap(); - - std::fs::remove_file(&file).unwrap(); } #[test] -#[ignore] fn test_custom_schema_validation_lax() { - let file = create_temp_file(); + let file_tmp = create_temp_file(); + let file = file_tmp.path().to_str().unwrap().to_string(); std::fs::write( &file, b"{ \"custom\": \"property\", \"another custom\": \"property\" }", ) .unwrap(); - let schema = create_temp_file(); + let schema_tmp = create_temp_file(); + let schema = schema_tmp.path().to_str().unwrap().to_string(); std::fs::write( &schema, b"{ \"properties\": { \"custom\": { \"type\": \"string\" }}}", @@ -427,18 +411,16 @@ fn test_custom_schema_validation_lax() { json.validate_schema(JsonFile::LAX_SCHEMA, Some(&schema)) .unwrap(); - - std::fs::remove_file(&file).unwrap(); - std::fs::remove_file(&schema).unwrap(); } #[test] -#[ignore] fn test_custom_schema_validation_strict() { - let file = create_temp_file(); + let file_tmp = create_temp_file(); + let file = file_tmp.path().to_str().unwrap().to_string(); std::fs::write(&file, b"{ \"custom\": \"property\" }").unwrap(); - let schema = create_temp_file(); + let schema_tmp = create_temp_file(); + let schema = schema_tmp.path().to_str().unwrap().to_string(); std::fs::write( &schema, b"{ \"properties\": { \"custom\": { \"type\": \"string\" }}}", @@ -449,17 +431,16 @@ fn test_custom_schema_validation_strict() { json.validate_schema(JsonFile::STRICT_SCHEMA, Some(&schema)) .unwrap(); - - std::fs::remove_file(&file).unwrap(); - std::fs::remove_file(&schema).unwrap(); } #[test] -#[ignore] fn test_auth_schema_validation_with_custom_data_source() { + // WORDING NOTE: upstream asserts justinrainbow's "github-oauth : String value found, but an + // object is required". The jsonschema crate reports the same type violation with different + // wording. let json = shirabe_php_shim::json_decode("{\"github-oauth\": \"foo\"}", false).unwrap(); let expected_message = "\"COMPOSER_AUTH\" does not match the expected JSON schema".to_string(); - let expected_error = "github-oauth : String value found, but an object is required".to_string(); + let expected_error = "github-oauth : \"foo\" is not of type \"object\"".to_string(); let err = JsonFile::validate_json_schema("COMPOSER_AUTH", &json, JsonFile::AUTH_SCHEMA, None) .unwrap_err(); |
