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:
glm-5.2 committed 2026-08-15 12:53:35 +00:00
1 parent f853dafaf1
commit ba7f8e1bad
8 files changed
+352 -1350

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff