mod dart_traits;
mod errors;
mod functions;
mod render_type;
pub(super) mod service_api;
mod trait_bridge;
mod types;
use crate::backends::dart::naming::dart_style;
use crate::core::backend::{
Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep, PostProcessor,
};
use crate::core::config::{DartStyle, Language, ResolvedCrateConfig, TraitBridgeConfig, resolve_output_dir};
use crate::core::ir::{ApiSurface, FunctionDef};
use std::collections::BTreeSet;
use std::path::PathBuf;
use crate::backends::dart::gen_ffi;
use crate::backends::dart::gen_rust_crate;
use dart_traits::emit_dart_traits;
use functions::emit_function;
use service_api as gen_service_api;
use trait_bridge::emit_trait_bridge_methods;
pub struct DartBackend;
impl Backend for DartBackend {
fn name(&self) -> &str {
"dart"
}
fn language(&self) -> Language {
Language::Dart
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
supports_callbacks: false,
supports_streaming: true,
supports_service_api: true,
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if dart_style(config) == DartStyle::Ffi {
return gen_ffi::emit(api, config);
}
let module_name = dart_module_name(&config.name);
let barrel_name = config
.dart
.as_ref()
.and_then(|c| c.lib_name.as_deref())
.map(|n| n.replace('-', "_"))
.unwrap_or_else(|| module_name.clone());
let exclude_functions: std::collections::HashSet<&str> = config
.dart
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let _exclude_types: std::collections::HashSet<&str> = config
.dart
.as_ref()
.map(|c| c.exclude_types.iter().map(String::as_str).collect())
.unwrap_or_default();
let deduped_functions = crate::codegen::fn_dedup::dedup_same_name_functions(&api.functions);
let visible_functions: Vec<&FunctionDef> = deduped_functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.filter(|f| {
!crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&f.name, &config.trait_bridges)
})
.collect();
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
body.push_str(&crate::backends::dart::template_env::render(
"dart_bridge_export.jinja",
minijinja::context! {
module_name => module_name.as_str(),
},
));
body.push_str("export 'traits.dart';\n");
let dart_backend_name = "dart";
let active_bridge_configs: Vec<&TraitBridgeConfig> = config
.trait_bridges
.iter()
.filter(|b| !b.exclude_languages.iter().any(|l| l == dart_backend_name))
.filter(|b| b.register_fn.is_some() || b.unregister_fn.is_some() || b.clear_fn.is_some())
.collect();
if !visible_functions.is_empty() || !active_bridge_configs.is_empty() {
body.push_str(&crate::backends::dart::template_env::render(
"dart_bridge_imports.jinja",
minijinja::context! {
module_name => module_name.as_str(),
},
));
body.push('\n');
let bridge_class = config.dart_bridge_class_name();
body.push_str(&crate::backends::dart::template_env::render(
"dart_bridge_class_open.jinja",
minijinja::context! {
bridge_class => bridge_class.as_str(),
},
));
for f in &visible_functions {
emit_function(f, &api.types, &api.enums, &mut body, &mut imports);
body.push('\n');
}
for bridge_cfg in &active_bridge_configs {
emit_trait_bridge_methods(bridge_cfg, &mut body);
}
emit_streaming_adapter_methods(config, &mut body, &mut imports);
body.push_str("}\n");
}
if body.contains("Int64List(") || body.contains("Uint8List(") || body.contains("Float64List(") {
imports.insert("import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';".to_string());
}
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n\n");
for import in &imports {
content.push_str(import);
content.push('\n');
}
if !imports.is_empty() {
content.push('\n');
}
content.push_str(&body);
let dir = resolve_output_dir(None, &config.name, "packages/dart/lib/src");
let path = PathBuf::from(format!("{dir}/{module_name}.dart"));
let barrel_dir = resolve_output_dir(None, &config.name, "packages/dart/lib");
let barrel_path = PathBuf::from(format!("{barrel_dir}/{barrel_name}.dart"));
let barrel_content = crate::backends::dart::template_env::render(
"dart_barrel_file.jinja",
minijinja::context! {
module_name => module_name.as_str(),
},
);
let mut files = vec![
GeneratedFile {
path,
content,
generated_header: false,
},
GeneratedFile {
path: barrel_path,
content: barrel_content,
generated_header: false,
},
];
let rust_crate_files = gen_rust_crate::emit(api, config)?;
files.extend(rust_crate_files);
let trait_names: Vec<&str> = config
.trait_bridges
.iter()
.filter(|b| !b.exclude_languages.iter().any(|l| l == dart_backend_name))
.map(|b| b.trait_name.as_str())
.collect();
let mut traits_content = String::new();
traits_content.push_str("// Generated by alef. Do not edit by hand.\n\n");
if !trait_names.is_empty() {
let (traits_body, traits_imports) = emit_dart_traits(api, &trait_names);
if !traits_body.is_empty() {
traits_content.push_str(&crate::backends::dart::template_env::render(
"dart_bridge_import.jinja",
minijinja::context! {
module_name => module_name.as_str(),
},
));
for import in &traits_imports {
traits_content.push_str(import);
traits_content.push('\n');
}
traits_content.push('\n');
traits_content.push_str(&traits_body);
} else {
traits_content.push_str("// Traits module (generated stub — no trait bridges configured).\n");
traits_content.push_str("// This file is kept for API surface consistency across language bindings.\n");
}
} else {
traits_content
.push_str("// Traits module (empty in Dart as Dart does not have trait systems like Rust).\n");
traits_content.push_str("// This file is kept for API surface consistency across language bindings.\n");
}
let traits_dir = resolve_output_dir(None, &config.name, "packages/dart/lib/src");
let traits_path = PathBuf::from(format!("{traits_dir}/traits.dart"));
files.push(GeneratedFile {
path: traits_path,
content: traits_content,
generated_header: false,
});
let lib_stem = config.name.replace('-', "_");
let repo_url = config.github_repo();
let crate_version = api.version.to_string();
let native_loader_ctx = minijinja::context! {
crate_name => config.name.as_str(),
lib_stem => lib_stem.as_str(),
version => &crate_version,
repo_url => &repo_url,
};
let helper_dir = resolve_output_dir(None, &config.name, "packages/dart/lib/src");
let helper_path = PathBuf::from(format!("{helper_dir}/native_loader.dart"));
let helper_content =
crate::backends::dart::template_env::render("dart_native_loader_helper.jinja", native_loader_ctx.clone());
files.push(GeneratedFile {
path: helper_path,
content: helper_content,
generated_header: false,
});
let bin_dir = resolve_output_dir(None, &config.name, "packages/dart/bin");
let bin_path = PathBuf::from(format!("{bin_dir}/download_libs.dart"));
let bin_content = crate::backends::dart::template_env::render("bin_download_libs.jinja", native_loader_ctx);
files.push(GeneratedFile {
path: bin_path,
content: bin_content,
generated_header: false,
});
Ok(files)
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "cargo",
crate_suffix: "-dart",
build_dep: BuildDependency::None,
post_build: vec![PostBuildStep::RunCommand {
cmd: "flutter_rust_bridge_codegen",
args: vec![
"generate",
"--config-file",
"packages/dart/rust/flutter_rust_bridge.yaml",
],
}],
})
}
fn build_config_with_config(&self, config: &ResolvedCrateConfig) -> Option<BuildConfig> {
self.build_config_for(config)
}
fn generate_service_api(
&self,
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
gen_service_api::generate(api, config)
}
}
impl DartBackend {
pub fn build_config_for(&self, config: &ResolvedCrateConfig) -> Option<BuildConfig> {
match dart_style(config) {
DartStyle::Ffi => Some(BuildConfig {
tool: "dart",
crate_suffix: "",
build_dep: BuildDependency::Ffi,
post_build: vec![],
}),
DartStyle::Frb => {
let module_name = dart_module_name(&config.name);
let lib_dart_dir = resolve_output_dir(None, &config.name, "packages/dart/lib/src");
let lib_dart_path = PathBuf::from(format!("{lib_dart_dir}/{module_name}_bridge_generated/lib.dart"));
let lib_freezed_path = PathBuf::from(format!(
"{lib_dart_dir}/{module_name}_bridge_generated/lib.freezed.dart"
));
let frb_generated_path = PathBuf::from(format!(
"{lib_dart_dir}/{module_name}_bridge_generated/frb_generated.dart"
));
let exclude_functions: Vec<String> = config
.dart
.as_ref()
.map(|c| c.exclude_functions.clone())
.unwrap_or_default();
let skip_frb = config.dart.as_ref().map(|c| c.skip_frb).unwrap_or(false);
let mut post_build_steps: Vec<PostBuildStep> = if skip_frb {
vec![]
} else {
vec![PostBuildStep::RunCommand {
cmd: "flutter_rust_bridge_codegen",
args: vec![
"generate",
"--config-file",
"packages/dart/rust/flutter_rust_bridge.yaml",
],
}]
};
post_build_steps.push(PostBuildStep::PostProcessFile {
path: lib_dart_path.clone(),
processor: PostProcessor::FrbDartExcludeFunctions(exclude_functions.clone()),
});
post_build_steps.push(PostBuildStep::PostProcessFile {
path: lib_dart_path.clone(),
processor: PostProcessor::FrbDartSealedVariants,
});
if !config.untagged_union_text_types.is_empty() {
post_build_steps.push(PostBuildStep::PostProcessFile {
path: lib_dart_path.clone(),
processor: PostProcessor::FrbDartInjectTextMethods(config.untagged_union_text_types.clone()),
});
}
post_build_steps.push(PostBuildStep::PostProcessFile {
path: frb_generated_path.clone(),
processor: PostProcessor::FrbDartExcludeFunctions(exclude_functions),
});
post_build_steps.push(PostBuildStep::PostProcessFile {
path: frb_generated_path.clone(),
processor: PostProcessor::FrbDartSealedVariants,
});
post_build_steps.push(PostBuildStep::PostProcessFile {
path: frb_generated_path.clone(),
processor: PostProcessor::FrbDartFixHandlerExecutorCalls,
});
for path in [lib_dart_path, frb_generated_path.clone(), lib_freezed_path] {
post_build_steps.push(PostBuildStep::PostProcessFile {
path,
processor: PostProcessor::DartStripTrailingWhitespace,
});
}
let lib_stem = format!("{}_dart", config.name.replace('-', "_"));
post_build_steps.push(PostBuildStep::StageDartNatives { lib_stem });
Some(BuildConfig {
tool: "cargo",
crate_suffix: "-dart",
build_dep: BuildDependency::None,
post_build: post_build_steps,
})
}
}
}
}
fn emit_streaming_adapter_methods(config: &ResolvedCrateConfig, out: &mut String, imports: &mut BTreeSet<String>) {
use crate::core::config::AdapterPattern;
use heck::ToLowerCamelCase;
let module_name = dart_module_name(&config.name);
for adapter in &config.adapters {
if !matches!(adapter.pattern, AdapterPattern::Streaming) {
continue;
}
if adapter.owner_type.is_none() || adapter.item_type.is_none() || adapter.params.is_empty() {
continue;
}
if adapter.skip_languages.iter().any(|l| l == "dart") {
continue;
}
let method_name = adapter.name.to_lower_camel_case();
let item_type = adapter.item_type.as_deref().unwrap_or("Object");
let owner_type = adapter.owner_type.as_deref().unwrap_or("");
let owner_param = owner_type.chars().next().unwrap_or('o').to_lowercase().to_string() + &owner_type[1..];
let request_type_full = adapter.params[0].ty.as_str();
let request_type = request_type_full.rsplit("::").next().unwrap_or(request_type_full);
let request_param = adapter.params[0].name.to_lower_camel_case();
let request_param = if request_param.is_empty() {
"request".to_string()
} else {
request_param
};
imports.insert("import 'dart:async' show Stream;".to_string());
out.push_str(&crate::backends::dart::template_env::render(
"dart_streaming_method.jinja",
minijinja::context! {
method_name => method_name,
item_type => item_type,
owner_type => owner_type,
owner_param => owner_param,
request_type => request_type,
request_param => request_param,
module_name => module_name.as_str(),
},
));
out.push('\n');
}
}
fn dart_module_name(crate_name: &str) -> String {
crate_name.replace('-', "_")
}