validate_json against consumer-provided JSON Schema (step 6)
- AlkTypeEngine::compile gains a 4th param json_schema: Option<&Value>. When Some, a standard jsonschema::Validator is built from the consumer-provided JSON Schema and stored for the JSON-validation path. When None, validate_json returns AlkTypeError::Schema and is_valid_json returns false (D-BAST-007). - validate_json / is_valid_json signatures unchanged (per semver contract). Behavior: they now validate against the consumer JSON Schema, not a custom-keyword validator built from the alktype schema. The BAST document is not involved in this path. - build_validator repurposed (deferred decision #2): same signature, now builds a standard jsonschema::Validator with no custom keywords. Behavioral break, not a type break. Re-export kept. - Removed the 19 jsonschema::Keyword implementations and the 4 define_*_validator! macros (dead on the bytes path since step 5, now dead on the JSON path too). The is_rfc3339_timestamp helper lives on in bast_validation.rs (already copied there in step 5). - All compile call sites updated to pass None for json_schema (the layout/read/write/validate_bytes tests don't need JSON validation). - New tests: validate_json accepts/rejects against consumer JSON Schema, returns Schema error when no JSON Schema supplied, is_valid_json false when no schema, independence from BAST doc, malformed JSON Schema build error, nested object JSON Schema. Verification: - cargo test --release: 409 pass (332 lib + 77 integration) - cargo clippy --all-targets -- -D warnings: clean - cargo build --target wasm32-unknown-unknown --release: clean - cargo doc --no-deps: clean
This commit is contained in:
1 parent
f853dafaf1
commit
ba7f8e1bad
8 files changed
+352
-1350
No files matched your search
+1
-1
@@ -803,7 +803,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = crate::AlkTypeEngine::compile(&doc, "ChunkHeader", crate::LayoutMode::Packed);
|
||||
let engine = crate::AlkTypeEngine::compile(&doc, "ChunkHeader", crate::LayoutMode::Packed, None);
|
||||
assert!(engine.is_ok(), "chunk header schema should compile: {engine:?}");
|
||||
}
|
||||
}
|
||||
+219
-49
@@ -1,9 +1,16 @@
|
||||
//! `AlkTypeEngine` — the compiled form of a BAST document.
|
||||
//!
|
||||
//! Combines the layout engine (both packed and aligned modes) and the
|
||||
//! jsonschema validator into a single struct. Built once at schema load
|
||||
//! time via [`AlkTypeEngine::compile`]. Used for repeated read/write/
|
||||
//! validate operations at access time.
|
||||
//! two validators into a single struct. Built once at schema load time
|
||||
//! via [`AlkTypeEngine::compile`]. Used for repeated read/write/validate
|
||||
//! operations at access time.
|
||||
//!
|
||||
//! Two validators, two inputs (D-BAST-006, D-BAST-007):
|
||||
//! - `validate_bytes` — the BAST-native validator walks the BAST type
|
||||
//! tree over a materialized `Value` (no `jsonschema` involvement).
|
||||
//! - `validate_json` — a standard `jsonschema::Validator` compiled from
|
||||
//! a consumer-provided JSON Schema (no custom keywords, no BAST
|
||||
//! involvement).
|
||||
//!
|
||||
//! See [validation.md](../../docs/architecture/validation.md)
|
||||
//! §"The AlkTypeEngine struct" and
|
||||
@@ -52,7 +59,7 @@ enum Layout {
|
||||
}
|
||||
|
||||
/// The compiled form of a BAST document. Combines the layout engine
|
||||
/// (both packed and aligned modes) and the jsonschema validator.
|
||||
/// (both packed and aligned modes) and the two validators.
|
||||
///
|
||||
/// Built once at schema load time via [`AlkTypeEngine::compile`].
|
||||
/// Used for repeated read/write/validate operations at access time.
|
||||
@@ -60,11 +67,20 @@ enum Layout {
|
||||
/// The consumer selects the layout mode at construction time. The engine
|
||||
/// then exposes mode-appropriate accessors: [`AlkTypeEngine::offset_map`]
|
||||
/// for aligned mode, [`AlkTypeEngine::layout_builder`] and
|
||||
/// [`AlkTypeEngine::sequential_reader`] for packed mode. The
|
||||
/// jsonschema validator is mode-agnostic and always available.
|
||||
/// [`AlkTypeEngine::sequential_reader`] for packed mode.
|
||||
///
|
||||
/// Validation is split (D-BAST-006, D-BAST-007):
|
||||
/// - [`AlkTypeEngine::validate_bytes`] uses the BAST-native validator
|
||||
/// ([`bast_validation`]) — no `jsonschema` involvement, no external
|
||||
/// JSON Schema required.
|
||||
/// - [`AlkTypeEngine::validate_json`] / [`AlkTypeEngine::is_valid_json`]
|
||||
/// use a standard `jsonschema::Validator` compiled at construction
|
||||
/// time from a consumer-provided JSON Schema. If no JSON Schema was
|
||||
/// supplied to [`AlkTypeEngine::compile`], the JSON-validation methods
|
||||
/// return [`AlkTypeError::Schema`].
|
||||
pub struct AlkTypeEngine {
|
||||
layout: Layout,
|
||||
validator: jsonschema::Validator,
|
||||
json_validator: Option<jsonschema::Validator>,
|
||||
endian: Endian,
|
||||
bast_doc: Value,
|
||||
root_name: String,
|
||||
@@ -74,15 +90,25 @@ impl AlkTypeEngine {
|
||||
/// Compile a BAST document into a [`AlkTypeEngine`].
|
||||
///
|
||||
/// This is the expensive operation — it parses the BAST document
|
||||
/// into a typed tree, computes the layout, and builds the jsonschema
|
||||
/// validator. Call once at load time; use the returned engine for
|
||||
/// repeated operations.
|
||||
/// into a typed tree, computes the layout, and (optionally) builds a
|
||||
/// standard `jsonschema::Validator` for the JSON-validation path.
|
||||
/// Call once at load time; use the returned engine for repeated
|
||||
/// operations.
|
||||
///
|
||||
/// `bast_doc` is the raw BAST JSON `Value` (`{ "$defs": { ... } }`).
|
||||
/// `root_name` selects which `$defs` entry is the top-level type
|
||||
/// (D-BAST-001). The `mode` parameter selects the layout strategy.
|
||||
/// The same document can be compiled in either mode.
|
||||
///
|
||||
/// `json_schema` is an optional consumer-provided standard JSON
|
||||
/// Schema (Draft 2020-12 or any draft `jsonschema` supports) used by
|
||||
/// [`AlkTypeEngine::validate_json`] / [`AlkTypeEngine::is_valid_json`]
|
||||
/// (D-BAST-007). Pass `None` when JSON validation is not needed;
|
||||
/// calling the JSON-validation methods then returns
|
||||
/// [`AlkTypeError::Schema`]. The JSON Schema is independent of the
|
||||
/// BAST document — BAST describes bytes, not JSON shape. It may be
|
||||
/// authored separately or derived from BAST via future codegen.
|
||||
///
|
||||
/// The engine retains a clone of the BAST `Value` so that
|
||||
/// [`AlkTypeEngine::sequential_reader`] and
|
||||
/// [`AlkTypeEngine::read_field`] can re-parse the typed tree on
|
||||
@@ -90,14 +116,15 @@ impl AlkTypeEngine {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`AlkTypeError::Schema`] if the document is malformed or
|
||||
/// the root type is not a struct. The error is propagated from
|
||||
/// [`BastDoc::new`], [`LayoutBuilder::new`],
|
||||
/// [`OffsetMap::compute`], or [`validation::build_validator`].
|
||||
/// Returns [`AlkTypeError::Schema`] if the BAST document is malformed
|
||||
/// or the root type is not a struct, propagated from [`BastDoc::new`],
|
||||
/// [`LayoutBuilder::new`], [`OffsetMap::compute`], or
|
||||
/// [`validation::build_validator`] (when `json_schema` is `Some`).
|
||||
pub fn compile(
|
||||
bast_doc: &Value,
|
||||
root_name: &str,
|
||||
mode: LayoutMode,
|
||||
json_schema: Option<&Value>,
|
||||
) -> Result<Self, AlkTypeError> {
|
||||
let doc = BastDoc::new(bast_doc, root_name)?;
|
||||
let root_def = doc.root_def();
|
||||
@@ -121,10 +148,13 @@ impl AlkTypeEngine {
|
||||
Layout::Aligned { offset_map }
|
||||
}
|
||||
};
|
||||
let validator = validation::build_validator(bast_doc)?;
|
||||
let json_validator = match json_schema {
|
||||
Some(schema) => Some(validation::build_validator(schema)?),
|
||||
None => None,
|
||||
};
|
||||
Ok(Self {
|
||||
layout,
|
||||
validator,
|
||||
json_validator,
|
||||
endian,
|
||||
bast_doc: bast_doc.clone(),
|
||||
root_name: root_name.to_string(),
|
||||
@@ -177,20 +207,50 @@ impl AlkTypeEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a JSON value against the schema. The jsonschema validator
|
||||
/// is already compiled — this is a fast check.
|
||||
/// Validate a JSON `Value` against the consumer-provided JSON Schema
|
||||
/// supplied to [`AlkTypeEngine::compile`] (D-BAST-007).
|
||||
///
|
||||
/// The validator is a standard `jsonschema::Validator` built at
|
||||
/// construction time from a plain JSON Schema document — no custom
|
||||
/// keywords, no BAST involvement. BAST describes bytes, not JSON
|
||||
/// shape; a JSON value is validated by a JSON Schema, which is a
|
||||
/// separate concern.
|
||||
///
|
||||
/// Returns `Ok(())` if valid, `Err(AlkTypeError::Validation(...))` if
|
||||
/// invalid.
|
||||
/// the instance violates the JSON Schema, or
|
||||
/// `Err(AlkTypeError::Schema(...))` if no JSON Schema was supplied to
|
||||
/// [`AlkTypeEngine::compile`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`AlkTypeError::Validation`] if the instance violates the JSON
|
||||
/// Schema. The payload is the `jsonschema::ValidationError` (D-BAST-009
|
||||
/// — uniform with the `validate_bytes` path).
|
||||
/// - [`AlkTypeError::Schema`] if `compile` was called with
|
||||
/// `json_schema: None`.
|
||||
pub fn validate_json(&self, instance: &Value) -> Result<(), AlkTypeError> {
|
||||
self.validator
|
||||
let validator = self.json_validator.as_ref().ok_or_else(|| {
|
||||
AlkTypeError::Schema(
|
||||
"validate_json requires a JSON Schema supplied to AlkTypeEngine::compile \
|
||||
(json_schema was None)"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
validator
|
||||
.validate(instance)
|
||||
.map_err(|e| AlkTypeError::Validation(e.to_owned()))
|
||||
}
|
||||
|
||||
/// Check if a JSON value is valid against the schema.
|
||||
/// Check if a JSON `Value` is valid against the consumer-provided
|
||||
/// JSON Schema supplied to [`AlkTypeEngine::compile`] (D-BAST-007).
|
||||
///
|
||||
/// Returns `false` if the instance is invalid **or** if no JSON
|
||||
/// Schema was supplied to [`AlkTypeEngine::compile`]. Use
|
||||
/// [`AlkTypeEngine::validate_json`] if the distinction matters.
|
||||
pub fn is_valid_json(&self, instance: &Value) -> bool {
|
||||
self.validator.is_valid(instance)
|
||||
self.json_validator
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.is_valid(instance))
|
||||
}
|
||||
|
||||
/// Validate a binary buffer against the schema by materializing a
|
||||
@@ -448,7 +508,7 @@ impl fmt::Debug for AlkTypeEngine {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AlkTypeEngine")
|
||||
.field("layout", &self.layout)
|
||||
.field("validator", &"<jsonschema::Validator>")
|
||||
.field("json_validator", &self.json_validator.as_ref().map(|_| "<jsonschema::Validator>"))
|
||||
.field("endian", &self.endian)
|
||||
.field("root_name", &self.root_name)
|
||||
.finish()
|
||||
@@ -511,7 +571,7 @@ mod tests {
|
||||
#[test]
|
||||
fn compile_aligned_builds_offset_map() {
|
||||
let doc = fixed_struct_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
assert_eq!(engine.mode(), LayoutMode::Aligned);
|
||||
assert!(engine.offset_map().is_some());
|
||||
assert!(engine.layout_builder().is_none());
|
||||
@@ -521,7 +581,7 @@ mod tests {
|
||||
#[test]
|
||||
fn compile_packed_builds_builder_and_reader() {
|
||||
let doc = fixed_struct_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
assert_eq!(engine.mode(), LayoutMode::Packed);
|
||||
assert!(engine.layout_builder().is_some());
|
||||
assert!(engine.sequential_reader().is_some());
|
||||
@@ -539,7 +599,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
assert_eq!(engine.endian(), Endian::Big);
|
||||
}
|
||||
|
||||
@@ -553,7 +613,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
assert_eq!(engine.endian(), Endian::Little);
|
||||
}
|
||||
|
||||
@@ -571,7 +631,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0] = 0xAB;
|
||||
buf[4..8].copy_from_slice(&0x01020304u32.to_le_bytes());
|
||||
@@ -595,7 +655,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 32];
|
||||
let len_bytes = 5u32.to_le_bytes();
|
||||
buf[0..4].copy_from_slice(&len_bytes);
|
||||
@@ -616,7 +676,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
let buf = [0u8; 4];
|
||||
let err = engine.read_field(&buf, "id").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
@@ -632,7 +692,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let buf = [0u8; 4];
|
||||
let err = engine.read_field(&buf, "missing").unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Offset { .. }), "got {err:?}");
|
||||
@@ -650,7 +710,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let buf = [0u8; 8];
|
||||
// `vals` itself is not in the offset map (only `vals[0]`/`vals[1]`
|
||||
// are). The lookup fails with `Offset`. Reading an individual
|
||||
@@ -679,7 +739,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 8];
|
||||
engine
|
||||
.write_field(&mut buf, "flag", &FieldValue::U8(0xAB))
|
||||
@@ -709,7 +769,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 32];
|
||||
engine
|
||||
.write_field(&mut buf, "name", &FieldValue::String("hello"))
|
||||
@@ -730,7 +790,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
let mut buf = [0u8; 4];
|
||||
let err = engine
|
||||
.write_field(&mut buf, "id", &FieldValue::U32(1))
|
||||
@@ -748,7 +808,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = [0u8; 4];
|
||||
let err = engine
|
||||
.write_field(&mut buf, "missing", &FieldValue::U32(1))
|
||||
@@ -766,7 +826,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = [0u8; 8];
|
||||
let err = engine
|
||||
.write_field(&mut buf, "id", &FieldValue::Struct { start: 0, end: 4 })
|
||||
@@ -777,7 +837,7 @@ mod tests {
|
||||
#[test]
|
||||
fn compile_returns_schema_error_for_invalid_top_level() {
|
||||
let doc = json!({ "type": "object", "properties": {} });
|
||||
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).unwrap_err();
|
||||
let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -793,14 +853,14 @@ mod tests {
|
||||
"A": { "kind": "struct", "fields": [] }
|
||||
}
|
||||
});
|
||||
let err = AlkTypeEngine::compile(&doc, "U", LayoutMode::Packed).unwrap_err();
|
||||
let err = AlkTypeEngine::compile(&doc, "U", LayoutMode::Packed, None).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_formats_without_panicking() {
|
||||
let doc = fixed_struct_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let s = format!("{engine:?}");
|
||||
assert!(s.contains("AlkTypeEngine"));
|
||||
assert!(s.contains("Aligned"));
|
||||
@@ -857,7 +917,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 12];
|
||||
buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
||||
buf[4] = 0x01;
|
||||
@@ -886,7 +946,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 16];
|
||||
let payload = [0xAA, 0xBB, 0xCC];
|
||||
let len_bytes = 3u32.to_le_bytes();
|
||||
@@ -908,7 +968,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None).expect("compile");
|
||||
let mut buf = vec![0u8; 32];
|
||||
let stamp = "2026-07-20T15:30:00Z";
|
||||
let len_bytes = (stamp.len() as u32).to_le_bytes();
|
||||
@@ -947,7 +1007,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_bytes_packed_accepts_valid_chunk_header() {
|
||||
let doc = chunk_header_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None).expect("compile");
|
||||
let buf = [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 12u8];
|
||||
assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass");
|
||||
}
|
||||
@@ -955,7 +1015,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_bytes_packed_rejects_short_buffer() {
|
||||
let doc = chunk_header_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None).expect("compile");
|
||||
let buf = [0u8; 4];
|
||||
let err = engine.validate_bytes(&buf).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
|
||||
@@ -964,7 +1024,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_bytes_aligned_accepts_valid_chunk_header() {
|
||||
let doc = chunk_header_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned, None).expect("compile");
|
||||
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
|
||||
assert!(engine.validate_bytes(&buf).is_ok(), "valid header should pass");
|
||||
}
|
||||
@@ -972,7 +1032,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_bytes_aligned_rejects_short_buffer() {
|
||||
let doc = chunk_header_doc();
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Aligned, None).expect("compile");
|
||||
let buf = [0u8; 6];
|
||||
let err = engine.validate_bytes(&buf).unwrap_err();
|
||||
assert!(
|
||||
@@ -995,7 +1055,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
let mut buf = vec![0u8; 16];
|
||||
buf[0] = 0xAB;
|
||||
buf[1..5].copy_from_slice(&0x01020304u32.to_le_bytes());
|
||||
@@ -1018,10 +1078,120 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed).expect("compile");
|
||||
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile");
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..4].copy_from_slice(&42u32.to_le_bytes());
|
||||
buf[4..8].copy_from_slice(&7u32.to_le_bytes());
|
||||
assert!(engine.validate_bytes(&buf).is_ok());
|
||||
}
|
||||
|
||||
// ----- validate_json / is_valid_json tests (step 6, D-BAST-007) -----
|
||||
|
||||
fn uint32_struct_bast() -> Value {
|
||||
json!({
|
||||
"$defs": {
|
||||
"S": {
|
||||
"kind": "struct",
|
||||
"fields": [ { "name": "id", "kind": "uint32" } ]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn id_json_schema() -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer", "minimum": 0, "maximum": 4294967295u64 }
|
||||
},
|
||||
"required": ["id"]
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_accepts_valid_instance() {
|
||||
let bast = uint32_struct_bast();
|
||||
let schema = id_json_schema();
|
||||
let engine =
|
||||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile");
|
||||
assert!(engine.validate_json(&json!({"id": 42})).is_ok());
|
||||
assert!(engine.is_valid_json(&json!({"id": 42})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_rejects_invalid_instance() {
|
||||
let bast = uint32_struct_bast();
|
||||
let schema = id_json_schema();
|
||||
let engine =
|
||||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile");
|
||||
let err = engine.validate_json(&json!({"id": -1})).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
|
||||
assert!(!engine.is_valid_json(&json!({"id": -1})));
|
||||
assert!(!engine.is_valid_json(&json!({"id": "x"})));
|
||||
assert!(!engine.is_valid_json(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_returns_schema_error_when_no_json_schema_supplied() {
|
||||
let bast = uint32_struct_bast();
|
||||
let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, None).expect("compile");
|
||||
let err = engine.validate_json(&json!({"id": 42})).unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_valid_json_returns_false_when_no_json_schema_supplied() {
|
||||
let bast = uint32_struct_bast();
|
||||
let engine = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, None).expect("compile");
|
||||
assert!(!engine.is_valid_json(&json!({"id": 42})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_independent_of_bast_document() {
|
||||
let bast = uint32_struct_bast();
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "maxLength": 3 }
|
||||
},
|
||||
"required": ["name"]
|
||||
});
|
||||
let engine =
|
||||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Aligned, Some(&schema)).expect("compile");
|
||||
assert!(engine.validate_json(&json!({"name": "hi"})).is_ok());
|
||||
assert!(engine.validate_json(&json!({"name": "toolong"})).is_err());
|
||||
assert!(!engine.is_valid_json(&json!({"name": "toolong"})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_with_json_schema_returns_schema_error_for_malformed_json_schema() {
|
||||
let bast = uint32_struct_bast();
|
||||
let bad_schema = json!({"type": "not-a-real-type"});
|
||||
let err = AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&bad_schema))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_json_validates_nested_object_json_schema() {
|
||||
let bast = uint32_struct_bast();
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "enum": ["a", "b"] }
|
||||
},
|
||||
"required": ["kind"]
|
||||
}
|
||||
},
|
||||
"required": ["packet"]
|
||||
});
|
||||
let engine =
|
||||
AlkTypeEngine::compile(&bast, "S", LayoutMode::Packed, Some(&schema)).expect("compile");
|
||||
assert!(engine.validate_json(&json!({"packet": {"kind": "a"}})).is_ok());
|
||||
assert!(engine.validate_json(&json!({"packet": {"kind": "c"}})).is_err());
|
||||
assert!(!engine.is_valid_json(&json!({"packet": {"kind": "c"}})));
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -22,9 +22,10 @@
|
||||
//! - **Validation** ([`validation`], [`bast_validation`]): two
|
||||
//! validators for two paths. `bast_validation` is the BAST-native
|
||||
//! validator for `validate_bytes` — a recursive walker over the BAST
|
||||
//! type tree (D-BAST-006). `validation` holds the v0.1.0 custom-keyword
|
||||
//! validators, still used by `validate_json` until step 6 reworks it
|
||||
//! to use a consumer-provided standard JSON Schema.
|
||||
//! type tree (D-BAST-006). `validation` builds a standard
|
||||
//! `jsonschema::Validator` from a consumer-provided JSON Schema for
|
||||
//! `validate_json` / `is_valid_json` (D-BAST-007) — no custom keywords,
|
||||
//! no BAST involvement.
|
||||
//! - **Builder** ([`builder`]): Fluent Rust API for constructing alktype
|
||||
//! JSON Schemas at runtime, producing `serde_json::Value` (ADR-009).
|
||||
//! (Step 7 converts the builder to emit BAST JSON.)
|
||||
|
||||
+5
-169
@@ -1,173 +1,9 @@
|
||||
//! Macros for generating repetitive code across the 19 AlkType kinds.
|
||||
//! Macros for generating repetitive read/write code across the
|
||||
//! endian-sensitive fixed-size kinds.
|
||||
//!
|
||||
//! These macros eliminate boilerplate in validation, data access, and
|
||||
//! dispatch. 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"
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
//! These macros eliminate boilerplate in data access. Each macro takes a
|
||||
//! compact specification and generates the full implementation, ensuring
|
||||
//! consistency across all types.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data access macros
|
||||
|
||||
+101
-1106
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user