Skip to content

Commit 7a32dd9

Browse files
authored
feat(rust/catalyst-types): Add JsonSchema type to the catalyst-types crate (#624)
* add json_schema type to the `catalyst-types` crate, bump version of the `catalyst-types` * wip * wip * wip
1 parent 11401c9 commit 7a32dd9

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

rust/catalyst-types/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "catalyst-types"
3-
version = "0.0.10"
3+
version = "0.0.11"
44
edition.workspace = true
55
license.workspace = true
66
authors.workspace = true
@@ -29,6 +29,8 @@ uuid = { version = "1.12.0", features = ["v4", "v7", "serde"] }
2929
chrono = "0.4.39"
3030
tracing = "0.1.41"
3131
strum = { version = "0.27.1", features = ["derive"] }
32+
jsonschema = "0.28.3"
33+
serde_json = { version = "1.0.134", features = ["raw_value"] }
3234

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

rust/catalyst-types/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
pub mod catalyst_id;
44
pub mod cbor_utils;
55
pub mod conversion;
6+
pub mod json_schema;
67
#[cfg(not(target_arch = "wasm32"))]
78
pub mod mmap_file;
89
pub mod problem_report;

0 commit comments

Comments
 (0)