#![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,
generation::indent::IndentConfig,
reflection::format::{ContainerFormat, Format, FormatHolder, Namespace, VariantFormat},
};
#[derive(Clone, Debug)]
pub struct CodeGeneratorConfig {
pub module_name: String,
pub external_definitions: ExternalDefinitions,
pub external_packages: ExternalPackages,
pub comments: DocComments,
pub package_manifest: bool,
pub features: BTreeSet<Feature>,
pub indent: IndentConfig,
pub used_format_types: BTreeSet<String>,
pub referenced_namespaces: BTreeSet<String>,
pub unit_variant_enums: BTreeSet<String>,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Ord, PartialOrd, Serialize)]
#[non_exhaustive]
pub enum Feature {
BigInt,
Bytes,
ListOfT,
MapOfT,
OptionOfT,
SetOfT,
TupleArray,
Uuid,
}
pub type ExternalDefinitions =
BTreeMap< String, Vec<String>>;
pub type ExternalPackages =
BTreeMap< String, ExternalPackage>;
pub type DocComments = 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_manifest(&self, _module_name: &str) -> std::result::Result<(), Error> {
Ok(())
}
}
impl CodeGeneratorConfig {
#[must_use]
pub const fn new(module_name: String) -> Self {
Self {
module_name,
external_definitions: BTreeMap::new(),
external_packages: BTreeMap::new(),
comments: BTreeMap::new(),
package_manifest: true,
features: BTreeSet::new(),
used_format_types: BTreeSet::new(),
referenced_namespaces: BTreeSet::new(),
unit_variant_enums: BTreeSet::new(),
indent: IndentConfig::Space(4),
}
}
#[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 const fn with_indent(mut self, indent: IndentConfig) -> Self {
self.indent = indent;
self
}
#[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]
pub const 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::Uuid => {
self.features.insert(Feature::Uuid);
}
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::Uuid => "uuid",
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, format) in registry {
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());
}
if let ContainerFormat::Enum(variants, _) = format
&& variants
.values()
.all(|v| matches!(v.value, VariantFormat::Unit))
{
self.unit_variant_enums.insert(name.name.clone());
}
}
}
}
#[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>,
}
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 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());
}
}