Replace gRPC Backend (#10)

**Rationale:**

Having two separate servers and communication methods resulted in additional maintenance & the need to convert often between backend & frontend data types.
By moving the backend communication off of gRPC and to just use websockets it both gives more control & allows for simplification of the implementation.

#8

**Changes:**

- Replaces gRPC backend.
  - New implementation automatically handles reconnect logic
- Implements an api layer
- Migrates examples to the api layer
- Implements a proc macro to make command handling easier
- Implements unit tests for the api layer (90+% coverage)
- Implements integration tests for the proc macro (90+% coverage)

Reviewed-on: #10
Co-authored-by: Sergey Savelyev <sergeysav.nn@gmail.com>
Co-committed-by: Sergey Savelyev <sergeysav.nn@gmail.com>
This commit was merged in pull request #10.
This commit is contained in:
2026-01-01 10:11:53 -08:00
committed by sergeysav
parent f658b55586
commit 788dd10a91
68 changed files with 3934 additions and 1504 deletions

View File

@@ -0,0 +1,123 @@
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(_) => unreachable!("Already checked this"),
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(_) => unreachable!("Already checked this"),
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
View 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)
}