use proc_macro2::Span;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, path::PathBuf, str::FromStr};
use syn::{
bracketed, parse::Parse, parse::ParseStream, parse::Result, punctuated::Punctuated,
spanned::Spanned, token, Error, LitStr, Token,
};
use weld_codegen::{
config::{ModelSource, OutputFile},
generators::{CodeGen, RustCodeGen},
render::Renderer,
sources_to_model,
writer::Writer,
};
const BASE_MODEL_URL: &str = "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces";
const CORE_MODEL: &str = "core/wasmcloud-core.smithy";
const MODEL_MODEL: &str = "core/wasmcloud-model.smithy";
#[proc_macro]
pub fn smithy_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let bindgen = syn::parse_macro_input!(input as BindgenConfig);
generate_source(bindgen)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn generate_source(bindgen: BindgenConfig) -> Result<proc_macro2::TokenStream> {
let call_site = Span::call_site();
let sources = bindgen
.sources
.into_iter()
.map(SmithySource::into)
.collect::<Vec<ModelSource>>();
let mut w = Writer::default();
let model = sources_to_model(&sources, &PathBuf::new(), 0).map_err(|e| {
Error::new(
call_site.span(),
format!("cannot compile model sources: {}", e),
)
})?;
let mut rust_gen = RustCodeGen::new(Some(&model));
let output_config = OutputFile {
namespace: Some(bindgen.namespace),
..Default::default()
};
let mut params = BTreeMap::<String, serde_json::Value>::default();
params.insert("model".into(), atelier_json::model_to_json(&model));
let mut renderer = Renderer::default();
let bytes = rust_gen
.init(Some(&model), &Default::default(), None, &mut renderer)
.and_then(|_| rust_gen.generate_file(&mut w, &model, &output_config, ¶ms))
.map_err(|e| {
Error::new(
call_site.span(),
format!("cannot generate rust source: {}", e),
)
})?;
proc_macro2::TokenStream::from_str(&String::from_utf8_lossy(&bytes)).map_err(|e| {
Error::new(
call_site.span(),
format!("cannot parse generated code: {}", e),
)
})
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct SmithySource {
url: Option<String>,
path: Option<String>,
files: Vec<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct BindgenConfig {
pub sources: Vec<SmithySource>,
pub namespace: String,
}
impl From<SmithySource> for ModelSource {
fn from(source: SmithySource) -> Self {
match (source.url, source.path) {
(Some(url), _) => ModelSource::Url { url, files: source.files },
(_, Some(path)) => ModelSource::Path { path: path.into(), files: source.files },
_ => unreachable!(),
}
}
}
mod kw {
syn::custom_keyword!(url);
syn::custom_keyword!(path);
syn::custom_keyword!(files);
}
enum Opt {
Url(String),
Path(String),
Files(Vec<String>),
}
impl Parse for Opt {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let l = input.lookahead1();
if l.peek(kw::url) {
input.parse::<kw::url>()?;
input.parse::<Token![:]>()?;
Ok(Opt::Url(input.parse::<LitStr>()?.value()))
} else if l.peek(kw::path) {
input.parse::<kw::path>()?;
input.parse::<Token![:]>()?;
Ok(Opt::Path(input.parse::<LitStr>()?.value()))
} else if l.peek(kw::files) {
input.parse::<kw::files>()?;
input.parse::<Token![:]>()?;
let content;
let _array = bracketed!(content in input);
let files = Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?
.into_iter()
.map(|val| val.value())
.collect();
Ok(Opt::Files(files))
} else {
Err(l.error())
}
}
}
impl Parse for SmithySource {
fn parse(input: ParseStream<'_>) -> syn::parse::Result<Self> {
let call_site = Span::call_site();
let mut source = SmithySource::default();
let content;
syn::braced!(content in input);
let fields = Punctuated::<Opt, Token![,]>::parse_terminated(&content)?;
for field in fields.into_pairs() {
match field.into_value() {
Opt::Url(s) => {
if source.url.is_some() {
return Err(Error::new(s.span(), "cannot specify second url"));
}
if source.path.is_some() {
return Err(Error::new(s.span(), "cannot specify path and url"));
}
source.url = Some(s)
}
Opt::Path(s) => {
if source.path.is_some() {
return Err(Error::new(s.span(), "cannot specify second path"));
}
if source.url.is_some() {
return Err(Error::new(s.span(), "cannot specify path and url"));
}
source.path = Some(s)
}
Opt::Files(val) => source.files = val,
}
}
if !(!source.files.is_empty()
|| (source.url.is_some() && source.url.as_ref().unwrap().ends_with(".smithy"))
|| (source.path.is_some() && source.path.as_ref().unwrap().ends_with(".smithy")))
{
return Err(Error::new(
call_site.span(),
"There must be at least one .smithy file",
));
}
if source.url.is_none() && source.path.is_none() {
source.url = Some(BASE_MODEL_URL.to_string());
}
Ok(source)
}
}
impl Parse for BindgenConfig {
fn parse(input: ParseStream<'_>) -> syn::parse::Result<Self> {
let call_site = Span::call_site();
let mut sources;
let l = input.lookahead1();
if l.peek(token::Brace) {
let source = input.parse::<SmithySource>()?;
sources = vec![source];
} else if l.peek(token::Bracket) {
let content;
syn::bracketed!(content in input);
sources = Punctuated::<SmithySource, Token![,]>::parse_terminated(&content)?
.into_iter()
.collect();
} else if l.peek(LitStr) {
let one_file = input.parse::<LitStr>()?;
sources = vec![SmithySource {
url: Some(BASE_MODEL_URL.into()),
path: None,
files: vec![
"core/wasmcloud-core.smithy".into(),
"core/wasmcloud-model.smithy".into(),
one_file.value(),
],
}];
} else {
return Err(Error::new(
call_site.span(),
"expected quoted path, or model source { url or path: ..., files: ,.. }, or list of model sources [...]"
));
}
input.parse::<Token![,]>()?;
let namespace = input.parse::<LitStr>()?.value();
let has_core = sources.iter().any(|s| {
(s.url.is_some() && s.url.as_ref().unwrap().ends_with(CORE_MODEL))
|| s.files.iter().any(|s| s.ends_with(CORE_MODEL))
});
let has_model = sources.iter().any(|s| {
(s.url.is_some() && s.url.as_ref().unwrap().ends_with(MODEL_MODEL))
|| s.files.iter().any(|s| s.ends_with(MODEL_MODEL))
});
if !has_core || !has_model {
sources.push(SmithySource {
url: Some(BASE_MODEL_URL.into()),
files: match (has_core, has_model) {
(false, false) => vec![CORE_MODEL.into(), MODEL_MODEL.into()],
(false, true) => vec![CORE_MODEL.into()],
(true, false) => vec![MODEL_MODEL.into()],
_ => unreachable!(),
},
path: None,
});
}
Ok(BindgenConfig { sources, namespace })
}
}