gc_plugin_abi 0.5.0

Gridcore Plugin API
Documentation
use super::errors::GCMockingError;
use super::gateway_instance_ctx::{
    GCGatewayMockInstanceCtx, GetEthernetInterfaceIntercept, GetLastDatapointValueIntercept, GetSerialInterfaceIntercept, LogIntercept,
    PublishDatapointValueIntercept,
};
use super::gateway_mock::GCGatewayMock;
use crate::{GCDatapoint, GCPluginInstance};
use serde::Deserialize;
use serde_json::Value;

#[derive(Deserialize)]
struct DatapointName {
    name: String,
}
#[derive(Deserialize)]
struct PluginConfigWithDatapoints {
    #[serde(default)]
    datapoint_publications: Vec<DatapointName>,
    #[serde(default)]
    datapoint_subscriptions: Vec<DatapointName>,
    plugin_specific_config: Value,
}

/// Builder for the GCGatewayMock
///
/// This builder allows to configure the plugin and setup callbacks for intercepting communications between the plugin
/// and the core.
///
/// # Example
/// ```rust
/// # use gc_plugin_abi::testing::{GCGatewayMockBuilder, GCGatewayMock};
/// # use gc_plugin_abi::{GCDatapoint, GCPluginInterface, GCPluginInstance, GCPluginInfo, GCBorrowedDatapointValue};
/// # use std::ffi::CString;
/// # use std::sync::Arc;
/// # use std::sync::atomic::{AtomicBool, Ordering};
/// # use gc_plugin_abi::gc_plugin;
/// #
/// # #[gc_plugin]
/// # pub struct ExamplePlugin<'a> {
/// #     plugin_interface: &'a GCPluginInterface,
/// # }
/// # impl<'a> ExamplePlugin<'a> {
/// #    pub fn new(plugin_interface: &'a GCPluginInterface) -> Self {
/// #        Self {
/// #            plugin_interface: plugin_interface,
/// #        }
/// #    }
/// # }
/// #
/// # impl<'a> GCPluginInstance<'a> for ExamplePlugin<'a> {
/// #
/// #     // Entrypoint which contains the interface to communicate with the core gateway
/// #     fn init(plugin_interface: &'a GCPluginInterface) -> Box<Self> {
/// #         Box::new(ExamplePlugin { plugin_interface })
/// #     }
/// #
/// #     fn get_plugin_info() -> GCPluginInfo {
/// #         GCPluginInfo::new("ExamplePlugin", "0.1.0", "0", 0)
/// #     }
/// #
/// #     // Receive datapoint by subscription
/// #     fn receive_datapoint(&self, _data_value: GCBorrowedDatapointValue) -> bool {
/// #         // Handle incoming datapoint
/// #         true
/// #     }
/// # }
///
/// const CONFIG: &str = r#"
/// {
///     "host": "localhost:9877"
/// }
/// "#;
///
/// let mut mock_builder = GCGatewayMockBuilder::new();
///
/// // Set the plugin configuration
/// mock_builder.set_config(CONFIG.to_string()).unwrap();
///
/// // Add a datapoint subscribed by the plugin
/// mock_builder.add_subscribed_datapoint(GCDatapoint::new(0, "example_name", "example_description", "example_unit"));
///
/// // Start the plugin
/// let gateway_mock: GCGatewayMock<ExamplePlugin> = mock_builder.start().unwrap();
/// ```
///
/// This simple example starts a plugin with a specific config and a single datapoint subscribed.
pub struct GCGatewayMockBuilder {
    pub(super) own_datapoints: Vec<GCDatapoint>,
    pub(super) subscribed_datapoints: Vec<GCDatapoint>,

    pub(super) core_ctx: Box<GCGatewayMockInstanceCtx>,
    pub(super) config: Box<Value>,
    pub(super) log_level: gc_abi::GCLogLevel,
}

impl Default for GCGatewayMockBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl GCGatewayMockBuilder {
    pub fn new() -> Self {
        Self {
            own_datapoints: Vec::new(),
            subscribed_datapoints: Vec::new(),
            core_ctx: GCGatewayMockInstanceCtx::new(),
            config: Box::new(serde_json::Value::Null),
            log_level: gc_abi::GCLogLevel::DEBUG,
        }
    }

    /// Add a datapoint to the list of datapoints owned by the plugin
    pub fn add_own_datapoint(&mut self, datapoint: GCDatapoint) -> &mut Self {
        self.own_datapoints.push(datapoint);
        self
    }

    /// Add a datapoint to the list of datapoints subscribed by the plugin
    pub fn add_subscribed_datapoint(&mut self, datapoint: GCDatapoint) -> &mut Self {
        self.subscribed_datapoints.push(datapoint);
        self
    }
    /// Set the log level for the plugin
    pub fn set_log_level(&mut self, level: gc_abi::GCLogLevel) -> &mut Self {
        self.log_level = level;
        self
    }

