#![allow(clippy::missing_errors_doc)]
use std::{
collections::{BTreeMap, BTreeSet},
path::{Path, PathBuf},
};
use derive_builder::Builder;
use serde::Serialize;
use thiserror::Error;
use crate::{
Registry,
reflection::format::{Format, FormatHolder, Namespace},
};
#[derive(Clone, Debug, Serialize)]
#[expect(
deprecated,
reason = "CodeGeneratorConfig still contains the deprecated CustomCode field for backwards compatibility"
)]
pub struct CodeGeneratorConfig {
pub module_name: String,
pub encoding: Encoding,
pub external_definitions: ExternalDefinitions,
pub external_packages: ExternalPackages,
pub comments: DocComments,
pub custom_code: CustomCode,
pub package_manifest: bool,
pub features: BTreeSet<Feature>,
pub used_format_types: BTreeSet<String>,
pub referenced_namespaces: BTreeSet<String>,
}
#[derive(Clone, Copy, Default, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize)]
pub enum Encoding {
#[default]
None,
Json,
Bincode,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Ord, PartialOrd, Serialize)]
pub enum Feature {
BigInt,
Bytes,
ListOfT,
MapOfT,
OptionOfT,
SetOfT,
TupleArray,
}
impl Encoding {
#[must_use]
pub fn is_none(self) -> bool {
self == Encoding::None
}
#[must_use]
pub fn is_json(self) -> bool {
self == Encoding::Json
}
#[must_use]
pub fn is_bincode(self) -> bool {
self == Encoding::Bincode
}
}
pub type ExternalDefinitions =
BTreeMap< String, Vec<String>>;
pub type ExternalPackages =
BTreeMap< String, ExternalPackage>;
pub type DocComments = BTreeMap< Vec<String>, String>;
#[deprecated(
since = "0.16.0",
note = "custom_code was only used by the Java generator, which is deprecated. Use Kotlin instead."
)]
pub type CustomCode =
BTreeMap< Vec<String>, String>;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("invalid UTF-8 in runtime file: {0}")]
Utf8(#[from] std::str::Utf8Error),
#[error("JSON serialization error: {0}")]
Json(#[from] serde_json::Error),
}
pub trait SourceInstaller {
fn install_module(
&mut self,
config: &CodeGeneratorConfig,
registry: &Registry,
) -> std::result::Result<(), Error>;
fn install_serde_runtime(&mut self) -> std::result::Result<(), Error>;
fn install_bincode_runtime(&self) -> std::result::Result<(), Error>;
fn install_manifest(&self, _module_name: &str) -> std::result::Result<(), Error> {
Ok(())
}
}
#[expect(
deprecated,
reason = "impl references the deprecated CustomCode type and with_custom_code method"
)]
impl CodeGeneratorConfig {
#[must_use]
pub fn new(module_name: String) -> Self {
Self {
module_name,
encoding: Encoding::default(),
external_definitions: BTreeMap::new(),
external_packages: BTreeMap::new(),
comments: BTreeMap::new(),
custom_code: BTreeMap::new(),
package_manifest: true,
features: BTreeSet::new(),
used_format_types: BTreeSet::new(),
referenced_namespaces: BTreeSet::new(),
}
}
#[must_use]
pub fn module_name(&self) -> &str {
&self.module_name
}
#[must_use]
pub fn with_parent(mut self, parent: &str) -> Self {
if parent == self.module_name() {
return self;
}
self.module_name = format!("{}.{}", parent, self.module_name());
self
}
#[must_use]
pub fn with_encoding(mut self, encoding: Encoding) -> Self {
self.encoding = encoding;
self
}
#[must_use]
pub fn has_encoding(&self) -> bool {
!self.encoding.is_none()
}
#[must_use]
pub fn with_external_definitions(mut self, external_definitions: ExternalDefinitions) -> Self {
self.external_definitions = external_definitions;
self
}
#[must_use]
pub fn with_comments(mut self, mut comments: DocComments) -> Self {
for comment in comments.values_mut() {
*comment = format!("{}\n", comment.trim());
}
self.comments = comments;
self
}
#[must_use]
#[deprecated(
since = "0.16.0",
note = "custom_code was only used by the Java generator, which is deprecated. Use Kotlin instead."
)]
pub fn with_custom_code(mut self, code: CustomCode) -> Self {
self.custom_code = code;
self
}
#[must_use]
pub fn with_package_manifest(mut self, package_manifest: bool) -> Self {
self.package_manifest = package_manifest;
self
}
pub fn update_from(&mut self, registry: &Registry) {
for format in registry.values() {
format
.visit(&mut |f| {
match f {
Format::I128 | Format::U128 => {
self.features.insert(Feature::BigInt);
}
Format::Bytes => {
self.features.insert(Feature::Bytes);
}
Format::Seq(..) => {
self.features.insert(Feature::ListOfT);
}
Format::Option(..) => {
self.features.insert(Feature::OptionOfT);
}
Format::Set(..) => {
self.features.insert(Feature::SetOfT);
}
Format::Map { .. } => {
self.features.insert(Feature::MapOfT);
}
Format::TupleArray { .. } => {
self.features.insert(Feature::TupleArray);
}
_ => (),
}
if let Format::TypeName(qualified_name) = f
&& let Namespace::Named(ns) = &qualified_name.namespace
&& ns != &self.module_name
{
self.referenced_namespaces.insert(ns.clone());
}
let format_key = match f {
Format::Unit => "unit",
Format::Bool => "bool",
Format::I8 => "int8",
Format::I16 => "int16",
Format::I32 => "int32",
Format::I64 => "int64",
Format::I128 => "int128",
Format::U8 => "uint8",
Format::U16 => "uint16",
Format::U32 => "uint32",
Format::U64 => "uint64",
Format::U128 => "uint128",
Format::F32 => "float32",
Format::F64 => "float64",
Format::Char => "char",
Format::Str => "str",
Format::Bytes => "bytes",
Format::Option(_) => "option",
Format::Seq(_) | Format::Set(_) => "seq",
Format::Map { .. } => "map",
Format::Tuple(_) => "tuple",
Format::TupleArray { .. } => "list_tuple",
Format::TypeName(_) | Format::Variable(_) => "",
};
if !format_key.is_empty() {
self.used_format_types.insert(format_key.to_string());
}
Ok(())
})
.expect("failed to parse registry");
}
for name in registry.keys() {
if let Namespace::Named(ns) = &name.namespace
&& ns != &self.module_name
{
let entry = self.external_definitions.entry(ns.to_owned()).or_default();
entry.push(name.name.clone());
}
}
}
}
impl Encoding {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Encoding::None => "none",
Encoding::Json => "json",
Encoding::Bincode => "bincode",
}
}
}
#[derive(Default, Builder)]
#[builder(
custom_constructor,
create_empty = "empty",
build_fn(private, name = "fallible_build")
)]
pub struct Config {
#[builder(setter(into))]
pub package_name: String,
#[builder(setter(into))]
pub out_dir: PathBuf,
#[builder(default = vec![], setter(each(name = "reference")))]
pub external_packages: Vec<ExternalPackage>,
#[builder(default, setter(custom))]
pub encoding: Encoding,
#[builder(default = false, setter(custom))]
pub add_extensions: bool,
}
impl Config {
pub fn builder(name: &str, out_dir: impl AsRef<Path>) -> ConfigBuilder {
ConfigBuilder {
package_name: Some(name.to_string()),
out_dir: Some(out_dir.as_ref().to_path_buf()),
..ConfigBuilder::empty()
}
}
}
impl ConfigBuilder {
#[must_use]
pub fn encoding(&mut self, encoding: Encoding) -> &mut Self {
self.encoding = Some(encoding);
self
}
#[must_use]
pub fn add_extensions(&mut self) -> &mut Self {
self.add_extensions = Some(true);
self
}
#[must_use]
pub fn build(&self) -> Config {
self.fallible_build()
.expect("All required fields were initialized")
}
}
#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum PackageLocation {
Path(String),
Url(String),
}
#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExternalPackage {
pub for_namespace: String,
pub location: PackageLocation,
pub module_name: Option<String>,
pub version: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_parent() {
let root_package = "root";
let child_package = "child";
let root_config = CodeGeneratorConfig::new(root_package.to_string());
let actual = root_config.with_parent(root_package).module_name;
let expected = root_package;
assert_eq!(&actual, expected);
let actual = CodeGeneratorConfig::new(child_package.to_string())
.with_parent(root_package)
.module_name;
let expected = format!("{root_package}.{child_package}");
assert_eq!(&actual, &expected);
}
#[test]
fn config_builder_populates_external_packages() {
let config = Config::builder("MyPackage", "/tmp/out")
.reference(ExternalPackage {
for_namespace: "serde".to_string(),
location: PackageLocation::Path("../Serde".to_string()),
module_name: None,
version: None,
})
.reference(ExternalPackage {
for_namespace: "other".to_string(),
location: PackageLocation::Path("../Other".to_string()),
module_name: None,
version: None,
})
.build();
assert_eq!(config.external_packages.len(), 2);
assert_eq!(config.external_packages[0].for_namespace, "serde");
assert_eq!(config.external_packages[1].for_namespace, "other");
}
#[test]
fn config_builder_defaults_external_packages_to_empty() {
let config = Config::builder("MyPackage", "/tmp/out").build();
assert!(config.external_packages.is_empty());
}
}