Skip to content

Commit 4329616

Browse files
committed
perf: Specialize required for length 2 & 3
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
1 parent 281f4c3 commit 4329616

File tree

1 file changed

+340
-5
lines changed

1 file changed

+340
-5
lines changed

crates/jsonschema/src/keywords/required.rs

Lines changed: 340 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,228 @@ impl Validate for SingleItemRequiredValidator {
147147
}
148148
}
149149

150+
/// Specialized validator for exactly 2 required properties.
151+
/// Uses fixed-size array and unrolled checks to avoid Vec/iterator overhead.
152+
pub(crate) struct Required2Validator {
153+
first: String,
154+
second: String,
155+
location: Location,
156+
}
157+
158+
impl Required2Validator {
159+
#[inline]
160+
pub(crate) fn compile(
161+
first: String,
162+
second: String,
163+
location: Location,
164+
) -> CompilationResult<'static> {
165+
Ok(Box::new(Required2Validator {
166+
first,
167+
second,
168+
location,
169+
}))
170+
}
171+
}
172+
173+
impl Validate for Required2Validator {
174+
#[inline]
175+
fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool {
176+
if let Value::Object(item) = instance {
177+
item.len() >= 2 && item.contains_key(&self.first) && item.contains_key(&self.second)
178+
} else {
179+
true
180+
}
181+
}
182+
183+
fn validate<'i>(
184+
&self,
185+
instance: &'i Value,
186+
location: &LazyLocation,
187+
tracker: Option<&RefTracker>,
188+
_ctx: &mut ValidationContext,
189+
) -> Result<(), ValidationError<'i>> {
190+
if let Value::Object(item) = instance {
191+
if !item.contains_key(&self.first) {
192+
return Err(ValidationError::required(
193+
self.location.clone(),
194+
crate::paths::capture_evaluation_path(tracker, &self.location),
195+
location.into(),
196+
instance,
197+
Value::String(self.first.clone()),
198+
));
199+
}
200+
if !item.contains_key(&self.second) {
201+
return Err(ValidationError::required(
202+
self.location.clone(),
203+
crate::paths::capture_evaluation_path(tracker, &self.location),
204+
location.into(),
205+
instance,
206+
Value::String(self.second.clone()),
207+
));
208+
}
209+
}
210+
Ok(())
211+
}
212+
213+
fn iter_errors<'i>(
214+
&self,
215+
instance: &'i Value,
216+
location: &LazyLocation,
217+
tracker: Option<&RefTracker>,
218+
_ctx: &mut ValidationContext,
219+
) -> ErrorIterator<'i> {
220+
if let Value::Object(item) = instance {
221+
let eval_path = crate::paths::capture_evaluation_path(tracker, &self.location);
222+
let mut errors = Vec::new();
223+
if !item.contains_key(&self.first) {
224+
errors.push(ValidationError::required(
225+
self.location.clone(),
226+
eval_path.clone(),
227+
location.into(),
228+
instance,
229+
Value::String(self.first.clone()),
230+
));
231+
}
232+
if !item.contains_key(&self.second) {
233+
errors.push(ValidationError::required(
234+
self.location.clone(),
235+
eval_path,
236+
location.into(),
237+
instance,
238+
Value::String(self.second.clone()),
239+
));
240+
}
241+
if !errors.is_empty() {
242+
return ErrorIterator::from_iterator(errors.into_iter());
243+
}
244+
}
245+
no_error()
246+
}
247+
}
248+
249+
/// Specialized validator for exactly 3 required properties.
250+
/// Uses fixed-size fields and unrolled checks to avoid Vec/iterator overhead.
251+
pub(crate) struct Required3Validator {
252+
first: String,
253+
second: String,
254+
third: String,
255+
location: Location,
256+
}
257+
258+
impl Required3Validator {
259+
#[inline]
260+
pub(crate) fn compile(
261+
first: String,
262+
second: String,
263+
third: String,
264+
location: Location,
265+
) -> CompilationResult<'static> {
266+
Ok(Box::new(Required3Validator {
267+
first,
268+
second,
269+
third,
270+
location,
271+
}))
272+
}
273+
}
274+
275+
impl Validate for Required3Validator {
276+
#[inline]
277+
fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool {
278+
if let Value::Object(item) = instance {
279+
item.len() >= 3
280+
&& item.contains_key(&self.first)
281+
&& item.contains_key(&self.second)
282+
&& item.contains_key(&self.third)
283+
} else {
284+
true
285+
}
286+
}
287+
288+
fn validate<'i>(
289+
&self,
290+
instance: &'i Value,
291+
location: &LazyLocation,
292+
tracker: Option<&RefTracker>,
293+
_ctx: &mut ValidationContext,
294+
) -> Result<(), ValidationError<'i>> {
295+
if let Value::Object(item) = instance {
296+
if !item.contains_key(&self.first) {
297+
return Err(ValidationError::required(
298+
self.location.clone(),
299+
crate::paths::capture_evaluation_path(tracker, &self.location),
300+
location.into(),
301+
instance,
302+
Value::String(self.first.clone()),
303+
));
304+
}
305+
if !item.contains_key(&self.second) {
306+
return Err(ValidationError::required(
307+
self.location.clone(),
308+
crate::paths::capture_evaluation_path(tracker, &self.location),
309+
location.into(),
310+
instance,
311+
Value::String(self.second.clone()),
312+
));
313+
}
314+
if !item.contains_key(&self.third) {
315+
return Err(ValidationError::required(
316+
self.location.clone(),
317+
crate::paths::capture_evaluation_path(tracker, &self.location),
318+
location.into(),
319+
instance,
320+
Value::String(self.third.clone()),
321+
));
322+
}
323+
}
324+
Ok(())
325+
}
326+
327+
fn iter_errors<'i>(
328+
&self,
329+
instance: &'i Value,
330+
location: &LazyLocation,
331+
tracker: Option<&RefTracker>,
332+
_ctx: &mut ValidationContext,
333+
) -> ErrorIterator<'i> {
334+
if let Value::Object(item) = instance {
335+
let eval_path = crate::paths::capture_evaluation_path(tracker, &self.location);
336+
let mut errors = Vec::new();
337+
if !item.contains_key(&self.first) {
338+
errors.push(ValidationError::required(
339+
self.location.clone(),
340+
eval_path.clone(),
341+
location.into(),
342+
instance,
343+
Value::String(self.first.clone()),
344+
));
345+
}
346+
if !item.contains_key(&self.second) {
347+
errors.push(ValidationError::required(
348+
self.location.clone(),
349+
eval_path.clone(),
350+
location.into(),
351+
instance,
352+
Value::String(self.second.clone()),
353+
));
354+
}
355+
if !item.contains_key(&self.third) {
356+
errors.push(ValidationError::required(
357+
self.location.clone(),
358+
eval_path,
359+
location.into(),
360+
instance,
361+
Value::String(self.third.clone()),
362+
));
363+
}
364+
if !errors.is_empty() {
365+
return ErrorIterator::from_iterator(errors.into_iter());
366+
}
367+
}
368+
no_error()
369+
}
370+
}
371+
150372
#[inline]
151373
pub(crate) fn compile<'a>(
152374
ctx: &compiler::Context,
@@ -164,8 +386,8 @@ pub(crate) fn compile_with_path(
164386
) -> Option<CompilationResult<'_>> {
165387
// IMPORTANT: If this function will ever return `None`, adjust `dependencies.rs` accordingly
166388
match schema {
167-
Value::Array(items) => {
168-
if items.len() == 1 {
389+
Value::Array(items) => match items.len() {
390+
1 => {
169391
let item = &items[0];
170392
if let Value::String(item) = item {
171393
Some(SingleItemRequiredValidator::compile(item, location))
@@ -178,10 +400,48 @@ pub(crate) fn compile_with_path(
178400
JsonType::String,
179401
)))
180402
}
181-
} else {
182-
Some(RequiredValidator::compile(items, location))
183403
}
184-
}
404+
2 => {
405+
let (first, second) = (&items[0], &items[1]);
406+
match (first, second) {
407+
(Value::String(first), Value::String(second)) => Some(
408+
Required2Validator::compile(first.clone(), second.clone(), location),
409+
),
410+
(Value::String(_), other) | (other, _) => {
411+
Some(Err(ValidationError::single_type_error(
412+
location.clone(),
413+
location,
414+
Location::new(),
415+
other,
416+
JsonType::String,
417+
)))
418+
}
419+
}
420+
}
421+
3 => {
422+
let (first, second, third) = (&items[0], &items[1], &items[2]);
423+
match (first, second, third) {
424+
(Value::String(first), Value::String(second), Value::String(third)) => {
425+
Some(Required3Validator::compile(
426+
first.clone(),
427+
second.clone(),
428+
third.clone(),
429+
location,
430+
))
431+
}
432+
(Value::String(_), Value::String(_), other)
433+
| (Value::String(_), other, _)
434+
| (other, _, _) => Some(Err(ValidationError::single_type_error(
435+
location.clone(),
436+
location,
437+
Location::new(),
438+
other,
439+
JsonType::String,
440+
))),
441+
}
442+
}
443+
_ => Some(RequiredValidator::compile(items, location)),
444+
},
185445
_ => Some(Err(ValidationError::single_type_error(
186446
location.clone(),
187447
location,
@@ -200,7 +460,82 @@ mod tests {
200460

201461
#[test_case(&json!({"required": ["a"]}), &json!({}), "/required")]
202462
#[test_case(&json!({"required": ["a", "b"]}), &json!({}), "/required")]
463+
#[test_case(&json!({"required": ["a", "b", "c"]}), &json!({}), "/required")]
203464
fn location(schema: &Value, instance: &Value, expected: &str) {
204465
tests_util::assert_schema_location(schema, instance, expected);
205466
}
467+
468+
// Required2Validator tests
469+
#[test_case(&json!({"a": 1, "b": 2}), true)]
470+
#[test_case(&json!({"a": 1, "b": 2, "c": 3}), true)]
471+
#[test_case(&json!({"a": 1}), false)]
472+
#[test_case(&json!({"b": 2}), false)]
473+
#[test_case(&json!({}), false)]
474+
#[test_case(&json!([1, 2]), true)] // Non-object passes
475+
fn required_2(instance: &Value, expected: bool) {
476+
let schema = json!({"required": ["a", "b"]});
477+
let validator = crate::validator_for(&schema).unwrap();
478+
assert_eq!(validator.is_valid(instance), expected);
479+
}
480+
481+
// Required3Validator tests
482+
#[test_case(&json!({"a": 1, "b": 2, "c": 3}), true)]
483+
#[test_case(&json!({"a": 1, "b": 2, "c": 3, "d": 4}), true)]
484+
#[test_case(&json!({"a": 1, "b": 2}), false)]
485+
#[test_case(&json!({"a": 1, "c": 3}), false)]
486+
#[test_case(&json!({"b": 2, "c": 3}), false)]
487+
#[test_case(&json!({}), false)]
488+
#[test_case(&json!("string"), true)] // Non-object passes
489+
fn required_3(instance: &Value, expected: bool) {
490+
let schema = json!({"required": ["a", "b", "c"]});
491+
let validator = crate::validator_for(&schema).unwrap();
492+
assert_eq!(validator.is_valid(instance), expected);
493+
}
494+
495+
#[test]
496+
fn required_2_iter_errors() {
497+
let schema = json!({"required": ["a", "b"]});
498+
let validator = crate::validator_for(&schema).unwrap();
499+
500+
// Missing both
501+
let instance = json!({});
502+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
503+
assert_eq!(errors.len(), 2);
504+
505+
// Missing one
506+
let instance = json!({"a": 1});
507+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
508+
assert_eq!(errors.len(), 1);
509+
510+
// All present
511+
let instance = json!({"a": 1, "b": 2});
512+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
513+
assert!(errors.is_empty());
514+
}
515+
516+
#[test]
517+
fn required_3_iter_errors() {
518+
let schema = json!({"required": ["a", "b", "c"]});
519+
let validator = crate::validator_for(&schema).unwrap();
520+
521+
// Missing all
522+
let instance = json!({});
523+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
524+
assert_eq!(errors.len(), 3);
525+
526+
// Missing two
527+
let instance = json!({"a": 1});
528+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
529+
assert_eq!(errors.len(), 2);
530+
531+
// Missing one
532+
let instance = json!({"a": 1, "b": 2});
533+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
534+
assert_eq!(errors.len(), 1);
535+
536+
// All present
537+
let instance = json!({"a": 1, "b": 2, "c": 3});
538+
let errors: Vec<_> = validator.iter_errors(&instance).collect();
539+
assert!(errors.is_empty());
540+
}
206541
}

0 commit comments

Comments
 (0)