    /// Set the plugin configuration
    ///
    /// This should be whatever goes within the ```plugin_specific_config``` field.
    /// ## Example
    ///
    /// ```rust
    /// use serde::Serialize;
    /// use gc_plugin_abi::testing::GCGatewayMockBuilder;
    ///
    /// /// Config loaded by the plugin
    /// #[derive(Serialize)]
    /// struct MyConfig {
    ///    example_field: u16,
    /// }
    ///
    /// let string_config = "{\"example_field\": 42}";
    ///
    /// let builder = GCGatewayMockBuilder::new()
    ///    .set_config(string_config.to_string());
    /// ```
    pub fn set_config(&mut self, config: String) -> Result<&mut Self, GCMockingError> {
        *self.config = serde_json::from_str(&config)?;
        Ok(self)
    }

    /// Set the plugin configuration including datapoints
    ///
    /// This configuration includes the datapoints to be published and subscribed by the plugin.
    /// It can be very useful to quickly test real plugin configurations without having to manually
    /// set the datapoints.
    ///
    /// This follows the same format as the configuration file used by the gateway.
    /// ## Example
    //// ```rust
    /// use gc_plugin_abi::testing::GCGatewayMockBuilder;
    ///
    /// let string_config = r#"{
    ///     "datapoint_publications": [{"name": "pub_dp1"}, {"name": "pub_dp2"}],
    ///     "datapoint_subscriptions": [{"name": "sub_dp1"}, {"name": "sub_dp2"}],
    ///     "plugin_specific_config": {"example_field": 42}
    /// }"#;
    ///
    /// let builder = GCGatewayMockBuilder::new()
    ///    .set_config_with_datapoints(string_config.to_string());
    ///
    /// ```
    pub fn set_config_with_datapoints(&mut self, config: String) -> Result<&mut Self, GCMockingError> {
        let config: PluginConfigWithDatapoints = serde_json::from_str(&config)?;
        let mut last_id = self
            .own_datapoints
            .iter()
            .chain(self.subscribed_datapoints.iter())
            .map(|dp| dp.get_id())
            .max()
            .unwrap_or(0)
            + 1;
        for dp in config.datapoint_publications {
            self.add_own_datapoint(GCDatapoint::new(last_id, &dp.name, "", ""));
            last_id += 1;
        }

        for dp in config.datapoint_subscriptions {
            self.add_subscribed_datapoint(GCDatapoint::new(last_id, &dp.name, "", ""));
            last_id += 1;
        }

        *self.config = config.plugin_specific_config;
        Ok(self)
    }

    /// Set a callback to be called whenever a datapoint is published
    ///
    /// Multiple calls to this function will overwrite the previous callback.
    ///
    /// # Example:
    /// ```rust
    /// use gc_plugin_abi::testing::{GCGatewayMockBuilder, GCGatewayMock};
    /// use gc_plugin_abi::GCDatapointValue;
    /// use std::sync::Arc;
    /// use std::sync::atomic::{AtomicBool, Ordering};
    ///
    /// let assert_called = Arc::new(AtomicBool::new(false));
    ///
    /// let mut mock_builder = GCGatewayMockBuilder::new();
    /// mock_builder.set_publish_datapoint_value_callback(Box::new(move |value: &GCDatapointValue| {
    ///     assert_called.store(true, Ordering::SeqCst);
    ///     return true;
    /// }));
    /// ```
    pub fn set_publish_datapoint_value_callback(&mut self, callback: Box<PublishDatapointValueIntercept>) -> &mut Self {
        self.core_ctx.publish_datapoint_callback = callback;
        self
    }

    /// Set a callback to be called whenever a log message is generated
    ///
    /// Multiple calls to this function will overwrite the previous callback.
    ///
    /// # Example:
    /// ```rust
    /// use gc_plugin_abi::testing::{GCGatewayMockBuilder, GCGatewayMock};
    /// use gc_plugin_abi::GCDatapointValue;
    /// use std::sync::Arc;
    /// use std::sync::atomic::{AtomicBool, Ordering};
    ///
    /// let assert_called = Arc::new(AtomicBool::new(false));
    ///
    /// let mut mock_builder = GCGatewayMockBuilder::new();
    /// mock_builder.set_log_callback(Box::new(move |level, line| {
    ///     assert_called.store(true, Ordering::SeqCst);
    /// }));
    /// ```
    pub fn set_log_callback(&mut self, callback: Box<LogIntercept>) -> &mut Self {
        self.core_ctx.log_callback = callback;
        self
    }

