adze_parsetable_metadata/
lib.rs1#![forbid(unsafe_op_in_unsafe_fn)]
7#![cfg_attr(feature = "strict_api", deny(unreachable_pub))]
8#![cfg_attr(not(feature = "strict_api"), warn(unreachable_pub))]
9#![cfg_attr(feature = "strict_docs", deny(missing_docs))]
10#![cfg_attr(not(feature = "strict_docs"), allow(missing_docs))]
11
12pub use adze_governance_metadata::{GovernanceMetadata, ParserFeatureProfileSnapshot};
14use serde::{Deserialize, Serialize};
15
16pub const MAGIC_NUMBER: [u8; 4] = [0x52, 0x53, 0x50, 0x54];
18
19pub const FORMAT_VERSION: u32 = 1;
21
22pub const METADATA_SCHEMA_VERSION: &str = "1.0";
24
25#[derive(Debug, thiserror::Error)]
27pub enum ParsetableError {
28 #[error("I/O error: {0}")]
30 Io(#[from] std::io::Error),
31
32 #[error("Serialization error: {0}")]
34 Serialization(String),
35
36 #[error("Invalid metadata: {0}")]
38 InvalidMetadata(String),
39
40 #[error("Grammar hash computation failed: {0}")]
42 HashError(String),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80pub struct ParsetableMetadata {
81 pub schema_version: String,
83 pub grammar: GrammarInfo,
85 pub generation: GenerationInfo,
87 pub statistics: TableStatistics,
89 pub features: FeatureFlags,
91 #[serde(default)]
93 pub feature_profile: Option<ParserFeatureProfileSnapshot>,
94 #[serde(default)]
96 pub governance: Option<GovernanceMetadata>,
97}
98
99impl ParsetableMetadata {
100 #[must_use = "parsing may fail; the Result should be checked"]
113 pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
114 serde_json::from_slice(bytes)
115 }
116
117 #[must_use = "parsing may fail; the Result should be checked"]
119 pub fn parse_json(payload: &str) -> Result<Self, serde_json::Error> {
120 serde_json::from_str(payload)
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126pub struct GrammarInfo {
127 pub name: String,
129 pub version: String,
131 pub language: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
137pub struct GenerationInfo {
138 pub timestamp: String,
140 pub tool_version: String,
142 pub rust_version: String,
144 pub host_triple: String,
146}
147
148#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
150pub struct TableStatistics {
151 pub state_count: usize,
153 pub symbol_count: usize,
155 pub rule_count: usize,
157 pub conflict_count: usize,
159 pub multi_action_cells: usize,
161}
162
163#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
179pub struct FeatureFlags {
180 pub glr_enabled: bool,
182 pub external_scanner: bool,
184 pub incremental: bool,
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn magic_number_is_rspt() {
194 assert_eq!(&MAGIC_NUMBER, b"RSPT");
195 }
196
197 #[test]
198 fn format_version_is_positive() {
199 const { assert!(FORMAT_VERSION > 0) };
200 }
201
202 #[test]
203 fn metadata_schema_version_is_non_empty() {
204 assert!(!METADATA_SCHEMA_VERSION.is_empty());
205 }
206
207 #[test]
208 fn parsetable_error_display() {
209 let err = ParsetableError::Serialization("bad json".to_string());
210 let msg = format!("{err}");
211 assert!(msg.contains("bad json"));
212
213 let err = ParsetableError::InvalidMetadata("missing field".to_string());
214 assert!(format!("{err}").contains("missing field"));
215
216 let err = ParsetableError::HashError("sha256 failed".to_string());
217 assert!(format!("{err}").contains("sha256 failed"));
218 }
219
220 #[test]
221 fn feature_flags_serde_roundtrip() {
222 let flags = FeatureFlags {
223 glr_enabled: true,
224 external_scanner: false,
225 incremental: true,
226 };
227 let json = serde_json::to_string(&flags).unwrap();
228 let deserialized: FeatureFlags = serde_json::from_str(&json).unwrap();
229 assert_eq!(flags, deserialized);
230 }
231
232 #[test]
233 fn table_statistics_serde_roundtrip() {
234 let stats = TableStatistics {
235 state_count: 100,
236 symbol_count: 50,
237 rule_count: 30,
238 conflict_count: 2,
239 multi_action_cells: 5,
240 };
241 let json = serde_json::to_string(&stats).unwrap();
242 let deserialized: TableStatistics = serde_json::from_str(&json).unwrap();
243 assert_eq!(stats, deserialized);
244 }
245
246 #[test]
247 fn full_metadata_serde_roundtrip() {
248 let metadata = ParsetableMetadata {
249 schema_version: METADATA_SCHEMA_VERSION.to_string(),
250 grammar: GrammarInfo {
251 name: "test_grammar".to_string(),
252 version: "1.0.0".to_string(),
253 language: "test".to_string(),
254 },
255 generation: GenerationInfo {
256 timestamp: "2025-01-01T00:00:00Z".to_string(),
257 tool_version: "0.1.0".to_string(),
258 rust_version: "1.92.0".to_string(),
259 host_triple: "x86_64-unknown-linux-gnu".to_string(),
260 },
261 statistics: TableStatistics {
262 state_count: 10,
263 symbol_count: 5,
264 rule_count: 3,
265 conflict_count: 0,
266 multi_action_cells: 0,
267 },
268 features: FeatureFlags {
269 glr_enabled: false,
270 external_scanner: false,
271 incremental: false,
272 },
273 feature_profile: None,
274 governance: None,
275 };
276 let json = serde_json::to_string_pretty(&metadata).unwrap();
277 let deserialized = ParsetableMetadata::parse_json(&json).unwrap();
278 assert_eq!(metadata, deserialized);
279 }
280
281 #[test]
282 fn metadata_from_bytes() {
283 let metadata = ParsetableMetadata {
284 schema_version: "1.0".to_string(),
285 grammar: GrammarInfo {
286 name: "json".to_string(),
287 version: "0.1.0".to_string(),
288 language: "json".to_string(),
289 },
290 generation: GenerationInfo {
291 timestamp: "2025-01-01T00:00:00Z".to_string(),
292 tool_version: "0.1.0".to_string(),
293 rust_version: "1.92.0".to_string(),
294 host_triple: "x86_64-unknown-linux-gnu".to_string(),
295 },
296 statistics: TableStatistics {
297 state_count: 1,
298 symbol_count: 1,
299 rule_count: 1,
300 conflict_count: 0,
301 multi_action_cells: 0,
302 },
303 features: FeatureFlags {
304 glr_enabled: false,
305 external_scanner: false,
306 incremental: false,
307 },
308 feature_profile: Some(ParserFeatureProfileSnapshot::new(false, false, true, false)),
309 governance: Some(GovernanceMetadata::with_counts("core", 5, 8, "core:5/8")),
310 };
311 let bytes = serde_json::to_vec(&metadata).unwrap();
312 let deserialized = ParsetableMetadata::from_bytes(&bytes).unwrap();
313 assert_eq!(metadata, deserialized);
314 }
315
316 #[test]
317 fn reexported_governance_types_accessible() {
318 let _snap = ParserFeatureProfileSnapshot::new(false, false, false, false);
319 let _meta = GovernanceMetadata::default();
320 }
321
322 #[test]
325 fn error_is_send_sync() {
326 fn assert_send<T: Send>() {}
327 fn assert_sync<T: Sync>() {}
328 assert_send::<ParsetableError>();
329 assert_sync::<ParsetableError>();
330 }
331
332 #[test]
333 fn error_io_display() {
334 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
335 let err = ParsetableError::Io(io_err);
336 let msg = err.to_string();
337 assert!(msg.contains("I/O error"), "got: {msg}");
338 assert!(msg.contains("file missing"), "got: {msg}");
339 }
340
341 #[test]
342 fn error_from_io() {
343 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no access");
344 let err: ParsetableError = io_err.into();
345 assert!(matches!(err, ParsetableError::Io(_)));
346 assert!(err.to_string().contains("no access"));
347 }
348
349 #[test]
350 fn error_io_source_chain() {
351 use std::error::Error;
352 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
353 let err = ParsetableError::Io(io_err);
354 let src = err.source().expect("Io variant should have a source");
355 assert!(src.to_string().contains("gone"));
356 }
357
358 #[test]
359 fn error_serialization_no_source() {
360 use std::error::Error;
361 let err = ParsetableError::Serialization("bad payload".into());
362 assert!(
363 err.source().is_none(),
364 "Serialization variant wraps no inner error"
365 );
366 }
367
368 #[test]
369 fn error_invalid_metadata_no_source() {
370 use std::error::Error;
371 let err = ParsetableError::InvalidMetadata("corrupt".into());
372 assert!(
373 err.source().is_none(),
374 "InvalidMetadata variant wraps no inner error"
375 );
376 }
377
378 #[test]
379 fn error_hash_no_source() {
380 use std::error::Error;
381 let err = ParsetableError::HashError("sha broke".into());
382 assert!(
383 err.source().is_none(),
384 "HashError variant wraps no inner error"
385 );
386 }
387
388 #[test]
389 fn error_construct_all_variants() {
390 let _ = ParsetableError::Io(std::io::Error::other("x"));
391 let _ = ParsetableError::Serialization("x".into());
392 let _ = ParsetableError::InvalidMetadata("x".into());
393 let _ = ParsetableError::HashError("x".into());
394 }
395
396 #[test]
397 fn error_debug_format() {
398 let err = ParsetableError::InvalidMetadata("dbg test".into());
399 let dbg = format!("{err:?}");
400 assert!(
401 dbg.contains("InvalidMetadata"),
402 "Debug should name the variant: {dbg}"
403 );
404 assert!(
405 dbg.contains("dbg test"),
406 "Debug should include payload: {dbg}"
407 );
408 }
409}