refactor(typedef): deduplicate shared code, fix bugs, add macros
Bug fixes: - sequential_reader: use get_typedef_kind_loose instead of strict get_typedef_kind so object-annotation form schemas parse correctly - validation: string_factory/bytes_factory accept object-annotation form Deduplication (moved to schema.rs): - get_typedef_kind_loose (was in 3 files) - is_fixed_size replaces local is_fixed_kind (was in 2 files) - resolve_ref_or_inline + resolve_ref (was in 2 files) - U32_SIZE and DISCRIMINATOR_PATH constants (were in 3-4 files) New macros (src/macros.rs): - define_int_validator!, define_uint_validator!, define_float_validator!, define_type_validator! (eliminate ~200 lines of boilerplate in validation.rs) - define_read_write_endian!, define_read_write_ne! (eliminate ~320 lines of boilerplate in data_access.rs) Net: ~1,170 lines removed, 288 tests pass, clippy clean
This commit is contained in:
1 parent
998f6b6dc9
commit
72aa79b6ee
10 files changed
+386
-799
No files matched your search
@@ -45,7 +45,7 @@ fn access_err(field_path: &str, reason: impl Into<String>) -> TypedefError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_array<const N: usize>(
|
pub(crate) fn read_array<const N: usize>(
|
||||||
buffer: &[u8],
|
buffer: &[u8],
|
||||||
offset: usize,
|
offset: usize,
|
||||||
field_path: &str,
|
field_path: &str,
|
||||||
@@ -74,7 +74,7 @@ fn read_array<const N: usize>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_array<const N: usize>(
|
pub(crate) fn write_array<const N: usize>(
|
||||||
buffer: &mut [u8],
|
buffer: &mut [u8],
|
||||||
offset: usize,
|
offset: usize,
|
||||||
bytes: [u8; N],
|
bytes: [u8; N],
|
||||||
@@ -115,112 +115,15 @@ fn u32_to(value: u32, endian: Endian) -> [u8; U32_SIZE] {
|
|||||||
// Fixed-size read functions
|
// Fixed-size read functions
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Read an `i8` at `offset` from `buffer`.
|
define_read_write_ne!(i8, read_i8, write_i8, 1, |bytes: [u8; 1]| bytes[0] as i8);
|
||||||
pub fn read_i8(buffer: &[u8], offset: usize, field_path: &str) -> Result<i8, TypedefError> {
|
define_read_write_endian!(i16, read_i16, write_i16, 2);
|
||||||
let bytes: [u8; 1] = read_array(buffer, offset, field_path)?;
|
define_read_write_endian!(i32, read_i32, write_i32, 4);
|
||||||
Ok(bytes[0] as i8)
|
define_read_write_ne!(u8, read_u8, write_u8, 1, |bytes: [u8; 1]| bytes[0]);
|
||||||
}
|
define_read_write_endian!(u16, read_u16, write_u16, 2);
|
||||||
|
define_read_write_endian!(u32, read_u32, write_u32, 4);
|
||||||
/// Read an `i16` at `offset` from `buffer`, applying `endian`.
|
define_read_write_endian!(u64, read_u64, write_u64, 8);
|
||||||
pub fn read_i16(
|
define_read_write_endian!(f32, read_f32, write_f32, 4);
|
||||||
buffer: &[u8],
|
define_read_write_endian!(f64, read_f64, write_f64, 8);
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<i16, TypedefError> {
|
|
||||||
let bytes: [u8; 2] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(match endian {
|
|
||||||
Endian::Little => i16::from_le_bytes(bytes),
|
|
||||||
Endian::Big => i16::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an `i32` at `offset` from `buffer`, applying `endian`.
|
|
||||||
pub fn read_i32(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<i32, TypedefError> {
|
|
||||||
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(match endian {
|
|
||||||
Endian::Little => i32::from_le_bytes(bytes),
|
|
||||||
Endian::Big => i32::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a `u8` at `offset` from `buffer`.
|
|
||||||
pub fn read_u8(buffer: &[u8], offset: usize, field_path: &str) -> Result<u8, TypedefError> {
|
|
||||||
let bytes: [u8; 1] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(bytes[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a `u16` at `offset` from `buffer`, applying `endian`.
|
|
||||||
pub fn read_u16(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<u16, TypedefError> {
|
|
||||||
let bytes: [u8; 2] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(match endian {
|
|
||||||
Endian::Little => u16::from_le_bytes(bytes),
|
|
||||||
Endian::Big => u16::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a `u32` at `offset` from `buffer`, applying `endian`.
|
|
||||||
pub fn read_u32(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<u32, TypedefError> {
|
|
||||||
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(u32_from(bytes, endian))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a `u64` at `offset` from `buffer`, applying `endian`.
|
|
||||||
pub fn read_u64(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<u64, TypedefError> {
|
|
||||||
let bytes: [u8; 8] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(match endian {
|
|
||||||
Endian::Little => u64::from_le_bytes(bytes),
|
|
||||||
Endian::Big => u64::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an `f32` at `offset` from `buffer`, applying `endian`.
|
|
||||||
pub fn read_f32(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<f32, TypedefError> {
|
|
||||||
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(match endian {
|
|
||||||
Endian::Little => f32::from_le_bytes(bytes),
|
|
||||||
Endian::Big => f32::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an `f64` at `offset` from `buffer`, applying `endian`.
|
|
||||||
pub fn read_f64(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<f64, TypedefError> {
|
|
||||||
let bytes: [u8; 8] = read_array(buffer, offset, field_path)?;
|
|
||||||
Ok(match endian {
|
|
||||||
Endian::Little => f64::from_le_bytes(bytes),
|
|
||||||
Endian::Big => f64::from_be_bytes(bytes),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a `bool` at `offset` from `buffer`.
|
/// Read a `bool` at `offset` from `buffer`.
|
||||||
///
|
///
|
||||||
@@ -239,143 +142,6 @@ pub fn read_bool(buffer: &[u8], offset: usize, field_path: &str) -> Result<bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a `TEnum` index (`u32`) at `offset` from `buffer`, applying `endian`.
|
|
||||||
///
|
|
||||||
/// The caller maps the returned index to the schema's `"enum"` array entry.
|
|
||||||
pub fn read_enum(
|
|
||||||
buffer: &[u8],
|
|
||||||
offset: usize,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<u32, TypedefError> {
|
|
||||||
read_u32(buffer, offset, field_path, endian)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Fixed-size write functions
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Write an `i8` `value` at `offset` into `buffer`.
|
|
||||||
pub fn write_i8(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: i8,
|
|
||||||
field_path: &str,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
write_array(buffer, offset, value.to_ne_bytes(), field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write an `i16` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_i16(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: i16,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
let bytes = match endian {
|
|
||||||
Endian::Little => value.to_le_bytes(),
|
|
||||||
Endian::Big => value.to_be_bytes(),
|
|
||||||
};
|
|
||||||
write_array(buffer, offset, bytes, field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write an `i32` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_i32(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: i32,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
let bytes = match endian {
|
|
||||||
Endian::Little => value.to_le_bytes(),
|
|
||||||
Endian::Big => value.to_be_bytes(),
|
|
||||||
};
|
|
||||||
write_array(buffer, offset, bytes, field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a `u8` `value` at `offset` into `buffer`.
|
|
||||||
pub fn write_u8(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: u8,
|
|
||||||
field_path: &str,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
write_array(buffer, offset, value.to_ne_bytes(), field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a `u16` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_u16(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: u16,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
let bytes = match endian {
|
|
||||||
Endian::Little => value.to_le_bytes(),
|
|
||||||
Endian::Big => value.to_be_bytes(),
|
|
||||||
};
|
|
||||||
write_array(buffer, offset, bytes, field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a `u32` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_u32(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: u32,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
write_array(buffer, offset, u32_to(value, endian), field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a `u64` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_u64(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: u64,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
let bytes = match endian {
|
|
||||||
Endian::Little => value.to_le_bytes(),
|
|
||||||
Endian::Big => value.to_be_bytes(),
|
|
||||||
};
|
|
||||||
write_array(buffer, offset, bytes, field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write an `f32` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_f32(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: f32,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
let bytes = match endian {
|
|
||||||
Endian::Little => value.to_le_bytes(),
|
|
||||||
Endian::Big => value.to_be_bytes(),
|
|
||||||
};
|
|
||||||
write_array(buffer, offset, bytes, field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write an `f64` `value` at `offset` into `buffer`, applying `endian`.
|
|
||||||
pub fn write_f64(
|
|
||||||
buffer: &mut [u8],
|
|
||||||
offset: usize,
|
|
||||||
value: f64,
|
|
||||||
field_path: &str,
|
|
||||||
endian: Endian,
|
|
||||||
) -> Result<(), TypedefError> {
|
|
||||||
let bytes = match endian {
|
|
||||||
Endian::Little => value.to_le_bytes(),
|
|
||||||
Endian::Big => value.to_be_bytes(),
|
|
||||||
};
|
|
||||||
write_array(buffer, offset, bytes, field_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a `bool` `value` at `offset` into `buffer`.
|
/// Write a `bool` `value` at `offset` into `buffer`.
|
||||||
///
|
///
|
||||||
/// `false` is encoded as `0x00`, `true` as `0x01`.
|
/// `false` is encoded as `0x00`, `true` as `0x01`.
|
||||||
@@ -393,6 +159,18 @@ pub fn write_bool(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a `TEnum` index (`u32`) at `offset` from `buffer`, applying `endian`.
|
||||||
|
///
|
||||||
|
/// The caller maps the returned index to the schema's `"enum"` array entry.
|
||||||
|
pub fn read_enum(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<u32, TypedefError> {
|
||||||
|
read_u32(buffer, offset, field_path, endian)
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a `TEnum` index (`u32`) `value` at `offset` into `buffer`, applying `endian`.
|
/// Write a `TEnum` index (`u32`) `value` at `offset` into `buffer`, applying `endian`.
|
||||||
pub fn write_enum(
|
pub fn write_enum(
|
||||||
buffer: &mut [u8],
|
buffer: &mut [u8],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::data_access;
|
|||||||
use crate::error::TypedefError;
|
use crate::error::TypedefError;
|
||||||
use crate::layout_builder::LayoutBuilder;
|
use crate::layout_builder::LayoutBuilder;
|
||||||
use crate::offset_map::OffsetMap;
|
use crate::offset_map::OffsetMap;
|
||||||
use crate::schema::{self, Endian};
|
use crate::schema::{self, get_typedef_kind_loose, Endian};
|
||||||
use crate::sequential_reader::{FieldValue, SequentialReader};
|
use crate::sequential_reader::{FieldValue, SequentialReader};
|
||||||
use crate::validation;
|
use crate::validation;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -206,7 +206,7 @@ impl TypedefEngine {
|
|||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "field schema not found in schema tree".to_string(),
|
reason: "field schema not found in schema tree".to_string(),
|
||||||
})?;
|
})?;
|
||||||
let kind = typedef_kind_loose(field_schema).ok_or_else(|| TypedefError::Offset {
|
let kind = get_typedef_kind_loose(field_schema).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "field schema has no TypeDef:* kind".to_string(),
|
reason: "field schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
@@ -400,23 +400,6 @@ fn lookup_field_schema<'a>(schema: &'a Value, field_path: &str) -> Option<&'a Va
|
|||||||
Some(current)
|
Some(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect a `TypeDef:*` kind from a schema node, accepting either the
|
|
||||||
/// boolean form (`{ "TypeDef:String": true }`) or the object-annotation
|
|
||||||
/// form (`{ "TypeDef:String": { "encoding": "..." } }`).
|
|
||||||
///
|
|
||||||
/// [`crate::schema::get_typedef_kind`] only recognizes the boolean form;
|
|
||||||
/// the engine's read/write dispatch needs to recognize the object form
|
|
||||||
/// too so that variable-length encoding annotations are honored.
|
|
||||||
fn typedef_kind_loose(node: &Value) -> Option<&str> {
|
|
||||||
let obj = node.as_object()?;
|
|
||||||
for key in obj.keys() {
|
|
||||||
if key.starts_with("TypeDef:") && obj.get(key).is_some_and(|v| !v.is_null()) {
|
|
||||||
return Some(key.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -754,6 +737,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn typedef_kind_loose_recognizes_object_form() {
|
fn typedef_kind_loose_recognizes_object_form() {
|
||||||
let node = json!({ "TypeDef:String": { "encoding": "offset-indirect" } });
|
let node = json!({ "TypeDef:String": { "encoding": "offset-indirect" } });
|
||||||
assert_eq!(typedef_kind_loose(&node), Some("TypeDef:String"));
|
assert_eq!(get_typedef_kind_loose(&node), Some("TypeDef:String"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,12 +46,10 @@
|
|||||||
//! the variant struct.
|
//! the variant struct.
|
||||||
|
|
||||||
use crate::error::TypedefError;
|
use crate::error::TypedefError;
|
||||||
use crate::schema::{self, DiscriminatorKind, Endian};
|
use crate::schema::{self, get_typedef_kind_loose, resolve_ref_or_inline, DiscriminatorKind, Endian, DISCRIMINATOR_PATH, U32_SIZE};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
const U32_SIZE: usize = 4;
|
|
||||||
const DISCRIMINATOR_PATH: &str = "__discriminator";
|
|
||||||
const VARIANT_KEY: &str = "__variant";
|
const VARIANT_KEY: &str = "__variant";
|
||||||
|
|
||||||
/// A field position computed by the LayoutBuilder.
|
/// A field position computed by the LayoutBuilder.
|
||||||
@@ -253,7 +251,7 @@ impl<'a> BuildCtx<'a> {
|
|||||||
field_path: &str,
|
field_path: &str,
|
||||||
offset: &mut usize,
|
offset: &mut usize,
|
||||||
) -> Result<(), TypedefError> {
|
) -> Result<(), TypedefError> {
|
||||||
let kind = typedef_kind_loose(field_schema).ok_or_else(|| TypedefError::Offset {
|
let kind = get_typedef_kind_loose(field_schema).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "field schema has no TypeDef:* kind".to_string(),
|
reason: "field schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
@@ -265,7 +263,7 @@ impl<'a> BuildCtx<'a> {
|
|||||||
"TypeDef:String" | "TypeDef:Bytes" | "TypeDef:Timestamp" | "TypeDef:Record" => {
|
"TypeDef:String" | "TypeDef:Bytes" | "TypeDef:Timestamp" | "TypeDef:Record" => {
|
||||||
self.walk_variable(field_path, offset, kind)
|
self.walk_variable(field_path, offset, kind)
|
||||||
}
|
}
|
||||||
fixed if is_fixed_kind(fixed) => {
|
fixed if schema::is_fixed_size(fixed) => {
|
||||||
let size = schema::type_size(fixed).ok_or_else(|| TypedefError::Offset {
|
let size = schema::type_size(fixed).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: format!("type_size returned None for fixed kind {fixed}"),
|
reason: format!("type_size returned None for fixed kind {fixed}"),
|
||||||
@@ -343,12 +341,12 @@ impl<'a> BuildCtx<'a> {
|
|||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "could not resolve TArray items schema".to_string(),
|
reason: "could not resolve TArray items schema".to_string(),
|
||||||
})?;
|
})?;
|
||||||
let elem_kind = typedef_kind_loose(element_schema).ok_or_else(|| TypedefError::Offset {
|
let elem_kind = get_typedef_kind_loose(element_schema).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "TArray element schema has no TypeDef:* kind".to_string(),
|
reason: "TArray element schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !is_fixed_kind(elem_kind) {
|
if !schema::is_fixed_size(elem_kind) {
|
||||||
return Err(TypedefError::Offset {
|
return Err(TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: format!(
|
reason: format!(
|
||||||
@@ -519,7 +517,7 @@ impl<'a> BuildCtx<'a> {
|
|||||||
reason: "could not resolve TUnion variant schema ($ref not found)".to_string(),
|
reason: "could not resolve TUnion variant schema ($ref not found)".to_string(),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
let v_kind = typedef_kind_loose(resolved).ok_or_else(|| TypedefError::Offset {
|
let v_kind = get_typedef_kind_loose(resolved).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "TUnion variant schema has no TypeDef:* kind".to_string(),
|
reason: "TUnion variant schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
@@ -582,7 +580,7 @@ impl<'a> BuildCtx<'a> {
|
|||||||
reason: "could not resolve TUnion variant schema ($ref not found)".to_string(),
|
reason: "could not resolve TUnion variant schema ($ref not found)".to_string(),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
let v_kind = typedef_kind_loose(resolved).ok_or_else(|| TypedefError::Offset {
|
let v_kind = get_typedef_kind_loose(resolved).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "TUnion variant schema has no TypeDef:* kind".to_string(),
|
reason: "TUnion variant schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
@@ -609,71 +607,6 @@ impl<'a> BuildCtx<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if `kind` is one of the fixed-size primitive kinds.
|
|
||||||
fn is_fixed_kind(kind: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
kind,
|
|
||||||
"TypeDef:Float32"
|
|
||||||
| "TypeDef:Float64"
|
|
||||||
| "TypeDef:Int8"
|
|
||||||
| "TypeDef:Int16"
|
|
||||||
| "TypeDef:Int32"
|
|
||||||
| "TypeDef:Uint8"
|
|
||||||
| "TypeDef:Uint16"
|
|
||||||
| "TypeDef:Uint32"
|
|
||||||
| "TypeDef:Boolean"
|
|
||||||
| "TypeDef:Enum"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect a `TypeDef:*` kind from a schema node, accepting either the
|
|
||||||
/// boolean form (`{ "TypeDef:String": true }`) or the object-annotation
|
|
||||||
/// form (`{ "TypeDef:String": { "encoding": "..." } }`).
|
|
||||||
///
|
|
||||||
/// [`crate::schema::get_typedef_kind`] only recognizes the boolean form;
|
|
||||||
/// packed layout computation also needs to recognize the object form so
|
|
||||||
/// that variable-length encoding annotations don't hide the kind.
|
|
||||||
fn typedef_kind_loose(node: &Value) -> Option<&str> {
|
|
||||||
let obj = node.as_object()?;
|
|
||||||
for key in obj.keys() {
|
|
||||||
if key.starts_with("TypeDef:") && obj.get(key).is_some_and(|v| !v.is_null()) {
|
|
||||||
return Some(key.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a `$ref` against the root schema, or return the inline schema.
|
|
||||||
///
|
|
||||||
/// If `node` has a `"$ref"` key, parse the JSON Pointer and walk `root`.
|
|
||||||
/// Otherwise, return `node` itself (it's an inline schema).
|
|
||||||
fn resolve_ref_or_inline<'b>(node: &'b Value, root: &'b Value) -> Option<&'b Value> {
|
|
||||||
let obj = node.as_object()?;
|
|
||||||
if let Some(Value::String(ref_path)) = obj.get("$ref") {
|
|
||||||
return resolve_ref(root, ref_path);
|
|
||||||
}
|
|
||||||
Some(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a JSON Pointer `$ref` (e.g., `"#/$defs/Read"`) against `root`.
|
|
||||||
fn resolve_ref<'b>(root: &'b Value, ref_path: &str) -> Option<&'b Value> {
|
|
||||||
let stripped = ref_path.strip_prefix('#').unwrap_or(ref_path);
|
|
||||||
let stripped = stripped.strip_prefix('/').unwrap_or(stripped);
|
|
||||||
if stripped.is_empty() {
|
|
||||||
return Some(root);
|
|
||||||
}
|
|
||||||
let mut current = root;
|
|
||||||
for segment in stripped.split('/') {
|
|
||||||
let decoded = segment.replace("~1", "/").replace("~0", "~");
|
|
||||||
if let Ok(idx) = decoded.parse::<usize>() {
|
|
||||||
current = current.get(idx)?;
|
|
||||||
} else {
|
|
||||||
current = current.get(&decoded)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
//! - **Engine** ([`engine`]): `TypedefEngine` — the compiled form of a
|
//! - **Engine** ([`engine`]): `TypedefEngine` — the compiled form of a
|
||||||
//! schema, combining layout and validation.
|
//! schema, combining layout and validation.
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
pub mod data_access;
|
pub mod data_access;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -36,8 +38,9 @@ pub use error::TypedefError;
|
|||||||
pub use layout_builder::{FieldPosition, LayoutBuilder, PackedLayout};
|
pub use layout_builder::{FieldPosition, LayoutBuilder, PackedLayout};
|
||||||
pub use offset_map::{ByteRange, OffsetMap};
|
pub use offset_map::{ByteRange, OffsetMap};
|
||||||
pub use schema::{
|
pub use schema::{
|
||||||
normalize_refs, parse_align, parse_discriminator, parse_encoding, parse_endian,
|
get_typedef_kind_loose, normalize_refs, parse_align, parse_discriminator, parse_encoding,
|
||||||
parse_max_length, DiscriminatorKind, Endian, VariableEncoding,
|
parse_endian, parse_max_length, resolve_ref, resolve_ref_or_inline, DiscriminatorKind, Endian,
|
||||||
|
VariableEncoding,
|
||||||
};
|
};
|
||||||
pub use sequential_reader::{FieldValue, SequentialReader};
|
pub use sequential_reader::{FieldValue, SequentialReader};
|
||||||
pub use tunion::UnionDispatch;
|
pub use tunion::UnionDispatch;
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
//! Macros for generating repetitive code across the 17 TypeDef kinds.
|
||||||
|
//!
|
||||||
|
//! These macros eliminate boilerplate in validation and data access.
|
||||||
|
//! Each macro takes a compact specification and generates the full
|
||||||
|
//! implementation, ensuring consistency across all types.
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Validation macros
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Generate a signed integer validator struct and its factory closure.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_int_validator {
|
||||||
|
($validator_struct:ident, $factory_fn:ident, $keyword:literal, $min:literal, $max:literal) => {
|
||||||
|
struct $validator_struct;
|
||||||
|
impl jsonschema::Keyword for $validator_struct {
|
||||||
|
fn validate<'i>(
|
||||||
|
&self,
|
||||||
|
instance: &'i serde_json::Value,
|
||||||
|
) -> Result<(), jsonschema::ValidationError<'i>> {
|
||||||
|
match instance.as_i64() {
|
||||||
|
Some(n) if ($min..=$max).contains(&n) => Ok(()),
|
||||||
|
_ => Err(jsonschema::ValidationError::custom(concat!(
|
||||||
|
"expected an integer in range [",
|
||||||
|
stringify!($min),
|
||||||
|
", ",
|
||||||
|
stringify!($max),
|
||||||
|
"]"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_valid(&self, instance: &serde_json::Value) -> bool {
|
||||||
|
instance
|
||||||
|
.as_i64()
|
||||||
|
.is_some_and(|n| ($min..=$max).contains(&n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn $factory_fn<'a>(
|
||||||
|
_parent: &'a serde_json::Map<String, serde_json::Value>,
|
||||||
|
value: &'a serde_json::Value,
|
||||||
|
_path: jsonschema::paths::Location,
|
||||||
|
) -> Result<Box<dyn jsonschema::Keyword>, jsonschema::ValidationError<'a>> {
|
||||||
|
if value.as_bool() == Some(true) {
|
||||||
|
Ok(Box::new($validator_struct))
|
||||||
|
} else {
|
||||||
|
Err(jsonschema::ValidationError::schema(concat!(
|
||||||
|
$keyword,
|
||||||
|
" must be set to true"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an unsigned integer validator struct and its factory closure.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_uint_validator {
|
||||||
|
($validator_struct:ident, $factory_fn:ident, $keyword:literal, $max:literal) => {
|
||||||
|
struct $validator_struct;
|
||||||
|
impl jsonschema::Keyword for $validator_struct {
|
||||||
|
fn validate<'i>(
|
||||||
|
&self,
|
||||||
|
instance: &'i serde_json::Value,
|
||||||
|
) -> Result<(), jsonschema::ValidationError<'i>> {
|
||||||
|
match instance.as_u64() {
|
||||||
|
Some(n) if n <= $max => Ok(()),
|
||||||
|
_ => Err(jsonschema::ValidationError::custom(concat!(
|
||||||
|
"expected an unsigned integer in range [0, ",
|
||||||
|
stringify!($max),
|
||||||
|
"]"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_valid(&self, instance: &serde_json::Value) -> bool {
|
||||||
|
instance.as_u64().is_some_and(|n| n <= $max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn $factory_fn<'a>(
|
||||||
|
_parent: &'a serde_json::Map<String, serde_json::Value>,
|
||||||
|
value: &'a serde_json::Value,
|
||||||
|
_path: jsonschema::paths::Location,
|
||||||
|
) -> Result<Box<dyn jsonschema::Keyword>, jsonschema::ValidationError<'a>> {
|
||||||
|
if value.as_bool() == Some(true) {
|
||||||
|
Ok(Box::new($validator_struct))
|
||||||
|
} else {
|
||||||
|
Err(jsonschema::ValidationError::schema(concat!(
|
||||||
|
$keyword,
|
||||||
|
" must be set to true"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a float validator struct and its factory closure.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_float_validator {
|
||||||
|
($validator_struct:ident, $factory_fn:ident, $keyword:literal, $error_msg:literal) => {
|
||||||
|
struct $validator_struct;
|
||||||
|
impl jsonschema::Keyword for $validator_struct {
|
||||||
|
fn validate<'i>(
|
||||||
|
&self,
|
||||||
|
instance: &'i serde_json::Value,
|
||||||
|
) -> Result<(), jsonschema::ValidationError<'i>> {
|
||||||
|
match instance.as_f64() {
|
||||||
|
Some(f) if f.is_finite() => Ok(()),
|
||||||
|
_ => Err(jsonschema::ValidationError::custom($error_msg)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_valid(&self, instance: &serde_json::Value) -> bool {
|
||||||
|
instance.as_f64().is_some_and(|f| f.is_finite())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn $factory_fn<'a>(
|
||||||
|
_parent: &'a serde_json::Map<String, serde_json::Value>,
|
||||||
|
value: &'a serde_json::Value,
|
||||||
|
_path: jsonschema::paths::Location,
|
||||||
|
) -> Result<Box<dyn jsonschema::Keyword>, jsonschema::ValidationError<'a>> {
|
||||||
|
if value.as_bool() == Some(true) {
|
||||||
|
Ok(Box::new($validator_struct))
|
||||||
|
} else {
|
||||||
|
Err(jsonschema::ValidationError::schema(concat!(
|
||||||
|
$keyword,
|
||||||
|
" must be set to true"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a simple type-check validator (object/array/boolean) and its factory.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_type_validator {
|
||||||
|
($validator_struct:ident, $factory_fn:ident, $keyword:literal, $check_method:ident, $error_msg:literal) => {
|
||||||
|
struct $validator_struct;
|
||||||
|
impl jsonschema::Keyword for $validator_struct {
|
||||||
|
fn validate<'i>(
|
||||||
|
&self,
|
||||||
|
instance: &'i serde_json::Value,
|
||||||
|
) -> Result<(), jsonschema::ValidationError<'i>> {
|
||||||
|
if instance.$check_method() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(jsonschema::ValidationError::custom($error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_valid(&self, instance: &serde_json::Value) -> bool {
|
||||||
|
instance.$check_method()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn $factory_fn<'a>(
|
||||||
|
_parent: &'a serde_json::Map<String, serde_json::Value>,
|
||||||
|
value: &'a serde_json::Value,
|
||||||
|
_path: jsonschema::paths::Location,
|
||||||
|
) -> Result<Box<dyn jsonschema::Keyword>, jsonschema::ValidationError<'a>> {
|
||||||
|
if value.as_bool() == Some(true) {
|
||||||
|
Ok(Box::new($validator_struct))
|
||||||
|
} else {
|
||||||
|
Err(jsonschema::ValidationError::schema(concat!(
|
||||||
|
$keyword,
|
||||||
|
" must be set to true"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Data access macros
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Generate a pair of read/write functions for a fixed-size endian-sensitive type.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_read_write_endian {
|
||||||
|
($rust_ty:ty, $read_name:ident, $write_name:ident, $size:literal) => {
|
||||||
|
#[doc = concat!(
|
||||||
|
"Read a `",
|
||||||
|
stringify!($rust_ty),
|
||||||
|
"` at `offset` from `buffer`, applying `endian`."
|
||||||
|
)]
|
||||||
|
pub fn $read_name(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: $crate::Endian,
|
||||||
|
) -> Result<$rust_ty, $crate::TypedefError> {
|
||||||
|
let bytes: [u8; $size] = $crate::data_access::read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
$crate::Endian::Little => <$rust_ty>::from_le_bytes(bytes),
|
||||||
|
$crate::Endian::Big => <$rust_ty>::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = concat!(
|
||||||
|
"Write a `",
|
||||||
|
stringify!($rust_ty),
|
||||||
|
"` `value` at `offset` into `buffer`, applying `endian`."
|
||||||
|
)]
|
||||||
|
pub fn $write_name(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: $rust_ty,
|
||||||
|
field_path: &str,
|
||||||
|
endian: $crate::Endian,
|
||||||
|
) -> Result<(), $crate::TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
$crate::Endian::Little => value.to_le_bytes(),
|
||||||
|
$crate::Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
$crate::data_access::write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a pair of read/write functions for a fixed-size endian-insensitive type.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_read_write_ne {
|
||||||
|
($rust_ty:ty, $read_name:ident, $write_name:ident, $size:literal, $read_expr:expr) => {
|
||||||
|
#[doc = concat!(
|
||||||
|
"Read a `",
|
||||||
|
stringify!($rust_ty),
|
||||||
|
"` at `offset` from `buffer`."
|
||||||
|
)]
|
||||||
|
pub fn $read_name(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<$rust_ty, $crate::TypedefError> {
|
||||||
|
let bytes: [u8; $size] = $crate::data_access::read_array(buffer, offset, field_path)?;
|
||||||
|
Ok($read_expr(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc = concat!(
|
||||||
|
"Write a `",
|
||||||
|
stringify!($rust_ty),
|
||||||
|
"` `value` at `offset` into `buffer`."
|
||||||
|
)]
|
||||||
|
pub fn $write_name(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: $rust_ty,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<(), $crate::TypedefError> {
|
||||||
|
$crate::data_access::write_array(buffer, offset, value.to_ne_bytes(), field_path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -13,14 +13,12 @@
|
|||||||
|
|
||||||
use crate::error::TypedefError;
|
use crate::error::TypedefError;
|
||||||
use crate::schema::{
|
use crate::schema::{
|
||||||
get_typedef_kind, natural_alignment, parse_align, parse_discriminator, parse_encoding,
|
get_typedef_kind, get_typedef_kind_loose, is_fixed_size, natural_alignment, parse_align,
|
||||||
parse_max_length, type_size, DiscriminatorKind, VariableEncoding,
|
parse_discriminator, parse_encoding, parse_max_length, resolve_ref_or_inline, type_size,
|
||||||
|
DiscriminatorKind, VariableEncoding, DISCRIMINATOR_PATH,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
/// The synthetic field path used for a TUnion byte-offset discriminator.
|
|
||||||
const DISCRIMINATOR_PATH: &str = "__discriminator";
|
|
||||||
|
|
||||||
/// A byte range within a buffer.
|
/// A byte range within a buffer.
|
||||||
///
|
///
|
||||||
/// Produced by [`OffsetMap::compute`] for each field in a schema. The
|
/// Produced by [`OffsetMap::compute`] for each field in a schema. The
|
||||||
@@ -209,7 +207,7 @@ impl<'a> ComputeCtx<'a> {
|
|||||||
field_path: &str,
|
field_path: &str,
|
||||||
struct_default_align: usize,
|
struct_default_align: usize,
|
||||||
) -> Result<FieldLayout, TypedefError> {
|
) -> Result<FieldLayout, TypedefError> {
|
||||||
let kind = typedef_kind_loose(field_schema).ok_or_else(|| TypedefError::Offset {
|
let kind = get_typedef_kind_loose(field_schema).ok_or_else(|| TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "field schema has no TypeDef:* kind".to_string(),
|
reason: "field schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
@@ -227,7 +225,7 @@ impl<'a> ComputeCtx<'a> {
|
|||||||
"TypeDef:String" | "TypeDef:Bytes" | "TypeDef:Record" | "TypeDef:Timestamp" => {
|
"TypeDef:String" | "TypeDef:Bytes" | "TypeDef:Record" | "TypeDef:Timestamp" => {
|
||||||
self.compute_variable_field(field_schema, field_path, struct_default_align)
|
self.compute_variable_field(field_schema, field_path, struct_default_align)
|
||||||
}
|
}
|
||||||
fixed if is_fixed_kind(fixed) => {
|
fixed if is_fixed_size(fixed) => {
|
||||||
self.compute_fixed_field(fixed, field_schema, field_path, struct_default_align)
|
self.compute_fixed_field(fixed, field_schema, field_path, struct_default_align)
|
||||||
}
|
}
|
||||||
other => Err(TypedefError::Offset {
|
other => Err(TypedefError::Offset {
|
||||||
@@ -474,7 +472,7 @@ impl<'a> ComputeCtx<'a> {
|
|||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: "TArray element schema has no TypeDef:* kind".to_string(),
|
reason: "TArray element schema has no TypeDef:* kind".to_string(),
|
||||||
})?;
|
})?;
|
||||||
if !is_fixed_kind(elem_kind) {
|
if !is_fixed_size(elem_kind) {
|
||||||
return Err(TypedefError::Offset {
|
return Err(TypedefError::Offset {
|
||||||
field_path: field_path.to_string(),
|
field_path: field_path.to_string(),
|
||||||
reason: format!(
|
reason: format!(
|
||||||
@@ -608,71 +606,6 @@ fn round_up(n: usize, align: usize) -> usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if `kind` is one of the fixed-size primitive kinds.
|
|
||||||
fn is_fixed_kind(kind: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
kind,
|
|
||||||
"TypeDef:Float32"
|
|
||||||
| "TypeDef:Float64"
|
|
||||||
| "TypeDef:Int8"
|
|
||||||
| "TypeDef:Int16"
|
|
||||||
| "TypeDef:Int32"
|
|
||||||
| "TypeDef:Uint8"
|
|
||||||
| "TypeDef:Uint16"
|
|
||||||
| "TypeDef:Uint32"
|
|
||||||
| "TypeDef:Boolean"
|
|
||||||
| "TypeDef:Enum"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect a `TypeDef:*` kind from a schema node, accepting either the
|
|
||||||
/// boolean form (`{ "TypeDef:String": true }`) or the object-annotation
|
|
||||||
/// form (`{ "TypeDef:String": { "encoding": "..." } }`).
|
|
||||||
///
|
|
||||||
/// [`crate::schema::get_typedef_kind`] only recognizes the boolean form;
|
|
||||||
/// offset computation also needs to recognize the object form so that
|
|
||||||
/// variable-length encoding annotations are honored.
|
|
||||||
fn typedef_kind_loose(node: &Value) -> Option<&str> {
|
|
||||||
let obj = node.as_object()?;
|
|
||||||
for key in obj.keys() {
|
|
||||||
if key.starts_with("TypeDef:") && obj.get(key).is_some_and(|v| !v.is_null()) {
|
|
||||||
return Some(key.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a `$ref` against the root schema, or return the inline schema.
|
|
||||||
///
|
|
||||||
/// If `node` has a `"$ref"` key, parse the JSON Pointer and walk `root`.
|
|
||||||
/// Otherwise, return `node` itself (it's an inline schema).
|
|
||||||
fn resolve_ref_or_inline<'a>(node: &'a Value, root: &'a Value) -> Option<&'a Value> {
|
|
||||||
let obj = node.as_object()?;
|
|
||||||
if let Some(Value::String(ref_path)) = obj.get("$ref") {
|
|
||||||
return resolve_ref(root, ref_path);
|
|
||||||
}
|
|
||||||
Some(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a JSON Pointer `$ref` (e.g., `"#/$defs/Read"`) against `root`.
|
|
||||||
fn resolve_ref<'a>(root: &'a Value, ref_path: &str) -> Option<&'a Value> {
|
|
||||||
let stripped = ref_path.strip_prefix('#').unwrap_or(ref_path);
|
|
||||||
let stripped = stripped.strip_prefix('/').unwrap_or(stripped);
|
|
||||||
if stripped.is_empty() {
|
|
||||||
return Some(root);
|
|
||||||
}
|
|
||||||
let mut current = root;
|
|
||||||
for segment in stripped.split('/') {
|
|
||||||
let decoded = segment.replace("~1", "/").replace("~0", "~");
|
|
||||||
if let Ok(idx) = decoded.parse::<usize>() {
|
|
||||||
current = current.get(idx)?;
|
|
||||||
} else {
|
|
||||||
current = current.get(&decoded)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ use serde_json::Value;
|
|||||||
|
|
||||||
const TYPEDEF_PREFIX: &str = "TypeDef:";
|
const TYPEDEF_PREFIX: &str = "TypeDef:";
|
||||||
|
|
||||||
|
pub(crate) const U32_SIZE: usize = 4;
|
||||||
|
pub(crate) const DISCRIMINATOR_PATH: &str = "__discriminator";
|
||||||
|
|
||||||
const BYTE_DISCRIMINATOR_TYPES: &[&str] = &["TypeDef:Uint8", "TypeDef:Uint16", "TypeDef:Uint32"];
|
const BYTE_DISCRIMINATOR_TYPES: &[&str] = &["TypeDef:Uint8", "TypeDef:Uint16", "TypeDef:Uint32"];
|
||||||
|
|
||||||
/// Returns the `TypeDef:*` kind string if the schema node declares one.
|
/// Returns the `TypeDef:*` kind string if the schema node declares one.
|
||||||
@@ -231,6 +234,54 @@ pub fn parse_discriminator(node: &Value) -> Result<DiscriminatorKind, TypedefErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect a `TypeDef:*` kind from a schema node, accepting either the
|
||||||
|
/// boolean form (`{ "TypeDef:String": true }`) or the object-annotation
|
||||||
|
/// form (`{ "TypeDef:String": { "encoding": "..." } }`).
|
||||||
|
///
|
||||||
|
/// [`get_typedef_kind`] only recognizes the boolean form; layout computation
|
||||||
|
/// and engine dispatch also need to recognize the object form so that
|
||||||
|
/// variable-length encoding annotations don't hide the kind.
|
||||||
|
pub fn get_typedef_kind_loose(node: &Value) -> Option<&str> {
|
||||||
|
let obj = node.as_object()?;
|
||||||
|
for key in obj.keys() {
|
||||||
|
if key.starts_with(TYPEDEF_PREFIX) && obj.get(key).is_some_and(|v| !v.is_null()) {
|
||||||
|
return Some(key.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a `$ref` against the root schema, or return the inline schema.
|
||||||
|
///
|
||||||
|
/// If `node` has a `"$ref"` key, parse the JSON Pointer and walk `root`.
|
||||||
|
/// Otherwise, return `node` itself (it's an inline schema).
|
||||||
|
pub fn resolve_ref_or_inline<'a>(node: &'a Value, root: &'a Value) -> Option<&'a Value> {
|
||||||
|
let obj = node.as_object()?;
|
||||||
|
if let Some(Value::String(ref_path)) = obj.get("$ref") {
|
||||||
|
return resolve_ref(root, ref_path);
|
||||||
|
}
|
||||||
|
Some(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a JSON Pointer `$ref` (e.g., `"#/$defs/Read"`) against `root`.
|
||||||
|
pub fn resolve_ref<'a>(root: &'a Value, ref_path: &str) -> Option<&'a Value> {
|
||||||
|
let stripped = ref_path.strip_prefix('#').unwrap_or(ref_path);
|
||||||
|
let stripped = stripped.strip_prefix('/').unwrap_or(stripped);
|
||||||
|
if stripped.is_empty() {
|
||||||
|
return Some(root);
|
||||||
|
}
|
||||||
|
let mut current = root;
|
||||||
|
for segment in stripped.split('/') {
|
||||||
|
let decoded = segment.replace("~1", "/").replace("~0", "~");
|
||||||
|
if let Ok(idx) = decoded.parse::<usize>() {
|
||||||
|
current = current.get(idx)?;
|
||||||
|
} else {
|
||||||
|
current = current.get(&decoded)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
|
||||||
/// Walk the schema tree. For every `"$ref"` whose value is a bare name
|
/// Walk the schema tree. For every `"$ref"` whose value is a bare name
|
||||||
/// (no `#` prefix), rewrite it to `"#/$defs/<name>"`. Full JSON Pointer refs
|
/// (no `#` prefix), rewrite it to `"#/$defs/<name>"`. Full JSON Pointer refs
|
||||||
/// (starting with `#`) pass through unchanged. Idempotent.
|
/// (starting with `#`) pass through unchanged. Idempotent.
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
|
|
||||||
use crate::data_access;
|
use crate::data_access;
|
||||||
use crate::error::TypedefError;
|
use crate::error::TypedefError;
|
||||||
use crate::schema::{self, DiscriminatorKind, Endian};
|
use crate::schema::{self, get_typedef_kind_loose, DiscriminatorKind, Endian, U32_SIZE};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
const U32_SIZE: usize = 4;
|
|
||||||
|
|
||||||
/// A value read from a field during sequential traversal.
|
/// A value read from a field during sequential traversal.
|
||||||
///
|
///
|
||||||
/// Composite kinds ([`FieldValue::Struct`], [`FieldValue::Union`],
|
/// Composite kinds ([`FieldValue::Struct`], [`FieldValue::Union`],
|
||||||
@@ -274,7 +272,7 @@ fn read_field_value<'a>(
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
endian: Endian,
|
endian: Endian,
|
||||||
) -> Result<(FieldValue<'a>, usize), TypedefError> {
|
) -> Result<(FieldValue<'a>, usize), TypedefError> {
|
||||||
let kind = schema::get_typedef_kind(field_schema).ok_or_else(|| {
|
let kind = get_typedef_kind_loose(field_schema).ok_or_else(|| {
|
||||||
TypedefError::Schema(format!(
|
TypedefError::Schema(format!(
|
||||||
"field {field_path} has no TypeDef:* kind: {field_schema}"
|
"field {field_path} has no TypeDef:* kind: {field_schema}"
|
||||||
))
|
))
|
||||||
@@ -598,7 +596,7 @@ fn read_array_value<'a>(
|
|||||||
(count, offset + U32_SIZE)
|
(count, offset + U32_SIZE)
|
||||||
};
|
};
|
||||||
|
|
||||||
let element_kind = schema::get_typedef_kind(items_schema).ok_or_else(|| {
|
let element_kind = get_typedef_kind_loose(items_schema).ok_or_else(|| {
|
||||||
TypedefError::Schema(format!(
|
TypedefError::Schema(format!(
|
||||||
"array {field_path} items schema has no TypeDef:* kind"
|
"array {field_path} items schema has no TypeDef:* kind"
|
||||||
))
|
))
|
||||||
@@ -735,7 +733,7 @@ fn resolve_and_walk_variant(
|
|||||||
"union {field_path} variant could not be resolved: {variant_schema}"
|
"union {field_path} variant could not be resolved: {variant_schema}"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let kind = schema::get_typedef_kind(resolved).ok_or_else(|| {
|
let kind = get_typedef_kind_loose(resolved).ok_or_else(|| {
|
||||||
TypedefError::Schema(format!(
|
TypedefError::Schema(format!(
|
||||||
"union {field_path} variant has no TypeDef:* kind: {resolved}"
|
"union {field_path} variant has no TypeDef:* kind: {resolved}"
|
||||||
))
|
))
|
||||||
@@ -787,7 +785,7 @@ fn resolve_variant_schema<'a>(
|
|||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if schema::get_typedef_kind(variant).is_some() {
|
if get_typedef_kind_loose(variant).is_some() {
|
||||||
Some(variant)
|
Some(variant)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -11,12 +11,10 @@
|
|||||||
|
|
||||||
use crate::data_access::{read_enum, read_string, read_u16, read_u32, read_u8};
|
use crate::data_access::{read_enum, read_string, read_u16, read_u32, read_u8};
|
||||||
use crate::error::TypedefError;
|
use crate::error::TypedefError;
|
||||||
use crate::schema::{get_typedef_kind, parse_discriminator, DiscriminatorKind, Endian};
|
use crate::schema::{get_typedef_kind, parse_discriminator, DiscriminatorKind, Endian, DISCRIMINATOR_PATH, U32_SIZE};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
const U32_SIZE: usize = 4;
|
|
||||||
const STRING_PREFIX_SIZE: usize = 4;
|
const STRING_PREFIX_SIZE: usize = 4;
|
||||||
const DISCRIMINATOR_PATH: &str = "__discriminator";
|
|
||||||
|
|
||||||
/// The result of reading a TUnion discriminator.
|
/// The result of reading a TUnion discriminator.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -177,7 +175,7 @@ pub fn read_field_discriminator(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
verify_mapping_key(union_schema, &key, &name, key.as_str())?;
|
verify_mapping_key(union_schema, &key, &name, &key)?;
|
||||||
|
|
||||||
let variant_offset = disc_field_offset
|
let variant_offset = disc_field_offset
|
||||||
.checked_add(discriminator_field_size)
|
.checked_add(discriminator_field_size)
|
||||||
|
|||||||
@@ -51,133 +51,30 @@ pub fn build_validator(schema: &Value) -> Result<jsonschema::Validator, TypedefE
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Numeric validators
|
// Numeric validators (generated via macros)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
struct Float32Validator;
|
define_int_validator!(Int8Validator, int8_factory, "TypeDef:Int8", -128, 127);
|
||||||
impl Keyword for Float32Validator {
|
define_int_validator!(Int16Validator, int16_factory, "TypeDef:Int16", -32768, 32767);
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
define_int_validator!(Int32Validator, int32_factory, "TypeDef:Int32", -2147483648, 2147483647);
|
||||||
match instance.as_f64() {
|
define_uint_validator!(Uint8Validator, uint8_factory, "TypeDef:Uint8", 255);
|
||||||
Some(f) if f.is_finite() => Ok(()),
|
define_uint_validator!(Uint16Validator, uint16_factory, "TypeDef:Uint16", 65535);
|
||||||
_ => Err(ValidationError::custom(
|
define_uint_validator!(Uint32Validator, uint32_factory, "TypeDef:Uint32", 4294967295);
|
||||||
"expected a finite f32-compatible number",
|
define_float_validator!(
|
||||||
)),
|
Float32Validator,
|
||||||
}
|
float32_factory,
|
||||||
}
|
"TypeDef:Float32",
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
"expected a finite f32-compatible number"
|
||||||
instance.as_f64().is_some_and(|f| f.is_finite())
|
);
|
||||||
}
|
define_float_validator!(
|
||||||
}
|
Float64Validator,
|
||||||
|
float64_factory,
|
||||||
struct Float64Validator;
|
"TypeDef:Float64",
|
||||||
impl Keyword for Float64Validator {
|
"expected a finite f64 number"
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
);
|
||||||
match instance.as_f64() {
|
|
||||||
Some(f) if f.is_finite() => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom("expected a finite f64 number")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.as_f64().is_some_and(|f| f.is_finite())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Int8Validator;
|
|
||||||
impl Keyword for Int8Validator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
match instance.as_i64() {
|
|
||||||
Some(n) if (-128..=127).contains(&n) => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom(
|
|
||||||
"expected an integer in range [-128, 127]",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.as_i64().is_some_and(|n| (-128..=127).contains(&n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Int16Validator;
|
|
||||||
impl Keyword for Int16Validator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
match instance.as_i64() {
|
|
||||||
Some(n) if (-32_768..=32_767).contains(&n) => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom(
|
|
||||||
"expected an integer in range [-32768, 32767]",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance
|
|
||||||
.as_i64()
|
|
||||||
.is_some_and(|n| (-32_768..=32_767).contains(&n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Int32Validator;
|
|
||||||
impl Keyword for Int32Validator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
match instance.as_i64() {
|
|
||||||
Some(n) if (-2_147_483_648..=2_147_483_647).contains(&n) => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom(
|
|
||||||
"expected an integer in range [-2147483648, 2147483647]",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance
|
|
||||||
.as_i64()
|
|
||||||
.is_some_and(|n| (-2_147_483_648..=2_147_483_647).contains(&n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Uint8Validator;
|
|
||||||
impl Keyword for Uint8Validator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
match instance.as_u64() {
|
|
||||||
Some(n) if n <= 255 => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom(
|
|
||||||
"expected an unsigned integer in range [0, 255]",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.as_u64().is_some_and(|n| n <= 255)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Uint16Validator;
|
|
||||||
impl Keyword for Uint16Validator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
match instance.as_u64() {
|
|
||||||
Some(n) if n <= 65_535 => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom(
|
|
||||||
"expected an unsigned integer in range [0, 65535]",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.as_u64().is_some_and(|n| n <= 65_535)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Uint32Validator;
|
|
||||||
impl Keyword for Uint32Validator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
match instance.as_u64() {
|
|
||||||
Some(n) if n <= 4_294_967_295 => Ok(()),
|
|
||||||
_ => Err(ValidationError::custom(
|
|
||||||
"expected an unsigned integer in range [0, 4294967295]",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.as_u64().is_some_and(|n| n <= 4_294_967_295)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// String and binary validators
|
// String and binary validators (hand-written: need maxLength from parent)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
struct StringValidator {
|
struct StringValidator {
|
||||||
@@ -301,215 +198,27 @@ fn is_rfc3339_timestamp(s: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Composite validators (structural checks delegated to jsonschema)
|
// Composite validators (generated via macros)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
struct StructValidator;
|
define_type_validator!(StructValidator, struct_factory, "TypeDef:Struct", is_object, "expected an object");
|
||||||
impl Keyword for StructValidator {
|
define_type_validator!(UnionValidator, union_factory, "TypeDef:Union", is_object, "expected an object for union");
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
define_type_validator!(ArrayValidator, array_factory, "TypeDef:Array", is_array, "expected an array");
|
||||||
if instance.is_object() {
|
define_type_validator!(RecordValidator, record_factory, "TypeDef:Record", is_object, "expected an object for record");
|
||||||
Ok(())
|
define_type_validator!(BooleanValidator, boolean_factory, "TypeDef:Boolean", is_boolean, "expected a boolean");
|
||||||
} else {
|
|
||||||
Err(ValidationError::custom("expected an object"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.is_object()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `TypeDef:Union` discriminator and variant conformance are validated
|
|
||||||
/// by jsonschema's built-in keywords (`properties`, `oneOf`, etc.). The
|
|
||||||
/// custom keyword only asserts the instance is an object.
|
|
||||||
struct UnionValidator;
|
|
||||||
impl Keyword for UnionValidator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
if instance.is_object() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::custom("expected an object for union"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.is_object()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArrayValidator;
|
|
||||||
impl Keyword for ArrayValidator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
if instance.is_array() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::custom("expected an array"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.is_array()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `TypeDef:Record` value-type conformance is validated by jsonschema's
|
|
||||||
/// built-in `additionalProperties` keyword. The custom keyword only
|
|
||||||
/// asserts the instance is an object.
|
|
||||||
struct RecordValidator;
|
|
||||||
impl Keyword for RecordValidator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
if instance.is_object() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::custom("expected an object for record"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.is_object()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BooleanValidator;
|
|
||||||
impl Keyword for BooleanValidator {
|
|
||||||
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
|
|
||||||
if instance.is_boolean() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::custom("expected a boolean"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
|
||||||
instance.is_boolean()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Factory closures
|
// Factory closures for non-macro-generated validators
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn float32_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Float32Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Float32 must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn float64_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Float64Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Float64 must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn int8_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Int8Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema("TypeDef:Int8 must be set to true"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn int16_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Int16Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema("TypeDef:Int16 must be set to true"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn int32_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Int32Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema("TypeDef:Int32 must be set to true"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uint8_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Uint8Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema("TypeDef:Uint8 must be set to true"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uint16_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Uint16Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Uint16 must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uint32_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(Uint32Validator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Uint32 must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn boolean_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(BooleanValidator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Boolean must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn string_factory<'a>(
|
fn string_factory<'a>(
|
||||||
parent: &'a Map<String, Value>,
|
parent: &'a Map<String, Value>,
|
||||||
value: &'a Value,
|
value: &'a Value,
|
||||||
_path: jsonschema::paths::Location,
|
_path: jsonschema::paths::Location,
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
||||||
if value.as_bool() != Some(true) {
|
if !value.is_boolean() && !value.is_object() {
|
||||||
return Err(ValidationError::schema(
|
return Err(ValidationError::schema(
|
||||||
"TypeDef:String must be set to true",
|
"TypeDef:String must be set to true or an annotation object",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let max_length = parent
|
let max_length = parent
|
||||||
@@ -524,8 +233,10 @@ fn bytes_factory<'a>(
|
|||||||
value: &'a Value,
|
value: &'a Value,
|
||||||
_path: jsonschema::paths::Location,
|
_path: jsonschema::paths::Location,
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
||||||
if value.as_bool() != Some(true) {
|
if !value.is_boolean() && !value.is_object() {
|
||||||
return Err(ValidationError::schema("TypeDef:Bytes must be set to true"));
|
return Err(ValidationError::schema(
|
||||||
|
"TypeDef:Bytes must be set to true or an annotation object",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let max_length = parent
|
let max_length = parent
|
||||||
.get("maxLength")
|
.get("maxLength")
|
||||||
@@ -546,58 +257,6 @@ fn enum_factory<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn struct_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(StructValidator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Struct must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn union_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(UnionValidator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema("TypeDef:Union must be set to true"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn array_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(ArrayValidator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema("TypeDef:Array must be set to true"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_factory<'a>(
|
|
||||||
_parent: &'a Map<String, Value>,
|
|
||||||
value: &'a Value,
|
|
||||||
_path: jsonschema::paths::Location,
|
|
||||||
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
|
|
||||||
if value.as_bool() == Some(true) {
|
|
||||||
Ok(Box::new(RecordValidator))
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::schema(
|
|
||||||
"TypeDef:Record must be set to true",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timestamp_factory<'a>(
|
fn timestamp_factory<'a>(
|
||||||
_parent: &'a Map<String, Value>,
|
_parent: &'a Map<String, Value>,
|
||||||
value: &'a Value,
|
value: &'a Value,
|
||||||
|
|||||||
Reference in new issue
Block a user