#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ConversionSelector {
FieldPath(String),
SemanticType(String),
NamedType(String),
}
impl ConversionSelector {
#[must_use]
pub fn semantic_type(name: impl Into<String>) -> Self {
Self::SemanticType(name.into())
}
#[must_use]
pub fn named_type(name: impl Into<String>) -> Self {
Self::NamedType(name.into())
}
#[must_use]
pub fn field_path(path: impl Into<String>) -> Self {
Self::FieldPath(path.into())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub enum DomainVarData {
#[default]
Bytes,
Strings,
#[cfg(feature = "compact_str")]
CompactStrings,
#[cfg(feature = "smol_str")]
SmolStrings,
#[cfg(feature = "bytes")]
BytesCrate,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub enum GenerationProfile {
#[default]
Full,
Lean,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ItemKind {
Enum,
Set,
Composite,
MessageDecoder,
MessageEncoder,
DomainStruct,
}
#[derive(Clone, Debug)]
pub struct EnumVariantInfo {
pub name: String,
pub snake_name: String,
pub label: String,
pub value: i128,
pub description: Option<String>,
}
#[derive(Clone, Debug)]
pub struct SetChoiceInfo {
pub name: String,
pub snake_name: String,
pub label: String,
pub bit_position: u8,
pub description: Option<String>,
}
#[derive(Clone, Debug)]
pub struct FieldInfo {
pub name: String,
pub rust_type: String,
pub offset: Option<usize>,
pub since_version: u16,
pub semantic_type: Option<String>,
pub presence: &'static str,
pub null_value: Option<u64>,
pub deprecated: bool,
pub description: Option<String>,
}
#[derive(Clone)]
pub enum ItemContext<'a> {
Enum {
schema: &'a crate::Schema,
name: String,
encoding_type: String,
variants: Vec<EnumVariantInfo>,
},
Set {
schema: &'a crate::Schema,
name: String,
encoding_type: String,
choices: Vec<SetChoiceInfo>,
},
Composite {
schema: &'a crate::Schema,
name: String,
fields: Vec<FieldInfo>,
},
MessageDecoder {
schema: &'a crate::Schema,
name: String,
template_id: u16,
block_length: usize,
fields: Vec<FieldInfo>,
},
MessageEncoder {
schema: &'a crate::Schema,
name: String,
template_id: u16,
block_length: usize,
fields: Vec<FieldInfo>,
},
DomainStruct {
schema: &'a crate::Schema,
name: String,
fields: Vec<FieldInfo>,
},
}
impl std::fmt::Debug for ItemContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (kind, name) = match self {
Self::Enum { name, .. } => ("Enum", name.as_str()),
Self::Set { name, .. } => ("Set", name.as_str()),
Self::Composite { name, .. } => ("Composite", name.as_str()),
Self::MessageDecoder { name, .. } => ("MessageDecoder", name.as_str()),
Self::MessageEncoder { name, .. } => ("MessageEncoder", name.as_str()),
Self::DomainStruct { name, .. } => ("DomainStruct", name.as_str()),
};
f.debug_struct("ItemContext")
.field("kind", &kind)
.field("name", &name)
.finish()
}
}
pub type HookFn = dyn Fn(&ItemContext<'_>) -> Vec<proc_macro2::TokenStream> + Send + Sync;
#[derive(Clone, Default)]
pub(crate) struct Hooks(Vec<std::sync::Arc<HookFn>>);
impl std::fmt::Debug for Hooks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Hooks").field(&self.0.len()).finish()
}
}
impl Hooks {
pub(crate) fn push(&mut self, hook: std::sync::Arc<HookFn>) {
self.0.push(hook);
}
pub(crate) fn iter(&self) -> std::slice::Iter<'_, std::sync::Arc<HookFn>> {
self.0.iter()
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Clone)]
pub struct GenerationConfig {
pub(crate) module_name: String,
pub(crate) shared_module: Option<String>,
pub(crate) domain_objects: bool,
pub(crate) domain_var_data: DomainVarData,
pub(crate) conversions: Vec<ConversionSelector>,
pub(crate) domain_types: Vec<(ConversionSelector, String)>,
pub(crate) external_sbe_rt_path: Option<String>,
pub(crate) error_from_path: Option<String>,
pub(crate) null_as_option: Vec<ConversionSelector>,
pub(crate) all_enums_as_option: bool,
pub(crate) auto_bool_domain: bool,
pub(crate) keyword_append_token: String,
pub(crate) deprecated_attrs: bool,
pub(crate) enable_display_debug: bool,
pub(crate) enable_meta_attributes: bool,
pub(crate) enable_dispatch: bool,
pub(crate) hooks: Hooks,
}
impl std::fmt::Debug for GenerationConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GenerationConfig")
.field("module_name", &self.module_name)
.field("shared_module", &self.shared_module)
.field("domain_objects", &self.domain_objects)
.field("domain_var_data", &self.domain_var_data)
.field("conversions", &self.conversions)
.field("domain_types", &self.domain_types)
.field("external_sbe_rt_path", &self.external_sbe_rt_path)
.field("error_from_path", &self.error_from_path)
.field("null_as_option", &self.null_as_option)
.field("all_enums_as_option", &self.all_enums_as_option)
.field("auto_bool_domain", &self.auto_bool_domain)
.field("keyword_append_token", &self.keyword_append_token)
.field("deprecated_attrs", &self.deprecated_attrs)
.field("enable_display_debug", &self.enable_display_debug)
.field("enable_meta_attributes", &self.enable_meta_attributes)
.field("enable_dispatch", &self.enable_dispatch)
.field("hooks", &self.hooks)
.finish()
}
}
impl GenerationConfig {
#[must_use]
pub fn new(module_name: impl Into<String>) -> Self {
Self {
module_name: module_name.into(),
shared_module: None,
domain_objects: false,
domain_var_data: DomainVarData::Bytes,
conversions: Vec::new(),
domain_types: Vec::new(),
external_sbe_rt_path: None,
error_from_path: None,
keyword_append_token: "_".into(),
deprecated_attrs: false,
null_as_option: Vec::new(),
all_enums_as_option: false,
auto_bool_domain: false,
enable_display_debug: true,
enable_meta_attributes: true,
enable_dispatch: true,
hooks: Hooks::default(),
}
}
#[must_use]
pub fn lean(module_name: impl Into<String>) -> Self {
Self::new(module_name).profile(GenerationProfile::Lean)
}
#[must_use]
pub(crate) fn module_name(&self) -> &str {
&self.module_name
}
#[must_use]
pub fn with_module_name(mut self, name: impl Into<String>) -> Self {
let name = name.into();
debug_assert!(
is_valid_module_ident(&name),
"module name '{name}' contains path separators, '.', '..', or is empty"
);
self.module_name = name;
self
}
#[must_use]
pub(crate) fn domain_objects_enabled(&self) -> bool {
self.domain_objects
}
pub(crate) fn has_conversions(&self) -> bool {
!self.conversions.is_empty() || !self.domain_types.is_empty() || self.auto_bool_domain
}
#[must_use]
pub(crate) fn external_sbe_rt_path(&self) -> Option<&str> {
self.external_sbe_rt_path.as_deref()
}
#[must_use]
pub fn with_external_sbe_rt(mut self, path: impl Into<String>) -> Self {
self.external_sbe_rt_path = Some(path.into());
self
}
#[must_use]
pub fn with_conversion(mut self, selector: ConversionSelector) -> Self {
if !self.conversions.contains(&selector) {
self.conversions.push(selector);
}
self
}
#[must_use]
pub fn with_null_as_option(mut self, selector: ConversionSelector) -> Self {
if !self.null_as_option.contains(&selector) {
self.null_as_option.push(selector);
}
self
}
#[must_use]
pub fn with_all_enums_as_option(mut self) -> Self {
self.all_enums_as_option = true;
self
}
#[must_use]
pub fn with_domain_type(
mut self,
selector: ConversionSelector,
rust_type: impl Into<String>,
) -> Self {
let sel = selector;
let ty = rust_type.into();
if !self.conversions.contains(&sel) {
self.conversions.push(sel.clone());
}
if let Some(existing) = self.domain_types.iter_mut().find(|(s, _)| s == &sel) {
existing.1 = ty;
} else {
self.domain_types.push((sel, ty));
}
self
}
#[must_use]
pub fn with_error_from_impls(mut self, path: impl Into<String>) -> Self {
self.error_from_path = Some(path.into());
self
}
#[must_use]
pub fn with_domain_objects(mut self, var_data: DomainVarData) -> Self {
self.domain_objects = true;
self.domain_var_data = var_data;
self
}
#[must_use]
pub fn with_shared_module(mut self, name: impl Into<String>) -> Self {
self.shared_module = Some(name.into());
self
}
#[must_use]
pub fn with_keyword_append_token(mut self, token: impl Into<String>) -> Self {
self.keyword_append_token = token.into();
self
}
#[must_use]
pub fn with_bool_domain_type(mut self, enable: bool) -> Self {
self.auto_bool_domain = enable;
self
}
#[must_use]
pub fn with_deprecated_attrs(mut self, enable: bool) -> Self {
self.deprecated_attrs = enable;
self
}
#[must_use]
pub fn with_display_debug(mut self, enable: bool) -> Self {
self.enable_display_debug = enable;
self
}
#[must_use]
pub fn with_meta_attributes(mut self, enable: bool) -> Self {
self.enable_meta_attributes = enable;
self
}
#[must_use]
pub fn with_dispatch(mut self, enable: bool) -> Self {
self.enable_dispatch = enable;
self
}
#[must_use]
pub fn profile(mut self, profile: GenerationProfile) -> Self {
match profile {
GenerationProfile::Full => {
self.enable_display_debug = true;
self.enable_meta_attributes = true;
self.enable_dispatch = true;
}
GenerationProfile::Lean => {
self.enable_display_debug = false;
self.enable_meta_attributes = false;
self.enable_dispatch = false;
self.domain_objects = false;
}
}
self
}
#[must_use]
pub fn with_hook<F>(mut self, hook: F) -> Self
where
F: Fn(&ItemContext) -> Vec<proc_macro2::TokenStream> + Send + Sync + 'static,
{
self.hooks.push(std::sync::Arc::new(hook));
self
}
pub(crate) fn has_hooks(&self) -> bool {
!self.hooks.is_empty()
}
pub(crate) fn run_hooks(&self, ctx: &ItemContext, out: &mut String) {
for hook in self.hooks.iter() {
for ts in hook(ctx) {
use std::fmt::Write;
let _ = writeln!(out, "{}", ts);
}
}
}
}
impl Default for GenerationConfig {
fn default() -> Self {
Self::new("messages")
}
}
pub(crate) fn is_valid_module_ident(name: &str) -> bool {
if name.is_empty()
|| name.contains('/')
|| name.contains('\\')
|| name.contains('.')
|| name == ".."
{
return false;
}
if is_rust_keyword_or_reserved(name) {
return false;
}
syn::parse_str::<syn::Ident>(name).is_ok()
}
fn is_rust_keyword_or_reserved(name: &str) -> bool {
matches!(
name,
"as" | "break" | "const" | "continue" | "crate" | "else" | "enum" | "extern"
| "false" | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match"
| "mod" | "move" | "mut" | "pub" | "ref" | "return" | "self" | "Self"
| "static" | "struct" | "super" | "trait" | "true" | "type" | "unsafe"
| "use" | "where" | "while"
| "async" | "await" | "dyn"
| "abstract" | "become" | "box" | "do" | "final" | "macro" | "override"
| "priv" | "typeof" | "unsized" | "virtual" | "yield"
| "try" | "gen"
)
}
#[cfg(test)]
mod tests {
use super::{ConversionSelector, DomainVarData, GenerationConfig, GenerationProfile};
#[test]
fn default_config_is_clean() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::default();
assert_eq!(config.module_name(), "messages");
assert!(!config.domain_objects_enabled());
assert!(!config.has_conversions());
Ok(())
}
#[test]
fn with_conversion_adds_selector() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::new("test")
.with_conversion(ConversionSelector::named_type("Decimal"));
assert!(config.has_conversions());
assert_eq!(config.conversions.len(), 1);
Ok(())
}
#[test]
fn profile_lean_preserves_explicit_conversions_and_domain_types()
-> Result<(), Box<dyn std::error::Error>> {
let full = GenerationConfig::new("m").profile(GenerationProfile::Full);
assert!(full.enable_display_debug);
assert!(full.enable_meta_attributes);
assert!(full.enable_dispatch);
let lean = GenerationConfig::new("m")
.with_domain_objects(DomainVarData::Bytes)
.with_conversion(ConversionSelector::named_type("Decimal"))
.with_domain_type(
ConversionSelector::named_type("Decimal"),
"rust_decimal::Decimal",
)
.profile(GenerationProfile::Lean);
assert!(!lean.enable_display_debug);
assert!(!lean.enable_meta_attributes);
assert!(!lean.enable_dispatch);
assert!(!lean.domain_objects);
assert!(
lean.has_conversions(),
"explicit conversions must survive Lean"
);
assert!(
!lean.domain_types.is_empty(),
"explicit domain types must survive Lean"
);
let override_dispatch = GenerationConfig::new("m")
.profile(GenerationProfile::Lean)
.with_dispatch(true);
assert!(override_dispatch.enable_dispatch);
Ok(())
}
#[test]
fn with_conversion_dedup() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::new("test")
.with_conversion(ConversionSelector::named_type("Decimal"))
.with_conversion(ConversionSelector::named_type("Decimal"));
assert_eq!(config.conversions.len(), 1);
Ok(())
}
#[test]
fn with_domain_type_adds_conversion_and_type() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::new("test").with_domain_type(
ConversionSelector::named_type("Decimal"),
"rust_decimal::Decimal",
);
assert!(config.has_conversions());
assert_eq!(config.conversions.len(), 1);
assert_eq!(config.domain_types.len(), 1);
Ok(())
}
#[test]
fn with_domain_type_dedup() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::new("test")
.with_domain_type(
ConversionSelector::named_type("Decimal"),
"rust_decimal::Decimal",
)
.with_domain_type(
ConversionSelector::named_type("Decimal"),
"rust_decimal::Decimal",
);
assert_eq!(config.domain_types.len(), 1);
Ok(())
}
#[test]
fn with_external_sbe_rt_sets_path() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::new("m").with_external_sbe_rt("crate::rt::sbe_rt");
assert_eq!(config.external_sbe_rt_path(), Some("crate::rt::sbe_rt"));
Ok(())
}
#[test]
fn module_ident_rejects_keywords_and_reserved() -> Result<(), Box<dyn std::error::Error>> {
use super::is_valid_module_ident;
assert!(is_valid_module_ident("messages"));
assert!(is_valid_module_ident("common_types"));
assert!(!is_valid_module_ident("gen")); assert!(!is_valid_module_ident("mod"));
assert!(!is_valid_module_ident("async"));
assert!(!is_valid_module_ident("try"));
assert!(!is_valid_module_ident(""));
assert!(!is_valid_module_ident("a.b"));
Ok(())
}
#[test]
fn new_config_has_correct_defaults() -> Result<(), Box<dyn std::error::Error>> {
let config = GenerationConfig::new("mymod");
assert_eq!(config.module_name(), "mymod");
assert!(!config.domain_objects_enabled());
assert!(config.conversions.is_empty());
assert!(config.domain_types.is_empty());
assert_eq!(config.domain_var_data, DomainVarData::Bytes);
assert!(config.enable_display_debug);
assert!(config.enable_meta_attributes);
assert!(config.enable_dispatch);
Ok(())
}
#[test]
fn with_domain_objects_var_data_modes() -> Result<(), Box<dyn std::error::Error>> {
let text = GenerationConfig::new("m").with_domain_objects(DomainVarData::Strings);
assert!(text.domain_objects_enabled());
assert_eq!(text.domain_var_data, DomainVarData::Strings);
let bytes = GenerationConfig::new("m").with_domain_objects(DomainVarData::Bytes);
assert!(bytes.domain_objects_enabled());
assert_eq!(bytes.domain_var_data, DomainVarData::Bytes);
Ok(())
}
#[test]
fn opt_in_codegen_flags_and_field_selector_are_recorded()
-> Result<(), Box<dyn std::error::Error>> {
let selector = ConversionSelector::field_path("Order.price");
assert_eq!(
selector,
ConversionSelector::FieldPath("Order.price".to_string())
);
let config = GenerationConfig::new("m")
.with_error_from_impls("crate::AppError")
.with_shared_module("shared")
.with_keyword_append_token("x")
.with_bool_domain_type(true)
.with_deprecated_attrs(true);
assert_eq!(config.error_from_path.as_deref(), Some("crate::AppError"));
assert_eq!(config.shared_module.as_deref(), Some("shared"));
assert_eq!(config.keyword_append_token, "x");
assert!(config.auto_bool_domain);
assert!(config.deprecated_attrs);
Ok(())
}
}