use std::{
borrow::Borrow,
collections::BTreeMap,
io::Write,
path::{Path, PathBuf},
};
use atelier_core::model::{
shapes::{AppliedTraits, HasTraits as _},
HasIdentity as _, Identifier, Model,
};
use crate::docgen::DocGen;
use crate::{
codegen_go::GoCodeGen,
codegen_py::PythonCodeGen,
codegen_rust::RustCodeGen,
config::{CodegenConfig, LanguageConfig, OutputFile, OutputLanguage},
error::{Error, Result},
format::{NullFormatter, SourceFormatter},
model::{get_trait, serialization_trait, CommentKind, NumberedMember},
render::Renderer,
wasmbus_model::{RenameItem, Serialization},
writer::Writer,
Bytes, JsonValue, ParamMap, TomlValue,
};
pub const COMMON_TEMPLATES: &[(&str, &str)] = &[];
#[derive(Debug, Default)]
pub struct Generator {}
impl<'model> Generator {
pub fn gen(
&self,
model: Option<&'model Model>,
config: CodegenConfig,
templates: Vec<(String, String)>,
output_dir: &Path,
defines: Vec<(String, TomlValue)>,
) -> Result<()> {
let mut json_model = match model {
Some(model) => atelier_json::model_to_json(model),
None => JsonValue::default(),
};
let output_dir = if output_dir.is_absolute() {
output_dir.to_path_buf()
} else {
config.base_dir.join(output_dir)
};
let mut renderer = Renderer::default();
for (name, template) in COMMON_TEMPLATES.iter() {
renderer.add_template((name, template))?;
}
for (language, mut lc) in config.languages.into_iter() {
if !config.output_languages.is_empty() && !config.output_languages.contains(&language) {
continue;
}
if let Some(template_dir) = &lc.templates {
let template_dir = if template_dir.is_absolute() {
template_dir.clone()
} else {
config.base_dir.join(template_dir)
};
for (name, tmpl) in templates_from_dir(&template_dir)? {
renderer.add_template((&name, &tmpl))?;
}
}
for (name, template) in templates.iter() {
renderer.add_template((name, template))?;
}
let output_dir = if lc.output_dir.is_absolute() {
lc.output_dir.clone()
} else {
output_dir.join(&lc.output_dir)
};
std::fs::create_dir_all(&output_dir).map_err(|e| {
Error::Io(format!(
"creating directory {}: {}",
output_dir.display(),
e
))
})?;
for (k, v) in defines.iter() {
lc.parameters.insert(k.to_string(), v.clone());
}
let base_params: BTreeMap<String, JsonValue> = to_json(&lc.parameters)?;
let mut cgen = gen_for_language(&language, model);
cgen.init(model, &lc, Some(&output_dir), &mut renderer)?;
let mut updated_files = Vec::new();
for file_config in lc.files.iter() {
if let Some(TomlValue::String(key)) = file_config.params.get("if_defined") {
match lc.parameters.get(key) {
None | Some(TomlValue::Boolean(false)) => {
continue;
}
Some(_) => {}
}
}
let mut params = base_params.clone();
params.insert("model".to_string(), json_model);
let file_params: BTreeMap<String, JsonValue> = to_json(&file_config.params)?;
params.extend(file_params.into_iter());
params.insert(
"_file".to_string(),
JsonValue::String(file_config.path.to_string_lossy().to_string()),
);
let out_path = output_dir.join(&file_config.path);
let parent = out_path.parent().unwrap();
std::fs::create_dir_all(parent).map_err(|e| {
Error::Io(format!("creating directory {}: {}", parent.display(), e))
})?;
if let Some(hbs) = &file_config.hbs {
let mut out = std::fs::File::create(&out_path).map_err(|e| {
Error::Io(format!("creating file {}: {}", &out_path.display(), e))
})?;
renderer.render(hbs, ¶ms, &mut out)?;
out.flush().map_err(|e| {
crate::Error::Io(format!(
"saving output file {}:{}",
&out_path.display(),
e
))
})?;
} else if let Some(model) = model {
let mut w: Writer = Writer::default();
let bytes = cgen.generate_file(&mut w, model, file_config, ¶ms)?;
std::fs::write(&out_path, &bytes).map_err(|e| {
Error::Io(format!("writing output file {}: {}", out_path.display(), e))
})?;
};
updated_files.push(out_path);
json_model = params.remove("model").unwrap();
}
cgen.format(updated_files, &lc)?;
}
Ok(())
}
}
fn gen_for_language<'model>(
language: &OutputLanguage,
model: Option<&'model Model>,
) -> Box<dyn CodeGen + 'model> {
match language {
OutputLanguage::Rust => Box::new(RustCodeGen::new(model)),
OutputLanguage::Python => Box::new(PythonCodeGen::new(model)),
OutputLanguage::TinyGo => Box::new(GoCodeGen::new(model, true)),
OutputLanguage::Go => Box::new(GoCodeGen::new(model, false)),
OutputLanguage::Html => Box::<DocGen>::default(),
OutputLanguage::Poly => Box::<PolyCodeGen>::default(),
_ => {
crate::error::print_warning(&format!("Target language {language} not implemented"));
Box::<NoCodeGen>::default()
}
}
}
pub trait CodeGen {
#[allow(unused_variables)]
fn init(
&mut self,
model: Option<&Model>,
lc: &LanguageConfig,
output_dir: Option<&Path>,
renderer: &mut Renderer,
) -> std::result::Result<(), Error> {
Ok(())
}
fn generate_file(
&mut self,
w: &mut Writer,
model: &Model,
file_config: &OutputFile,
params: &ParamMap,
) -> Result<Bytes> {
self.init_file(w, model, file_config, params)?;
self.write_source_file_header(w, model, params)?;
self.declare_types(w, model, params)?;
self.write_services(w, model, params)?;
self.finalize(w)
}
#[allow(unused_variables)]
fn init_file(
&mut self,
w: &mut Writer,
model: &Model,
file_config: &OutputFile,
params: &ParamMap,
) -> Result<()> {
Ok(())
}
#[allow(unused_variables)]
fn write_source_file_header(
&mut self,
w: &mut Writer,
model: &Model,
params: &ParamMap,
) -> Result<()> {
Ok(())
}
#[allow(unused_variables)]
fn declare_types(&mut self, w: &mut Writer, model: &Model, params: &ParamMap) -> Result<()> {
Ok(())
}
#[allow(unused_variables)]
fn write_services(&mut self, w: &mut Writer, model: &Model, params: &ParamMap) -> Result<()> {
Ok(())
}
fn finalize(&mut self, w: &mut Writer) -> Result<Bytes> {
Ok(w.take().freeze())
}
#[allow(unused_variables)]
fn write_documentation(&mut self, w: &mut Writer, _id: &Identifier, text: &str) {
for line in text.split('\n') {
let line = line.trim_end_matches(|c| c == '\r' || c == ' ' || c == '\t');
self.write_comment(w, CommentKind::Documentation, line);
}
}
#[allow(unused_variables)]
fn write_comment(&mut self, w: &mut Writer, kind: CommentKind, line: &str) {
w.write(b"// ");
w.write(line);
w.write(b"\n");
}
fn write_ident(&self, w: &mut Writer, id: &Identifier) {
w.write(&self.to_type_name_case(&id.to_string()));
}
fn write_ident_with_suffix(
&mut self,
w: &mut Writer,
id: &Identifier,
suffix: &str,
) -> Result<()> {
self.write_ident(w, id);
w.write(suffix); Ok(())
}
fn output_language(&self) -> OutputLanguage;
fn has_rename_trait(&self, traits: &AppliedTraits) -> Option<String> {
if let Ok(Some(items)) = get_trait::<Vec<RenameItem>>(traits, crate::model::rename_trait())
{
let lang = self.output_language().to_string();
return items.iter().find(|i| i.lang == lang).map(|i| i.name.clone());
}
None
}
fn get_file_extension(&self) -> &'static str {
self.output_language().extension()
}
fn to_method_name_case(&self, name: &str) -> String;
fn to_method_name(&self, method_id: &Identifier, method_traits: &AppliedTraits) -> String {
if let Some(name) = self.has_rename_trait(method_traits) {
name
} else {
self.to_method_name_case(&method_id.to_string())
}
}
fn to_field_name_case(&self, name: &str) -> String;
fn to_field_name(
&self,
member_id: &Identifier,
member_traits: &AppliedTraits,
) -> std::result::Result<String, Error> {
if let Some(name) = self.has_rename_trait(member_traits) {
Ok(name)
} else {
Ok(self.to_field_name_case(&member_id.to_string()))
}
}
fn to_type_name_case(&self, s: &str) -> String;
fn get_field_name_and_ser_name(&self, field: &NumberedMember) -> Result<(String, String)> {
let field_name = self.to_field_name(field.id(), field.traits())?;
let ser_name = if let Some(Serialization { name: Some(ser_name) }) =
get_trait(field.traits(), serialization_trait())?
{
ser_name
} else {
field.id().to_string()
};
Ok((field_name, ser_name))
}
fn op_dispatch_name(&self, id: &Identifier) -> String {
crate::strings::to_pascal_case(&id.to_string())
}
fn full_dispatch_name(&self, service_id: &Identifier, method_id: &Identifier) -> String {
format!(
"{}.{}",
&self.to_type_name_case(&service_id.to_string()),
&self.op_dispatch_name(method_id)
)
}
fn source_formatter(&self, formatter: Vec<String>) -> Result<Box<dyn SourceFormatter>>;
fn format(
&mut self,
files: Vec<PathBuf>,
lc: &LanguageConfig,
) -> Result<()> {
if !lc.parameters.contains_key("create_interface") {
let formatter = self.source_formatter(lc.formatter.clone())?;
let extension = self.output_language().extension();
let sources = files
.into_iter()
.filter(|path| match path.extension() {
Some(s) => s.to_string_lossy().as_ref() == extension,
_ => false,
})
.collect::<Vec<PathBuf>>();
if !sources.is_empty() {
ensure_files_exist(&sources)?;
let file_names: Vec<std::borrow::Cow<'_, str>> =
sources.iter().map(|p| p.to_string_lossy()).collect();
let borrowed = file_names.iter().map(|s| s.borrow()).collect::<Vec<&str>>();
formatter.run(&borrowed)?;
}
}
Ok(())
}
}
fn ensure_files_exist(source_files: &[std::path::PathBuf]) -> Result<()> {
let missing = source_files
.iter()
.filter(|p| !p.is_file())
.map(|p| p.to_string_lossy().into_owned())
.collect::<Vec<String>>();
if !missing.is_empty() {
return Err(Error::Formatter(format!(
"missing source file(s) '{}'",
missing.join(",")
)));
}
Ok(())
}
#[derive(Debug, Default)]
struct PolyCodeGen {}
impl CodeGen for PolyCodeGen {
fn output_language(&self) -> OutputLanguage {
OutputLanguage::Poly
}
fn to_method_name_case(&self, name: &str) -> String {
crate::strings::to_snake_case(name)
}
fn to_field_name_case(&self, name: &str) -> String {
crate::strings::to_snake_case(name)
}
fn to_type_name_case(&self, name: &str) -> String {
crate::strings::to_pascal_case(name)
}
fn source_formatter(&self, _: Vec<String>) -> Result<Box<dyn SourceFormatter>> {
Ok(Box::<NullFormatter>::default())
}
}
#[allow(dead_code)]
pub fn spaces(indent_level: u8) -> &'static str {
const SP: &str =
" \
\
";
&SP[0..((indent_level * 4) as usize)]
}
pub fn to_json<S: serde::Serialize, T: serde::de::DeserializeOwned>(val: S) -> Result<T> {
let s = serde_json::to_string(&val)?;
Ok(serde_json::from_str(&s)?)
}
pub fn find_files(dir: &Path, extension: &str) -> Result<Vec<PathBuf>> {
if dir.is_dir() {
let mut results = Vec::new();
for entry in std::fs::read_dir(dir)
.map_err(|e| Error::Io(format!("reading directory {}: {}", dir.display(), e)))?
{
let entry = entry.map_err(|e| crate::Error::Io(format!("scanning folder: {e}")))?;
let path = entry.path();
if path.is_dir() {
results.append(&mut find_files(&path, extension)?);
} else {
let ext = path
.extension()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if ext == extension {
results.push(path)
}
}
}
Ok(results)
} else if dir.is_file()
&& &dir
.extension()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default()
== "smithy"
{
Ok(vec![dir.to_owned()])
} else {
Err(Error::Other(format!(
"'{}' is not a valid folder or '.{}' file",
dir.display(),
extension
)))
}
}
pub fn templates_from_dir(start: &std::path::Path) -> Result<Vec<(String, String)>> {
let mut templates = Vec::new();
for path in crate::gen::find_files(start, "hbs")?.iter() {
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if !stem.is_empty() {
let template = std::fs::read_to_string(path)
.map_err(|e| Error::Io(format!("reading template {}: {}", path.display(), e)))?;
templates.push((stem, template));
}
}
Ok(templates)
}
#[derive(Default)]
struct NoCodeGen {}
impl CodeGen for NoCodeGen {
fn output_language(&self) -> OutputLanguage {
OutputLanguage::Poly
}
fn get_file_extension(&self) -> &'static str {
""
}
fn to_method_name_case(&self, name: &str) -> String {
crate::strings::to_snake_case(name)
}
fn to_field_name_case(&self, name: &str) -> String {
crate::strings::to_snake_case(name)
}
fn to_type_name_case(&self, name: &str) -> String {
crate::strings::to_pascal_case(name)
}
fn source_formatter(&self, _: Vec<String>) -> Result<Box<dyn SourceFormatter>> {
Ok(Box::<NullFormatter>::default())
}
}