synta_codegen/lib.rs
1//! ASN.1 schema parser and Rust code generator
2//!
3//! This library parses ASN.1 module definitions and generates Rust code
4//! using the `synta` library's derive macros.
5//!
6//! # Quick start
7//!
8//! ```no_run
9//! use synta_codegen::{parse, generate};
10//!
11//! let schema = r#"
12//! Certificate DEFINITIONS ::= BEGIN
13//! Certificate ::= SEQUENCE {
14//! version INTEGER,
15//! serialNumber INTEGER
16//! }
17//! END
18//! "#;
19//!
20//! let module = parse(schema).unwrap();
21//! let rust_code = generate(&module).unwrap();
22//! println!("{}", rust_code);
23//! ```
24//!
25//! # Configuration
26//!
27//! [`generate_with_config`] accepts a [`CodeGenConfig`] that controls several
28//! aspects of the emitted code.
29//!
30//! ## Owned vs. borrowed string types
31//!
32//! By default all ASN.1 string and binary types (`OCTET STRING`, `BIT STRING`,
33//! `UTF8String`, `PrintableString`, `IA5String`) are generated as **owned**
34//! heap-allocating types (`OctetString`, `BitString`, …). This is convenient
35//! when constructing structs programmatically.
36//!
37//! For parse-only workloads (e.g. X.509 certificate inspection) you can switch
38//! to **zero-copy borrowed** types (`OctetStringRef<'a>`, `BitStringRef<'a>`,
39//! …) that borrow directly from the input buffer. Structs that contain these
40//! fields automatically gain a `'a` lifetime parameter.
41//!
42//! ```no_run
43//! use synta_codegen::{parse, generate_with_config, CodeGenConfig, StringTypeMode};
44//!
45//! let schema = r#"
46//! Msg DEFINITIONS ::= BEGIN
47//! Msg ::= SEQUENCE {
48//! payload OCTET STRING,
49//! label UTF8String
50//! }
51//! END
52//! "#;
53//!
54//! let module = parse(schema).unwrap();
55//! let config = CodeGenConfig {
56//! string_type_mode: StringTypeMode::Borrowed,
57//! ..Default::default()
58//! };
59//! // Emits:
60//! // pub struct Msg<'a> {
61//! // pub payload: OctetStringRef<'a>,
62//! // pub label: Utf8StringRef<'a>,
63//! // }
64//! let rust_code = generate_with_config(&module, config).unwrap();
65//! println!("{}", rust_code);
66//! ```
67//!
68//! String types that have no zero-copy variant (`TeletexString`, `BmpString`,
69//! `UniversalString`, `GeneralString`, `NumericString`, `VisibleString`) are
70//! always emitted as owned types regardless of [`StringTypeMode`].
71//!
72//! Named bit strings (`BIT STRING { flag(0), … }`) are always emitted as
73//! owned `BitString` because they are decoded into a concrete bit-field type.
74//!
75//! ## Derive macro gating
76//!
77//! By default every `Asn1Sequence` / `Asn1Set` / `Asn1Choice` derive and its
78//! associated `asn1(…)` helper attributes are wrapped in
79//! `#[cfg_attr(feature = "derive", …)]`. This lets the consuming crate make
80//! `synta-derive` an **optional** dependency controlled by a Cargo feature:
81//!
82//! ```toml
83//! # Cargo.toml of the consuming crate (default behaviour)
84//! [dependencies]
85//! synta-derive = { version = "0.1", optional = true }
86//!
87//! [features]
88//! derive = ["dep:synta-derive"]
89//! ```
90//!
91//! Third-party crates that **always** depend on `synta-derive` and do not want
92//! to expose a `derive` Cargo feature can use [`DeriveMode::Always`]:
93//!
94//! ```no_run
95//! use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};
96//!
97//! let schema = r#"
98//! Msg DEFINITIONS ::= BEGIN
99//! Msg ::= SEQUENCE { id INTEGER }
100//! END
101//! "#;
102//!
103//! let module = parse(schema).unwrap();
104//! let config = CodeGenConfig {
105//! derive_mode: DeriveMode::Always,
106//! ..Default::default()
107//! };
108//! // Emits:
109//! // #[derive(Debug, Clone, PartialEq)]
110//! // #[derive(Asn1Sequence)] ← no cfg_attr wrapper
111//! // pub struct Msg { pub id: Integer }
112//! let rust_code = generate_with_config(&module, config).unwrap();
113//! println!("{}", rust_code);
114//! ```
115//!
116//! If the crate uses a feature name other than `"derive"`, pass it via
117//! [`DeriveMode::Custom`]:
118//!
119//! ```no_run
120//! use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};
121//!
122//! # let schema = "Msg DEFINITIONS ::= BEGIN Msg ::= SEQUENCE { id INTEGER } END";
123//! # let module = parse(schema).unwrap();
124//! let config = CodeGenConfig {
125//! derive_mode: DeriveMode::Custom("asn1-derive".to_string()),
126//! ..Default::default()
127//! };
128//! // Emits:
129//! // #[cfg_attr(feature = "asn1-derive", derive(Asn1Sequence))]
130//! let rust_code = generate_with_config(&module, config).unwrap();
131//! println!("{}", rust_code);
132//! ```
133//!
134//! ## Import path prefix
135//!
136//! Use [`CodeGenConfig::with_crate_imports`], [`CodeGenConfig::with_super_imports`],
137//! or [`CodeGenConfig::with_custom_prefix`] to emit `use` statements instead of
138//! the default comment-only import annotations.
139//!
140//! ## Constrained INTEGER type selection
141//!
142//! When a top-level `INTEGER` type carries a value-range constraint (e.g.
143//! `INTEGER (0..100)`), synta-codegen generates a newtype whose inner field is
144//! the **smallest native Rust integer primitive** that covers the declared range,
145//! rather than the arbitrary-precision `Integer` type:
146//!
147//! - Lower bound ≥ 0 → unsigned: `u8` (≤255), `u16` (≤65535), `u32` (≤4294967295), `u64`.
148//! - Lower bound < 0 → signed: `i8`, `i16`, `i32`, `i64`.
149//! - Unconstrained bounds (`MIN`/`MAX`, named values) → `i64`.
150//!
151//! Using a primitive type means the generated struct automatically derives
152//! `Copy`, `PartialOrd`, and `Ord`, and avoids heap allocation. For example,
153//! `Percentage ::= INTEGER (0..100)` produces:
154//!
155//! ```text
156//! #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
157//! pub struct Percentage(u8);
158//!
159//! impl Percentage {
160//! pub fn new(value: u8) -> Result<Self, &'static str> { ... }
161//! pub const fn new_unchecked(value: u8) -> Self { Percentage(value) }
162//! pub const fn get(&self) -> u8 { self.0 }
163//! pub fn into_inner(self) -> u8 { self.0 }
164//! }
165//! ```
166//!
167//! The equivalent C generation uses `uint8_t` / `uint16_t` / `uint32_t` /
168//! `uint64_t` for non-negative ranges and `int8_t` / `int16_t` / `int32_t` /
169//! `int64_t` for signed ranges.
170
171pub mod ast;
172pub mod c_cmake_codegen;
173pub mod c_codegen;
174pub mod c_impl_codegen;
175pub mod c_meson_codegen;
176pub mod codegen;
177pub mod import_graph;
178pub mod naming;
179pub mod parser;
180
181pub use ast::{Definition, Module, Type};
182pub use c_cmake_codegen::{generate_cmake, CMakeConfig};
183pub use c_codegen::{generate_c, generate_c_with_config, CCodeGenConfig};
184pub use c_impl_codegen::{generate_c_impl, CImplConfig, PatternMode};
185pub use c_meson_codegen::{generate_meson, MesonConfig};
186pub use codegen::{generate, generate_with_config, CodeGenConfig, DeriveMode, StringTypeMode};
187pub use import_graph::{detect_cycles, topological_order, ImportCycle};
188pub use naming::module_file_stem;
189pub use parser::{parse, ParseError};
190
191/// Parse ASN.1 schema and generate Rust code in one step
192pub fn parse_and_generate(input: &str) -> Result<String, Box<dyn std::error::Error>> {
193 let module = parse(input)?;
194 let code = generate(&module)?;
195 Ok(code)
196}
197
198/// Parse ASN.1 schema and generate C header in one step
199pub fn parse_and_generate_c(input: &str) -> Result<String, Box<dyn std::error::Error>> {
200 let module = parse(input)?;
201 let code = generate_c(&module)?;
202 Ok(code)
203}
204
205/// Parse ASN.1 schema and generate C implementation in one step
206pub fn parse_and_generate_c_impl(
207 input: &str,
208 header_file: &str,
209) -> Result<String, Box<dyn std::error::Error>> {
210 let module = parse(input)?;
211 let config = CImplConfig {
212 header_file: header_file.to_string(),
213 arena_mode: false,
214 pattern_mode: Default::default(),
215 with_containing: false,
216 };
217 let code = generate_c_impl(&module, config)?;
218 Ok(code)
219}