use crate::app::extract_app_meta;
use crate::context::partition_context_params;
use crate::server_attrs::{has_server_hidden, has_server_skip, validate_server_attrs};
use heck::{ToPascalCase, ToSnakeCase};
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use server_less_parse::{
MethodInfo, ParamInfo, extract_methods, get_impl_name, unwrap_option_type, unwrap_result_ok_type,
unwrap_vec_type,
};
use syn::{ItemImpl, Token, parse::Parse};
#[derive(Default)]
pub(crate) struct SmithyArgs {
namespace: Option<String>,
version: Option<String>,
}
impl Parse for SmithyArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = SmithyArgs::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
input.parse::<Token![=]>()?;
match ident.to_string().as_str() {
"namespace" => {
let lit: syn::LitStr = input.parse()?;
args.namespace = Some(lit.value());
}
"version" => {
let lit: syn::LitStr = input.parse()?;
args.version = Some(lit.value());
}
other => {
const VALID: &[&str] = &["namespace", "version"];
let suggestion = crate::did_you_mean(other, VALID)
.map(|s| format!(" — did you mean `{s}`?"))
.unwrap_or_default();
return Err(syn::Error::new(
ident.span(),
format!(
"unknown argument `{other}`{suggestion}. Valid arguments: namespace, version"
),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(args)
}
}
pub(crate) fn expand_smithy(args: SmithyArgs, mut impl_block: ItemImpl) -> syn::Result<TokenStream2> {
crate::reject_generic_impl(&impl_block)?;
let app_meta = extract_app_meta(&mut impl_block.attrs);
let struct_name = get_impl_name(&impl_block)?;
let (impl_generics, _ty_generics, where_clause) = impl_block.generics.split_for_impl();
let self_ty = &impl_block.self_ty;
let struct_name_str = struct_name.to_string();
let all_methods = extract_methods(&impl_block)?;
for m in &all_methods {
validate_server_attrs(m)?;
}
let methods: Vec<_> = all_methods
.into_iter()
.filter(|m| !has_server_skip(m) && !has_server_hidden(m))
.collect();
let namespace = args
.namespace
.or_else(|| {
app_meta
.name
.as_ref()
.map(|n| format!("com.example.{}", n.to_snake_case()))
})
.unwrap_or_else(|| format!("com.example.{}", struct_name_str.to_snake_case()));
let version = args
.version
.or_else(|| app_meta.version.into_explicit())
.unwrap_or_else(|| "2024-01-01".to_string());
let schema_path = impl_block.attrs.iter().find_map(|attr| {
if attr.path().is_ident("schema")
&& let syn::Meta::NameValue(nv) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
return Some(s.value());
}
None
});
let operations: Vec<String> = methods.iter().map(generate_operation).collect();
let structures: Vec<String> = methods.iter().flat_map(generate_structures).collect();
let smithy_schema = format!(
r#"$version: "2"
namespace {namespace}
/// {service_name} service
service {service_name} {{
version: "{version}"
operations: [
{operation_list}
]
}}
{operations}
{structures}
"#,
namespace = namespace,
service_name = struct_name_str,
version = version,
operation_list = methods
.iter()
.map(|m| format!(" {}", m.name_str().to_pascal_case()))
.collect::<Vec<_>>()
.join("\n"),
operations = operations.join("\n\n"),
structures = structures.join("\n\n")
);
let validation_method = if let Some(path) = schema_path {
quote! {
pub fn validate_schema() -> ::std::result::Result<(), ::server_less::SchemaValidationError> {
let expected = include_str!(#path);
let generated = Self::smithy_schema();
let expected_lines: ::std::collections::HashSet<String> = expected
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
let generated_lines: ::std::collections::HashSet<String> = generated
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
let mut error = ::server_less::SchemaValidationError::new("Smithy");
for line in &expected_lines {
if !generated_lines.contains(line) {
error.add_missing(line.clone());
}
}
for line in &generated_lines {
if !expected_lines.contains(line) {
error.add_extra(line.clone());
}
}
if error.has_differences() {
Err(error)
} else {
Ok(())
}
}
pub fn assert_schema_matches() {
if let Err(err) = Self::validate_schema() {
panic!("{}", err);
}
}
}
} else {
quote! {}
};
let maybe_impl = if crate::is_protocol_impl_emitter(&impl_block, "smithy") {
quote! { #impl_block }
} else {
quote! {}
};
Ok(quote! {
#maybe_impl
impl #impl_generics #self_ty #where_clause {
pub fn smithy_schema() -> &'static str {
#smithy_schema
}
pub fn write_smithy(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::write(path, Self::smithy_schema())
}
#validation_method
}
})
}
fn generate_operation(method: &MethodInfo) -> String {
let op_name = method.name_str().to_pascal_case();
let input_name = format!("{}Input", op_name);
let output_name = format!("{}Output", op_name);
let doc = method
.docs
.as_ref()
.map(|d| format!("/// {}\n", d))
.unwrap_or_default();
format!(
r#"{doc}operation {op_name} {{
input: {input_name}
output: {output_name}
}}"#,
doc = doc,
op_name = op_name,
input_name = input_name,
output_name = output_name
)
}
fn generate_structures(method: &MethodInfo) -> Vec<String> {
let op_name = method.name_str().to_pascal_case();
let input_name = format!("{}Input", op_name);
let output_name = format!("{}Output", op_name);
let (_, schema_params) = partition_context_params(&method.params).unwrap_or((None, method.params.iter().collect()));
let input_fields: Vec<String> = schema_params.iter().map(|p| generate_field(p)).collect();
let input_struct = if input_fields.is_empty() {
format!("structure {} {{}}", input_name)
} else {
format!(
"structure {} {{\n{}\n}}",
input_name,
input_fields.join("\n")
)
};
let ret = &method.return_info;
let output_struct = if ret.is_unit {
format!("structure {} {{}}", output_name)
} else {
let smithy_type = rust_type_to_smithy(&ret.ty);
format!(
"structure {} {{\n @required\n result: {}\n}}",
output_name, smithy_type
)
};
vec![input_struct, output_struct]
}
fn generate_field(param: &ParamInfo) -> String {
let name = param.name_str().to_snake_case();
let ty = if let Some(inner) = unwrap_option_type(¶m.ty) {
inner.clone()
} else {
param.ty.clone()
};
let smithy_type = rust_type_to_smithy(&Some(ty));
let required = if param.is_optional {
""
} else {
"@required\n "
};
format!(" {required}{name}: {smithy_type}")
}
fn rust_type_to_smithy(ty: &Option<syn::Type>) -> String {
let Some(ty) = ty else {
return "Unit".to_string();
};
rust_type_to_smithy_ty(ty)
}
fn rust_type_to_smithy_ty(ty: &syn::Type) -> String {
if let Some(ok) = unwrap_result_ok_type(ty) {
return rust_type_to_smithy_ty(ok);
}
if let Some(inner) = unwrap_option_type(ty) {
return rust_type_to_smithy_ty(inner);
}
if unwrap_vec_type(ty).is_some() {
return "StringList".to_string();
}
let ident = if let syn::Type::Path(tp) = ty {
tp.path.segments.last().map(|s| s.ident.to_string())
} else {
None
};
match ident.as_deref() {
Some("String") | Some("str") => "String".to_string(),
Some("i8") | Some("u8") => "Byte".to_string(),
Some("i16") | Some("u16") => "Short".to_string(),
Some("i32") | Some("u32") => "Integer".to_string(),
Some("i64") | Some("u64") => "Long".to_string(),
Some("f32") => "Float".to_string(),
Some("f64") => "Double".to_string(),
Some("bool") => "Boolean".to_string(),
_ => "Document".to_string(),
}
}