Skip to main content

adze_parsetable_metadata/
lib.rs

1//! Typed metadata contract for `.parsetable` artifacts.
2//!
3//! This crate owns the serialization model used by tablegen and runtime2 when
4//! emitting/parsing parsetable metadata payloads.
5
6#![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
12/// Re-exported governance types used in `.parsetable` metadata payloads.
13pub use adze_governance_metadata::{GovernanceMetadata, ParserFeatureProfileSnapshot};
14use serde::{Deserialize, Serialize};
15
16/// Magic number identifying .parsetable files: "RSPT".
17pub const MAGIC_NUMBER: [u8; 4] = [0x52, 0x53, 0x50, 0x54];
18
19/// Current .parsetable format version.
20pub const FORMAT_VERSION: u32 = 1;
21
22/// Metadata schema version.
23pub const METADATA_SCHEMA_VERSION: &str = "1.0";
24
25/// Error type for metadata and table container operations.
26#[derive(Debug, thiserror::Error)]
27pub enum ParsetableError {
28    /// I/O error during file operations.
29    #[error("I/O error: {0}")]
30    Io(#[from] std::io::Error),
31
32    /// Serialization error.
33    #[error("Serialization error: {0}")]
34    Serialization(String),
35
36    /// Invalid metadata payload.
37    #[error("Invalid metadata: {0}")]
38    InvalidMetadata(String),
39
40    /// Grammar hash computation failed.
41    #[error("Grammar hash computation failed: {0}")]
42    HashError(String),
43}
44
45/// Metadata embedded in .parsetable files.
46///
47/// # Examples
48///
49/// ```
50/// use adze_parsetable_metadata::*;
51///
52/// let metadata = ParsetableMetadata {
53///     schema_version: METADATA_SCHEMA_VERSION.to_string(),
54///     grammar: GrammarInfo {
55///         name: "json".into(),
56///         version: "1.0.0".into(),
57///         language: "json".into(),
58///     },
59///     generation: GenerationInfo {
60///         timestamp: "2025-01-01T00:00:00Z".into(),
61///         tool_version: "0.1.0".into(),
62///         rust_version: "1.92.0".into(),
63///         host_triple: "x86_64-unknown-linux-gnu".into(),
64///     },
65///     statistics: TableStatistics {
66///         state_count: 10, symbol_count: 5, rule_count: 3,
67///         conflict_count: 0, multi_action_cells: 0,
68///     },
69///     features: FeatureFlags {
70///         glr_enabled: false, external_scanner: false, incremental: false,
71///     },
72///     feature_profile: None,
73///     governance: None,
74/// };
75/// let json = serde_json::to_string(&metadata).unwrap();
76/// let parsed = ParsetableMetadata::parse_json(&json).unwrap();
77/// assert_eq!(metadata, parsed);
78/// ```
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80pub struct ParsetableMetadata {
81    /// Metadata schema version.
82    pub schema_version: String,
83    /// Grammar information.
84    pub grammar: GrammarInfo,
85    /// Generation information.
86    pub generation: GenerationInfo,
87    /// Parse table statistics.
88    pub statistics: TableStatistics,
89    /// Feature flags.
90    pub features: FeatureFlags,
91    /// Feature profile snapshot for this build artifact.
92    #[serde(default)]
93    pub feature_profile: Option<ParserFeatureProfileSnapshot>,
94    /// BDD progress snapshot attached at generation time.
95    #[serde(default)]
96    pub governance: Option<GovernanceMetadata>,
97}
98
99impl ParsetableMetadata {
100    /// Parse metadata from a JSON payload.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use adze_parsetable_metadata::FeatureFlags;
106    ///
107    /// let flags = FeatureFlags { glr_enabled: true, external_scanner: false, incremental: false };
108    /// let bytes = serde_json::to_vec(&flags).unwrap();
109    /// let parsed: FeatureFlags = serde_json::from_slice(&bytes).unwrap();
110    /// assert_eq!(flags, parsed);
111    /// ```
112    #[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    /// Parse metadata from a UTF-8 JSON string.
118    #[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/// Grammar identification information.
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126pub struct GrammarInfo {
127    /// Grammar name.
128    pub name: String,
129    /// Grammar version.
130    pub version: String,
131    /// Language name.
132    pub language: String,
133}
134
135/// Generation metadata.
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
137pub struct GenerationInfo {
138    /// ISO8601 timestamp.
139    pub timestamp: String,
140    /// Tool version.
141    pub tool_version: String,
142    /// Rust compiler version.
143    pub rust_version: String,
144    /// Host triple.
145    pub host_triple: String,
146}
147
148/// Parse table statistics.
149#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
150pub struct TableStatistics {
151    /// Number of parser states.
152    pub state_count: usize,
153    /// Number of symbols.
154    pub symbol_count: usize,
155    /// Number of production rules.
156    pub rule_count: usize,
157    /// Number of GLR conflicts.
158    pub conflict_count: usize,
159    /// Number of action table cells with multiple actions.
160    pub multi_action_cells: usize,
161}
162
163/// Parser feature flags for metadata export.
164///
165/// # Examples
166///
167/// ```
168/// use adze_parsetable_metadata::FeatureFlags;
169///
170/// let flags = FeatureFlags {
171///     glr_enabled: true,
172///     external_scanner: false,
173///     incremental: true,
174/// };
175/// assert!(flags.glr_enabled);
176/// assert!(!flags.external_scanner);
177/// ```
178#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
179pub struct FeatureFlags {
180    /// GLR parsing feature flag.
181    pub glr_enabled: bool,
182    /// External scanner support flag.
183    pub external_scanner: bool,
184    /// Incremental parsing support flag.
185    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    // --- ParsetableError comprehensive tests ---
323
324    #[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}