use crate::backends::rustler::gen_bindings::helpers::{
gen_elixir_enum_module_with_known_types, gen_elixir_opaque_module, gen_elixir_struct_module, gen_native_ex,
};
use crate::backends::rustler::template_env;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use ahash::AHashSet;
use heck::ToSnakeCase;
use std::collections::HashMap;
use std::path::PathBuf;
pub(super) struct PublicFileContext<'a> {
pub(super) app_name: &'a str,
pub(super) app_module: &'a str,
pub(super) crate_name: &'a str,
pub(super) exclude_functions: &'a AHashSet<String>,
pub(super) exclude_types: &'a AHashSet<&'a str>,
pub(super) opaque_types: &'a AHashSet<String>,
}
pub(super) fn generated_module_files(
api: &ApiSurface,
config: &ResolvedCrateConfig,
context: PublicFileContext<'_>,
) -> (String, Vec<GeneratedFile>) {
let output_dir = elixir_output_dir(config);
let enum_defaults = enum_defaults(api);
let mut files = Vec::new();
push_native_module_file(api, config, &context, &output_dir, &mut files);
push_struct_module_files(api, &context, &output_dir, &enum_defaults, &mut files);
push_opaque_module_files(api, config, &context, &output_dir, &mut files);
push_enum_module_files(api, &context, &output_dir, &mut files);
(output_dir, files)
}
pub(super) fn append_stream_error_exception(content: &mut String, config: &ResolvedCrateConfig, app_module: &str) {
let streaming_adapters: Vec<_> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
.collect();
if streaming_adapters.is_empty() {
return;
}
let exception_module = format!("{app_module}.StreamError");
let rendered = template_env::render(
"elixir_stream_error_exception.jinja",
minijinja::context! {
exception_module => &exception_module,
},
);
let dedented = rendered
.lines()
.map(|line| line.strip_prefix(" ").unwrap_or(line))
.collect::<Vec<_>>()
.join("\n");
content.push('\n');
content.push_str(&dedented);
if !content.ends_with('\n') {
content.push('\n');
}
}
fn elixir_output_dir(config: &ResolvedCrateConfig) -> String {
if let Some(elixir_output) = config.output_paths.get("elixir") {
let path = elixir_output.to_string_lossy();
if let Some(idx) = path.find("/native/") {
format!("{}/lib/", &path[..idx])
} else {
path.into_owned()
}
} else {
"packages/elixir/lib/".to_owned()
}
}
fn enum_defaults(api: &ApiSurface) -> HashMap<String, String> {
api.enums
.iter()
.filter_map(|enum_def| {
let default_variant = enum_def
.variants
.iter()
.find(|variant| variant.is_default)
.or_else(|| enum_def.variants.first())?;
Some((
enum_def.name.clone(),
crate::codegen::naming::pascal_to_snake(&default_variant.name),
))
})
.collect()
}
fn push_native_module_file(
api: &ApiSurface,
config: &ResolvedCrateConfig,
context: &PublicFileContext<'_>,
output_dir: &str,
files: &mut Vec<GeneratedFile>,
) {
let native_content = gen_native_ex(
api,
context.app_name,
context.app_module,
context.crate_name,
config,
context.exclude_functions,
context.exclude_types,
);
files.push(GeneratedFile {
path: PathBuf::from(output_dir)
.join(context.app_name.to_snake_case())
.join("native.ex"),
content: native_content,
generated_header: false,
});
}
fn push_struct_module_files(
api: &ApiSurface,
context: &PublicFileContext<'_>,
output_dir: &str,
enum_defaults: &HashMap<String, String>,
files: &mut Vec<GeneratedFile>,
) {
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !context.exclude_types.contains(typ.name.as_str()))
{
if typ.is_opaque || typ.fields.is_empty() {
continue;
}
let struct_content = gen_elixir_struct_module(typ, context.app_module, enum_defaults, context.opaque_types);
let file_name = format!("{}.ex", typ.name.to_snake_case());
files.push(GeneratedFile {
path: PathBuf::from(output_dir)
.join(context.app_name.to_snake_case())
.join(file_name),
content: struct_content,
generated_header: false,
});
}
}
fn push_opaque_module_files(
api: &ApiSurface,
config: &ResolvedCrateConfig,
context: &PublicFileContext<'_>,
output_dir: &str,
files: &mut Vec<GeneratedFile>,
) {
for typ in api
.types
.iter()
.filter(|typ| typ.is_opaque && !typ.is_trait && !context.exclude_types.contains(typ.name.as_str()))
{
let opaque_content = gen_elixir_opaque_module(typ, context.app_module, config);
let file_name = format!("{}.ex", typ.name.to_snake_case());
files.push(GeneratedFile {
path: PathBuf::from(output_dir)
.join(context.app_name.to_snake_case())
.join(file_name),
content: opaque_content,
generated_header: false,
});
}
}
fn push_enum_module_files(
api: &ApiSurface,
context: &PublicFileContext<'_>,
output_dir: &str,
files: &mut Vec<GeneratedFile>,
) {
let known_type_names: AHashSet<String> = api.types.iter().map(|typ| typ.name.clone()).collect();
for enum_def in &api.enums {
let enum_content = gen_elixir_enum_module_with_known_types(enum_def, context.app_module, &known_type_names);
let file_name = format!("{}.ex", enum_def.name.to_snake_case());
files.push(GeneratedFile {
path: PathBuf::from(output_dir)
.join(context.app_name.to_snake_case())
.join(file_name),
content: enum_content,
generated_header: false,
});
}
}