#![doc = include_str!("../README.md")]
use std::{
collections::BTreeMap,
fmt::Debug,
fs,
path::{Path, PathBuf},
str,
};
use proc_macro2::{Ident, Span, TokenStream};
use serde::Serialize;
use tests::build_tests;
use toml::Value;
use clap::Parser;
use v_escape_codegen_base::generate as generate_base;
mod tests;
fn ident(s: &str) -> Ident {
Ident::new(s, Span::call_site())
}
#[derive(Parser, Debug)]
#[clap(
author,
version,
about = "Generate escape functions from template files",
long_about = "A tool for generating SIMD-optimized escape functions from template files.
Creates a new crate with escape_fmt and escape_string functions based on character mappings defined in src/_lib.rs.
Example usage:
mkdir my_escape
cd my_escape
cargo init --lib
cat <<EOF > src/_lib.rs
new!(
'&' -> \"&\",
'/' -> \"/\",
'<' -> \"<\",
'>' -> \">\",
'\"' -> \""\",
'\\'' -> \"'\"
);
EOF
v_escape-codegen -i .",
after_help = "For more information, see: https://github.com/zzau13/v_escape"
)]
struct Args {
#[clap(short, long, default_value = "./", value_name = "DIR")]
pub input_dir: PathBuf,
}
#[derive(Serialize)]
struct Dep {
workspace: bool,
}
fn generate(dir: impl AsRef<Path>) -> anyhow::Result<()> {
_generate(dir.as_ref())
}
fn read_cargo(p: &Path) -> anyhow::Result<(Value, String)> {
let cargo_src =
fs::read_to_string(p).map_err(|e| anyhow::anyhow!("Failed to read Cargo.toml: {}", e))?;
let mut cargo_value = cargo_src
.parse::<Value>()
.map_err(|e| anyhow::anyhow!("Failed to parse Cargo.toml: {}", e))?;
let doc: Value = toml::from_str(
r"
[docs.rs]
all-features = true
",
)
.map_err(|e| anyhow::anyhow!("Failed to parse TOML: {}", e))?;
let cargo_mut = cargo_value
.as_table_mut()
.ok_or_else(|| anyhow::anyhow!("Expected a table for cargo_value"))?;
cargo_mut
.get_mut("package")
.ok_or_else(|| anyhow::anyhow!("Expected a package section in Cargo.toml"))?
.as_table_mut()
.ok_or_else(|| anyhow::anyhow!("Expected a table for package"))?
.insert("metadata".to_string(), doc);
let mut features = BTreeMap::new();
features.insert("default", vec!["std", "string", "fmt", "bytes"]);
features.insert("std", vec!["v_escape-base/std", "alloc"]);
features.insert("alloc", vec!["v_escape-base/alloc"]);
features.insert("string", vec!["v_escape-base/string"]);
features.insert("fmt", vec!["v_escape-base/fmt"]);
features.insert("bytes", vec!["v_escape-base/bytes"]);
cargo_mut.insert("features".into(), Value::from(features));
if !cargo_mut.contains_key("dependencies") {
cargo_mut.insert(
"dependencies".into(),
Value::from(BTreeMap::<String, Value>::new()),
);
}
let dependencies = cargo_mut
.get_mut("dependencies")
.ok_or_else(|| anyhow::anyhow!("Expected a table for dependencies"))?;
dependencies
.as_table_mut()
.ok_or_else(|| anyhow::anyhow!("Expected a table for dependencies"))?
.insert(
"v_escape-base".into(),
Value::try_from(Dep { workspace: true })?,
);
let package_name = cargo_value
.as_table()
.ok_or_else(|| anyhow::anyhow!("Expected a table for cargo_value"))?
.get("package")
.ok_or_else(|| anyhow::anyhow!("Expected a package section in Cargo.toml"))?
.as_table()
.ok_or_else(|| anyhow::anyhow!("Expected a table for package"))?
.get("name")
.ok_or_else(|| anyhow::anyhow!("Expected a name in package section"))?
.as_str()
.ok_or_else(|| anyhow::anyhow!("Expected a name as str"))?;
let package_name = package_name.to_string();
Ok((cargo_value, package_name))
}
fn _generate(dir: &Path) -> anyhow::Result<()> {
let head =
"//! autogenerated by v_escape_codegen@".to_string() + env!("CARGO_PKG_VERSION") + "\n";
if !dir.is_dir() {
anyhow::bail!("input_dir should be a directory");
}
let cargo = dir.join("Cargo.toml");
let (cargo_value, name) = read_cargo(&cargo)?;
let src = dir.join("src");
let test = dir.join("tests");
if !test.exists() {
fs::create_dir(&test)?;
}
let template = src.join("_lib.rs");
let template_src = fs::read_to_string(&template)?;
let (code, (escapes, escaped)) = generate_base(
template_src
.parse::<TokenStream>()
.map_err(|e| anyhow::anyhow!("Failed to parse template source: {}", e))?,
"v_escape_base",
)?;
let code_pretty = prettyplease::unparse(
&syn::parse2(code)
.map_err(|e| anyhow::anyhow!("Failed to parse code to TokenStream: {}", e))?,
);
let code_test = build_tests(&ident(&name), &escapes, &escaped);
let code_test_pretty = prettyplease::unparse(
&syn::parse2(code_test)
.map_err(|e| anyhow::anyhow!("Failed to parse code to TokenStream: {}", e))?,
);
fs::write(&cargo, toml::to_string_pretty(&cargo_value)?)?;
fs::write(src.join("lib.rs"), head.clone() + &code_pretty)?;
fs::write(test.join("lib.rs"), head.clone() + &code_test_pretty)?;
Ok(())
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let dir: PathBuf = args.input_dir;
generate(dir)
}