use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, Expr, Ident, LitStr, Token};
use crate::validation::{is_valid_mime_type, suggest_mime_type};
pub struct SkillArgs {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
pub examples: Vec<String>,
pub input_modes: Vec<String>,
pub output_modes: Vec<String>,
}
impl Parse for SkillArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut id = None;
let mut name = None;
let mut description = None;
let mut tags = Vec::new();
let mut examples = Vec::new();
let mut input_modes = Vec::new();
let mut output_modes = Vec::new();
while !input.is_empty() {
let key: Ident = input.parse()?;
let _: Token![=] = input.parse()?;
match key.to_string().as_str() {
"id" => {
let value: LitStr = input.parse()?;
id = Some(value.value());
}
"name" => {
let value: LitStr = input.parse()?;
name = Some(value.value());
}
"description" => {
let value: LitStr = input.parse()?;
description = Some(value.value());
}
"tags" => {
tags = parse_string_array(input)?;
}
"examples" => {
examples = parse_string_array(input)?;
}
"input_modes" => {
input_modes = parse_string_array(input)?;
}
"output_modes" => {
output_modes = parse_string_array(input)?;
}
_ => {
return Err(syn::Error::new(
key.span(),
format!("Unknown skill attribute: {key}"),
));
}
}
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
}
}
let id = id.ok_or_else(|| syn::Error::new(input.span(), "Missing required field: id"))?;
let name =
name.ok_or_else(|| syn::Error::new(input.span(), "Missing required field: name"))?;
let description = description
.ok_or_else(|| syn::Error::new(input.span(), "Missing required field: description"))?;
Ok(Self {
id,
name,
description,
tags,
examples,
input_modes,
output_modes,
})
}
}
fn parse_string_array(input: ParseStream) -> syn::Result<Vec<String>> {
let content;
syn::bracketed!(content in input);
let exprs = content.parse_terminated(Expr::parse, Token![,])?;
exprs
.into_iter()
.map(|expr| {
if let Expr::Lit(lit) = expr {
if let syn::Lit::Str(s) = lit.lit {
Ok(s.value())
} else {
Err(syn::Error::new_spanned(
lit,
"Expected string literal in array",
))
}
} else {
Err(syn::Error::new_spanned(expr, "Expected string literal"))
}
})
.collect()
}
pub fn validate_mime_types(mime_types: &[String]) -> syn::Result<()> {
for mime_type in mime_types {
if !is_valid_mime_type(mime_type) {
let suggestions = suggest_mime_type(mime_type);
let error_msg = if suggestions.is_empty() {
format!("Invalid MIME type: '{mime_type}'. See https://www.iana.org/assignments/media-types/media-types.xhtml for valid types.")
} else {
let suggested = suggestions.join(", ");
format!("Invalid MIME type: '{mime_type}'. Did you mean one of: {suggested}?")
};
return Err(syn::Error::new(proc_macro2::Span::call_site(), error_msg));
}
}
Ok(())
}
#[allow(clippy::needless_pass_by_value)] pub fn generate_skill_impl(args: SkillArgs, item: TokenStream) -> TokenStream {
if let Err(e) = validate_mime_types(&args.input_modes) {
return e.to_compile_error();
}
if let Err(e) = validate_mime_types(&args.output_modes) {
return e.to_compile_error();
}
let struct_item: syn::ItemStruct = match syn::parse2(item.clone()) {
Ok(s) => s,
Err(e) => return e.to_compile_error(),
};
let struct_name = &struct_item.ident;
let metadata_name = syn::Ident::new(
&format!("{}_METADATA", struct_name.to_string().to_uppercase()),
struct_name.span(),
);
let id = &args.id;
let name = &args.name;
let description = &args.description;
let tags: Vec<&str> = args.tags.iter().map(String::as_str).collect();
let examples: Vec<&str> = args.examples.iter().map(String::as_str).collect();
let input_modes: Vec<&str> = args.input_modes.iter().map(String::as_str).collect();
let output_modes: Vec<&str> = args.output_modes.iter().map(String::as_str).collect();
quote! {
#item
static #metadata_name: ::radkit::agent::SkillMetadata = ::radkit::agent::SkillMetadata::new(
#id,
#name,
#description,
&[#(#tags),*],
&[#(#examples),*],
&[#(#input_modes),*],
&[#(#output_modes),*],
);
impl ::radkit::agent::RegisteredSkill for #struct_name {
fn metadata() -> &'static ::radkit::agent::SkillMetadata {
&#metadata_name
}
}
}
}