    /// Set a callback to be called whenever the last value of a datapoint is requested
    ///
    /// Multiple calls to this function will overwrite the previous callback.
    ///
    /// # Example:
    /// ```rust
    /// use gc_plugin_abi::testing::{GCGatewayMockBuilder, GCGatewayMock};
    /// use gc_plugin_abi::{GCDatapointValue, GCDatapointID, GCDatapointValueQuality};
    /// use std::sync::Arc;
    /// use std::sync::atomic::{AtomicBool, Ordering};
    ///
    /// let assert_called = Arc::new(AtomicBool::new(false));
    ///
    /// let mut mock_builder = GCGatewayMockBuilder::new();
    /// mock_builder.set_get_last_datapoint_value_callback(Box::new(move |id: GCDatapointID| {
    ///     assert_called.store(true, Ordering::SeqCst);
    ///     return GCDatapointValue::new_boolean(id, 0, false, GCDatapointValueQuality::new_good());
    /// }));
    /// ```
    pub fn set_get_last_datapoint_value_callback(&mut self, callback: Box<GetLastDatapointValueIntercept>) -> &mut Self {
        self.core_ctx.get_last_datapoint_callback = callback;
        self
    }

    /// Set a callback to be called whenever an ethernet interface is requested by name
    ///
    /// Return `Some(GCEthernetInterface)` to simulate a found interface, or `None` to simulate not found.
    /// Multiple calls to this function will overwrite the previous callback.
    pub fn set_get_ethernet_interface_callback(&mut self, callback: Box<GetEthernetInterfaceIntercept>) -> &mut Self {
        self.core_ctx.get_ethernet_interface_callback = callback;
        self
    }

    /// Set a callback to be called whenever a serial interface is requested by name
    ///
    /// Return `Some(GCSerialInterface)` to simulate a found interface, or `None` to simulate not found.
    /// Multiple calls to this function will overwrite the previous callback.
    pub fn set_get_serial_interface_callback(&mut self, callback: Box<GetSerialInterfaceIntercept>) -> &mut Self {
        self.core_ctx.get_serial_interface_callback = callback;
        self
    }

    /// Start the plugin
    pub fn start<'a, T: GCPluginInstance<'a>>(self) -> Result<GCGatewayMock<'a, T>, GCMockingError> {
        let mut mock = GCGatewayMock::new(self)?;
        mock.start()?;
        Ok(mock)
    }

    /// Start the plugin from a different startup function
    /// This can be usefull when using dependency injection within the plugin init function for testing
    ///
    /// # Example:
    /// ```rust
    /// # use gc_plugin_abi::testing::{GCGatewayMockBuilder, GCGatewayMock};
    /// # use gc_plugin_abi::{GCDatapoint, GCPluginInterface, GCPluginInstance, GCPluginInfo, GCBorrowedDatapointValue};
    /// # use std::ffi::CString;
    /// # use std::sync::Arc;
    /// # use std::sync::atomic::{AtomicBool, Ordering};
    /// # use gc_plugin_abi::gc_plugin;
    /// #
    /// #[gc_plugin]
    /// pub struct ExamplePlugin<'a> {
    ///     plugin_interface: &'a GCPluginInterface,
    /// }
    /// # impl<'a> GCPluginInstance<'a> for ExamplePlugin<'a> {
    /// #
    /// #     // Entrypoint which contains the interface to communicate with the core gateway
    /// #     fn init(plugin_interface: &'a GCPluginInterface) -> Box<Self> {
    /// #         Box::new(ExamplePlugin { plugin_interface })
    /// #     }
    /// #
    /// #     fn get_plugin_info() -> GCPluginInfo {
    /// #         GCPluginInfo::new("ExamplePlugin", "0.1.0", "0", 0)
    /// #     }
    /// #
    /// #     // Receive datapoint by subscription
    /// #     fn receive_datapoint(&self, _data_value: GCBorrowedDatapointValue) -> bool {
    /// #         // Handle incoming datapoint
    /// #         true
    /// #     }
    /// # }
    ///
    /// pub fn init_example_plugin<'a>(plugin_interface: &'a GCPluginInterface) -> Box<ExamplePlugin<'a>> {
    ///    Box::new(ExamplePlugin { plugin_interface })
    /// }
    ///
    /// let mut mock_builder = GCGatewayMockBuilder::new();
    /// mock_builder.start_from(init_example_plugin).unwrap();
    /// ```
    pub fn start_from<'a, T, F>(self, f: F) -> Result<GCGatewayMock<'a, T>, GCMockingError>
    where
        T: GCPluginInstance<'a>,
        F: FnOnce(&'a crate::GCPluginInterface) -> Box<T> + 'a,
    {
        let mut mock = GCGatewayMock::new(self)?;
        mock.start_from(f)?;
        Ok(mock)
    }

    /// Build the GCGatewayMock
    pub fn build<'a, T: GCPluginInstance<'a>>(self) -> Result<GCGatewayMock<'a, T>, GCMockingError> {
        GCGatewayMock::new(self)
    }
}