corvus_json_schema 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.lock +390 -0
- data/Cargo.toml +8 -0
- data/LICENSE +201 -0
- data/README.md +111 -0
- data/VERSIONHISTORY.md +7 -0
- data/ext/corvus_json_schema/Cargo.toml +18 -0
- data/ext/corvus_json_schema/extconf.rb +6 -0
- data/ext/corvus_json_schema/rustfmt.toml +2 -0
- data/ext/corvus_json_schema/src/lib.rs +643 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +35 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/LICENSE +201 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +139 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +1118 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/dialect.rs +108 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +905 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan/fused.rs +1187 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan.rs +1728 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval.rs +1451 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/formats.rs +1021 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/instance.rs +228 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/lib.rs +189 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/loader.rs +403 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/metaschemas.rs +29 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/node.rs +417 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/numbers.rs +244 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/options.rs +108 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +1591 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/results.rs +316 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/uri.rs +274 -0
- data/lib/corvus_json_schema/version.rb +5 -0
- data/lib/corvus_json_schema.rb +65 -0
- metadata +94 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
//! The native extension of the `corvus_json_schema` gem: the corvus-json-schema crate, with Ruby values read in place
|
|
2
|
+
//! (see src-rs/BINDINGS.md). The Ruby layer (lib/corvus_json_schema.rb) gives the public API.
|
|
3
|
+
|
|
4
|
+
use std::cell::{Cell, RefCell};
|
|
5
|
+
|
|
6
|
+
use corvus::{
|
|
7
|
+
ArrayView, CompileOptions, Dialect, Instance, JsonSchemaResultsCollector, Kind, ObjectView, ResultsLevel, View,
|
|
8
|
+
};
|
|
9
|
+
use magnus::prelude::*;
|
|
10
|
+
use magnus::rb_sys::AsRawValue;
|
|
11
|
+
use magnus::{
|
|
12
|
+
Error, ExceptionClass, Float, Integer, RArray, RHash, RModule, RString, Ruby, Symbol, Value, function, method,
|
|
13
|
+
r_hash::ForEach, value::BoxValue,
|
|
14
|
+
};
|
|
15
|
+
use serde_json::{Map, Number, Value as Json};
|
|
16
|
+
|
|
17
|
+
/// Arrays and objects nested deeper than this are converted (with a depth check) rather than read in place.
|
|
18
|
+
const MAX_NESTING: u32 = 256;
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
21
|
+
// Errors
|
|
22
|
+
|
|
23
|
+
thread_local! {
|
|
24
|
+
/// The first exception a Ruby callback raised during the current call (format validators run inside the
|
|
25
|
+
/// evaluator, which cannot carry a Ruby exception out).
|
|
26
|
+
static CALLBACK_ERROR: RefCell<Option<Error>> = const { RefCell::new(None) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn take_callback_error() -> Result<(), Error> {
|
|
30
|
+
CALLBACK_ERROR.with(|e| e.borrow_mut().take()).map_or(Ok(()), Err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
34
|
+
let module: RModule = ruby.class_object().const_get("CorvusJsonSchema").expect("the CorvusJsonSchema module");
|
|
35
|
+
module.const_get(name).expect("an error class")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn raise(ruby: &Ruby, class: &str, message: impl Into<String>) -> Error {
|
|
39
|
+
Error::new(error_class(ruby, class), message.into())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
43
|
+
// Ruby values read in place
|
|
44
|
+
|
|
45
|
+
/// The state of one evaluation: whether a value could not be read in place (so the caller converts the instance and
|
|
46
|
+
/// evaluates that instead), and the pairs of the hashes read, gathered when the evaluator first looks into one (Ruby
|
|
47
|
+
/// has no public lazy hash iterator).
|
|
48
|
+
struct Context {
|
|
49
|
+
fallback: Cell<bool>,
|
|
50
|
+
gathered: RefCell<Vec<Pairs>>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// A hash's pairs: the keys' text and the values.
|
|
54
|
+
type Pairs = Box<[(&'static str, Value)]>;
|
|
55
|
+
|
|
56
|
+
impl Context {
|
|
57
|
+
fn new() -> Context {
|
|
58
|
+
Context { fallback: Cell::new(false), gathered: RefCell::new(Vec::new()) }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// A Ruby value read as a JSON value: `Hash` (String or Symbol keys), `Array`, `String` (UTF-8), `Symbol` (as its
|
|
63
|
+
/// name), `Integer`, `Float`, `true`, `false` and `nil`. Anything else, or nesting deeper than `MAX_NESTING`, sets the
|
|
64
|
+
/// context's fallback.
|
|
65
|
+
///
|
|
66
|
+
/// The values stay reachable from the instance the caller passed (on the stack) for the whole evaluation, and no Ruby
|
|
67
|
+
/// code runs during it (with Ruby format validators the caller converts the instance first), so no garbage collection
|
|
68
|
+
/// can free or move them.
|
|
69
|
+
#[derive(Clone, Copy)]
|
|
70
|
+
struct RbInstance<'a> {
|
|
71
|
+
v: Value,
|
|
72
|
+
ctx: &'a Context,
|
|
73
|
+
depth: u32,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl<'a> RbInstance<'a> {
|
|
77
|
+
fn root(v: Value, ctx: &'a Context) -> Self {
|
|
78
|
+
RbInstance { v, ctx, depth: 0 }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[inline(always)]
|
|
82
|
+
fn child(self, v: Value) -> Self {
|
|
83
|
+
RbInstance { v, ctx: self.ctx, depth: self.depth + 1 }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[cold]
|
|
87
|
+
fn unsupported(self) -> View<'a, Self> {
|
|
88
|
+
self.ctx.fallback.set(true);
|
|
89
|
+
View::Null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// A hash's pairs, gathered once into the context (which outlives `'a`).
|
|
93
|
+
fn gather(self, h: RHash) -> Option<&'a [(&'a str, Value)]> {
|
|
94
|
+
let mut pairs: Vec<(&'static str, Value)> = Vec::with_capacity(h.len());
|
|
95
|
+
let mut ok = true;
|
|
96
|
+
let result = h.foreach(|k: Value, v: Value| match key_str(k) {
|
|
97
|
+
Some(k) => {
|
|
98
|
+
pairs.push((k, v));
|
|
99
|
+
Ok(ForEach::Continue)
|
|
100
|
+
}
|
|
101
|
+
None => {
|
|
102
|
+
ok = false;
|
|
103
|
+
Ok(ForEach::Stop)
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if result.is_err() || !ok {
|
|
107
|
+
return None;
|
|
108
|
+
}
|
|
109
|
+
let boxed: Pairs = pairs.into_boxed_slice();
|
|
110
|
+
let slice: *const [(&'static str, Value)] = &*boxed;
|
|
111
|
+
self.ctx.gathered.borrow_mut().push(boxed);
|
|
112
|
+
// SAFETY: the box lives in the context, which outlives 'a, and is never removed or changed while the context
|
|
113
|
+
// lives (a box's contents do not move when the vector holding it grows).
|
|
114
|
+
Some(unsafe { &*slice })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// A string's text when it is valid UTF-8. The borrow lasts while the string is neither changed nor moved (see
|
|
119
|
+
/// `RbInstance`).
|
|
120
|
+
fn str_of<'a>(s: RString) -> Option<&'a str> {
|
|
121
|
+
// SAFETY: as above.
|
|
122
|
+
let bytes: &'a [u8] = unsafe { std::mem::transmute::<&[u8], &'a [u8]>(s.as_slice()) };
|
|
123
|
+
std::str::from_utf8(bytes).ok()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn key_str<'a>(k: Value) -> Option<&'a str> {
|
|
127
|
+
if let Some(s) = RString::from_value(k) {
|
|
128
|
+
return str_of(s);
|
|
129
|
+
}
|
|
130
|
+
Symbol::from_value(k).and_then(|s| s.to_r_string().ok()).and_then(str_of)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
impl<'a> Instance<'a> for RbInstance<'a> {
|
|
134
|
+
type Array = RbArray<'a>;
|
|
135
|
+
type Object = RbObject<'a>;
|
|
136
|
+
|
|
137
|
+
fn view(self) -> View<'a, Self> {
|
|
138
|
+
let v = self.v;
|
|
139
|
+
if v.is_nil() {
|
|
140
|
+
return View::Null;
|
|
141
|
+
}
|
|
142
|
+
if let Some(s) = RString::from_value(v) {
|
|
143
|
+
return str_of(s).map_or_else(|| self.unsupported(), View::String);
|
|
144
|
+
}
|
|
145
|
+
if let Some(h) = RHash::from_value(v) {
|
|
146
|
+
if self.depth >= MAX_NESTING {
|
|
147
|
+
return self.unsupported();
|
|
148
|
+
}
|
|
149
|
+
return match self.gather(h) {
|
|
150
|
+
Some(pairs) => View::Object(RbObject { pairs, parent: self }),
|
|
151
|
+
None => self.unsupported(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if let Some(a) = RArray::from_value(v) {
|
|
155
|
+
if self.depth >= MAX_NESTING {
|
|
156
|
+
return self.unsupported();
|
|
157
|
+
}
|
|
158
|
+
// SAFETY: the array is neither changed nor moved during the evaluation (see `RbInstance`).
|
|
159
|
+
let items: &'a [Value] = unsafe { std::mem::transmute::<&[Value], &'a [Value]>(a.as_slice()) };
|
|
160
|
+
return View::Array(RbArray { items, parent: self });
|
|
161
|
+
}
|
|
162
|
+
let ruby = Ruby::get_with(v);
|
|
163
|
+
if v.as_raw() == ruby.qtrue().as_raw() {
|
|
164
|
+
return View::Bool(true);
|
|
165
|
+
}
|
|
166
|
+
if v.as_raw() == ruby.qfalse().as_raw() {
|
|
167
|
+
return View::Bool(false);
|
|
168
|
+
}
|
|
169
|
+
if let Some(i) = Integer::from_value(v) {
|
|
170
|
+
if let Ok(n) = i.to_i64() {
|
|
171
|
+
return View::Number(n.into());
|
|
172
|
+
}
|
|
173
|
+
if let Ok(n) = i.to_u64() {
|
|
174
|
+
return View::Number(n.into());
|
|
175
|
+
}
|
|
176
|
+
// Beyond 64 bits, the nearest double (as for a JSON parser without arbitrary precision).
|
|
177
|
+
return Float::from_value(i.as_value()).map_or_else(
|
|
178
|
+
|| integer_to_f64(i).and_then(Number::from_f64).map_or_else(|| self.unsupported(), View::Number),
|
|
179
|
+
|f| Number::from_f64(f.to_f64()).map_or_else(|| self.unsupported(), View::Number),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if let Some(f) = Float::from_value(v) {
|
|
183
|
+
return Number::from_f64(f.to_f64()).map_or_else(|| self.unsupported(), View::Number);
|
|
184
|
+
}
|
|
185
|
+
if let Some(s) = Symbol::from_value(v) {
|
|
186
|
+
return s.to_r_string().ok().and_then(str_of).map_or_else(|| self.unsupported(), View::String);
|
|
187
|
+
}
|
|
188
|
+
self.unsupported()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn kind(self) -> Kind {
|
|
192
|
+
let v = self.v;
|
|
193
|
+
if v.is_nil() {
|
|
194
|
+
return Kind::Null;
|
|
195
|
+
}
|
|
196
|
+
match self.view() {
|
|
197
|
+
View::Null => Kind::Null,
|
|
198
|
+
View::Bool(_) => Kind::Bool,
|
|
199
|
+
View::Number(_) => Kind::Number,
|
|
200
|
+
View::String(_) => Kind::String,
|
|
201
|
+
View::Array(_) => Kind::Array,
|
|
202
|
+
View::Object(_) => Kind::Object,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// An integer beyond 64 bits as the nearest double.
|
|
208
|
+
fn integer_to_f64(i: Integer) -> Option<f64> {
|
|
209
|
+
// SAFETY: rb_big2dbl does not raise (it warns and returns an infinity beyond a double's range).
|
|
210
|
+
let f = unsafe { rb_sys::rb_big2dbl(i.as_raw()) };
|
|
211
|
+
f.is_finite().then_some(f)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[derive(Clone, Copy)]
|
|
215
|
+
struct RbArray<'a> {
|
|
216
|
+
items: &'a [Value],
|
|
217
|
+
parent: RbInstance<'a>,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
impl<'a> ArrayView<'a> for RbArray<'a> {
|
|
221
|
+
type Item = RbInstance<'a>;
|
|
222
|
+
|
|
223
|
+
fn len(self) -> usize {
|
|
224
|
+
self.items.len()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn get(self, index: usize) -> RbInstance<'a> {
|
|
228
|
+
self.parent.child(self.items[index])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn iter(self) -> impl Iterator<Item = RbInstance<'a>> {
|
|
232
|
+
let parent = self.parent;
|
|
233
|
+
self.items.iter().map(move |&v| parent.child(v))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[derive(Clone, Copy)]
|
|
238
|
+
struct RbObject<'a> {
|
|
239
|
+
pairs: &'a [(&'a str, Value)],
|
|
240
|
+
parent: RbInstance<'a>,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
impl<'a> ObjectView<'a> for RbObject<'a> {
|
|
244
|
+
type Item = RbInstance<'a>;
|
|
245
|
+
|
|
246
|
+
fn len(self) -> usize {
|
|
247
|
+
self.pairs.len()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn get(self, name: &str) -> Option<RbInstance<'a>> {
|
|
251
|
+
self.pairs.iter().find(|(k, _)| *k == name).map(|&(_, v)| self.parent.child(v))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
fn iter(self) -> impl Iterator<Item = (&'a str, RbInstance<'a>)> {
|
|
255
|
+
let parent = self.parent;
|
|
256
|
+
self.pairs.iter().map(move |&(k, v)| (k, parent.child(v)))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
261
|
+
// Conversions
|
|
262
|
+
|
|
263
|
+
/// A Ruby value as JSON (for schemas, and for instances that cannot be read in place).
|
|
264
|
+
fn to_json(ruby: &Ruby, v: Value, depth: u32) -> Result<Json, Error> {
|
|
265
|
+
if depth > 1024 {
|
|
266
|
+
return Err(Error::new(ruby.exception_arg_error(), "the value nests too deeply"));
|
|
267
|
+
}
|
|
268
|
+
if v.is_nil() {
|
|
269
|
+
return Ok(Json::Null);
|
|
270
|
+
}
|
|
271
|
+
if v.as_raw() == ruby.qtrue().as_raw() {
|
|
272
|
+
return Ok(Json::Bool(true));
|
|
273
|
+
}
|
|
274
|
+
if v.as_raw() == ruby.qfalse().as_raw() {
|
|
275
|
+
return Ok(Json::Bool(false));
|
|
276
|
+
}
|
|
277
|
+
if let Some(s) = RString::from_value(v) {
|
|
278
|
+
return Ok(Json::String(s.to_string()?));
|
|
279
|
+
}
|
|
280
|
+
if let Some(s) = Symbol::from_value(v) {
|
|
281
|
+
return Ok(Json::String(s.name()?.into_owned()));
|
|
282
|
+
}
|
|
283
|
+
if let Some(i) = Integer::from_value(v) {
|
|
284
|
+
if let Ok(n) = i.to_i64() {
|
|
285
|
+
return Ok(Json::Number(n.into()));
|
|
286
|
+
}
|
|
287
|
+
if let Ok(n) = i.to_u64() {
|
|
288
|
+
return Ok(Json::Number(n.into()));
|
|
289
|
+
}
|
|
290
|
+
return integer_to_f64(i)
|
|
291
|
+
.and_then(Number::from_f64)
|
|
292
|
+
.map(Json::Number)
|
|
293
|
+
.ok_or_else(|| Error::new(ruby.exception_range_error(), "an integer beyond the range of a double"));
|
|
294
|
+
}
|
|
295
|
+
if let Some(f) = Float::from_value(v) {
|
|
296
|
+
return Number::from_f64(f.to_f64())
|
|
297
|
+
.map(Json::Number)
|
|
298
|
+
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "NaN and infinities are not JSON numbers"));
|
|
299
|
+
}
|
|
300
|
+
if let Some(a) = RArray::from_value(v) {
|
|
301
|
+
let mut out = Vec::with_capacity(a.len());
|
|
302
|
+
for item in a.into_iter() {
|
|
303
|
+
out.push(to_json(ruby, item, depth + 1)?);
|
|
304
|
+
}
|
|
305
|
+
return Ok(Json::Array(out));
|
|
306
|
+
}
|
|
307
|
+
if let Some(h) = RHash::from_value(v) {
|
|
308
|
+
let mut out = Map::with_capacity(h.len());
|
|
309
|
+
let mut failure: Option<Error> = None;
|
|
310
|
+
h.foreach(|k: Value, item: Value| {
|
|
311
|
+
let key = if let Some(s) = RString::from_value(k) {
|
|
312
|
+
s.to_string()
|
|
313
|
+
} else if let Some(s) = Symbol::from_value(k) {
|
|
314
|
+
s.name().map(|n| n.into_owned())
|
|
315
|
+
} else {
|
|
316
|
+
Err(Error::new(ruby.exception_type_error(), "object keys must be Strings or Symbols"))
|
|
317
|
+
};
|
|
318
|
+
match key.and_then(|k| Ok((k, to_json(ruby, item, depth + 1)?))) {
|
|
319
|
+
Ok((k, j)) => {
|
|
320
|
+
out.insert(k, j);
|
|
321
|
+
Ok(ForEach::Continue)
|
|
322
|
+
}
|
|
323
|
+
Err(e) => {
|
|
324
|
+
failure = Some(e);
|
|
325
|
+
Ok(ForEach::Stop)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
})?;
|
|
329
|
+
return match failure {
|
|
330
|
+
Some(e) => Err(e),
|
|
331
|
+
None => Ok(Json::Object(out)),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
Err(Error::new(ruby.exception_type_error(), format!("a {} is not a JSON value", unsafe { v.classname() })))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// JSON as a Ruby value (for results and annotations): objects as Hashes with String keys.
|
|
338
|
+
fn to_ruby(ruby: &Ruby, j: &Json) -> Result<Value, Error> {
|
|
339
|
+
Ok(match j {
|
|
340
|
+
Json::Null => ruby.qnil().as_value(),
|
|
341
|
+
Json::Bool(b) => {
|
|
342
|
+
if *b {
|
|
343
|
+
ruby.qtrue().as_value()
|
|
344
|
+
} else {
|
|
345
|
+
ruby.qfalse().as_value()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
Json::Number(n) => {
|
|
349
|
+
if let Some(i) = n.as_i64() {
|
|
350
|
+
ruby.integer_from_i64(i).as_value()
|
|
351
|
+
} else if let Some(u) = n.as_u64() {
|
|
352
|
+
ruby.integer_from_u64(u).as_value()
|
|
353
|
+
} else {
|
|
354
|
+
ruby.float_from_f64(n.as_f64().unwrap_or(f64::NAN)).as_value()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
Json::String(s) => ruby.str_new(s).as_value(),
|
|
358
|
+
Json::Array(a) => {
|
|
359
|
+
let out = ruby.ary_new_capa(a.len());
|
|
360
|
+
for item in a {
|
|
361
|
+
out.push(to_ruby(ruby, item)?)?;
|
|
362
|
+
}
|
|
363
|
+
out.as_value()
|
|
364
|
+
}
|
|
365
|
+
Json::Object(o) => {
|
|
366
|
+
let out = ruby.hash_new();
|
|
367
|
+
for (k, v) in o {
|
|
368
|
+
out.aset(ruby.str_new(k), to_ruby(ruby, v)?)?;
|
|
369
|
+
}
|
|
370
|
+
out.as_value()
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
376
|
+
// Callbacks
|
|
377
|
+
|
|
378
|
+
/// A Ruby Proc kept alive for the validator (a boxed value is marked by the garbage collector).
|
|
379
|
+
struct Callback(BoxValue<Value>);
|
|
380
|
+
|
|
381
|
+
// SAFETY: callbacks run only on the Ruby thread that evaluates (validators hold the GVL while they evaluate); the crate
|
|
382
|
+
// requires Send + Sync of the closures that hold them.
|
|
383
|
+
unsafe impl Send for Callback {}
|
|
384
|
+
unsafe impl Sync for Callback {}
|
|
385
|
+
|
|
386
|
+
impl Callback {
|
|
387
|
+
fn call(&self, arg: &str) -> Result<Value, Error> {
|
|
388
|
+
// Callbacks run on the Ruby thread evaluating (see above).
|
|
389
|
+
let ruby = Ruby::get().expect("a Ruby thread");
|
|
390
|
+
self.0.funcall("call", (ruby.str_new(arg),))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
395
|
+
// Validator
|
|
396
|
+
|
|
397
|
+
#[magnus::wrap(class = "CorvusJsonSchema::Validator", free_immediately, size)]
|
|
398
|
+
struct Validator {
|
|
399
|
+
inner: corvus::Validator,
|
|
400
|
+
/// Format validators written in Ruby run Ruby code during an evaluation, so instances are converted first.
|
|
401
|
+
ruby_formats: bool,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
fn depth_error(ruby: &Ruby, e: corvus::SchemaEvaluationDepthError) -> Error {
|
|
405
|
+
raise(ruby, "DepthError", e.to_string())
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
impl Validator {
|
|
409
|
+
/// Evaluates a Ruby value: read in place, or converted when it holds something the reader does not (or when Ruby
|
|
410
|
+
/// format validators could run Ruby code during the evaluation).
|
|
411
|
+
fn check(&self, ruby: &Ruby, value: Value) -> Result<bool, Error> {
|
|
412
|
+
if !self.ruby_formats {
|
|
413
|
+
let ctx = Context::new();
|
|
414
|
+
let result = self.inner.validate_instance(RbInstance::root(value, &ctx));
|
|
415
|
+
if !ctx.fallback.get() {
|
|
416
|
+
return result.map_err(|e| depth_error(ruby, e));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
let json = to_json(ruby, value, 0)?;
|
|
420
|
+
let result = self.inner.validate(&json);
|
|
421
|
+
take_callback_error()?;
|
|
422
|
+
result.map_err(|e| depth_error(ruby, e))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
fn valid(ruby: &Ruby, rb_self: &Self, value: Value) -> Result<bool, Error> {
|
|
426
|
+
rb_self.check(ruby, value)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fn valid_json(ruby: &Ruby, rb_self: &Self, text: RString) -> Result<bool, Error> {
|
|
430
|
+
// With Ruby format validators the text is copied first: Ruby code run during the evaluation could move it.
|
|
431
|
+
let owned;
|
|
432
|
+
let json: &str = if rb_self.ruby_formats {
|
|
433
|
+
owned = text.to_string()?;
|
|
434
|
+
&owned
|
|
435
|
+
} else {
|
|
436
|
+
str_of(text).ok_or_else(|| raise(ruby, "InvalidJsonError", "the JSON text is not valid UTF-8"))?
|
|
437
|
+
};
|
|
438
|
+
let result = rb_self.inner.validate_json(json);
|
|
439
|
+
take_callback_error()?;
|
|
440
|
+
match result {
|
|
441
|
+
Ok(valid) => Ok(valid),
|
|
442
|
+
Err(corvus::JsonValidationError::InvalidJson(e)) => Err(raise(ruby, "InvalidJsonError", e.to_string())),
|
|
443
|
+
Err(corvus::JsonValidationError::DepthExceeded(e)) => Err(depth_error(ruby, e)),
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/// Evaluates into the collector (replacing its results), reporting every keyword at its level.
|
|
448
|
+
fn evaluate(ruby: &Ruby, rb_self: &Self, value: Value, collector: &Collector) -> Result<bool, Error> {
|
|
449
|
+
let mut c = JsonSchemaResultsCollector::new(collector.level);
|
|
450
|
+
if !rb_self.ruby_formats {
|
|
451
|
+
let ctx = Context::new();
|
|
452
|
+
let result = rb_self.inner.evaluate_instance(RbInstance::root(value, &ctx), &mut c);
|
|
453
|
+
if !ctx.fallback.get() {
|
|
454
|
+
*collector.inner.borrow_mut() = c;
|
|
455
|
+
return result.map_err(|e| depth_error(ruby, e));
|
|
456
|
+
}
|
|
457
|
+
c = JsonSchemaResultsCollector::new(collector.level);
|
|
458
|
+
}
|
|
459
|
+
let json = to_json(ruby, value, 0)?;
|
|
460
|
+
let result = rb_self.inner.evaluate(&json, &mut c);
|
|
461
|
+
take_callback_error()?;
|
|
462
|
+
*collector.inner.borrow_mut() = c;
|
|
463
|
+
result.map_err(|e| depth_error(ruby, e))
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/// Compiles a schema (a Ruby value, or JSON text) with the options the Ruby layer passes.
|
|
468
|
+
#[allow(clippy::too_many_arguments)]
|
|
469
|
+
fn compile(
|
|
470
|
+
ruby: &Ruby,
|
|
471
|
+
schema: Value,
|
|
472
|
+
dialect: Option<i64>,
|
|
473
|
+
assert_format: Option<bool>,
|
|
474
|
+
assert_format_in_legacy_drafts: bool,
|
|
475
|
+
assert_content: bool,
|
|
476
|
+
formats: Option<RHash>,
|
|
477
|
+
resolver: Option<Value>,
|
|
478
|
+
base_uri: Option<String>,
|
|
479
|
+
entry_point: Option<String>,
|
|
480
|
+
max_depth: u32,
|
|
481
|
+
) -> Result<Validator, Error> {
|
|
482
|
+
let schema: Json = match RString::from_value(schema) {
|
|
483
|
+
Some(text) => serde_json::from_str(&text.to_string()?)
|
|
484
|
+
.map_err(|e| raise(ruby, "InvalidJsonError", format!("the schema is not valid JSON: {e}")))?,
|
|
485
|
+
None => to_json(ruby, schema, 0)?,
|
|
486
|
+
};
|
|
487
|
+
let mut options = CompileOptions {
|
|
488
|
+
default_dialect: match dialect {
|
|
489
|
+
None | Some(2020) => Dialect::Draft202012,
|
|
490
|
+
Some(4) => Dialect::Draft4,
|
|
491
|
+
Some(6) => Dialect::Draft6,
|
|
492
|
+
Some(7) => Dialect::Draft7,
|
|
493
|
+
Some(2019) => Dialect::Draft201909,
|
|
494
|
+
Some(d) => return Err(Error::new(ruby.exception_arg_error(), format!("{d} is not a dialect"))),
|
|
495
|
+
},
|
|
496
|
+
assert_format,
|
|
497
|
+
assert_format_in_legacy_drafts,
|
|
498
|
+
assert_content,
|
|
499
|
+
base_uri,
|
|
500
|
+
entry_point,
|
|
501
|
+
max_depth,
|
|
502
|
+
..CompileOptions::default()
|
|
503
|
+
};
|
|
504
|
+
let mut ruby_formats = false;
|
|
505
|
+
if let Some(formats) = formats {
|
|
506
|
+
formats.foreach(|name: Value, f: Value| {
|
|
507
|
+
let name = match Symbol::from_value(name) {
|
|
508
|
+
Some(s) => s.name()?.into_owned(),
|
|
509
|
+
None => RString::try_convert(name)?.to_string()?,
|
|
510
|
+
};
|
|
511
|
+
let callback = Callback(BoxValue::new(f));
|
|
512
|
+
options.formats.insert(
|
|
513
|
+
name,
|
|
514
|
+
std::sync::Arc::new(move |s: &str| match callback.call(s) {
|
|
515
|
+
Ok(v) => v.to_bool(),
|
|
516
|
+
Err(e) => {
|
|
517
|
+
CALLBACK_ERROR.with(|c| {
|
|
518
|
+
c.borrow_mut().get_or_insert(e);
|
|
519
|
+
});
|
|
520
|
+
false
|
|
521
|
+
}
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
ruby_formats = true;
|
|
525
|
+
Ok(ForEach::Continue)
|
|
526
|
+
})?;
|
|
527
|
+
}
|
|
528
|
+
// The first failure of the resolver (a Ruby exception, or a value that is not JSON), as text: the crate's resolver
|
|
529
|
+
// can only say a document is unknown.
|
|
530
|
+
let resolver_error: std::sync::Arc<std::sync::Mutex<Option<String>>> = Default::default();
|
|
531
|
+
if let Some(r) = resolver.filter(|r| !r.is_nil()) {
|
|
532
|
+
let callback = Callback(BoxValue::new(r));
|
|
533
|
+
let failure = resolver_error.clone();
|
|
534
|
+
options.resolve_document = Some(std::sync::Arc::new(move |uri: &str| {
|
|
535
|
+
let fail = |message: String| {
|
|
536
|
+
failure.lock().unwrap().get_or_insert(message);
|
|
537
|
+
None
|
|
538
|
+
};
|
|
539
|
+
let ruby = Ruby::get().expect("a Ruby thread");
|
|
540
|
+
match callback.call(uri) {
|
|
541
|
+
Ok(v) if v.is_nil() => None,
|
|
542
|
+
Ok(v) => match RString::from_value(v) {
|
|
543
|
+
Some(text) => match text.to_string().map(|t| serde_json::from_str::<Json>(&t)) {
|
|
544
|
+
Ok(Ok(json)) => Some(json),
|
|
545
|
+
Ok(Err(e)) => fail(format!("the resolver returned invalid JSON for {uri}: {e}")),
|
|
546
|
+
Err(e) => fail(format!("the resolver failed for {uri}: {e}")),
|
|
547
|
+
},
|
|
548
|
+
None => {
|
|
549
|
+
to_json(&ruby, v, 0).map_or_else(|e| fail(format!("the resolver failed for {uri}: {e}")), Some)
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
Err(e) => fail(format!("the resolver failed for {uri}: {e}")),
|
|
553
|
+
}
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
match corvus::compile_with(&schema, &options) {
|
|
557
|
+
Ok(inner) => Ok(Validator { inner, ruby_formats }),
|
|
558
|
+
Err(e) => {
|
|
559
|
+
let message = match resolver_error.lock().unwrap().take() {
|
|
560
|
+
Some(r) => format!("{} ({r})", e.message()),
|
|
561
|
+
None => e.message().to_owned(),
|
|
562
|
+
};
|
|
563
|
+
Err(raise(ruby, "CompilationError", message))
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
569
|
+
// Collector
|
|
570
|
+
|
|
571
|
+
#[magnus::wrap(class = "CorvusJsonSchema::Collector", free_immediately, size)]
|
|
572
|
+
struct Collector {
|
|
573
|
+
level: ResultsLevel,
|
|
574
|
+
inner: RefCell<JsonSchemaResultsCollector>,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
impl Collector {
|
|
578
|
+
fn new(ruby: &Ruby, level: i64) -> Result<Collector, Error> {
|
|
579
|
+
let level = match level {
|
|
580
|
+
0 => ResultsLevel::Basic,
|
|
581
|
+
1 => ResultsLevel::Detailed,
|
|
582
|
+
2 => ResultsLevel::Verbose,
|
|
583
|
+
l => return Err(Error::new(ruby.exception_arg_error(), format!("{l} is not a results level"))),
|
|
584
|
+
};
|
|
585
|
+
Ok(Collector { level, inner: RefCell::new(JsonSchemaResultsCollector::new(level)) })
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/// The rows as Hashes: `is_match`, `message`, `evaluation_location`, `schema_location`, `instance_location`.
|
|
589
|
+
fn results(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
590
|
+
let c = rb_self.inner.borrow();
|
|
591
|
+
let out = ruby.ary_new_capa(c.results().len());
|
|
592
|
+
for r in c.results() {
|
|
593
|
+
let row = ruby.hash_new();
|
|
594
|
+
row.aset(ruby.to_symbol("is_match"), r.is_match)?;
|
|
595
|
+
row.aset(ruby.to_symbol("message"), ruby.str_new(&r.message))?;
|
|
596
|
+
row.aset(ruby.to_symbol("evaluation_location"), ruby.str_new(&r.evaluation_location))?;
|
|
597
|
+
row.aset(ruby.to_symbol("schema_location"), ruby.str_new(&r.schema_evaluation_location))?;
|
|
598
|
+
row.aset(ruby.to_symbol("instance_location"), ruby.str_new(&r.document_evaluation_location))?;
|
|
599
|
+
out.push(row)?;
|
|
600
|
+
}
|
|
601
|
+
Ok(out)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// The annotations of a verbose evaluation, grouped by instance location, keyword and schema location.
|
|
605
|
+
fn annotations(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
606
|
+
let grouped = corvus::collect_annotations(&rb_self.inner.borrow());
|
|
607
|
+
let json =
|
|
608
|
+
serde_json::to_value(grouped).map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
609
|
+
to_ruby(ruby, &json)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
fn clear(&self) {
|
|
613
|
+
*self.inner.borrow_mut() = JsonSchemaResultsCollector::new(self.level);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
618
|
+
// Module
|
|
619
|
+
|
|
620
|
+
#[magnus::init]
|
|
621
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
622
|
+
let module = ruby.define_module("CorvusJsonSchema")?;
|
|
623
|
+
let error = module.define_error("Error", ruby.exception_standard_error())?;
|
|
624
|
+
module.define_error("CompilationError", error)?;
|
|
625
|
+
module.define_error("DepthError", error)?;
|
|
626
|
+
module.define_error("InvalidJsonError", error)?;
|
|
627
|
+
|
|
628
|
+
let native = module.define_module("Native")?;
|
|
629
|
+
native.define_singleton_method("compile", function!(compile, 10))?;
|
|
630
|
+
native.define_singleton_method("crate_version", function!(|| corvus::VERSION, 0))?;
|
|
631
|
+
|
|
632
|
+
let validator = module.define_class("Validator", ruby.class_object())?;
|
|
633
|
+
validator.define_method("valid?", method!(Validator::valid, 1))?;
|
|
634
|
+
validator.define_method("valid_json?", method!(Validator::valid_json, 1))?;
|
|
635
|
+
validator.define_method("native_evaluate", method!(Validator::evaluate, 2))?;
|
|
636
|
+
|
|
637
|
+
let collector = module.define_class("Collector", ruby.class_object())?;
|
|
638
|
+
collector.define_singleton_method("native_new", function!(Collector::new, 1))?;
|
|
639
|
+
collector.define_method("results", method!(Collector::results, 0))?;
|
|
640
|
+
collector.define_method("annotations", method!(Collector::annotations, 0))?;
|
|
641
|
+
collector.define_method("clear", method!(Collector::clear, 0))?;
|
|
642
|
+
Ok(())
|
|
643
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "corvus-json-schema"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
rust-version = "1.85"
|
|
6
|
+
description = "A high-performance JSON Schema evaluator (draft 4 to 2020-12) with results collection and annotations, ported from the Corvus.Text.Json V5 runtime evaluator."
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
repository = "https://github.com/corvus-dotnet/Corvus.JsonSchema"
|
|
9
|
+
homepage = "https://github.com/corvus-dotnet/Corvus.JsonSchema/tree/main/src-rs/corvus-json-schema"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
documentation = "https://docs.rs/corvus-json-schema"
|
|
12
|
+
keywords = ["json-schema", "validation", "jsonschema", "validator"]
|
|
13
|
+
categories = ["encoding", "parser-implementations"]
|
|
14
|
+
# The published crate: the library and its documents. The tests read the repository's JSON-Schema-Test-Suite
|
|
15
|
+
# submodule and the C# metaschemas, so they only run from a checkout. LICENSE is a copy of the repository's (CI checks
|
|
16
|
+
# it matches).
|
|
17
|
+
include = ["src/**", "Cargo.toml", "README.md", "LICENSE"]
|
|
18
|
+
|
|
19
|
+
[dependencies]
|
|
20
|
+
regex = "1.13.1"
|
|
21
|
+
regress = "0.12.0"
|
|
22
|
+
serde_json = { version = "1.0.151", features = ["preserve_order", "float_roundtrip"] }
|
|
23
|
+
|
|
24
|
+
[profile.release]
|
|
25
|
+
codegen-units = 1
|
|
26
|
+
lto = "fat"
|
|
27
|
+
panic = "abort"
|
|
28
|
+
|
|
29
|
+
[profile.bench]
|
|
30
|
+
codegen-units = 1
|
|
31
|
+
lto = "fat"
|
|
32
|
+
|
|
33
|
+
[profile.profiling]
|
|
34
|
+
inherits = "release"
|
|
35
|
+
debug = "line-tables-only"
|