Skip to content

Commit 9d001c2

Browse files
authored
feat(rust/signed-doc): Add support for jsonschema draft 2020-12 (#493)
* feat: jsonschema 2020-12 * feat: dynamic version * feat: jsonschema struct * chore: lintfix * chore: fmtfix * chore: lintfix * fix: content schema wrapper * fix: remove access
1 parent 4693538 commit 9d001c2

File tree

3 files changed

+159
-25
lines changed

3 files changed

+159
-25
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//! A wrapper around a JSON Schema validator.
2+
3+
use std::ops::Deref;
4+
5+
use anyhow::anyhow;
6+
use jsonschema::{options, Draft, Validator};
7+
use serde_json::Value;
8+
9+
/// Wrapper around a JSON Schema validator.
10+
///
11+
/// Attempts to detect the draft version from the `$schema` field.
12+
/// If not specified, it tries Draft2020-12 first, then falls back to Draft7.
13+
/// Returns an error if schema is invalid for both.
14+
pub(crate) struct JsonSchema(Validator);
15+
16+
impl Deref for JsonSchema {
17+
type Target = Validator;
18+
19+
fn deref(&self) -> &Self::Target {
20+
&self.0
21+
}
22+
}
23+
24+
impl TryFrom<&Value> for JsonSchema {
25+
type Error = anyhow::Error;
26+
27+
fn try_from(schema: &Value) -> std::result::Result<Self, Self::Error> {
28+
let draft_version = if let Some(schema) = schema.get("$schema").and_then(|s| s.as_str()) {
29+
if schema.contains("draft-07") {
30+
Some(Draft::Draft7)
31+
} else if schema.contains("2020-12") {
32+
Some(Draft::Draft202012)
33+
} else {
34+
None
35+
}
36+
} else {
37+
None
38+
};
39+
40+
if let Some(draft) = draft_version {
41+
let validator = options()
42+
.with_draft(draft)
43+
.build(schema)
44+
.map_err(|e| anyhow!("Invalid JSON Schema: {e}"))?;
45+
46+
Ok(JsonSchema(validator))
47+
} else {
48+
// if draft not specified or not detectable:
49+
// try draft2020-12
50+
if let Ok(validator) = options().with_draft(Draft::Draft202012).build(schema) {
51+
return Ok(JsonSchema(validator));
52+
}
53+
54+
// fallback to draft7
55+
if let Ok(validator) = options().with_draft(Draft::Draft7).build(schema) {
56+
return Ok(JsonSchema(validator));
57+
}
58+
59+
Err(anyhow!(
60+
"Could not detect draft version and schema is not valid against Draft2020-12 or Draft7"
61+
))
62+
}
63+
}
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use serde_json::json;
69+
70+
use super::*;
71+
72+
#[test]
73+
fn valid_draft7_schema() {
74+
let schema = json!({
75+
"$schema": "http://json-schema.org/draft-07/schema#",
76+
"type": "object",
77+
"properties": {
78+
"name": { "type": "string" }
79+
}
80+
});
81+
82+
let result = JsonSchema::try_from(&schema);
83+
assert!(result.is_ok(), "Expected Draft7 schema to be valid");
84+
}
85+
86+
#[test]
87+
fn valid_draft2020_12_schema() {
88+
let schema = json!({
89+
"$schema": "https://json-schema.org/draft/2020-12/schema",
90+
"type": "object",
91+
"properties": {
92+
"age": { "type": "integer" }
93+
}
94+
});
95+
96+
let result = JsonSchema::try_from(&schema);
97+
assert!(result.is_ok(), "Expected Draft2020-12 schema to be valid");
98+
}
99+
100+
#[test]
101+
fn schema_without_draft_should_fallback() {
102+
// Valid in both Draft2020-12 and Draft7
103+
let schema = json!({
104+
"type": "object",
105+
"properties": {
106+
"id": { "type": "number" }
107+
}
108+
});
109+
110+
let result = JsonSchema::try_from(&schema);
111+
assert!(
112+
result.is_ok(),
113+
"Expected schema without $schema to fallback and succeed"
114+
);
115+
}
116+
117+
#[test]
118+
fn invalid_schema_should_error() {
119+
// Invalid schema: "type" is not a valid keyword here
120+
let schema = json!({
121+
"$schema": "http://json-schema.org/draft-07/schema#",
122+
"type": "not-a-valid-type"
123+
});
124+
125+
let result = JsonSchema::try_from(&schema);
126+
assert!(
127+
result.is_err(),
128+
"Expected invalid schema to return an error"
129+
);
130+
}
131+
132+
#[test]
133+
fn empty_object_schema() {
134+
let schema = json!({});
135+
136+
let result = JsonSchema::try_from(&schema);
137+
assert!(result.is_ok());
138+
}
139+
}

rust/signed_doc/src/validator/mod.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Catalyst Signed Documents validation logic
22
3+
pub(crate) mod json_schema;
34
pub(crate) mod rules;
45
pub(crate) mod utils;
56

@@ -116,15 +117,15 @@ fn proposal_submission_action_rule() -> Rules {
116117
CATEGORY_PARAMETERS.clone(),
117118
];
118119

119-
let proposal_action_json_schema = jsonschema::options()
120-
.with_draft(jsonschema::Draft::Draft7)
121-
.build(
122-
&serde_json::from_str(include_str!(
123-
"./../../../../specs/definitions/signed_docs/docs/payload_schemas/proposal_submission_action.schema.json"
124-
))
125-
.expect("Must be a valid json file"),
126-
)
127-
.expect("Must be a valid json scheme file");
120+
let proposal_action_json_schema_content = &serde_json::from_str(include_str!(
121+
"./../../../../specs/definitions/signed_docs/docs/payload_schemas/proposal_submission_action.schema.json"
122+
))
123+
.expect("Must be a valid json file");
124+
125+
let proposal_action_json_schema =
126+
json_schema::JsonSchema::try_from(proposal_action_json_schema_content)
127+
.expect("Must be a valid json scheme file");
128+
128129
Rules {
129130
content_type: ContentTypeRule {
130131
exp: ContentType::Json,

rust/signed_doc/src/validator/rules/template.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ use std::fmt::Write;
44

55
use super::doc_ref::referenced_doc_check;
66
use crate::{
7-
metadata::ContentType, providers::CatalystSignedDocumentProvider,
8-
validator::utils::validate_doc_refs, CatalystSignedDocument, DocType,
7+
metadata::ContentType,
8+
providers::CatalystSignedDocumentProvider,
9+
validator::{json_schema, utils::validate_doc_refs},
10+
CatalystSignedDocument, DocType,
911
};
1012

1113
/// Enum represents different content schemas, against which documents content would be
1214
/// validated.
1315
pub(crate) enum ContentSchema {
1416
/// Draft 7 JSON schema
15-
Json(jsonschema::Validator),
17+
Json(json_schema::JsonSchema),
1618
}
1719

1820
/// Document's content validation rule
@@ -131,18 +133,15 @@ fn templated_json_schema_check(
131133
);
132134
return false;
133135
};
134-
let Ok(schema_validator) = jsonschema::options()
135-
.with_draft(jsonschema::Draft::Draft7)
136-
.build(&template_json_schema)
137-
else {
136+
let Ok(schema) = json_schema::JsonSchema::try_from(&template_json_schema) else {
138137
doc.report().functional_validation(
139138
"Template document content must be Draft 7 JSON schema",
140139
"Invalid referenced template document content",
141140
);
142141
return false;
143142
};
144143

145-
content_schema_check(doc, &ContentSchema::Json(schema_validator))
144+
content_schema_check(doc, &ContentSchema::Json(schema))
146145
}
147146

148147
/// Validating the document's content against the provided schema
@@ -427,17 +426,12 @@ mod tests {
427426
#[tokio::test]
428427
async fn content_rule_static_test() {
429428
let provider = TestCatalystSignedDocumentProvider::default();
430-
431-
let json_schema = ContentSchema::Json(
432-
jsonschema::options()
433-
.with_draft(jsonschema::Draft::Draft7)
434-
.build(&serde_json::json!({}))
435-
.unwrap(),
436-
);
429+
let schema = json_schema::JsonSchema::try_from(&serde_json::json!({})).unwrap();
430+
let content_schema = ContentSchema::Json(schema);
437431
let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap();
438432

439433
// all correct
440-
let rule = ContentRule::Static(json_schema);
434+
let rule = ContentRule::Static(content_schema);
441435
let doc = Builder::new().with_content(json_content.clone()).build();
442436
assert!(rule.check(&doc, &provider).await.unwrap());
443437

0 commit comments

Comments
 (0)