mod templates;
#[cfg(test)]
mod tests;
use crate::error::ExtensionError;
use crate::validate::{validate_extension_name, validate_spdx_license};
#[derive(Debug, Clone)]
pub struct ScaffoldConfig {
pub name: String,
pub description: String,
pub version: String,
pub license: String,
pub maintainer: String,
pub github_repo: String,
pub excluded_platforms: Vec<String>,
pub target_duckdb_version: String,
pub use_unstable_c_api: bool,
pub git_ref: String,
}
pub const REF_PLACEHOLDER: &str = "REPLACE_WITH_COMMIT_HASH";
impl Default for ScaffoldConfig {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
version: String::from("0.1.0"),
license: String::from("MIT"),
maintainer: String::new(),
github_repo: String::new(),
excluded_platforms: Vec::new(),
target_duckdb_version: String::from(crate::DUCKDB_API_VERSION),
use_unstable_c_api: false,
git_ref: String::from(REF_PLACEHOLDER),
}
}
}
#[derive(Debug, Clone)]
pub struct GeneratedFile {
pub path: String,
pub content: String,
}
fn validate_target_duckdb_version(config: &ScaffoldConfig) -> Result<(), ExtensionError> {
let version = config.target_duckdb_version.as_str();
let numeric = version.strip_prefix('v').ok_or_else(|| {
ExtensionError::new(format!(
"target_duckdb_version must start with 'v' (e.g. \"v1.5.5\"), got {version:?}"
))
})?;
let parts: Vec<&str> = numeric.split('.').collect();
let well_formed = parts.len() == 3
&& parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()));
if !well_formed {
return Err(ExtensionError::new(format!(
"target_duckdb_version must be 'vMAJOR.MINOR.PATCH' (e.g. \"v1.5.5\"), got {version:?}"
)));
}
if !config.use_unstable_c_api && version != crate::DUCKDB_API_VERSION {
return Err(ExtensionError::new(format!(
"with use_unstable_c_api = false the extension is stamped C_STRUCT, so \
target_duckdb_version is the C extension API version and must be {:?}; got \
{version:?}. Set use_unstable_c_api = true to pin the binary to DuckDB \
{version} instead.",
crate::DUCKDB_API_VERSION
)));
}
if config.use_unstable_c_api && version == crate::DUCKDB_API_VERSION {
return Err(ExtensionError::new(format!(
"with use_unstable_c_api = true the extension is stamped C_STRUCT_UNSTABLE and \
DuckDB requires target_duckdb_version to be the exact DuckDB release it will be \
loaded into (e.g. \"v1.5.5\"); {:?} is the C extension API version, not a \
DuckDB release to pin to.",
crate::DUCKDB_API_VERSION
)));
}
Ok(())
}
pub fn generate_scaffold(config: &ScaffoldConfig) -> Result<Vec<GeneratedFile>, ExtensionError> {
validate_extension_name(&config.name)?;
crate::validate::validate_extension_version(&config.version)?;
validate_spdx_license(&config.license)?;
for platform in &config.excluded_platforms {
crate::validate::validate_platform(platform)?;
}
validate_target_duckdb_version(config)?;
let files = vec![
GeneratedFile {
path: "Cargo.toml".to_string(),
content: templates::generate_cargo_toml(config),
},
GeneratedFile {
path: "Makefile".to_string(),
content: templates::generate_makefile(config),
},
GeneratedFile {
path: "extension_config.cmake".to_string(),
content: templates::generate_extension_config_cmake(config),
},
GeneratedFile {
path: "src/lib.rs".to_string(),
content: templates::generate_lib_rs(config),
},
GeneratedFile {
path: "src/wasm_lib.rs".to_string(),
content: templates::generate_wasm_lib(),
},
GeneratedFile {
path: "description.yml".to_string(),
content: templates::generate_description_yml(config),
},
GeneratedFile {
path: format!("test/sql/{}.test", config.name),
content: templates::generate_sqllogictest(config),
},
GeneratedFile {
path: ".github/workflows/extension-ci.yml".to_string(),
content: templates::generate_extension_ci(config),
},
GeneratedFile {
path: ".gitmodules".to_string(),
content: templates::generate_gitmodules(),
},
GeneratedFile {
path: ".gitignore".to_string(),
content: templates::generate_gitignore(),
},
GeneratedFile {
path: ".cargo/config.toml".to_string(),
content: templates::generate_cargo_config(),
},
];
Ok(files)
}