Skip to content

Commit

Permalink
Reduce boilerplate code with macros (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
bewee authored Jan 30, 2022
1 parent 797f852 commit d408809
Show file tree
Hide file tree
Showing 42 changed files with 2,340 additions and 593 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jsonschema = "0.12.1"
chrono = "0.4.19"
as-any = "0.2.0"
mockall_double = "0.2.0"
gateway-addon-rust-codegen = { path = "gateway-addon-rust-codegen" }

[dependencies.serde]
version = "1.0"
Expand Down
3 changes: 3 additions & 0 deletions gateway-addon-rust-codegen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
Cargo.lock
/.idea
12 changes: 12 additions & 0 deletions gateway-addon-rust-codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "gateway-addon-rust-codegen"
version = "1.0.0-alpha.1"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
2 changes: 2 additions & 0 deletions gateway-addon-rust-codegen/rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
imports_granularity = "Crate"
format_code_in_doc_comments = true
129 changes: 129 additions & 0 deletions gateway-addon-rust-codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use std::str::FromStr;
use syn::DeriveInput;

#[proc_macro_attribute]
pub fn adapter(_args: TokenStream, input: TokenStream) -> TokenStream {
apply_macro(input, "adapter", "Adapter", None)
}

#[proc_macro_attribute]
pub fn device(_args: TokenStream, input: TokenStream) -> TokenStream {
apply_macro(input, "device", "Device", None)
}

#[proc_macro_attribute]
pub fn property(_args: TokenStream, input: TokenStream) -> TokenStream {
apply_macro(input, "property", "Property", Some("Value"))
}

#[proc_macro_attribute]
pub fn event(_args: TokenStream, input: TokenStream) -> TokenStream {
apply_macro(input, "event", "Event", Some("Data"))
}

#[proc_macro_attribute]
pub fn api_handler(_args: TokenStream, input: TokenStream) -> TokenStream {
apply_macro(input, "api_handler", "ApiHandler", None)
}

fn apply_macro(
input: TokenStream,
name_snail_case: &str,
name_camel_case: &str,
generic_name: Option<&str>,
) -> TokenStream {
if let Ok(ast) = syn::parse2::<DeriveInput>(input.into()) {
alter_struct(ast, name_snail_case, name_camel_case, generic_name).into()
} else {
panic!("`{}` has to be used with structs", name_snail_case)
}
}

fn alter_struct(
ast: DeriveInput,
name_snail_case: &str,
name_camel_case: &str,
generic_name: Option<&str>,
) -> TokenStream2 {
let struct_name = ast.ident.clone();
let visibility = ast.vis.clone();
let struct_built_name = TokenStream2::from_str(&format!("Built{}", struct_name)).unwrap();

let trait_handle_wrapper = TokenStream2::from_str(&format!(
"gateway_addon_rust::{}::Built{}",
name_snail_case, name_camel_case
))
.unwrap();
let trait_build = TokenStream2::from_str(&format!(
"gateway_addon_rust::{}::{}Builder",
name_snail_case, name_camel_case
))
.unwrap();
let struct_built = TokenStream2::from_str(&format!("Built{}", name_camel_case)).unwrap();
let struct_handle = TokenStream2::from_str(&if let Some(generic_name) = generic_name {
format!(
"gateway_addon_rust::{name_snail_case}::{name_camel_case}Handle<<{struct_name} as gateway_addon_rust::{name_snail_case}::{name_camel_case}Structure>::{generic_name}>",
name_snail_case = name_snail_case,
name_camel_case = name_camel_case,
struct_name = struct_name,
generic_name = generic_name,
)
} else {
format!(
"gateway_addon_rust::{}::{}Handle",
name_snail_case, name_camel_case
)
})
.unwrap();
let fn_handle = TokenStream2::from_str(&format!("{}_handle", name_snail_case)).unwrap();
let fn_handle_mut = TokenStream2::from_str(&format!("{}_handle_mut", name_snail_case)).unwrap();
let typedef = TokenStream2::from_str(&if let Some(generic_name) = generic_name {
format!(
"type {generic_name} = <{struct_name} as gateway_addon_rust::{name_snail_case}::{name_camel_case}Structure>::{generic_name};",
name_snail_case = name_snail_case,
name_camel_case = name_camel_case,
struct_name = struct_name,
generic_name = generic_name,
)
} else {
"".to_owned()
})
.unwrap();

quote! {
#ast
impl #trait_build for #struct_name {
type #struct_built = #struct_built_name;
fn build(data: Self, #fn_handle: #struct_handle) -> Self::#struct_built {
#struct_built_name { data, #fn_handle }
}
}
#visibility struct #struct_built_name {
data: #struct_name,
#fn_handle: #struct_handle,
}
impl #trait_handle_wrapper for #struct_built_name {
#typedef
fn #fn_handle(&self) -> &#struct_handle {
&self.#fn_handle
}
fn #fn_handle_mut(&mut self) -> &mut #struct_handle {
&mut self.#fn_handle
}
}
impl std::ops::Deref for #struct_built_name {
type Target = #struct_name;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl std::ops::DerefMut for #struct_built_name {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
}
}
6 changes: 6 additions & 0 deletions src/action/action_description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ impl<T: Input> ActionDescription<T> {
}

