# Rust Code Generation
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [1. Quick Start](#1-quick-start)
- [CLI](#cli)
- [Library API](#library-api)
- [2. Type Mappings](#2-type-mappings)
- [3. Naming Conventions](#3-naming-conventions)
- [Type Names -- PascalCase](#type-names-pascalcase)
- [Field Names -- snake_case](#field-names-snake_case)
- [Named Value Constants -- SCREAMING_SNAKE_CASE](#named-value-constants-screamingsnakecase)
- [Module Names in Use Statements](#module-names-in-use-statements)
- [4. Constraint Validation](#4-constraint-validation)
- [Value Range Constraints](#value-range-constraints)
- [Size Constraints](#size-constraints)
- [Permitted Alphabet Constraints (FROM)](#permitted-alphabet-constraints-from)
- [PATTERN Constraints](#pattern-constraints)
- [CONTAINING Constraints](#containing-constraints)
- [Union, Intersection, and Complement Constraints](#union-intersection-and-complement-constraints)
- [Named Bits](#named-bits)
- [Inner Type Constraints](#inner-type-constraints)
- [Additional Methods on Constrained Newtypes](#additional-methods-on-constrained-newtypes)
- [5. OPTIONAL and DEFAULT Handling](#5-optional-and-default-handling)
- [OPTIONAL Fields](#optional-fields)
- [DEFAULT Values](#default-values)
- [6. Tagged Fields](#6-tagged-fields)
- [Attribute syntax](#attribute-syntax)
- [Example](#example)
- [7. IMPORTS and Module References](#7-imports-and-module-references)
- [No Import Prefix (default)](#no-import-prefix-default)
- [--crate-imports](#-crate-imports)
- [--super-imports](#-super-imports)
- [--module-prefix](#-module-prefix)
- [Multiple import sources](#multiple-import-sources)
- [8. no_std Support](#8-no_std-support)
- [9. build.rs Integration](#9-buildrs-integration)
- [Multi-module build.rs](#multi-module-buildrs)
- [Recommended project layout](#recommended-project-layout)
- [10. Library API Reference](#10-library-api-reference)
- [`parse`](#parse)
- [`generate`](#generate)
- [`generate_with_config`](#generatewithconfig)
- [`CodeGenConfig`](#codegenconfig)
- [`StringTypeMode`](#stringtypemode)
- [`DeriveMode`](#derivemode)
- [11. Owned vs. Borrowed String Types](#11-owned-vs-borrowed-string-types)
- [Default: Owned types](#default-owned-types)
- [Borrowed mode: zero-copy Ref types](#borrowed-mode-zero-copy-ref-types)
- [Which types are affected](#which-types-are-affected)
- [Lifetime propagation](#lifetime-propagation)
- [Named bit strings are always owned](#named-bit-strings-are-always-owned)
- [What Is Not Generated](#what-is-not-generated)
This document describes how synta-codegen generates Rust source code from ASN.1 module
definitions. It covers type mappings, naming conventions, constraint validation, OPTIONAL
and DEFAULT handling, tagged fields, import modes, no_std support, build.rs integration,
and the library API.
---
## 1. Quick Start
### CLI
Generate Rust from an ASN.1 schema file:
```sh
synta-codegen --lang rust schema.asn1 -o src/generated.rs
```
`--lang rust` is the default and may be omitted.
Generate into a directory (one file per ASN.1 module):
```sh
synta-codegen schema.asn1 --output-dir src/gen/
```
Import mode flags (select one):
```sh
synta-codegen schema.asn1 --crate-imports # use crate::module::Type
synta-codegen schema.asn1 --super-imports # use super::module::Type
synta-codegen schema.asn1 --module-prefix my_crate # use my_crate::module::Type
```
For no_std environments:
```sh
synta-codegen schema.asn1 --use-core -o src/generated.rs
```
### Library API
```rust
use synta_codegen::{parse, generate, generate_with_config, CodeGenConfig};
let src = std::fs::read_to_string("schema.asn1")?;
let module = parse(&src)?;
// Defaults: std paths, no import prefix, owned string types
let rust_code = generate(&module)?;
// With crate-relative imports
let rust_code = generate_with_config(&module, CodeGenConfig::with_crate_imports())?;
// With zero-copy borrowed string types (for parse-only workloads)
use synta_codegen::StringTypeMode;
let config = CodeGenConfig {
string_type_mode: StringTypeMode::Borrowed,
..Default::default()
};
let rust_code = generate_with_config(&module, config)?;
```
---
## 2. Type Mappings
The table below lists every ASN.1 built-in type and the Rust type or structure that
synta-codegen emits.
| SEQUENCE | `struct` with named `pub` fields | — |
| SET | `struct` with named `pub` fields (same as SEQUENCE) | — |
| SEQUENCE OF T | type alias `type Foo = Vec<T>` (SEQUENCE tag 0x30) | — |
| SET OF T | type alias `type Foo = SetOf<T>` (SET tag 0x31) | — |
| CHOICE | `enum` with one variant per alternative | — |
| INTEGER | newtype wrapping `synta::Integer` | — |
| INTEGER (constrained) | newtype wrapping a native primitive (`u8`/`i8`…`u64`/`i64`); range-checked `new()` | — |
| ENUMERATED | `enum` with `i64` discriminant variants | — |
| BOOLEAN | `synta::Boolean` | — |
| OCTET STRING | `synta::OctetString` | `synta::OctetStringRef<'a>` |
| OCTET STRING (const.) | newtype wrapping `synta::OctetString` | newtype wrapping `synta::OctetStringRef<'a>` |
| BIT STRING | `synta::BitString` | `synta::BitStringRef<'a>` |
| BIT STRING (named) | `synta::BitString` (always owned; see §11) | `synta::BitString` (always owned) |
| BIT STRING (const.) | newtype wrapping `synta::BitString` | newtype wrapping `synta::BitStringRef<'a>` |
| OBJECT IDENTIFIER | `synta::ObjectIdentifier` | — |
| NULL | `synta::Null` | — |
| REAL | `synta::Real` | — |
| UTF8String | `synta::Utf8String` | `synta::Utf8StringRef<'a>` |
| PrintableString | `synta::PrintableString` | `synta::PrintableStringRef<'a>` |
| IA5String | `synta::IA5String` | `synta::IA5StringRef<'a>` |
| TeletexString | `synta::TeletexString` | `synta::TeletexString` (no Ref variant) |
| UniversalString | `synta::UniversalString` | `synta::UniversalString` (no Ref variant) |
| BMPString | `synta::BmpString` | `synta::BmpString` (no Ref variant) |
| GeneralString | `synta::GeneralString` | `synta::GeneralString` (no Ref variant) |
| NumericString | `synta::NumericString` | `synta::NumericString` (no Ref variant) |
| VisibleString | `synta::VisibleString` | `synta::VisibleString` (no Ref variant) |
| UTCTime | `synta::UtcTime` | — |
| GeneralizedTime | `synta::GeneralizedTime` | — |
| ANY | `synta::Element<'a>` (or `synta::RawDer<'a>` with `any_as_raw_der`) | — |
| ANY DEFINED BY | `synta::Element<'a>` (or `synta::RawDer<'a>` with `any_as_raw_der`) | — |
| TypeRef (no extra constraint) | type alias `type Foo = Bar` | — |
| TypeRef (extra constraint) | newtype `struct Foo(pub Bar)` with checked `new()` | — |
"—" means the type is unaffected by `StringTypeMode`. See [§11](#11-owned-vs-borrowed-string-types) for details and examples.
---
## 3. Naming Conventions
Naming follows the rules in `naming.rs`. The three categories are type names, field
names, and named-value constants.
### Type Names -- PascalCase
Each hyphen-delimited segment is converted: first character uppercased, remaining
characters lowercased. All-caps heuristic applies per segment, not per character.
| `Certificate` | `Certificate` |
| `KDC-REQ` | `KdcReq` |
| `TBSCertificate` | `Tbscertificate` |
| `my-Type` | `MyType` |
| `PA-DATA` | `PaData` |
### Field Names -- snake_case
Hyphens are replaced with underscores. Rust keywords receive an `r#` prefix.
| `serialNumber` | `serial_number` |
| `issuer-unique-id` | `issuer_unique_id` |
| `type` | `r#type` |
| `mod` | `r#mod` |
| `version` | `version` |
### Named Value Constants -- SCREAMING_SNAKE_CASE
Named values in INTEGER or ENUMERATED types are placed as associated constants on the
generated type.
ASN.1:
```asn1
Protocol ::= INTEGER { tcp(6), udp(17) }
```
Generated Rust:
```rust
pub struct Protocol(pub synta::Integer);
impl Protocol {
pub const TCP: i64 = 6;
pub const UDP: i64 = 17;
}
```
### Module Names in Use Statements
ASN.1 module names are converted to snake_case when used in Rust `use` paths.
Each hyphen-separated segment is lowercased independently; camelCase boundaries
are split before each uppercase letter that follows a lowercase letter.
| `BaseTypes` | `base_types` |
| `UserModule` | `user_module` |
| `CamelCaseModule` | `camel_case_module` |
| `kebab-case-module` | `kebab_case_module` |
| `SCREAMING-CASE` | `screaming_case` |
| `PKIX1Explicit88` | `p_k_i_x1_explicit88` |
The module name from the ASN.1 `DEFINITIONS` header is used, not the file name.
---
## 4. Constraint Validation
Constraints are checked inside a `new()` constructor on the generated newtype. The
constructor returns `Result<Self, &'static str>`.
### Value Range Constraints
ASN.1:
```asn1
Port ::= INTEGER (1..65535)
```
Generated Rust:
```rust
pub struct Port(u16);
impl Port {
pub fn new(value: u16) -> std::result::Result<Self, &'static str> {
let val: i64 = value as i64;
if (1..=65535).contains(&val) {
Ok(Self(value))
} else {
Err("must be in range 1..65535")
}
}
pub const fn new_unchecked(value: u16) -> Self { Self(value) }
pub const fn get(&self) -> u16 { self.0 }
pub fn into_inner(self) -> u16 { self.0 }
}
```
The inner field type is the smallest unsigned or signed native Rust integer
that covers the declared range: `u8`/`u16`/`u32`/`u64` when the lower bound
is ≥ 0, or `i8`/`i16`/`i32`/`i64` when the lower bound is negative. Because
the inner type is a primitive, the generated struct derives `Copy`, `PartialOrd`,
and `Ord` automatically.
### Size Constraints
ASN.1:
```asn1
Label ::= UTF8String (SIZE (1..64))
```
Generated Rust:
```rust
pub struct Label(pub synta::Utf8String);
impl Label {
pub fn new(value: synta::Utf8String) -> std::result::Result<Self, &'static str> {
let len = value.as_str().len();
if !(1..=64).contains(&len) {
return Err("Label: size out of range 1..64");
}
Ok(Self(value))
}
}
```
### Permitted Alphabet Constraints (FROM)
ASN.1:
```asn1
The `new()` constructor iterates over each character and checks membership in the
permitted set.
### PATTERN Constraints
When the `regex` feature is enabled, a static `Lazy<Regex>` is generated and matched
inside `new()`. When the feature is absent, a TODO comment is emitted instead.
With `regex` feature:
```rust,ignore
use once_cell::sync::Lazy;
use regex::Regex;
static FOO_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[A-Z]{2}$").expect("invalid regex")
});
impl Foo {
pub fn new(value: synta::PrintableString) -> Result<Self, &'static str> {
if !FOO_PATTERN.is_match(value.as_str()) {
return Err("Foo: value does not match required pattern");
}
Ok(Self(value))
}
}
```
Without `regex` feature:
```rust
pub struct Foo(pub synta::PrintableString);
impl Foo {
pub fn new(value: synta::PrintableString) -> std::result::Result<Self, &'static str> {
// TODO: pattern constraint ^[A-Z]{2}$ not validated (enable feature "regex")
Ok(Self(value))
}
}
```
### CONTAINING Constraints
When the `validate_containing` feature is enabled, the constructor decodes the inner
type from the raw value using a scratch decoder. When the feature is absent, a TODO
comment is emitted.
### Union, Intersection, and Complement Constraints
- Union (`|`): `new()` returns `Ok` if any one condition passes.
- Intersection (`^`): `new()` returns `Ok` only if all conditions pass.
- Complement (`ALL EXCEPT`): `new()` returns `Ok` if the inner constraint does NOT pass.
### Named Bits
ASN.1:
```asn1
KeyUsage ::= BIT STRING {
digitalSignature (0),
keyEncipherment (2)
}
```
Generated Rust:
```rust
pub struct KeyUsage(pub synta::BitString);
impl KeyUsage {
pub const DIGITAL_SIGNATURE: u32 = 0;
pub const KEY_ENCIPHERMENT: u32 = 2;
}
```
### Inner Type Constraints
For `SEQUENCE OF` or `SET OF` with element constraints, `new()` iterates over elements
and validates each one.
ASN.1:
```asn1
PortList ::= SEQUENCE OF INTEGER (1..65535)
```
The generated `new()` on `PortList` checks that every element is within `1..65535`.
### Additional Methods on Constrained Newtypes
Every constrained newtype -- whether wrapping INTEGER, a string type, OCTET STRING,
or BIT STRING -- receives additional methods beyond `new()`.
**For constrained INTEGER:**
```rust
pub struct Port(u16);
impl Port {
// Validated constructor; accepts the native primitive type
pub fn new(value: u16) -> std::result::Result<Self, &'static str> {
let val: i64 = value as i64;
if (1..=65535).contains(&val) {
Ok(Self(value))
} else {
Err("must be in range 1..65535")
}
}
// Bypass validation -- use only for data already known to be valid
pub const fn new_unchecked(value: u16) -> Self { Self(value) }
// Return the inner value by copy (primitives are Copy)
pub const fn get(&self) -> u16 { self.0 }
// Consume into the inner value
pub fn into_inner(self) -> u16 { self.0 }
}
// TryFrom<Integer> is generated for the decode path:
// wire Integer → as_i64 → range-check → native primitive
impl core::convert::TryFrom<Integer> for Port {
type Error = &'static str;
fn try_from(value: Integer) -> std::result::Result<Self, Self::Error> {
let n = value.as_i64().map_err(|_| "integer value out of i64 range")?;
let v = u16::try_from(n).map_err(|_| "must be in range 1..65535")?;
Self::new(v)
}
}
```
**For constrained string types (IA5String, PrintableString, Utf8String):**
```rust
pub struct Label(pub synta::Utf8String);
impl Label {
pub fn new(value: synta::Utf8String) -> std::result::Result<Self, &'static str> {
let len = value.as_str().len();
if !(1..=64).contains(&len) {
return Err("Label: size out of range 1..64");
}
Ok(Self(value))
}
pub fn new_unchecked(value: synta::Utf8String) -> Self { Self(value) }
pub fn get(&self) -> &synta::Utf8String { &self.0 }
pub fn as_str(&self) -> &str { self.0.as_str() } // text string types only
pub fn into_inner(self) -> synta::Utf8String { self.0 }
}
impl core::convert::TryFrom<synta::Utf8String> for Label {
type Error = &'static str;
fn try_from(value: synta::Utf8String) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
```
`as_str()` is generated for text string types (`IA5String`, `PrintableString`,
`Utf8String`). It is not generated for `OctetString` or `BitString` constrained
newtypes.
Note: `new_unchecked` and `get` on INTEGER constrained types are `const fn`; the
string equivalents are not.
---
## 5. OPTIONAL and DEFAULT Handling
### OPTIONAL Fields
Fields marked `OPTIONAL` in ASN.1 are generated as `Option<T>` in the Rust struct.
Field order matches the ASN.1 definition order.
ASN.1:
```asn1
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT INTEGER DEFAULT 0,
serialNumber INTEGER,
issuerUniqueID [1] IMPLICIT BIT STRING OPTIONAL
}
```
Generated Rust:
```rust
#[derive(Debug, Clone, PartialEq)]
pub struct Tbscertificate {
pub version: Option<synta::Integer>,
pub serial_number: synta::Integer,
pub issuer_unique_id: Option<synta::BitString>,
}
```
### DEFAULT Values
When every field in a struct is either OPTIONAL or carries a DEFAULT value,
synta-codegen generates `impl Default` for the struct.
- DEFAULT integer literals -> `const` used as the default expression.
- DEFAULT boolean -> `true` or `false` literal.
- DEFAULT named values -> reference to the named constant on the type.
`impl Default` is NOT generated when some fields are mandatory and carry no default.
---
## 6. Tagged Fields
Context tags (`[N] EXPLICIT` and `[N] IMPLICIT`) are parsed and preserved in the ASN.1
AST. Actual encode and decode behavior is handled by the synta library's derive macros
(`Asn1Sequence`, `Asn1Choice`, `Asn1Set`). synta-codegen emits a `#[cfg_attr]` field
attribute that passes the tag number and mode to the derive macro.
### Attribute syntax
Every context-specific tag attribute must carry an explicit mode keyword.
Writing `asn1(tag(N))` without `implicit` or `explicit` is a compile error in
the Synta derive macros. When the ASN.1 module declares a default tagging mode,
synta-codegen resolves that default at generation time and always emits one of the
two explicit forms:
```rust
#[cfg_attr(feature = "derive", asn1(tag(N, implicit)))]
pub field: T,
#[cfg_attr(feature = "derive", asn1(tag(N, explicit)))]
pub field: T,
```
### Example
ASN.1 (module default tagging: IMPLICIT):
```asn1
Message ::= SEQUENCE {
priority [0] INTEGER OPTIONAL, -- inherits module default: IMPLICIT
expires [1] IMPLICIT GeneralizedTime OPTIONAL,
metadata [2] EXPLICIT OCTET STRING OPTIONAL
}
```
Generated Rust:
```rust
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "derive", derive(Asn1Sequence))]
pub struct Message {
// [0] inherits the module default (IMPLICIT); codegen resolves it at generation time.
#[cfg_attr(feature = "derive", asn1(tag(0, implicit)))]
pub priority: Option<synta::Integer>,
#[cfg_attr(feature = "derive", asn1(tag(1, implicit)))]
pub expires: Option<synta::GeneralizedTime>,
#[cfg_attr(feature = "derive", asn1(tag(2, explicit)))]
pub metadata: Option<synta::OctetString>,
}
```
The `#[cfg_attr(feature = "derive", ...)]` guard means the attribute is active only when
the `derive` feature is enabled, keeping the generated types usable without the
`synta-derive` crate.
---
## 7. IMPORTS and Module References
ASN.1 IMPORTS are converted to Rust `use` statements. Three modes are available,
selected by CLI flag.
### No Import Prefix (default)
No `use` statements are emitted for imported types. Types are expected to be in scope
by other means (e.g., a hand-written `mod.rs`).
### --crate-imports
```rust
use crate::pkix::AlgorithmIdentifier;
use crate::pkix::Name;
```
Module name is the snake_case form of the ASN.1 module name from which the type is
imported.
### --super-imports
```rust
use super::pkix::AlgorithmIdentifier;
use super::pkix::Name;
```
Useful when the generated file lives inside a subdirectory module and the parent module
re-exports the dependencies.
### --module-prefix
```sh
synta-codegen schema.asn1 --module-prefix my_crate
```
Generates:
```rust
use my_crate::pkix::AlgorithmIdentifier;
use my_crate::pkix::Name;
```
The prefix is prepended verbatim followed by `::`.
### Multiple import sources
ASN.1 may import from several modules in one `IMPORTS` block:
```asn1
IMPORTS
Type1, Type2 FROM Module1
Type3 FROM Module2
Type4, Type5 FROM Module3;
```
Each source module produces one `use` statement. Multiple types from the same module
are grouped into a brace list:
```rust
use crate::module1::{Type1, Type2};
use crate::module2::Type3;
use crate::module3::{Type4, Type5};
```
---
## 8. no_std Support
The `--use-core` flag replaces `std::` paths with `core::` equivalents in the generated
code. This is required when targeting embedded or other no_std environments.
```sh
synta-codegen schema.asn1 --use-core -o src/generated.rs
```
Effect on generated code:
```rust
// Without --use-core
use std::convert::TryFrom;
// With --use-core
use core::convert::TryFrom;
```
The synta library itself supports no_std when its `std` feature is disabled. Generated
code with `--use-core` is compatible with that configuration.
---
## 9. build.rs Integration
synta-codegen can be called from a Cargo build script to regenerate types automatically
whenever the schema file changes.
Add synta-codegen as a build dependency in `Cargo.toml`:
```toml
[build-dependencies]
synta-codegen = { path = "../synta-codegen" }
```
In `build.rs`:
```rust
use std::path::PathBuf;
use synta_codegen::{parse, generate_with_config, CodeGenConfig};
fn main() {
let schema = std::fs::read_to_string("schema.asn1")
.expect("failed to read schema.asn1");
let module = parse(&schema).expect("failed to parse ASN.1");
// Use crate-relative imports; all other options default.
let config = CodeGenConfig::with_crate_imports();
let code = generate_with_config(&module, config)
.expect("failed to generate Rust code");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
std::fs::write(out_dir.join("generated_types.rs"), code)
.expect("failed to write generated_types.rs");
println!("cargo:rerun-if-changed=schema.asn1");
}
```
In `src/lib.rs` (or wherever the types are needed):
```rust
include!(concat!(env!("OUT_DIR"), "/generated_types.rs"));
```
### Multi-module build.rs
When a project splits its schema across several ASN.1 files that import from each
other, list them in dependency order (base modules first) and generate each in turn:
```rust
use synta_codegen::{parse, generate_with_config, CodeGenConfig};
use std::{fs, path::PathBuf};
fn main() {
// Modules listed in dependency order: base first, dependents after.
let modules = ["base_types", "user_module", "admin_module"];
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let config = CodeGenConfig::with_crate_imports();
for name in &modules {
let schema = fs::read_to_string(format!("schemas/{name}.asn1"))
.unwrap_or_else(|e| panic!("failed to read {name}.asn1: {e}"));
let module = parse(&schema)
.unwrap_or_else(|e| panic!("failed to parse {name}.asn1: {e}"));
let code = generate_with_config(&module, config.clone())
.unwrap_or_else(|e| panic!("failed to generate {name}: {e}"));
fs::write(out_dir.join(format!("{name}.rs")), code)
.unwrap_or_else(|e| panic!("failed to write {name}.rs: {e}"));
println!("cargo:rerun-if-changed=schemas/{name}.asn1");
}
}
```
In `src/lib.rs`:
```rust
mod base_types { include!(concat!(env!("OUT_DIR"), "/base_types.rs")); }
mod user_module { include!(concat!(env!("OUT_DIR"), "/user_module.rs")); }
mod admin_module{ include!(concat!(env!("OUT_DIR"), "/admin_module.rs"));}
pub use base_types::{BaseType1, BaseType2};
pub use user_module::User;
```
The generated `use crate::base_types::*` statements in `user_module.rs` resolve
correctly because the modules are declared at crate root.
### Recommended project layout
```
my-project/
+-- Cargo.toml
+-- build.rs
+-- schemas/
| +-- base_types.asn1
| +-- user_module.asn1
| +-- admin_module.asn1
+-- src/
+-- lib.rs (contains the include! macros above)
+-- manual.rs (hand-written code that uses the generated types)
```
---
## 10. Library API Reference
### `parse`
```rust
pub fn parse(input: &str) -> Result<Module, ParseError>
```
Parses an ASN.1 module definition from `input` and returns a `Module` value
representing the parsed AST. Returns `ParseError` on syntax or semantic errors.
### `generate`
```rust
pub fn generate(module: &Module) -> Result<String, std::fmt::Error>
```
Generates Rust source code for `module` using default configuration: owned string
types, `std` paths, no import prefix. Returns the generated source as a `String`.
Use `generate_with_config` to customise any of these settings.
### `generate_with_config`
```rust
pub fn generate_with_config(
module: &Module,
config: CodeGenConfig,
) -> Result<String, std::fmt::Error>
```
Generates Rust source code applying the settings in `config`.
### `CodeGenConfig`
```rust
pub struct CodeGenConfig {
/// Module path prefix for `use` statements emitted for ASN.1 IMPORTS.
/// `None` (default): imports are annotated as comments only.
pub module_path_prefix: Option<String>,
/// Emit `core::convert::TryFrom` instead of `std::convert::TryFrom`.
/// Required for `#![no_std]` targets. Default: `false`.
pub use_core: bool,
/// Type names from IMPORTS that should not be re-declared locally.
pub skip_imported_types: std::collections::HashSet<String>,
/// Lifetime requirements for imported types.
/// Key: type name; value: lifetime string (e.g. `"'a"`).
pub imported_type_lifetimes: std::collections::HashMap<String, String>,
/// Whether string/binary types use owned or zero-copy borrowed forms.
/// Default: `StringTypeMode::Owned`.
pub string_type_mode: StringTypeMode,
/// When `true`, `ANY` and `ANY DEFINED BY` fields are generated as
/// `RawDer<'a>` instead of `Element<'a>` for zero-copy lazy decoding.
/// Default: `false`.
pub any_as_raw_der: bool,
/// Controls how derive macros and their helper attributes are emitted.
/// Default: `DeriveMode::FeatureGated`.
pub derive_mode: DeriveMode,
}
```
Three convenience constructors set `module_path_prefix` and leave everything else at
its default:
```rust
// use crate::module_name::Type
let config = CodeGenConfig::with_crate_imports();
// use super::module_name::Type
let config = CodeGenConfig::with_super_imports();
// use my_lib::module_name::Type
let config = CodeGenConfig::with_custom_prefix("my_lib");
```
All three constructors produce configs with `StringTypeMode::Owned` and
`use_core: false`. Use struct-update syntax to override individual fields:
```rust
use synta_codegen::{CodeGenConfig, StringTypeMode};
let config = CodeGenConfig {
string_type_mode: StringTypeMode::Borrowed,
..CodeGenConfig::with_crate_imports()
};
```
### `StringTypeMode`
```rust
pub enum StringTypeMode {
Owned, // default
Borrowed,
}
```
Controls whether the five ASN.1 string/binary types that have a zero-copy `Ref`
variant are emitted as owned heap-allocating types or as borrowed references.
See [§11](#11-owned-vs-borrowed-string-types) for a full explanation.
### `DeriveMode`
```rust
pub enum DeriveMode {
FeatureGated, // default
Always,
Custom(String),
}
```
Controls how derive macros (`Asn1Sequence`, `Asn1Choice`, `Asn1Set`) and their
helper attributes are emitted in generated code.
- **`FeatureGated`** (default) — wraps every annotation in
`#[cfg_attr(feature = "derive", …)]`. The consuming crate must expose a
`derive` Cargo feature that pulls in `synta-derive`:
```toml
[features]
derive = ["dep:synta-derive"]
```
This is the safest default: `synta-derive` is only compiled when the consumer
opts in, keeping the mandatory dependency tree small.
- **`Always`** — emits `#[derive(Asn1Sequence)]` unconditionally with no
`cfg_attr` gate. Use this when the consuming crate always depends on
`synta-derive` and does not want to expose a separate `derive` feature.
```rust
use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};
let config = CodeGenConfig {
derive_mode: DeriveMode::Always,
..CodeGenConfig::with_crate_imports()
};
let code = generate_with_config(&module, config)?;
```
- **`Custom(name)`** — uses a caller-supplied feature name in the `cfg_attr`
guard instead of `"derive"`. Useful when the consuming crate exposes its own
feature name for derive support:
```rust
use synta_codegen::{parse, generate_with_config, CodeGenConfig, DeriveMode};
let config = CodeGenConfig {
derive_mode: DeriveMode::Custom("asn1-derive".to_string()),
..CodeGenConfig::with_crate_imports()
};
let code = generate_with_config(&module, config)?;
```
---
## 11. Owned vs. Borrowed String Types
Several ASN.1 string and binary types have two Rust representations in synta:
| `OCTET STRING` | `OctetString` | `OctetStringRef<'a>` |
| `BIT STRING` | `BitString` | `BitStringRef<'a>` |
| `UTF8String` | `Utf8String` | `Utf8StringRef<'a>` |
| `PrintableString` | `PrintableString` | `PrintableStringRef<'a>`|
| `IA5String` | `IA5String` | `IA5StringRef<'a>` |
`CodeGenConfig::string_type_mode` selects which form synta-codegen emits.
### Default: Owned types
`StringTypeMode::Owned` (the default) emits the owned forms. Owned types heap-allocate
their contents on decode and have no lifetime parameter. This is the most convenient
choice when you need to construct structs programmatically (e.g. in tests or message
builders).
```asn1
Msg ::= SEQUENCE {
label UTF8String,
data OCTET STRING
}
```
Generated Rust (default):
```rust
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "derive", derive(Asn1Sequence))]
pub struct Msg {
pub label: Utf8String,
pub data: OctetString,
}
```
### Borrowed mode: zero-copy Ref types
`StringTypeMode::Borrowed` emits the `Ref` forms that borrow directly from the decoder's
input buffer. No heap allocation occurs for the string content at decode time. This is
optimal for parse-only workloads such as X.509 certificate inspection, where structs are
decoded, inspected, and discarded — never constructed from scratch.
```rust
use synta_codegen::{CodeGenConfig, StringTypeMode};
let config = CodeGenConfig {
string_type_mode: StringTypeMode::Borrowed,
..Default::default()
};
let code = generate_with_config(&module, config)?;
```
Generated Rust (borrowed mode):
```rust
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "derive", derive(Asn1Sequence))]
pub struct Msg<'a> {
pub label: Utf8StringRef<'a>,
pub data: OctetStringRef<'a>,
}
```
### Which types are affected
Only the five types listed in the table above are affected. Other string types —
`TeletexString`, `UniversalString`, `BmpString`, `GeneralString`, `NumericString`,
`VisibleString` — have no `Ref` variant in synta and are always emitted as owned types
regardless of the mode.
Type aliases inherit the mode:
```asn1
MyLabel ::= UTF8String
```
Borrowed mode emits:
```rust
pub type MyLabel<'a> = Utf8StringRef<'a>;
```
### Lifetime propagation
When a struct contains a field of a borrowed type (directly or transitively), synta-codegen
adds a `'a` lifetime parameter to that struct. Structs that only contain owned types or
unaffected types receive no lifetime parameter.
```asn1
Inner ::= SEQUENCE { name UTF8String }
Outer ::= SEQUENCE { inner Inner, count INTEGER }
```
Borrowed mode:
```rust
pub struct Inner<'a> { pub name: Utf8StringRef<'a> }
pub struct Outer<'a> { pub inner: Inner<'a>, pub count: Integer }
```
`Outer` gains `<'a>` because it contains `Inner<'a>`.
### Named bit strings are always owned
A `BIT STRING` with named bits (a named-bit list) is always emitted as an owned
`BitString` regardless of mode, because it is decoded into a concrete bit-field type
and the bit constants are expressed as plain `u32` offsets into it.
```asn1
KeyUsage ::= BIT STRING {
digitalSignature (0),
keyEncipherment (2)
} (SIZE (32..MAX))
```
Generated Rust (both modes):
```rust
pub struct KeyUsage(pub synta::BitString); // always owned
impl KeyUsage {
pub const DIGITAL_SIGNATURE: u32 = 0;
pub const KEY_ENCIPHERMENT: u32 = 2;
}
```
---
## What Is Not Generated
The following are parsed and understood by synta-codegen but produce no Rust output:
- Table constraints and information object class instances/object sets (X.681/X.682). CLASS definitions are parsed and emit a documentation comment but no DER-encodable type.
- Serde derive attributes (available as a separate synta-derive feature, not via codegen).
- Trait implementations beyond `Debug`, `Clone`, and `PartialEq`.
- Explicit encode/decode methods -- these are handled entirely by the synta library's
derive macros at compile time.
- User-defined constraint validation hooks.