mod field_data;
mod resolver;
use proc_macro::TokenStream;
use quote::quote;
use resolver::get_default_field_data;
use syn::token::Comma;
use syn::{
braced, parse::Parse, parse::ParseStream, parse_macro_input, punctuated::Punctuated, token,
Ident, LitStr, Result, Token,
};
#[allow(dead_code)]
struct PropertyField {
name: Ident,
colon_token: Token![=],
path: LitStr,
force_optional: Option<Token![?]>,
}
#[allow(dead_code)]
struct VimObjectMacro {
struct_token: Token![struct],
struct_name: Ident,
colon_token: Token![:],
object_type: Ident,
brace_token: token::Brace,
fields: Punctuated<PropertyField, Token![,]>,
}
impl Parse for PropertyField {
fn parse(input: ParseStream) -> Result<Self> {
let name: Ident = input.parse()?;
let colon_token: Token![=] = input.parse()?;
let path: LitStr = input.parse()?;
let force_optional: Option<Token![?]> = input.parse()?;
Ok(PropertyField {
name,
colon_token,
path,
force_optional,
})
}
}
impl Parse for VimObjectMacro {
fn parse(input: ParseStream) -> Result<Self> {
let content;
Ok(VimObjectMacro {
struct_token: input.parse()?,
struct_name: input.parse()?,
colon_token: input.parse()?,
object_type: input.parse()?,
brace_token: braced!(content in input),
fields: Punctuated::parse_terminated(&content)?,
})
}
}
struct FieldInfo<'a> {
property_field: &'a PropertyField,
field_data: resolver::FieldData,
}
#[proc_macro]
pub fn vim_retrievable(input: TokenStream) -> TokenStream {
let VimObjectMacro {
struct_token: _,
struct_name,
colon_token: _,
object_type: managed_object_type,
brace_token: _,
fields,
} = parse_macro_input!(input as VimObjectMacro);
let (field_infos, errors) = resolve_fields(&managed_object_type, &fields);
let struct_tokens = generate_struct_decl(&struct_name, &field_infos);
let struct_impl_tokens =
generate_retrieve_struct_impl(&struct_name, &managed_object_type, &field_infos);
let try_from_object_content = generate_try_from_object_content(&struct_name, &field_infos);
let output = quote! {
#( #errors )*
#struct_tokens
#struct_impl_tokens
#try_from_object_content
};
output.into()
}
#[proc_macro]
pub fn vim_updatable(input: TokenStream) -> TokenStream {
let VimObjectMacro {
struct_token: _,
struct_name,
colon_token: _,
object_type: managed_object_type,
brace_token: _,
fields,
} = parse_macro_input!(input as VimObjectMacro);
let (field_infos, errors) = resolve_fields(&managed_object_type, &fields);
let struct_tokens = generate_struct_decl(&struct_name, &field_infos);
let struct_impl_tokens =
generate_updateable_struct_impl(&struct_name, &managed_object_type, &field_infos);
let try_from_object_content = generate_try_from_object_update(&struct_name, &field_infos);
let output = quote! {
#( #errors )*
#struct_tokens
#struct_impl_tokens
#try_from_object_content
};
output.into()
}
fn resolve_fields<'a>(
managed_object_type: &Ident,
fields: &'a Punctuated<PropertyField, Comma>,
) -> (Vec<FieldInfo<'a>>, Vec<proc_macro2::TokenStream>) {
let mut field_infos = Vec::new();
let mut errors: Vec<proc_macro2::TokenStream> = Vec::new();
for property_field in fields {
let path_str = property_field.path.value();
let res = resolver::resolve_path(&managed_object_type.to_string(), &path_str);
let mut field_data = match res {
Ok(field_type) => field_type,
Err(e) => {
let msg = format!("Error resolving path: {}", e);
errors.push(syn::Error::new(property_field.path.span(), msg).to_compile_error());
get_default_field_data()
}
};
if property_field.force_optional.is_some() && !field_data.is_optional {
field_data.is_optional = true;
field_data.data_type = format!("Option<{}>", field_data.data_type);
}
field_infos.push(FieldInfo {
property_field,
field_data,
});
}
(field_infos, errors)
}
fn generate_struct_decl(struct_name: &Ident, fields: &Vec<FieldInfo>) -> proc_macro2::TokenStream {
let mut field_declarations: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut docs: Vec<&'static str> = Vec::with_capacity(fields.len());
for f in fields {
let field_name = &f.property_field.name;
let parsed_field_type: syn::Type = syn::parse_str(&f.field_data.data_type).unwrap();
let decl = quote! {
#field_name : #parsed_field_type
};
field_declarations.push(decl);
docs.push(f.field_data.doc.unwrap_or(""))
}
let struct_tokens = quote! {
#[derive(Debug)]
pub struct #struct_name {
#[doc = "Object identifier"]
pub id: vim_rs::types::structs::ManagedObjectReference,
#(#[doc = #docs]
pub #field_declarations,)*
}
};
struct_tokens
}
fn generate_retrieve_struct_impl(
struct_name: &Ident,
managed_object_type: &Ident,
fields: &Vec<FieldInfo>,
) -> proc_macro2::TokenStream {
let prop_spec = prop_spec(managed_object_type, fields);
let id = id();
quote! {
impl vim_rs::core::pc_helpers::Queriable for #struct_name {
#prop_spec
}
impl #struct_name {
pub #id
}
}
}
fn generate_updateable_struct_impl(
struct_name: &Ident,
managed_object_type: &Ident,
fields: &Vec<FieldInfo>,
) -> proc_macro2::TokenStream {
let prop_spec = prop_spec(managed_object_type, fields);
let id = id();
let apply_update = generate_apply_update(fields);
quote! {
impl vim_rs::core::pc_helpers::Queriable for #struct_name {
#prop_spec
}
impl vim_rs::core::pc_cache::Cacheable for #struct_name {
#id
#apply_update
}
}
}
fn prop_spec(managed_object_type: &Ident, fields: &Vec<FieldInfo>) -> proc_macro2::TokenStream {
let field_paths: Vec<&str> = fields
.iter()
.map(|f| f.field_data.vim_path.as_str())
.collect();
let prop_paths_quoted: Vec<proc_macro2::TokenStream> = field_paths
.iter()
.map(|path| quote! { #path.into() })
.collect();
quote! {
fn prop_spec() -> vim_rs::types::structs::PropertySpec {
vim_rs::types::structs::PropertySpec {
all: Some(false),
path_set: Some(vec![
#(#prop_paths_quoted),*
]),
r#type: vim_rs::types::enums::MoTypesEnum::#managed_object_type.as_str().to_string(),
}
}
}
}
fn id() -> proc_macro2::TokenStream {
quote! {
fn id(&self) -> &vim_rs::types::structs::ManagedObjectReference {
&self.id
}
}
}
fn generate_try_from_object_content(
struct_name: &Ident,
fields: &Vec<FieldInfo>,
) -> proc_macro2::TokenStream {
let mut field_declarations: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut field_conversions: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut field_assignments: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut idx = 1;
for field in fields {
let field_alias: Ident = syn::parse_str(&format!("field{}", idx)).unwrap();
let field_name = &field.property_field.name;
field_declarations.push(quote! { let mut #field_alias = None; });
match field.field_data.processing_type {
resolver::FieldProcessingType::Enum(enum_field_name) => {
field_conversions.push(generate_enum_field_from_content(
field,
&field_alias,
&enum_field_name,
));
}
resolver::FieldProcessingType::Struct => {
field_conversions.push(generate_struct_field_from_content(
field,
&field_alias,
&field.field_data.data_type,
));
}
resolver::FieldProcessingType::Trait => {
field_conversions.push(generate_trait_field_from_content(
field,
&field_alias,
&field.field_data.data_type,
));
}
}
if field.field_data.is_optional {
field_assignments.push(quote! { #field_name: #field_alias });
} else {
let field_name_str = field.field_data.vim_path.as_str();
field_assignments.push(quote! { #field_name: #field_alias.ok_or_else(|| vim_rs::core::error::Error::missing_required_field(#field_name_str.to_string()))? });
}
idx += 1;
}
quote! {
impl core::convert::TryFrom<vim_rs::types::structs::ObjectContent> for #struct_name {
type Error = vim_rs::core::error::Error;
fn try_from(row: vim_rs::types::structs::ObjectContent) -> vim_rs::core::error::Result<Self> {
let id = row.obj;
let Some(row) = row.prop_set else {
return Err(vim_rs::core::error::Error::no_data_found());
};
#(#field_declarations)*
for prop in row {
match prop.name.as_str() {
#(#field_conversions)*
name => {
return Err(vim_rs::core::error::Error::unexpected_property_path(name.to_string()));
}
}
}
Ok(#struct_name {
id,
#(#field_assignments),*
})
}
}
}
}
fn generate_try_from_object_update(
struct_name: &Ident,
fields: &Vec<FieldInfo>,
) -> proc_macro2::TokenStream {
let mut field_declarations: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut field_conversions: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut field_assignments: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
let mut idx = 1;
for field in fields {
let field_alias: Ident = syn::parse_str(&format!("field{}", idx)).unwrap();
let field_name = &field.property_field.name;
field_declarations.push(quote! { let mut #field_alias = None; });
match field.field_data.processing_type {
resolver::FieldProcessingType::Enum(enum_field_name) => {
field_conversions.push(generate_enum_field_from_update(
field,
&field_alias,
&enum_field_name,
));
}
resolver::FieldProcessingType::Struct => {
field_conversions.push(generate_struct_field_from_update(
field,
&field_alias,
&field.field_data.data_type,
));
}
resolver::FieldProcessingType::Trait => {
field_conversions.push(generate_trait_field_from_update(
field,
&field_alias,
&field.field_data.data_type,
));
}
}
if field.field_data.is_optional {
field_assignments.push(quote! { #field_name: #field_alias });
} else {
let field_name_str = field.field_data.vim_path.as_str();
field_assignments.push(quote! { #field_name: #field_alias.ok_or_else(|| vim_rs::core::error::Error::missing_required_field(#field_name_str.to_string()))? });
}
idx += 1;
}
quote! {
impl core::convert::TryFrom<vim_rs::types::structs::ObjectUpdate> for #struct_name {
type Error = vim_rs::core::error::Error;
fn try_from(row: vim_rs::types::structs::ObjectUpdate) -> vim_rs::core::error::Result<Self> {
let id = row.obj;
let Some(row) = row.change_set else {
return Err(vim_rs::core::error::Error::no_data_found());
};
#(#field_declarations)*
for prop in row {
if matches!(prop.op, vim_rs::types::enums::PropertyChangeOpEnum::Add | vim_rs::types::enums::PropertyChangeOpEnum::Remove | vim_rs::types::enums::PropertyChangeOpEnum::Other_(_)) {
continue;
}
match prop.name.as_str() {
#(#field_conversions)*
name => {
return Err(vim_rs::core::error::Error::unexpected_property_path(name.to_string()));
}
}
}
Ok(#struct_name {
id,
#(#field_assignments),*
})
}
}
}
}
fn generate_apply_update(fields: &Vec<FieldInfo>) -> proc_macro2::TokenStream {
let mut field_conversions: Vec<proc_macro2::TokenStream> = Vec::with_capacity(fields.len());
for field in fields {
match field.field_data.processing_type {
resolver::FieldProcessingType::Enum(enum_field_name) => {
field_conversions.push(generate_enum_field_apply(field, &enum_field_name));
}
resolver::FieldProcessingType::Struct => {
field_conversions.push(generate_struct_field_apply(
field,
&field.field_data.data_type,
));
}
resolver::FieldProcessingType::Trait => {
field_conversions.push(generate_trait_field_apply(
field,
&field.field_data.data_type,
));
}
}
}
quote! {
fn apply_update(&mut self, update: vim_rs::types::structs::ObjectUpdate) -> vim_rs::core::pc_helpers::Result<()> {
let Some(row) = update.change_set else {
return Ok(());
};
for prop in row {
if matches!(prop.op, vim_rs::types::enums::PropertyChangeOpEnum::Add | vim_rs::types::enums::PropertyChangeOpEnum::Remove | vim_rs::types::enums::PropertyChangeOpEnum::Other_(_)) {
continue;
}
match prop.name.as_str() {
#(#field_conversions)*
name => {
return Err(vim_rs::core::error::Error::unexpected_property_path(name.to_string()));
}
}
}
Ok(())
}
}
}
fn generate_enum_field_from_content(
field: &FieldInfo,
field_alias: &Ident,
enum_field_name: &str,
) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
let enum_field = Ident::new(enum_field_name, field.property_field.path.span());
quote! {
#path => {
#field_alias = match prop.val {
vim_rs::types::vim_any::VimAny::Value(vim_rs::types::boxed_types::ValueElements::#enum_field(vd)) => Some(vd),
ref val => return Err(vim_rs::core::error::Error::invalid_property_type(#path.to_string(), #enum_field_name.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_enum_field_from_update(
field: &FieldInfo,
field_alias: &Ident,
enum_field_name: &str,
) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
let enum_field = Ident::new(enum_field_name, field.property_field.path.span());
quote! {
#path => {
#field_alias = match prop.val {
Some(vim_rs::types::vim_any::VimAny::Value(vim_rs::types::boxed_types::ValueElements::#enum_field(vd))) => Some(vd),
None => continue,
Some(ref val) => return Err(vim_rs::core::error::Error::invalid_property_type(#path.to_string(), #enum_field_name.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_enum_field_apply(field: &FieldInfo, enum_field_name: &str) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
let enum_field = Ident::new(enum_field_name, field.property_field.path.span());
let field_name = &field.property_field.name;
let none_code;
let value_code;
if field.field_data.is_optional {
none_code = quote! { None };
value_code = quote! { Some(vd) };
} else {
none_code = quote! { return Err(vim_rs::core::error::Error::missing_required_field(#path.to_string())) };
value_code = quote! { vd };
};
quote! {
#path => {
self.#field_name = match prop.val {
Some(vim_rs::types::vim_any::VimAny::Value(vim_rs::types::boxed_types::ValueElements::#enum_field(vd))) => #value_code,
None => #none_code,
Some(ref val) => return Err(vim_rs::core::error::Error::invalid_property_type(#path.to_string(), #enum_field_name.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_struct_field_from_content(
field: &FieldInfo,
field_alias: &Ident,
struct_type: &str,
) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
quote! {
#path => {
#field_alias = match prop.val {
vim_rs::types::vim_any::VimAny::Object(obj) => {
let name = obj.data_type().as_str();
match obj.as_any_box().downcast() {
Ok(val) => Some(*val),
Err(_) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #struct_type.to_string(), name.to_string())),
}
},
ref val => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #struct_type.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_struct_field_from_update(
field: &FieldInfo,
field_alias: &Ident,
struct_type: &str,
) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
quote! {
#path => {
#field_alias = match prop.val {
Some(vim_rs::types::vim_any::VimAny::Object(obj)) => {
let name = obj.data_type().as_str();
match obj.as_any_box().downcast() {
Ok(val) => Some(*val),
Err(_) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #struct_type.to_string(), name.to_string())),
}
},
None => continue,
Some(ref val) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #struct_type.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_struct_field_apply(field: &FieldInfo, struct_type: &str) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
let field_name = &field.property_field.name;
let none_code;
let value_code;
if field.field_data.is_optional {
none_code = quote! { None };
value_code = quote! { Some(*val) };
} else {
none_code = quote! { return Err(vim_rs::core::error::Error::missing_required_field(#path.to_string())) };
value_code = quote! { *val };
};
quote! {
#path => {
self.#field_name = match prop.val {
Some(vim_rs::types::vim_any::VimAny::Object(obj)) => {
let name = obj.data_type().as_str();
match obj.as_any_box().downcast() {
Ok(val) => #value_code,
Err(_) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #struct_type.to_string(), name.to_string())),
}
},
None => #none_code,
Some(ref val) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #struct_type.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_trait_field_from_content(
field: &FieldInfo,
field_alias: &Ident,
trait_type: &str,
) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
quote! {
#path => {
#field_alias = match prop.val {
vim_rs::types::vim_any::VimAny::Object(obj) => {
let name = obj.data_type().as_str();
match vim_rs::types::convert::CastInto::into_box(obj) {
Ok(val) => Some(val),
Err(_) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #trait_type.to_string(), name.to_string())),
}
},
ref val => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #trait_type.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_trait_field_from_update(
field: &FieldInfo,
field_alias: &Ident,
trait_type: &str,
) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
quote! {
#path => {
#field_alias = match prop.val {
Some(vim_rs::types::vim_any::VimAny::Object(obj)) => {
let name = obj.data_type().as_str();
match vim_rs::types::convert::CastInto::into_box(obj) {
Ok(val) => Some(val),
Err(_) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #trait_type.to_string(), name.to_string())),
}
},
None => continue,
Some(ref val) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #trait_type.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}
fn generate_trait_field_apply(field: &FieldInfo, trait_type: &str) -> proc_macro2::TokenStream {
let path = &field.field_data.vim_path;
let field_name = &field.property_field.name;
let none_code;
let value_code;
if field.field_data.is_optional {
none_code = quote! { None };
value_code = quote! { Some(val) };
} else {
none_code = quote! { return Err(vim_rs::core::error::Error::missing_required_field(#path.to_string())) };
value_code = quote! { val };
};
quote! {
#path => {
self.#field_name = match prop.val {
Some(vim_rs::types::vim_any::VimAny::Object(obj)) => {
let name = obj.data_type().as_str();
match vim_rs::types::convert::CastInto::into_box(obj) {
Ok(val) => #value_code,
Err(_) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #trait_type.to_string(), name.to_string())),
}
},
None => #none_code,
Some(ref val) => return Err(vim_rs::core::error::Error::invalid_property_type( #path.to_string(), #trait_type.to_string(), vim_rs::core::pc_helpers::type_name(val))),
};
}
}
}