/// Set `@type`.
#[must_use]
pub fn at_type(mut self, at_type: AtType) -> Self {
self.at_type = Some(at_type);
self
}

/// Set `description`.
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
Expand All @@ -88,12 +90,14 @@ impl<T: Input> ActionDescription<T> {
/// }))
/// # ;
/// ```
#[must_use]
pub fn input(mut self, input: serde_json::Value) -> Self {
self.input = Some(input);
self
}

/// Set `links`.
#[must_use]
pub fn links(mut self, links: Vec<Link>) -> Self {
self.links = Some(links);
self
Expand All @@ -119,6 +123,7 @@ impl<T: Input> ActionDescription<T> {
/// })
/// # ;
/// ```
#[must_use]
pub fn link(mut self, link: Link) -> Self {
match self.links {
None => self.links = Some(vec![link]),
Expand All @@ -128,6 +133,7 @@ impl<T: Input> ActionDescription<T> {
}

/// Set `title`.
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
Expand Down
4 changes: 2 additions & 2 deletions src/action/action_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ impl Input for NoInput {
}

fn deserialize(value: serde_json::Value) -> Result<Self, WebthingsError> {
if value == serde_json::Value::Null {
if value == json!(null) || value == json!({}) {
Ok(NoInput)
} else {
Err(WebthingsError::Serialization(serde_json::Error::custom(
"Expected no input",
format!("Expected no input, got {:?}", value),
)))
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/action/action_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ pub trait Action: Send + Sync + 'static {
Err("Action does not implement canceling".to_owned())
}

/// Called once after initialization.
fn post_init(&mut self) {}

#[doc(hidden)]
fn full_description(&self) -> FullActionDescription {
self.description().into_full_description()
Expand Down Expand Up @@ -134,6 +137,9 @@ pub trait ActionBase: Send + Sync + AsAny + 'static {

#[doc(hidden)]
async fn cancel(&mut self, action_id: String) -> Result<(), String>;

#[doc(hidden)]
fn post_init(&mut self) {}
}

impl Downcast for dyn ActionBase {}
Expand All @@ -158,10 +164,16 @@ impl<T: Action> ActionBase for T {
async fn cancel(&mut self, action_id: String) -> Result<(), String> {
<T as Action>::cancel(self, action_id).await
}

fn post_init(&mut self) {
<T as Action>::post_init(self)
}
}

#[cfg(test)]
pub(crate) mod tests {
use std::ops::{Deref, DerefMut};

use crate::{action::Input, Action, ActionDescription, ActionHandle};
use async_trait::async_trait;
use mockall::mock;
Expand All @@ -170,23 +182,40 @@ pub(crate) mod tests {
pub ActionHelper<T: Input> {
pub fn perform(&mut self, action_handle: ActionHandle<T>) -> Result<(), String>;
pub fn cancel(&mut self, action_id: String) -> Result<(), String>;
pub fn post_init(&mut self);
}
}

pub struct MockAction<T: Input> {
action_name: String,
pub action_helper: MockActionHelper<T>,
pub expect_post_init: bool,
}

impl<T: Input> MockAction<T> {
pub fn new(action_name: String) -> Self {
Self {
action_name,
expect_post_init: false,
action_helper: MockActionHelper::new(),
}
}
}

impl<T: Input> Deref for MockAction<T> {
type Target = MockActionHelper<T>;

fn deref(&self) -> &Self::Target {
&self.action_helper
}
}

impl<T: Input> DerefMut for MockAction<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.action_helper
}
}

#[async_trait]
impl<T: Input> Action for MockAction<T> {
type Input = T;
Expand All @@ -210,5 +239,11 @@ pub(crate) mod tests {
async fn cancel(&mut self, action_id: String) -> Result<(), String> {
self.action_helper.cancel(action_id)
}

fn post_init(&mut self) {
if self.expect_post_init {
self.action_helper.post_init();
}
}
}
}
Loading

0 comments on commit d408809

Please sign in to comment.