1use proc_macro2::Span;
4use serde::{Deserialize, Serialize};
5use std::{collections::BTreeMap, path::PathBuf, str::FromStr};
6use syn::{
7 bracketed, parse::Parse, parse::ParseStream, parse::Result, punctuated::Punctuated,
8 spanned::Spanned, token, Error, LitStr, Token,
9};
10use weld_codegen::{
11 config::{ModelSource, OutputFile},
12 generators::{CodeGen, RustCodeGen},
13 render::Renderer,
14 sources_to_model,
15 writer::Writer,
16};
17
18const BASE_MODEL_URL: &str = "https://cdn.jsdelivr.net/gh/wasmcloud/interfaces";
19const CORE_MODEL: &str = "core/wasmcloud-core.smithy";
20const MODEL_MODEL: &str = "core/wasmcloud-model.smithy";
21
22#[proc_macro]
108pub fn smithy_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
109 let bindgen = syn::parse_macro_input!(input as BindgenConfig);
110 generate_source(bindgen)
111 .unwrap_or_else(syn::Error::into_compile_error)
112 .into()
113}
114
115fn generate_source(bindgen: BindgenConfig) -> Result<proc_macro2::TokenStream> {
117 let call_site = Span::call_site();
118 let sources = bindgen
119 .sources
120 .into_iter()
121 .map(SmithySource::into)
122 .collect::<Vec<ModelSource>>();
123 let mut w = Writer::default();
124 let model = sources_to_model(&sources, &PathBuf::new(), 0).map_err(|e| {
125 Error::new(
126 call_site.span(),
127 format!("cannot compile model sources: {}", e),
128 )
129 })?;
130 let mut rust_gen = RustCodeGen::new(Some(&model));
131 let output_config = OutputFile {
132 namespace: Some(bindgen.namespace),
133 ..Default::default()
134 };
135 let mut params = BTreeMap::<String, serde_json::Value>::default();
136 params.insert("model".into(), atelier_json::model_to_json(&model));
137 let mut renderer = Renderer::default();
138 let bytes = rust_gen
139 .init(Some(&model), &Default::default(), None, &mut renderer)
140 .and_then(|_| rust_gen.generate_file(&mut w, &model, &output_config, ¶ms))
141 .map_err(|e| {
142 Error::new(
143 call_site.span(),
144 format!("cannot generate rust source: {}", e),
145 )
146 })?;
147 proc_macro2::TokenStream::from_str(&String::from_utf8_lossy(&bytes)).map_err(|e| {
148 Error::new(
149 call_site.span(),
150 format!("cannot parse generated code: {}", e),
151 )
152 })
153}
154
155#[derive(Debug, Default, Serialize, Deserialize)]
156struct SmithySource {
157 url: Option<String>,
158 path: Option<String>,
159 files: Vec<String>,
160}
161
162#[derive(Debug, Default, Serialize, Deserialize)]
164struct BindgenConfig {
165 pub sources: Vec<SmithySource>,
166 pub namespace: String,
167}
168
169impl From<SmithySource> for ModelSource {
170 fn from(source: SmithySource) -> Self {
171 match (source.url, source.path) {
172 (Some(url), _) => ModelSource::Url { url, files: source.files },
173 (_, Some(path)) => ModelSource::Path { path: path.into(), files: source.files },
174 _ => unreachable!(),
175 }
176 }
177}
178
179mod kw {
180 syn::custom_keyword!(url);
181 syn::custom_keyword!(path);
182 syn::custom_keyword!(files);
183}
184
185enum Opt {
186 Url(String),
187 Path(String),
188 Files(Vec<String>),
189}
190
191impl Parse for Opt {
192 fn parse(input: ParseStream<'_>) -> Result<Self> {
193 let l = input.lookahead1();
194 if l.peek(kw::url) {
195 input.parse::<kw::url>()?;
196 input.parse::<Token![:]>()?;
197 Ok(Opt::Url(input.parse::<LitStr>()?.value()))
198 } else if l.peek(kw::path) {
199 input.parse::<kw::path>()?;
200 input.parse::<Token![:]>()?;
201 Ok(Opt::Path(input.parse::<LitStr>()?.value()))
202 } else if l.peek(kw::files) {
203 input.parse::<kw::files>()?;
204 input.parse::<Token![:]>()?;
205 let content;
206 let _array = bracketed!(content in input);
207 let files = Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?
208 .into_iter()
209 .map(|val| val.value())
210 .collect();
211 Ok(Opt::Files(files))
212 } else {
213 Err(l.error())
214 }
215 }
216}
217
218impl Parse for SmithySource {
219 fn parse(input: ParseStream<'_>) -> syn::parse::Result<Self> {
220 let call_site = Span::call_site();
221 let mut source = SmithySource::default();
222 let content;
223 syn::braced!(content in input);
224 let fields = Punctuated::<Opt, Token![,]>::parse_terminated(&content)?;
225 for field in fields.into_pairs() {
226 match field.into_value() {
227 Opt::Url(s) => {
228 if source.url.is_some() {
229 return Err(Error::new(s.span(), "cannot specify second url"));
230 }
231 if source.path.is_some() {
232 return Err(Error::new(s.span(), "cannot specify path and url"));
233 }
234 source.url = Some(s)
235 }
236 Opt::Path(s) => {
237 if source.path.is_some() {
238 return Err(Error::new(s.span(), "cannot specify second path"));
239 }
240 if source.url.is_some() {
241 return Err(Error::new(s.span(), "cannot specify path and url"));
242 }
243 source.path = Some(s)
244 }
245 Opt::Files(val) => source.files = val,
246 }
247 }
248 if !(!source.files.is_empty()
249 || (source.url.is_some() && source.url.as_ref().unwrap().ends_with(".smithy"))
250 || (source.path.is_some() && source.path.as_ref().unwrap().ends_with(".smithy")))
251 {
252 return Err(Error::new(
253 call_site.span(),
254 "There must be at least one .smithy file",
255 ));
256 }
257 if source.url.is_none() && source.path.is_none() {
258 source.url = Some(BASE_MODEL_URL.to_string());
259 }
260 Ok(source)
261 }
262}
263
264impl Parse for BindgenConfig {
265 fn parse(input: ParseStream<'_>) -> syn::parse::Result<Self> {
266 let call_site = Span::call_site();
267 let mut sources;
268
269 let l = input.lookahead1();
270 if l.peek(token::Brace) {
271 let source = input.parse::<SmithySource>()?;
273 sources = vec![source];
274 } else if l.peek(token::Bracket) {
275 let content;
277 syn::bracketed!(content in input);
278 sources = Punctuated::<SmithySource, Token![,]>::parse_terminated(&content)?
279 .into_iter()
280 .collect();
281 } else if l.peek(LitStr) {
282 let one_file = input.parse::<LitStr>()?;
284 sources = vec![SmithySource {
285 url: Some(BASE_MODEL_URL.into()),
286 path: None,
287 files: vec![
288 "core/wasmcloud-core.smithy".into(),
289 "core/wasmcloud-model.smithy".into(),
290 one_file.value(),
291 ],
292 }];
293 } else {
294 return Err(Error::new(
295 call_site.span(),
296 "expected quoted path, or model source { url or path: ..., files: ,.. }, or list of model sources [...]"
297 ));
298 }
299 input.parse::<Token![,]>()?;
300 let namespace = input.parse::<LitStr>()?.value();
301
302 let has_core = sources.iter().any(|s| {
304 (s.url.is_some() && s.url.as_ref().unwrap().ends_with(CORE_MODEL))
305 || s.files.iter().any(|s| s.ends_with(CORE_MODEL))
306 });
307 let has_model = sources.iter().any(|s| {
308 (s.url.is_some() && s.url.as_ref().unwrap().ends_with(MODEL_MODEL))
309 || s.files.iter().any(|s| s.ends_with(MODEL_MODEL))
310 });
311 if !has_core || !has_model {
312 sources.push(SmithySource {
313 url: Some(BASE_MODEL_URL.into()),
314 files: match (has_core, has_model) {
315 (false, false) => vec![CORE_MODEL.into(), MODEL_MODEL.into()],
316 (false, true) => vec![CORE_MODEL.into()],
317 (true, false) => vec![MODEL_MODEL.into()],
318 _ => unreachable!(),
319 },
320 path: None,
321 });
322 }
323 Ok(BindgenConfig { sources, namespace })
324 }
325}