use std::{
env,
error::Error,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
};
use walkdir::{DirEntry, WalkDir};
const EXTENSIONS_ROOT: &str = "extensions";
const TEXT_ROOT: &str = "text";
fn text(out_dir: &Path) -> Result<(), Box<dyn Error>> {
use heck::ToSnakeCase;
use schemars::schema::{RootSchema, Schema};
use typify::{TypeSpace, TypeSpaceSettings};
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let mut out_file = File::create(out_dir.join("substrait_text").with_extension("rs"))?;
for schema_path in WalkDir::new(TEXT_ROOT)
.into_iter()
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_file() || entry.file_type().is_symlink())
.filter(|entry| {
entry
.path()
.extension()
.filter(|&extension| extension == "yaml") .is_some()
})
.map(DirEntry::into_path)
.inspect(|entry| {
println!("cargo:rerun-if-changed={}", entry.display());
})
{
let schema = serde_yaml::from_reader::<_, RootSchema>(File::open(&schema_path)?)?;
let metadata = schema.schema.metadata.as_ref();
let id = metadata
.and_then(|metadata| metadata.id.as_ref())
.map(ToString::to_string)
.unwrap_or_else(|| {
panic!(
"$id missing in schema metadata (`{}`)",
schema_path.display()
)
});
let title = metadata
.and_then(|metadata| metadata.title.as_ref())
.map(|title| title.to_snake_case())
.unwrap_or_else(|| {
panic!(
"title missing in schema metadata (`{}`)",
schema_path.display()
)
});
let mut type_space = TypeSpace::new(
TypeSpaceSettings::default()
.with_map_type("::indexmap::IndexMap")
.with_struct_builder(true)
.with_derive("PartialEq".to_string()),
);
type_space.add_ref_types(schema.definitions)?;
type_space.add_type(&Schema::Object(schema.schema))?;
out_file.write_fmt(format_args!(
r#"
#[doc = "Generated types for `{id}`"]
pub mod {title} {{
{}
}}"#,
prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream())?),
))?;
let file_name = schema_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let const_name = schema_path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
out_file.write_fmt(format_args!(
r#"
#[doc = "Raw source of the `{file_name}` text schema."]
pub const {const_name}: &str = include_str!({:?});
"#,
manifest_dir.join(&schema_path)
))?;
}
Ok(())
}
fn extensions(out_dir: &Path) -> Result<(), Box<dyn Error>> {
use std::collections::HashMap;
let substrait_extensions_file = out_dir.join("extensions.in");
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let mut output = String::from(
r#"// SPDX-License-Identifier: Apache-2.0
// Note that this file is auto-generated and auto-synced using `build.rs`. It is
// included in `extensions.rs`.
"#,
);
let mut extension_paths: Vec<PathBuf> = WalkDir::new(EXTENSIONS_ROOT)
.into_iter()
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_file())
.filter(|entry| {
entry
.path()
.extension()
.filter(|&extension| extension == "yaml")
.is_some()
})
.map(DirEntry::into_path)
.collect();
extension_paths.sort();
let mut map = HashMap::<String, String>::default();
let mut urns: Vec<(String, String)> = Vec::new();
for extension in &extension_paths {
println!("cargo:rerun-if-changed={}", extension.display());
let name = extension
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let var_name = name.to_uppercase();
output.push_str(&format!(
r#"
/// Included source of the `{name}` extension YAML file.
pub const {var_name}: &str = include_str!({:?});
"#,
manifest_dir.join(extension)
));
let value = serde_yaml::from_str::<serde_yaml::Value>(&fs::read_to_string(extension)?)?;
let urn = value
.get("urn")
.and_then(serde_yaml::Value::as_str)
.unwrap_or_else(|| panic!("`urn` missing in `{}`", extension.display()))
.to_string();
urns.push((urn, var_name.clone()));
map.insert(name, var_name);
}
output.push_str(
r#"
use std::collections::HashMap;
use std::sync::LazyLock;
use crate::text::simple_extensions::SimpleExtensions;
/// Map with the Substrait core extensions, keyed by file stem (e.g.
/// `functions_arithmetic`).
pub static EXTENSIONS: LazyLock<HashMap<&'static str, SimpleExtensions>> = LazyLock::new(|| {
let mut map = HashMap::new();"#,
);
for (name, var_name) in map {
output.push_str(&format!(
r#"
map.insert("{name}", serde_yaml::from_str({var_name}).expect("a valid core extension"));"#
));
}
output.push_str(
r#"
map
});
"#,
);
output.push_str(
r#"
/// All Substrait core extensions as `(urn, raw YAML source)` pairs, where `urn`
/// is the value of the extension file's top-level `urn` field.
pub static SIMPLE_EXTENSIONS: &[(&str, &str)] = &["#,
);
for (urn, var_name) in &urns {
output.push_str(&format!(
r#"
("{urn}", {var_name}),"#
));
}
output.push_str(
r#"
];
"#,
);
fs::write(substrait_extensions_file, output)?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
println!("cargo:rerun-if-env-changed=FORCE_REBUILD");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
text(out_dir.as_path())?;
extensions(out_dir.as_path())?;
Ok(())
}