uses a proc-macro to automate command definitions
This commit is contained in:
19
api-proc-macro/Cargo.toml
Normal file
19
api-proc-macro/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
[package]
|
||||
name = "api-proc-macro"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
authors = ["Sergey <me@sergeysav.com>"]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
api-core = { path = "../api-core" }
|
||||
proc-macro-error = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = { workspace = true }
|
||||
api = { path = "../api" }
|
||||
129
api-proc-macro/src/into_command_definition.rs
Normal file
129
api-proc-macro/src/into_command_definition.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use proc_macro_error::abort;
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Generics};
|
||||
|
||||
pub fn derive_into_command_definition_impl(
|
||||
item: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let DeriveInput {
|
||||
ident,
|
||||
data,
|
||||
generics,
|
||||
..
|
||||
}: DeriveInput = parse_macro_input!(item as DeriveInput);
|
||||
|
||||
let data = match data {
|
||||
Data::Struct(data) => data,
|
||||
Data::Enum(data) => abort!(
|
||||
data.enum_token,
|
||||
"IntoCommandDefinition not supported for enum"
|
||||
),
|
||||
Data::Union(data) => abort!(
|
||||
data.union_token,
|
||||
"IntoCommandDefinition not supported for union"
|
||||
),
|
||||
};
|
||||
|
||||
let generics = add_trait_bounds(generics);
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
|
||||
let num_fields = data.fields.len();
|
||||
|
||||
let create_param_stream = match &data.fields {
|
||||
Fields::Named(fields) => {
|
||||
let field_entries = fields.named.iter().map(|field| {
|
||||
let name = field.ident.clone().map(|id| id.to_string());
|
||||
let field_type = &field.ty;
|
||||
quote_spanned! { field.span() =>
|
||||
parameters.push(api::messages::command::CommandParameterDefinition {
|
||||
name: #name.to_string(),
|
||||
data_type: <#field_type as api::data_type::ToDataType>::DATA_TYPE,
|
||||
});
|
||||
}
|
||||
});
|
||||
quote! { #(#field_entries)* }
|
||||
}
|
||||
Fields::Unnamed(fields) => abort!(
|
||||
fields,
|
||||
"IntoCommandDefinition not supported for unnamed structs"
|
||||
),
|
||||
Fields::Unit => quote! {},
|
||||
};
|
||||
let parse_param_stream = match &data.fields {
|
||||
Fields::Named(fields) => {
|
||||
let field_entries = fields.named.iter().map(|field| {
|
||||
let name = &field.ident;
|
||||
let name_string = field.ident.clone().map(|id| id.to_string());
|
||||
let field_type = &field.ty;
|
||||
quote_spanned! { field.span() =>
|
||||
let #name: #field_type = (*command
|
||||
.parameters
|
||||
.get(#name_string)
|
||||
.ok_or_else(|| api::messages::command::IntoCommandDefinitionError::ParameterMissing(#name_string.to_string()))?)
|
||||
.try_into()
|
||||
.map_err(|_| api::messages::command::IntoCommandDefinitionError::MismatchedType {
|
||||
parameter: #name_string.to_string(),
|
||||
expected: <#field_type as api::data_type::ToDataType>::DATA_TYPE,
|
||||
})?;
|
||||
}
|
||||
});
|
||||
quote! { #(#field_entries)* }
|
||||
}
|
||||
Fields::Unnamed(fields) => abort!(
|
||||
fields,
|
||||
"IntoCommandDefinition not supported for unnamed structs"
|
||||
),
|
||||
Fields::Unit => quote! {},
|
||||
};
|
||||
let param_name_stream = match &data.fields {
|
||||
Fields::Named(fields) => {
|
||||
let field_entries = fields.named.iter().map(|field| {
|
||||
let name = &field.ident;
|
||||
quote_spanned! { field.span() => #name, }
|
||||
});
|
||||
quote! { #(#field_entries)* }
|
||||
}
|
||||
Fields::Unnamed(fields) => abort!(
|
||||
fields,
|
||||
"IntoCommandDefinition not supported for unnamed structs"
|
||||
),
|
||||
Fields::Unit => quote! {},
|
||||
};
|
||||
|
||||
let result = quote! {
|
||||
impl #impl_generics api::messages::command::IntoCommandDefinition for #ident #ty_generics #where_clause {
|
||||
fn create(name: std::string::String) -> api::messages::command::CommandDefinition {
|
||||
let mut parameters = std::vec::Vec::with_capacity( #num_fields );
|
||||
#create_param_stream
|
||||
api::messages::command::CommandDefinition {
|
||||
name: name,
|
||||
parameters: parameters,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(command: api::messages::command::Command) -> core::result::Result<Self, api::messages::command::IntoCommandDefinitionError> {
|
||||
#parse_param_stream
|
||||
Ok(Self {
|
||||
#param_name_stream
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result.into()
|
||||
}
|
||||
|
||||
fn add_trait_bounds(mut generics: Generics) -> Generics {
|
||||
for param in &mut generics.params {
|
||||
if let GenericParam::Type(ref mut type_param) = *param {
|
||||
type_param
|
||||
.bounds
|
||||
.push(parse_quote!(api::data_type::ToDataType));
|
||||
type_param.bounds.push(parse_quote!(
|
||||
core::convert::TryFrom<api::data_value::DataValue>
|
||||
));
|
||||
}
|
||||
}
|
||||
generics
|
||||
}
|
||||
11
api-proc-macro/src/lib.rs
Normal file
11
api-proc-macro/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro_error::proc_macro_error;
|
||||
|
||||
mod into_command_definition;
|
||||
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_derive(IntoCommandDefinition)]
|
||||
pub fn derive_into_command_definition(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
into_command_definition::derive_into_command_definition_impl(item)
|
||||
}
|
||||
10
api-proc-macro/tests/into_command_definition/enum_fails.rs
Normal file
10
api-proc-macro/tests/into_command_definition/enum_fails.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use api_proc_macro::IntoCommandDefinition;
|
||||
|
||||
#[derive(IntoCommandDefinition)]
|
||||
enum TestEnum {
|
||||
Variant
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: IntoCommandDefinition not supported for enum
|
||||
--> tests/into_command_definition/enum_fails.rs:4:1
|
||||
|
|
||||
4 | enum TestEnum {
|
||||
| ^^^^
|
||||
12
api-proc-macro/tests/into_command_definition/union_fails.rs
Normal file
12
api-proc-macro/tests/into_command_definition/union_fails.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use api_proc_macro::IntoCommandDefinition;
|
||||
|
||||
#[derive(IntoCommandDefinition)]
|
||||
#[repr(C)]
|
||||
union TestUnion {
|
||||
f1: u32,
|
||||
f2: f32,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: IntoCommandDefinition not supported for union
|
||||
--> tests/into_command_definition/union_fails.rs:5:1
|
||||
|
|
||||
5 | union TestUnion {
|
||||
| ^^^^^
|
||||
@@ -0,0 +1,8 @@
|
||||
use api_proc_macro::IntoCommandDefinition;
|
||||
|
||||
#[derive(IntoCommandDefinition)]
|
||||
struct TestUnnamedStruct(f32, f64, bool);
|
||||
|
||||
fn main() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: IntoCommandDefinition not supported for unnamed structs
|
||||
--> tests/into_command_definition/unnamed_struct_fails.rs:4:25
|
||||
|
|
||||
4 | struct TestUnnamedStruct(f32, f64, bool);
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,151 @@
|
||||
use api_core::command::{
|
||||
Command, CommandHeader, CommandParameterDefinition, IntoCommandDefinition,
|
||||
};
|
||||
use api_core::data_type::DataType;
|
||||
use api_core::data_value::DataValue;
|
||||
use api_proc_macro::IntoCommandDefinition;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_enum_fails() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/into_command_definition/enum_fails.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_union_fails() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/into_command_definition/union_fails.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unnamed_struct_fails() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/into_command_definition/unnamed_struct_fails.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_command() {
|
||||
#[derive(IntoCommandDefinition)]
|
||||
struct TestStruct {
|
||||
#[allow(unused)]
|
||||
a: f32,
|
||||
#[allow(unused)]
|
||||
b: f64,
|
||||
#[allow(unused)]
|
||||
c: bool,
|
||||
}
|
||||
|
||||
let command_definition = TestStruct::create("Test".to_string());
|
||||
|
||||
assert_eq!(command_definition.name, "Test");
|
||||
assert_eq!(command_definition.parameters.capacity(), 3);
|
||||
assert_eq!(
|
||||
command_definition.parameters[0],
|
||||
CommandParameterDefinition {
|
||||
name: "a".to_string(),
|
||||
data_type: DataType::Float32,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
command_definition.parameters[1],
|
||||
CommandParameterDefinition {
|
||||
name: "b".to_string(),
|
||||
data_type: DataType::Float64,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
command_definition.parameters[2],
|
||||
CommandParameterDefinition {
|
||||
name: "c".to_string(),
|
||||
data_type: DataType::Boolean,
|
||||
}
|
||||
);
|
||||
|
||||
let mut parameters = HashMap::new();
|
||||
parameters.insert("a".to_string(), DataValue::Float32(1.0));
|
||||
parameters.insert("b".to_string(), DataValue::Float64(2.0));
|
||||
parameters.insert("c".to_string(), DataValue::Boolean(true));
|
||||
let result = TestStruct::parse(Command {
|
||||
header: CommandHeader {
|
||||
timestamp: Default::default(),
|
||||
},
|
||||
parameters,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(result.a, 1.0f32);
|
||||
assert_eq!(result.b, 2.0f64);
|
||||
assert_eq!(result.c, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_command() {
|
||||
#[derive(IntoCommandDefinition)]
|
||||
struct TestStruct<T> {
|
||||
#[allow(unused)]
|
||||
a: T,
|
||||
}
|
||||
|
||||
let command_definition = TestStruct::<f32>::create("Test".to_string());
|
||||
assert_eq!(command_definition.name, "Test");
|
||||
assert_eq!(command_definition.parameters.capacity(), 1);
|
||||
assert_eq!(
|
||||
command_definition.parameters[0],
|
||||
CommandParameterDefinition {
|
||||
name: "a".to_string(),
|
||||
data_type: DataType::Float32,
|
||||
}
|
||||
);
|
||||
let mut parameters = HashMap::new();
|
||||
parameters.insert("a".to_string(), DataValue::Float32(1.0));
|
||||
let result = TestStruct::<f32>::parse(Command {
|
||||
header: CommandHeader {
|
||||
timestamp: Default::default(),
|
||||
},
|
||||
parameters,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(result.a, 1.0f32);
|
||||
|
||||
let command_definition = TestStruct::<f64>::create("Test2".to_string());
|
||||
assert_eq!(command_definition.name, "Test2");
|
||||
assert_eq!(command_definition.parameters.capacity(), 1);
|
||||
assert_eq!(
|
||||
command_definition.parameters[0],
|
||||
CommandParameterDefinition {
|
||||
name: "a".to_string(),
|
||||
data_type: DataType::Float64,
|
||||
}
|
||||
);
|
||||
let mut parameters = HashMap::new();
|
||||
parameters.insert("a".to_string(), DataValue::Float64(2.0));
|
||||
let result = TestStruct::<f64>::parse(Command {
|
||||
header: CommandHeader {
|
||||
timestamp: Default::default(),
|
||||
},
|
||||
parameters,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(result.a, 2.0f64);
|
||||
|
||||
let command_definition = TestStruct::<bool>::create("Test3".to_string());
|
||||
assert_eq!(command_definition.name, "Test3");
|
||||
assert_eq!(command_definition.parameters.capacity(), 1);
|
||||
assert_eq!(
|
||||
command_definition.parameters[0],
|
||||
CommandParameterDefinition {
|
||||
name: "a".to_string(),
|
||||
data_type: DataType::Boolean,
|
||||
}
|
||||
);
|
||||
let mut parameters = HashMap::new();
|
||||
parameters.insert("a".to_string(), DataValue::Boolean(true));
|
||||
let result = TestStruct::<bool>::parse(Command {
|
||||
header: CommandHeader {
|
||||
timestamp: Default::default(),
|
||||
},
|
||||
parameters,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(result.a, true);
|
||||
}
|
||||
Reference in New Issue
Block a user