From 4251a2d47da7fb69653db256ff89ead6db91b79c Mon Sep 17 00:00:00 2001 From: William Lyles <26171886+wilyle@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:27:25 -0800 Subject: [PATCH] Make data adapters pluggable in `freyja_main` (#114) Updates the `freyja_main` function and the `freyja_main!` macro to add support for pluggable data adapters. Closes #19 --- common/src/data_adapter_selector.rs | 6 +- freyja/Cargo.toml | 11 ++-- freyja/examples/in-memory-with-fn.rs | 34 ++++++++++- freyja/examples/in-memory.rs | 8 ++- freyja/examples/mocks.rs | 8 ++- freyja/src/cartographer.rs | 2 +- freyja/src/data_adapter_selector_impl.rs | 13 ++-- freyja/src/emitter.rs | 2 +- freyja/src/lib.rs | 21 +++---- proc_macros/src/freyja_main/generate.rs | 16 ++++- proc_macros/src/freyja_main/parse.rs | 76 ++++++++++++++++++++---- proc_macros/src/lib.rs | 12 +++- 12 files changed, 165 insertions(+), 44 deletions(-) diff --git a/common/src/data_adapter_selector.rs b/common/src/data_adapter_selector.rs index 488efe00..5446beb2 100644 --- a/common/src/data_adapter_selector.rs +++ b/common/src/data_adapter_selector.rs @@ -11,8 +11,12 @@ use crate::{data_adapter::DataAdapterFactory, entity::Entity}; #[async_trait] pub trait DataAdapterSelector { /// Registers a `DataAdapterFactory` with this selector. - fn register( + /// + /// # Arguments + /// - `factory`: the factory to register + fn register( &mut self, + factory: Box, ) -> Result<(), DataAdapterSelectorError>; /// Updates an existing data adapter to include an entity if possible, diff --git a/freyja/Cargo.toml b/freyja/Cargo.toml index f11e8796..e6ac78c3 100644 --- a/freyja/Cargo.toml +++ b/freyja/Cargo.toml @@ -17,12 +17,6 @@ proc-macros = { workspace = true } time = { workspace = true } tokio = { workspace = true } -grpc-data-adapter = { workspace = true } -http-mock-data-adapter = { workspace = true } -in-memory-mock-data-adapter = { workspace = true } -managed-subscribe-data-adapter = { workspace = true } -mqtt-data-adapter = { workspace = true } - [dev-dependencies] # Dependencies for testing mockall = { workspace = true } @@ -34,3 +28,8 @@ in-memory-mock-digital-twin-adapter = { workspace = true } in-memory-mock-mapping-adapter = { workspace = true } mock-digital-twin-adapter = { workspace = true } mock-mapping-service-adapter = { workspace = true } +grpc-data-adapter = { workspace = true } +http-mock-data-adapter = { workspace = true } +in-memory-mock-data-adapter = { workspace = true } +managed-subscribe-data-adapter = { workspace = true } +mqtt-data-adapter = { workspace = true } \ No newline at end of file diff --git a/freyja/examples/in-memory-with-fn.rs b/freyja/examples/in-memory-with-fn.rs index 26f01727..faebcd72 100644 --- a/freyja/examples/in-memory-with-fn.rs +++ b/freyja/examples/in-memory-with-fn.rs @@ -2,18 +2,46 @@ // Licensed under the MIT license. // SPDX-License-Identifier: MIT +use freyja_common::data_adapter::DataAdapterFactory; +use grpc_data_adapter::grpc_data_adapter_factory::GRPCDataAdapterFactory; +use http_mock_data_adapter::http_mock_data_adapter_factory::HttpMockDataAdapterFactory; use in_memory_mock_cloud_adapter::in_memory_mock_cloud_adapter::InMemoryMockCloudAdapter; +use in_memory_mock_data_adapter::in_memory_mock_data_adapter_factory::InMemoryMockDataAdapterFactory; use in_memory_mock_digital_twin_adapter::in_memory_mock_digital_twin_adapter::InMemoryMockDigitalTwinAdapter; use in_memory_mock_mapping_adapter::in_memory_mock_mapping_adapter::InMemoryMockMappingAdapter; +use managed_subscribe_data_adapter::managed_subscribe_data_adapter_factory::ManagedSubscribeDataAdapterFactory; +use mqtt_data_adapter::mqtt_data_adapter_factory::MqttDataAdapterFactory; +// This example shows how you can use the freyja_main function manually rather than using the freyja_main! macro. +// This is useful when you need to do some additional work such as complex adapter setup or dependency resolution before invoking freyja_main. +// The following code is functionally equivalent to the expanded macro. #[tokio::main] async fn main() -> Result<(), Box> { - // This example shows how you can use the freyja_main function manually rather than using the freyja_main! macro. - // This is useful when you need to do some additional work such as complex adapter setup or dependency resolution before invoking freyja_main. + let factories: Vec> = vec![ + Box::new( + GRPCDataAdapterFactory::create_new().expect("Could not create GRPCDataAdapterFactory"), + ), + Box::new( + HttpMockDataAdapterFactory::create_new() + .expect("Could not create HttpMockDataAdapterFactory"), + ), + Box::new( + InMemoryMockDataAdapterFactory::create_new() + .expect("Could not create InMemoryMockDataAdapterFactory"), + ), + Box::new( + ManagedSubscribeDataAdapterFactory::create_new() + .expect("Could not create ManagedSubscribeDataAdapterFactory"), + ), + Box::new( + MqttDataAdapterFactory::create_new().expect("Could not create MqttDataAdapterFactory"), + ), + ]; + freyja::freyja_main::< InMemoryMockDigitalTwinAdapter, InMemoryMockCloudAdapter, InMemoryMockMappingAdapter, - >() + >(factories) .await } diff --git a/freyja/examples/in-memory.rs b/freyja/examples/in-memory.rs index ef4accf5..45c83cc6 100644 --- a/freyja/examples/in-memory.rs +++ b/freyja/examples/in-memory.rs @@ -3,7 +3,13 @@ // SPDX-License-Identifier: MIT use in_memory_mock_cloud_adapter::in_memory_mock_cloud_adapter::InMemoryMockCloudAdapter; +use in_memory_mock_data_adapter::in_memory_mock_data_adapter_factory::InMemoryMockDataAdapterFactory; use in_memory_mock_digital_twin_adapter::in_memory_mock_digital_twin_adapter::InMemoryMockDigitalTwinAdapter; use in_memory_mock_mapping_adapter::in_memory_mock_mapping_adapter::InMemoryMockMappingAdapter; -freyja::freyja_main! {InMemoryMockDigitalTwinAdapter, InMemoryMockCloudAdapter, InMemoryMockMappingAdapter} +freyja::freyja_main! { + InMemoryMockDigitalTwinAdapter, + InMemoryMockCloudAdapter, + InMemoryMockMappingAdapter, + [InMemoryMockDataAdapterFactory], +} diff --git a/freyja/examples/mocks.rs b/freyja/examples/mocks.rs index 64a9646e..aada80dd 100644 --- a/freyja/examples/mocks.rs +++ b/freyja/examples/mocks.rs @@ -2,8 +2,14 @@ // Licensed under the MIT license. // SPDX-License-Identifier: MIT +use http_mock_data_adapter::http_mock_data_adapter_factory::HttpMockDataAdapterFactory; use in_memory_mock_cloud_adapter::in_memory_mock_cloud_adapter::InMemoryMockCloudAdapter; use mock_digital_twin_adapter::mock_digital_twin_adapter::MockDigitalTwinAdapter; use mock_mapping_service_adapter::mock_mapping_service_adapter::MockMappingServiceAdapter; -freyja::freyja_main! {MockDigitalTwinAdapter, InMemoryMockCloudAdapter, MockMappingServiceAdapter} +freyja::freyja_main! { + MockDigitalTwinAdapter, + InMemoryMockCloudAdapter, + MockMappingServiceAdapter, + [HttpMockDataAdapterFactory], +} diff --git a/freyja/src/cartographer.rs b/freyja/src/cartographer.rs index a65de7db..759fd442 100644 --- a/freyja/src/cartographer.rs +++ b/freyja/src/cartographer.rs @@ -290,7 +290,7 @@ mod cartographer_tests { #[async_trait] impl DataAdapterSelector for DataAdapterSelector { - fn register(&mut self) -> Result<(), DataAdapterSelectorError>; + fn register(&mut self, factory: Box) -> Result<(), DataAdapterSelectorError>; async fn create_or_update_adapter(&self, entity: &Entity) -> Result<(), DataAdapterSelectorError>; async fn request_entity_value(&self, entity_id: &str) -> Result<(), DataAdapterSelectorError>; } diff --git a/freyja/src/data_adapter_selector_impl.rs b/freyja/src/data_adapter_selector_impl.rs index cdc90308..aadc17fd 100644 --- a/freyja/src/data_adapter_selector_impl.rs +++ b/freyja/src/data_adapter_selector_impl.rs @@ -64,12 +64,14 @@ impl DataAdapterSelectorImpl { #[async_trait] impl DataAdapterSelector for DataAdapterSelectorImpl { /// Registers a `DataAdapterFactory` with this selector. - fn register( + /// + /// # Arguments + /// - `factory`: the factory to register + fn register( &mut self, + factory: Box, ) -> Result<(), DataAdapterSelectorError> { - let factory = - TFactory::create_new().map_err(DataAdapterSelectorError::data_adapter_error)?; - self.factories.push(Box::new(factory)); + self.factories.push(factory); Ok(()) } @@ -232,7 +234,8 @@ mod data_adapter_selector_tests { async fn handle_start_data_adapter_request_return_err_test() { let signals: Arc = Arc::new(SignalStore::new()); let mut uut = DataAdapterSelectorImpl::new(signals); - uut.register::().unwrap(); + uut.register(Box::new(GRPCDataAdapterFactory::create_new().unwrap())) + .unwrap(); let entity = Entity { id: String::from(AMBIENT_AIR_TEMPERATURE_ID), diff --git a/freyja/src/emitter.rs b/freyja/src/emitter.rs index 7119c6d3..81cdebd7 100644 --- a/freyja/src/emitter.rs +++ b/freyja/src/emitter.rs @@ -234,7 +234,7 @@ mod emitter_tests { #[async_trait] impl DataAdapterSelector for DataAdapterSelector { - fn register(&mut self) -> Result<(), DataAdapterSelectorError>; + fn register(&mut self, factory: Box) -> Result<(), DataAdapterSelectorError>; async fn create_or_update_adapter(&self, entity: &Entity) -> Result<(), DataAdapterSelectorError>; async fn request_entity_value(&self, entity_id: &str) -> Result<(), DataAdapterSelectorError>; } diff --git a/freyja/src/lib.rs b/freyja/src/lib.rs index 0ee6ef3e..bf21bca6 100644 --- a/freyja/src/lib.rs +++ b/freyja/src/lib.rs @@ -20,25 +20,22 @@ use emitter::Emitter; use freyja_common::{ cloud_adapter::CloudAdapter, cmd_utils::{get_log_level, parse_args}, + data_adapter::DataAdapterFactory, data_adapter_selector::DataAdapterSelector, digital_twin_adapter::DigitalTwinAdapter, mapping_adapter::MappingAdapter, signal_store::SignalStore, }; -use grpc_data_adapter::grpc_data_adapter_factory::GRPCDataAdapterFactory; -use http_mock_data_adapter::http_mock_data_adapter_factory::HttpMockDataAdapterFactory; -use in_memory_mock_data_adapter::in_memory_mock_data_adapter_factory::InMemoryMockDataAdapterFactory; -use managed_subscribe_data_adapter::managed_subscribe_data_adapter_factory::ManagedSubscribeDataAdapterFactory; -use mqtt_data_adapter::mqtt_data_adapter_factory::MqttDataAdapterFactory; - use crate::data_adapter_selector_impl::DataAdapterSelectorImpl; pub async fn freyja_main< TDigitalTwinAdapter: DigitalTwinAdapter, TCloudAdapter: CloudAdapter, TMappingAdapter: MappingAdapter, ->() -> Result<(), Box> { +>( + factories: Vec>, +) -> Result<(), Box> { let args = parse_args(env::args()).expect("Failed to parse args"); // Setup logging @@ -51,11 +48,11 @@ pub async fn freyja_main< let signal_store = Arc::new(SignalStore::new()); let mut data_adapter_selector = DataAdapterSelectorImpl::new(signal_store.clone()); - data_adapter_selector.register::()?; - data_adapter_selector.register::()?; - data_adapter_selector.register::()?; - data_adapter_selector.register::()?; - data_adapter_selector.register::()?; + for factory in factories.into_iter() { + data_adapter_selector + .register(factory) + .expect("Could not register factory"); + } let data_adapter_selector = Arc::new(Mutex::new(data_adapter_selector)); diff --git a/proc_macros/src/freyja_main/generate.rs b/proc_macros/src/freyja_main/generate.rs index 79eb2818..a8b5286f 100644 --- a/proc_macros/src/freyja_main/generate.rs +++ b/proc_macros/src/freyja_main/generate.rs @@ -20,13 +20,27 @@ pub(crate) fn generate(ir: FreyjaMainOutput) -> TokenStream { dt_adapter_type, cloud_adapter_type, mapping_adapter_type, + data_adapter_factory_types, }, } = ir; quote! { #[tokio::main] async fn main() -> Result<(), Box> { - freyja::freyja_main::<#dt_adapter_type, #cloud_adapter_type, #mapping_adapter_type>().await + use freyja_common::data_adapter::DataAdapterFactory; + let factories: Vec> = vec![ + #(Box::new( + #data_adapter_factory_types::create_new() + .expect(concat!("Could not create ", stringify!(#data_adapter_factory_types))) + )),* + ]; + + freyja::freyja_main::< + #dt_adapter_type, + #cloud_adapter_type, + #mapping_adapter_type + >(factories) + .await } } } diff --git a/proc_macros/src/freyja_main/parse.rs b/proc_macros/src/freyja_main/parse.rs index d46c6087..2c28c9a8 100644 --- a/proc_macros/src/freyja_main/parse.rs +++ b/proc_macros/src/freyja_main/parse.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use proc_macro2::TokenStream; +use syn::bracketed; use syn::parse::{Parse, ParseStream}; use syn::{punctuated::Punctuated, Ident, Token}; @@ -21,6 +22,7 @@ pub(crate) struct FreyjaMainArgs { pub dt_adapter_type: Ident, pub cloud_adapter_type: Ident, pub mapping_adapter_type: Ident, + pub data_adapter_factory_types: Vec, } impl Parse for FreyjaMainArgs { @@ -30,16 +32,36 @@ impl Parse for FreyjaMainArgs { /// /// - `input`: the input stream fn parse(input: ParseStream) -> syn::Result { - let mut args = Punctuated::::parse_terminated(input)?.into_iter(); - - if args.len() != 3 { - panic!("Expected exactly three arguments to freyja_main"); + let dt_adapter_type = input.parse::().unwrap(); + let _ = input.parse::().unwrap(); + let cloud_adapter_type = input.parse::().unwrap(); + let _ = input.parse::().unwrap(); + let mapping_adapter_type = input.parse::().unwrap(); + let _ = input.parse::().unwrap(); + + let data_adapter_content; + let _ = bracketed!(data_adapter_content in input); + let data_adapter_factory_types = + Punctuated::::parse_terminated(&data_adapter_content) + .unwrap() + .into_iter() + .collect(); + + let trailing_comma_result = if !input.is_empty() { + Some(input.parse::()) + } else { + None + }; + + if !input.is_empty() || trailing_comma_result.is_some_and(|r| r.is_err()) { + panic!("Unexpected tokens at end of input"); } Ok(FreyjaMainArgs { - dt_adapter_type: args.next().unwrap(), - cloud_adapter_type: args.next().unwrap(), - mapping_adapter_type: args.next().unwrap(), + dt_adapter_type, + cloud_adapter_type, + mapping_adapter_type, + data_adapter_factory_types, }) } } @@ -56,25 +78,33 @@ mod freyja_main_parse_tests { let foo_ident = format_ident!("Foo"); let bar_ident = format_ident!("Bar"); let baz_ident = format_ident!("Baz"); + let factory_idents = vec![format_ident!("DA1"), format_ident!("DA2")]; + let factory_idents_clone = factory_idents.clone(); - let input = quote! { #foo_ident, #bar_ident, #baz_ident }; + let input = quote! { #foo_ident, #bar_ident, #baz_ident, [#(#factory_idents),*] }; let output = parse(input); assert_eq!(output.dt_adapter_type, foo_ident); assert_eq!(output.cloud_adapter_type, bar_ident); assert_eq!(output.mapping_adapter_type, baz_ident); + for ident in factory_idents.iter() { + assert!(output.data_adapter_factory_types.contains(ident)); + } // Now try a different order - let input = quote! { #baz_ident, #foo_ident, #bar_ident }; + let input = quote! { #baz_ident, #foo_ident, #bar_ident, [#(#factory_idents_clone),*] }; let output = parse(input); assert_eq!(output.dt_adapter_type, baz_ident); assert_eq!(output.cloud_adapter_type, foo_ident); assert_eq!(output.mapping_adapter_type, bar_ident); + for ident in factory_idents { + assert!(output.data_adapter_factory_types.contains(&ident)); + } } #[test] - fn parse_panics_with_incorrect_number_of_arguments() { + fn parse_panics_with_invalid_input() { let foo_ident = format_ident!("Foo"); let bar_ident = format_ident!("Bar"); let baz_ident = format_ident!("Baz"); @@ -88,4 +118,30 @@ mod freyja_main_parse_tests { let result = catch_unwind(|| parse(input)); assert!(result.is_err()); } + + #[test] + fn parse_accepts_trailing_comma() { + let foo_ident = format_ident!("Foo"); + let bar_ident = format_ident!("Bar"); + let baz_ident = format_ident!("Baz"); + let factory_idents = vec![format_ident!("DA1"), format_ident!("DA2")]; + + let input = quote! { #foo_ident, #bar_ident, #baz_ident, [#(#factory_idents),*], }; + let result = catch_unwind(|| parse(input)); + assert!(result.is_ok()); + } + + #[test] + fn parse_panics_with_invalid_trailing_content() { + let foo_ident = format_ident!("Foo"); + let bar_ident = format_ident!("Bar"); + let baz_ident = format_ident!("Baz"); + let factory_idents = vec![format_ident!("DA1"), format_ident!("DA2")]; + let qux_ident = format_ident!("Qux"); + + let input = + quote! { #foo_ident, #bar_ident, #baz_ident, [#(#factory_idents),*], #qux_ident }; + let result = catch_unwind(|| parse(input)); + assert!(result.is_err()); + } } diff --git a/proc_macros/src/lib.rs b/proc_macros/src/lib.rs index 4bc74735..93f76224 100644 --- a/proc_macros/src/lib.rs +++ b/proc_macros/src/lib.rs @@ -83,7 +83,7 @@ pub fn error(ts: TokenStream) -> TokenStream { /// /// *FreyjaMainPredicate*: /// -///     *DigitalTwinAdapterType* `,` *CloudAdapterType* `,` *MappingClientType* +///     *DigitalTwinAdapterType* `,` *CloudAdapterType* `,` *MappingAdapterType* `, [` *DataAdapterFactoryTypeList* `]` /// /// *DigitalTwinAdapterType*: /// @@ -93,7 +93,15 @@ pub fn error(ts: TokenStream) -> TokenStream { /// ///     IDENTIFIER /// -/// *MappingClientType*: +/// *MappingAdapterType*: +/// +///     IDENTIFIER +/// +/// *DataAdapterFactoryTypeList*: +/// +///     *DataAdapterFactoryType* (`,` *DataAdapterFactoryTypeList*) +/// +/// *DataAdapterFactoryType*: /// ///     IDENTIFIER ///