From 99e3ba41f45bce71a0682710f567c11aa6b2b157 Mon Sep 17 00:00:00 2001 From: William Lyles <26171886+wilyle@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:31:51 -0700 Subject: [PATCH 1/5] mock cloud connector --- Cargo.lock | 33 ++++ Cargo.toml | 1 + mocks/mock_cloud_connector/Cargo.toml | 25 +++ mocks/mock_cloud_connector/README.md | 25 +++ mocks/mock_cloud_connector/build.rs | 11 ++ .../mock_cloud_connector_config.default.json | 3 + mocks/mock_cloud_connector/src/config.rs | 12 ++ mocks/mock_cloud_connector/src/main.rs | 68 ++++++++ .../src/mock_cloud_connector_impl.rs | 31 ++++ proto/cloud_connector/Cargo.toml | 7 +- proto/cloud_connector/src/lib.rs | 164 ++++++++++++++++++ 11 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 mocks/mock_cloud_connector/Cargo.toml create mode 100644 mocks/mock_cloud_connector/README.md create mode 100644 mocks/mock_cloud_connector/build.rs create mode 100644 mocks/mock_cloud_connector/res/mock_cloud_connector_config.default.json create mode 100644 mocks/mock_cloud_connector/src/config.rs create mode 100644 mocks/mock_cloud_connector/src/main.rs create mode 100644 mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs diff --git a/Cargo.lock b/Cargo.lock index 3994ee4..c414e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,8 +245,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "cloud-connector-proto" version = "0.1.0" dependencies = [ + "freyja-build-common", "prost", "prost-types", + "serde", + "serde_json", "time", "tonic", "tonic-build", @@ -393,6 +396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1116,6 +1120,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mock-cloud-connector" +version = "0.1.0" +dependencies = [ + "async-trait", + "cloud-connector-proto", + "env_logger", + "freyja-build-common", + "freyja-common", + "log", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tonic", +] + [[package]] name = "mock-digital-twin" version = "0.1.0" @@ -1878,10 +1899,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -1890,6 +1913,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index bcb3d09..d82e02f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "build_common", "common", "freyja", + "mocks/mock_cloud_connector", "mocks/mock_digital_twin", "mocks/mock_mapping_service", "proc_macros", diff --git a/mocks/mock_cloud_connector/Cargo.toml b/mocks/mock_cloud_connector/Cargo.toml new file mode 100644 index 0000000..4dc0695 --- /dev/null +++ b/mocks/mock_cloud_connector/Cargo.toml @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +[package] +name = "mock-cloud-connector" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +async-trait = { workspace = true } +cloud-connector-proto = { workspace = true } +env_logger = { workspace = true } +freyja-build-common = { workspace = true } +freyja-common = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tonic = { workspace = true } + +[build-dependencies] +freyja-build-common = { workspace = true } \ No newline at end of file diff --git a/mocks/mock_cloud_connector/README.md b/mocks/mock_cloud_connector/README.md new file mode 100644 index 0000000..359a67b --- /dev/null +++ b/mocks/mock_cloud_connector/README.md @@ -0,0 +1,25 @@ +# Mock Cloud Connector + +The Mock Cloud Connector mocks the behavior of a Cloud Connector. This enables functionality similar to the [In-Memory Mock Cloud Adapter](../../adapters/cloud/in_memory_mock_cloud_adapter/README.md). + +The Mock Cloud Connector implements the [Cloud Connector API](../../interfaces/cloud_connector/v1/cloud_connector.proto), making it compatible with the [gRPC Cloud Adapter](../../adapters/cloud/grpc_cloud_adapter/README.md). + +## Configuration + +This mock supports the following configuration: + +- `server_authority`: The authority that will be used for hosting the mock cloud connector service. + +This mock supports [config overrides](../../docs/tutorials/config-overrides.md). The override filename is `mock_cloud_connector_config.json`, and the default config is located at `res/mock_cloud_connector_config.default.json`. + +## Behavior + +This cloud connector prints the requests that it receives to the console, enabling users to verify that data is flowing from Freyja to the cloud connector. As a mock, this cloud connector does not have any cloud connectivity. + +## Build and Run + +To build and run the Mock Cloud Connector, run the following command: + +```shell +cargo run -p mock-cloud-connector +``` diff --git a/mocks/mock_cloud_connector/build.rs b/mocks/mock_cloud_connector/build.rs new file mode 100644 index 0000000..1c2b617 --- /dev/null +++ b/mocks/mock_cloud_connector/build.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use freyja_build_common::copy_config; + +const CONFIG_FILE_STEM: &str = "mock_cloud_connector_config"; + +fn main() { + copy_config(CONFIG_FILE_STEM); +} diff --git a/mocks/mock_cloud_connector/res/mock_cloud_connector_config.default.json b/mocks/mock_cloud_connector/res/mock_cloud_connector_config.default.json new file mode 100644 index 0000000..f16ca97 --- /dev/null +++ b/mocks/mock_cloud_connector/res/mock_cloud_connector_config.default.json @@ -0,0 +1,3 @@ +{ + "server_authority": "0.0.0.0:5176" +} diff --git a/mocks/mock_cloud_connector/src/config.rs b/mocks/mock_cloud_connector/src/config.rs new file mode 100644 index 0000000..50e5a97 --- /dev/null +++ b/mocks/mock_cloud_connector/src/config.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; + +/// Config for the mock cloud connector +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + /// The server authority for hosting a gRPC server + pub server_authority: String, +} diff --git a/mocks/mock_cloud_connector/src/main.rs b/mocks/mock_cloud_connector/src/main.rs new file mode 100644 index 0000000..ff1f267 --- /dev/null +++ b/mocks/mock_cloud_connector/src/main.rs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +mod config; +mod mock_cloud_connector_impl; + +use std::env; + +use cloud_connector_proto::v1::cloud_connector_server::CloudConnectorServer; +use env_logger::Target; +use log::{info, LevelFilter}; +use tonic::transport::Server; + +use crate::{ + config::Config, + mock_cloud_connector_impl::MockCloudConnectorImpl, +}; +use freyja_build_common::config_file_stem; +use freyja_common::{ + cmd_utils::{get_log_level, parse_args}, + config_utils, out_dir, +}; + +/// Starts the following threads and tasks: +/// - A thread which listens for input from the command window +/// - A task which handles async get responses +/// - A task which handles publishing to subscribers +/// - A gRPC server to accept incoming requests +#[tokio::main] +async fn main() { + let args = parse_args(env::args()).expect("Failed to parse args"); + + // Setup logging + let log_level = get_log_level(&args, LevelFilter::Info).expect("Could not parse log level"); + env_logger::Builder::new() + .filter(None, log_level) + .target(Target::Stdout) + .init(); + + let config: Config = config_utils::read_from_files( + config_file_stem!(), + config_utils::JSON_EXT, + out_dir!(), + |e| log::error!("{}", e), + |e| log::error!("{}", e), + ) + .unwrap(); + + // Server setup + info!( + "Mock Cloud Connector Server starting at {}", + config.server_authority + ); + + let addr = config + .server_authority + .parse() + .expect("Unable to parse server address"); + + let mock_cloud_connector = MockCloudConnectorImpl { }; + + Server::builder() + .add_service(CloudConnectorServer::new(mock_cloud_connector)) + .serve(addr) + .await + .unwrap(); +} diff --git a/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs b/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs new file mode 100644 index 0000000..eeab3e3 --- /dev/null +++ b/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// SPDX-License-Identifier: MIT + +use async_trait::async_trait; +use cloud_connector_proto::v1::{cloud_connector_server::CloudConnector, UpdateDigitalTwinRequest, UpdateDigitalTwinResponse}; +use log::info; +use tonic::{Request, Response, Status}; + +/// Implements an In-Vehicle Digital Twin Server +pub struct MockCloudConnectorImpl { +} + +#[async_trait] +impl CloudConnector for MockCloudConnectorImpl { + /// Update the digital twin + /// + /// # Arguments + /// - `request`: the update request+ + async fn update_digital_twin ( + &self, + request: Request, + ) -> Result, Status> { + let message_json = serde_json::to_string_pretty(&request.into_inner()) + .map_err(|_| Status::invalid_argument("Could not parse request"))?; + + info!("Mock Cloud Connector received a message!\n{message_json}"); + + Ok(Response::new(UpdateDigitalTwinResponse {})) + } +} diff --git a/proto/cloud_connector/Cargo.toml b/proto/cloud_connector/Cargo.toml index d456ad8..c30fea0 100644 --- a/proto/cloud_connector/Cargo.toml +++ b/proto/cloud_connector/Cargo.toml @@ -11,8 +11,11 @@ license = "MIT" [dependencies] prost = { workspace = true } prost-types = { workspace = true } -time = { workspace = true } +serde = { workspace = true } +time = { workspace = true, features = ["serde-human-readable"] } tonic = { workspace = true } +serde_json = { workspace = true } [build-dependencies] -tonic-build = { workspace = true } \ No newline at end of file +tonic-build = { workspace = true } +freyja-build-common = { workspace = true } \ No newline at end of file diff --git a/proto/cloud_connector/src/lib.rs b/proto/cloud_connector/src/lib.rs index 2f95e77..b7bffe6 100644 --- a/proto/cloud_connector/src/lib.rs +++ b/proto/cloud_connector/src/lib.rs @@ -9,10 +9,59 @@ pub mod v1 { use std::collections::HashMap; use prost_types::{value::Kind, Timestamp, Value}; + use serde::ser::{Serialize, Serializer, SerializeStruct}; use time::OffsetDateTime; tonic::include_proto!("cloud_connector"); + // Because the members of UpdateDigitalTwinRequest do not implement serialize + // and are not owned by this project, implementing Serialize has to be done manually. + impl Serialize for UpdateDigitalTwinRequest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer + { + let serialize_none = |state: &mut ::SerializeStruct, key: &'static str| { + state.serialize_field(key, &None::<()>) + }; + + let mut state = serializer.serialize_struct("UpdateDigitalTwinRequest", 3)?; + + // Serialize value + const VALUE_FIELD: &str = "value"; + match self.value.as_ref() { + None => serialize_none(&mut state, VALUE_FIELD)?, + Some(v) => match v.kind.as_ref() { + None => serialize_none(&mut state, VALUE_FIELD)?, + Some(k) => match k { + Kind::NullValue(_) => serialize_none(&mut state, VALUE_FIELD)?, + Kind::NumberValue(n) => state.serialize_field(VALUE_FIELD, &n)?, + Kind::StringValue(s) => state.serialize_field(VALUE_FIELD, &s)?, + Kind::BoolValue(b) => state.serialize_field(VALUE_FIELD, &b)?, + _ => serialize_none(&mut state, VALUE_FIELD)?, + } + } + } + + // Serialize timestamp + // Note that the nanos are discarded for simplicity + const TIMESTAMP_FIELD: &str = "timestamp"; + match self.timestamp { + None => serialize_none(&mut state, TIMESTAMP_FIELD)?, + Some(Timestamp { seconds, .. }) => { + let timestamp = OffsetDateTime::from_unix_timestamp(seconds).unwrap(); + state.serialize_field(TIMESTAMP_FIELD, ×tamp)?; + }, + } + + // Serialize metadata + state.serialize_field("metadata", &self.metadata)?; + + // End serialization + state.end() + } + } + /// Helper class to deal with the verbose contracts that tonic generates #[derive(Default)] pub struct UpdateDigitalTwinRequestBuilder { @@ -123,3 +172,118 @@ pub mod v1 { } } } + +#[cfg(test)] +mod cloud_connector_tests { + use serde_json::{json, Map, Value}; + + use self::v1::UpdateDigitalTwinRequestBuilder; + + use super::*; + + use crate::v1::UpdateDigitalTwinRequest; + + fn validate_request_json(request: UpdateDigitalTwinRequest, key: &str, value: Value) { + let serialize_result = serde_json::to_string(&request); + assert!(serialize_result.is_ok()); + let serialized = serialize_result.unwrap(); + + let deserialize_result = serde_json::from_str(&serialized); + assert!(deserialize_result.is_ok()); + let json: Value = deserialize_result.unwrap(); + + assert_eq!(json[key], value); + } + + #[test] + fn test_serialize_no_value() { + let request = UpdateDigitalTwinRequestBuilder::new() + .build(); + + validate_request_json(request, "value", Value::Null); + } + + #[test] + fn test_serialize_null() { + let request = UpdateDigitalTwinRequestBuilder::new() + .null_value() + .build(); + + validate_request_json(request, "value", Value::Null); + } + + #[test] + fn test_serialize_bool() { + let b = true; + let request = UpdateDigitalTwinRequestBuilder::new() + .bool_value(b) + .build(); + + validate_request_json(request, "value", Value::Bool(b)); + } + + #[test] + fn test_serialize_number() { + let n = 42.0; + let request = UpdateDigitalTwinRequestBuilder::new() + .number_value(n) + .build(); + + validate_request_json(request, "value", json!(n)); + } + + #[test] + fn test_serialize_string() { + let s = "foo"; + let request = UpdateDigitalTwinRequestBuilder::new() + .string_value(s.into()) + .build(); + + validate_request_json(request, "value", Value::String(s.into())); + } + + #[test] + fn test_serialize_no_timestamp() { + let request = UpdateDigitalTwinRequestBuilder::new() + .build(); + + validate_request_json(request, "timestamp", Value::Null); + } + + #[test] + fn test_serialize_timestamp() { + let request = UpdateDigitalTwinRequestBuilder::new() + .timestamp_now() + .build(); + + let serialize_result = serde_json::to_string(&request); + assert!(serialize_result.is_ok()); + let serialized = serialize_result.unwrap(); + + let deserialize_result = serde_json::from_str(&serialized); + assert!(deserialize_result.is_ok()); + let json: Value = deserialize_result.unwrap(); + + assert_ne!(json["timestamp"], Value::Null); + } + + #[test] + fn test_serialize_no_metadata() { + let request = UpdateDigitalTwinRequestBuilder::new() + .build(); + + validate_request_json(request, "metadata", Value::Object(Map::new())); + } + + #[test] + fn test_serialize_metadata() { + let metadata = ("foo", "bar"); + let request = UpdateDigitalTwinRequestBuilder::new() + .add_metadata(metadata.0.into(), metadata.1.into()) + .build(); + + let mut map = Map::new(); + map.insert(metadata.0.into(), Value::String(metadata.1.into())); + validate_request_json(request, "metadata", Value::Object(map)); + } +} \ No newline at end of file From 859dfdc6f432a3a48e83c42dc120d162e80bc693 Mon Sep 17 00:00:00 2001 From: William Lyles <26171886+wilyle@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:56:36 -0700 Subject: [PATCH 2/5] fixes after testing --- Cargo.lock | 1 + .../grpc_cloud_adapter/src/grpc_cloud_adapter.rs | 12 +++--------- .../src/in_memory_mock_cloud_adapter.rs | 2 +- ..._memory_mock_data_adapter_config.default.json | 6 +++--- .../src/in_memory_mock_data_adapter.rs | 4 ++-- common/Cargo.toml | 1 + common/src/cloud_adapter.rs | 3 ++- freyja/src/emitter.rs | 2 +- proto/cloud_connector/src/lib.rs | 16 +++++++++++++--- 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c414e70..01900d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,6 +574,7 @@ dependencies = [ "serde", "serde_json", "strum_macros", + "time", "tokio", ] diff --git a/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs b/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs index 97e47a1..6760a20 100644 --- a/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs +++ b/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs @@ -3,17 +3,14 @@ // SPDX-License-Identifier: MIT use std::time::Duration; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use async_trait::async_trait; use log::debug; use tokio::sync::Mutex; use tonic::transport::Channel; -use cloud_connector_proto::{ - prost_types::Timestamp, - v1::{cloud_connector_client::CloudConnectorClient, UpdateDigitalTwinRequestBuilder}, -}; +use cloud_connector_proto::v1::{cloud_connector_client::CloudConnectorClient, UpdateDigitalTwinRequestBuilder}; use freyja_build_common::config_file_stem; use freyja_common::{ cloud_adapter::{CloudAdapter, CloudAdapterError, CloudMessageRequest, CloudMessageResponse}, @@ -81,12 +78,9 @@ impl CloudAdapter for GRPCCloudAdapter { ) -> Result { debug!("Received a request to send to the cloud"); - let timestamp = Timestamp::from_str(cloud_message.signal_timestamp.as_str()) - .map_err(CloudAdapterError::deserialize)?; - let request = UpdateDigitalTwinRequestBuilder::new() .string_value(cloud_message.signal_value) - .timestamp(timestamp) + .timestamp_offset(cloud_message.signal_timestamp) .metadata(cloud_message.metadata) .build(); diff --git a/adapters/cloud/in_memory_mock_cloud_adapter/src/in_memory_mock_cloud_adapter.rs b/adapters/cloud/in_memory_mock_cloud_adapter/src/in_memory_mock_cloud_adapter.rs index 945bdba..463220d 100644 --- a/adapters/cloud/in_memory_mock_cloud_adapter/src/in_memory_mock_cloud_adapter.rs +++ b/adapters/cloud/in_memory_mock_cloud_adapter/src/in_memory_mock_cloud_adapter.rs @@ -75,7 +75,7 @@ mod in_memory_mock_cloud_adapter_tests { let cloud_message = CloudMessageRequest { metadata: HashMap::new(), signal_value: String::from("72"), - signal_timestamp: OffsetDateTime::now_utc().to_string(), + signal_timestamp: OffsetDateTime::now_utc(), }; assert!(cloud_adapter.send_to_cloud(cloud_message).await.is_ok()); diff --git a/adapters/data/in_memory_mock_data_adapter/res/in_memory_mock_data_adapter_config.default.json b/adapters/data/in_memory_mock_data_adapter/res/in_memory_mock_data_adapter_config.default.json index 3a9891a..971add6 100644 --- a/adapters/data/in_memory_mock_data_adapter/res/in_memory_mock_data_adapter_config.default.json +++ b/adapters/data/in_memory_mock_data_adapter/res/in_memory_mock_data_adapter_config.default.json @@ -2,19 +2,19 @@ "signal_update_frequency_ms": 1000, "entities": [ { - "entity_id": "dtmi:sdv:Vehicle:Cabin:HVAC:AmbientAirTemperature;1", + "entity_id": "dtmi:sdv:HVAC:AmbientAirTemperature;1", "values": { "Static": 42.0 } }, { - "entity_id": "dtmi:sdv:Vehicle:Cabin:HVAC:IsAirConditioningActive;1", + "entity_id": "dtmi:sdv:HVAC:IsAirConditioningActive;1", "values": { "Static": 0.0 } }, { - "entity_id": "dtmi:sdv:Vehicle:OBD:HybridBatteryRemaining;1", + "entity_id": "dtmi:sdv:OBD:HybridBatteryRemaining;1", "values": { "Stepwise": { "start": 77.7, diff --git a/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs b/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs index df78002..676a460 100644 --- a/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs +++ b/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs @@ -138,9 +138,9 @@ impl DataAdapter for InMemoryMockDataAdapter { { let data = data.lock().await; for entity_id in entities_with_subscribe { - if Self::generate_signal_value(&entity_id, signals.clone(), &data).is_err() + if let Err(e) = Self::generate_signal_value(&entity_id, signals.clone(), &data) { - warn!("Attempt to set value for non-existent entity {entity_id}"); + warn!("Attempt to set value for non-existent entity {entity_id}: {e}"); } } } diff --git a/common/Cargo.toml b/common/Cargo.toml index 237b470..32c6112 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -17,4 +17,5 @@ proc-macros = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } strum_macros = { workspace = true } +time = { workspace = true } tokio = { workspace = true } \ No newline at end of file diff --git a/common/src/cloud_adapter.rs b/common/src/cloud_adapter.rs index 8d84353..41415b2 100644 --- a/common/src/cloud_adapter.rs +++ b/common/src/cloud_adapter.rs @@ -6,6 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; use tokio::sync::Mutex; use crate::service_discovery_adapter_selector::ServiceDiscoveryAdapterSelector; @@ -42,7 +43,7 @@ pub struct CloudMessageRequest { pub signal_value: String, // Timestamp of when the signal was emitted - pub signal_timestamp: String, + pub signal_timestamp: OffsetDateTime, } /// Represents a response to a message sent to the cloud digital twin diff --git a/freyja/src/emitter.rs b/freyja/src/emitter.rs index 62e3b97..cd2bb2e 100644 --- a/freyja/src/emitter.rs +++ b/freyja/src/emitter.rs @@ -173,7 +173,7 @@ impl let cloud_message = CloudMessageRequest { metadata: signal.target.metadata.clone(), signal_value: converted, - signal_timestamp: OffsetDateTime::now_utc().to_string(), + signal_timestamp: OffsetDateTime::now_utc(), }; let response = self diff --git a/proto/cloud_connector/src/lib.rs b/proto/cloud_connector/src/lib.rs index b7bffe6..21a6a89 100644 --- a/proto/cloud_connector/src/lib.rs +++ b/proto/cloud_connector/src/lib.rs @@ -134,9 +134,11 @@ pub mod v1 { self } - /// Set the request timestamp to the current time - pub fn timestamp_now(mut self) -> Self { - let timestamp = OffsetDateTime::now_utc(); + /// Set the request timestamp to an `OffsetDateTime` + /// + /// # Arguments + /// - `timestamp`: the timestamp to set + pub fn timestamp_offset(mut self, timestamp: OffsetDateTime) -> Self { self.request.timestamp = Some( Timestamp::date_time( timestamp.year().into(), @@ -152,6 +154,14 @@ pub mod v1 { self } + /// Set the request timestamp to the current time + pub fn timestamp_now(mut self) -> Self { + let timestamp = OffsetDateTime::now_utc(); + self = self.timestamp_offset(timestamp); + + self + } + /// Set the request metadata. This overwrites any previously set metadata /// /// # Arguments From 43dcff9cad28b26b5a797e98b987e433d70af690 Mon Sep 17 00:00:00 2001 From: William Lyles <26171886+wilyle@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:58:28 -0700 Subject: [PATCH 3/5] cleanup --- Cargo.lock | 1 - mocks/mock_cloud_connector/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01900d0..1a54e8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,7 +1134,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-stream", "tonic", ] diff --git a/mocks/mock_cloud_connector/Cargo.toml b/mocks/mock_cloud_connector/Cargo.toml index 4dc0695..e3515ce 100644 --- a/mocks/mock_cloud_connector/Cargo.toml +++ b/mocks/mock_cloud_connector/Cargo.toml @@ -18,7 +18,6 @@ log = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } -tokio-stream = { workspace = true } tonic = { workspace = true } [build-dependencies] From 46f7b26529f48dade123e767457e7a3bcdcbdf05 Mon Sep 17 00:00:00 2001 From: William Lyles <26171886+wilyle@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:31:46 -0700 Subject: [PATCH 4/5] fmt --- .../src/grpc_cloud_adapter.rs | 6 +- .../src/in_memory_mock_data_adapter.rs | 3 +- mocks/mock_cloud_connector/src/main.rs | 7 +- .../src/mock_cloud_connector_impl.rs | 13 ++- proto/cloud_connector/Cargo.toml | 2 +- proto/cloud_connector/src/lib.rs | 108 +++++++++--------- 6 files changed, 71 insertions(+), 68 deletions(-) diff --git a/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs b/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs index 6760a20..6794382 100644 --- a/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs +++ b/adapters/cloud/grpc_cloud_adapter/src/grpc_cloud_adapter.rs @@ -2,15 +2,17 @@ // Licensed under the MIT license. // SPDX-License-Identifier: MIT -use std::time::Duration; use std::sync::Arc; +use std::time::Duration; use async_trait::async_trait; use log::debug; use tokio::sync::Mutex; use tonic::transport::Channel; -use cloud_connector_proto::v1::{cloud_connector_client::CloudConnectorClient, UpdateDigitalTwinRequestBuilder}; +use cloud_connector_proto::v1::{ + cloud_connector_client::CloudConnectorClient, UpdateDigitalTwinRequestBuilder, +}; use freyja_build_common::config_file_stem; use freyja_common::{ cloud_adapter::{CloudAdapter, CloudAdapterError, CloudMessageRequest, CloudMessageResponse}, diff --git a/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs b/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs index 676a460..3fca99f 100644 --- a/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs +++ b/adapters/data/in_memory_mock_data_adapter/src/in_memory_mock_data_adapter.rs @@ -138,7 +138,8 @@ impl DataAdapter for InMemoryMockDataAdapter { { let data = data.lock().await; for entity_id in entities_with_subscribe { - if let Err(e) = Self::generate_signal_value(&entity_id, signals.clone(), &data) + if let Err(e) = + Self::generate_signal_value(&entity_id, signals.clone(), &data) { warn!("Attempt to set value for non-existent entity {entity_id}: {e}"); } diff --git a/mocks/mock_cloud_connector/src/main.rs b/mocks/mock_cloud_connector/src/main.rs index ff1f267..7006016 100644 --- a/mocks/mock_cloud_connector/src/main.rs +++ b/mocks/mock_cloud_connector/src/main.rs @@ -12,10 +12,7 @@ use env_logger::Target; use log::{info, LevelFilter}; use tonic::transport::Server; -use crate::{ - config::Config, - mock_cloud_connector_impl::MockCloudConnectorImpl, -}; +use crate::{config::Config, mock_cloud_connector_impl::MockCloudConnectorImpl}; use freyja_build_common::config_file_stem; use freyja_common::{ cmd_utils::{get_log_level, parse_args}, @@ -58,7 +55,7 @@ async fn main() { .parse() .expect("Unable to parse server address"); - let mock_cloud_connector = MockCloudConnectorImpl { }; + let mock_cloud_connector = MockCloudConnectorImpl {}; Server::builder() .add_service(CloudConnectorServer::new(mock_cloud_connector)) diff --git a/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs b/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs index eeab3e3..0d3edba 100644 --- a/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs +++ b/mocks/mock_cloud_connector/src/mock_cloud_connector_impl.rs @@ -3,21 +3,22 @@ // SPDX-License-Identifier: MIT use async_trait::async_trait; -use cloud_connector_proto::v1::{cloud_connector_server::CloudConnector, UpdateDigitalTwinRequest, UpdateDigitalTwinResponse}; +use cloud_connector_proto::v1::{ + cloud_connector_server::CloudConnector, UpdateDigitalTwinRequest, UpdateDigitalTwinResponse, +}; use log::info; use tonic::{Request, Response, Status}; -/// Implements an In-Vehicle Digital Twin Server -pub struct MockCloudConnectorImpl { -} +/// Implements a Mock Cloud Connector +pub struct MockCloudConnectorImpl {} #[async_trait] impl CloudConnector for MockCloudConnectorImpl { /// Update the digital twin - /// + /// /// # Arguments /// - `request`: the update request+ - async fn update_digital_twin ( + async fn update_digital_twin( &self, request: Request, ) -> Result, Status> { diff --git a/proto/cloud_connector/Cargo.toml b/proto/cloud_connector/Cargo.toml index c30fea0..0c6a433 100644 --- a/proto/cloud_connector/Cargo.toml +++ b/proto/cloud_connector/Cargo.toml @@ -12,9 +12,9 @@ license = "MIT" prost = { workspace = true } prost-types = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } time = { workspace = true, features = ["serde-human-readable"] } tonic = { workspace = true } -serde_json = { workspace = true } [build-dependencies] tonic-build = { workspace = true } diff --git a/proto/cloud_connector/src/lib.rs b/proto/cloud_connector/src/lib.rs index 21a6a89..b63eaee 100644 --- a/proto/cloud_connector/src/lib.rs +++ b/proto/cloud_connector/src/lib.rs @@ -9,7 +9,7 @@ pub mod v1 { use std::collections::HashMap; use prost_types::{value::Kind, Timestamp, Value}; - use serde::ser::{Serialize, Serializer, SerializeStruct}; + use serde::ser::{Serialize, SerializeStruct, Serializer}; use time::OffsetDateTime; tonic::include_proto!("cloud_connector"); @@ -19,9 +19,10 @@ pub mod v1 { impl Serialize for UpdateDigitalTwinRequest { fn serialize(&self, serializer: S) -> Result where - S: Serializer + S: Serializer, { - let serialize_none = |state: &mut ::SerializeStruct, key: &'static str| { + let serialize_null_field = |state: &mut ::SerializeStruct, + key: &'static str| { state.serialize_field(key, &None::<()>) }; @@ -30,28 +31,28 @@ pub mod v1 { // Serialize value const VALUE_FIELD: &str = "value"; match self.value.as_ref() { - None => serialize_none(&mut state, VALUE_FIELD)?, + None => serialize_null_field(&mut state, VALUE_FIELD)?, Some(v) => match v.kind.as_ref() { - None => serialize_none(&mut state, VALUE_FIELD)?, + None => serialize_null_field(&mut state, VALUE_FIELD)?, Some(k) => match k { - Kind::NullValue(_) => serialize_none(&mut state, VALUE_FIELD)?, + Kind::NullValue(_) => serialize_null_field(&mut state, VALUE_FIELD)?, Kind::NumberValue(n) => state.serialize_field(VALUE_FIELD, &n)?, Kind::StringValue(s) => state.serialize_field(VALUE_FIELD, &s)?, Kind::BoolValue(b) => state.serialize_field(VALUE_FIELD, &b)?, - _ => serialize_none(&mut state, VALUE_FIELD)?, - } - } + _ => serialize_null_field(&mut state, VALUE_FIELD)?, + }, + }, } // Serialize timestamp // Note that the nanos are discarded for simplicity const TIMESTAMP_FIELD: &str = "timestamp"; match self.timestamp { - None => serialize_none(&mut state, TIMESTAMP_FIELD)?, + None => serialize_null_field(&mut state, TIMESTAMP_FIELD)?, Some(Timestamp { seconds, .. }) => { let timestamp = OffsetDateTime::from_unix_timestamp(seconds).unwrap(); state.serialize_field(TIMESTAMP_FIELD, ×tamp)?; - }, + } } // Serialize metadata @@ -135,7 +136,7 @@ pub mod v1 { } /// Set the request timestamp to an `OffsetDateTime` - /// + /// /// # Arguments /// - `timestamp`: the timestamp to set pub fn timestamp_offset(mut self, timestamp: OffsetDateTime) -> Self { @@ -193,43 +194,42 @@ mod cloud_connector_tests { use crate::v1::UpdateDigitalTwinRequest; - fn validate_request_json(request: UpdateDigitalTwinRequest, key: &str, value: Value) { + fn serialize_round_trip(request: &UpdateDigitalTwinRequest) -> Value { let serialize_result = serde_json::to_string(&request); assert!(serialize_result.is_ok()); let serialized = serialize_result.unwrap(); - let deserialize_result = serde_json::from_str(&serialized); + let deserialize_result = serde_json::from_str::(&serialized); assert!(deserialize_result.is_ok()); - let json: Value = deserialize_result.unwrap(); - - assert_eq!(json[key], value); + deserialize_result.unwrap() } #[test] fn test_serialize_no_value() { - let request = UpdateDigitalTwinRequestBuilder::new() - .build(); - - validate_request_json(request, "value", Value::Null); + let request = UpdateDigitalTwinRequestBuilder::new().build(); + + let result = serialize_round_trip(&request); + + assert_eq!(result["value"], Value::Null); } #[test] fn test_serialize_null() { - let request = UpdateDigitalTwinRequestBuilder::new() - .null_value() - .build(); - - validate_request_json(request, "value", Value::Null); + let request = UpdateDigitalTwinRequestBuilder::new().null_value().build(); + + let result = serialize_round_trip(&request); + + assert_eq!(result["value"], Value::Null); } #[test] fn test_serialize_bool() { let b = true; - let request = UpdateDigitalTwinRequestBuilder::new() - .bool_value(b) - .build(); - - validate_request_json(request, "value", Value::Bool(b)); + let request = UpdateDigitalTwinRequestBuilder::new().bool_value(b).build(); + + let result = serialize_round_trip(&request); + + assert_eq!(result["value"], Value::Bool(b)); } #[test] @@ -238,8 +238,10 @@ mod cloud_connector_tests { let request = UpdateDigitalTwinRequestBuilder::new() .number_value(n) .build(); - - validate_request_json(request, "value", json!(n)); + + let result = serialize_round_trip(&request); + + assert_eq!(result["value"], json!(n)); } #[test] @@ -248,16 +250,19 @@ mod cloud_connector_tests { let request = UpdateDigitalTwinRequestBuilder::new() .string_value(s.into()) .build(); - - validate_request_json(request, "value", Value::String(s.into())); + + let result = serialize_round_trip(&request); + + assert_eq!(result["value"], Value::String(s.into())); } #[test] fn test_serialize_no_timestamp() { - let request = UpdateDigitalTwinRequestBuilder::new() - .build(); - - validate_request_json(request, "timestamp", Value::Null); + let request = UpdateDigitalTwinRequestBuilder::new().build(); + + let result = serialize_round_trip(&request); + + assert_eq!(result["timestamp"], Value::Null); } #[test] @@ -266,23 +271,18 @@ mod cloud_connector_tests { .timestamp_now() .build(); - let serialize_result = serde_json::to_string(&request); - assert!(serialize_result.is_ok()); - let serialized = serialize_result.unwrap(); - - let deserialize_result = serde_json::from_str(&serialized); - assert!(deserialize_result.is_ok()); - let json: Value = deserialize_result.unwrap(); + let result = serialize_round_trip(&request); - assert_ne!(json["timestamp"], Value::Null); + assert_ne!(result["timestamp"], Value::Null); } #[test] fn test_serialize_no_metadata() { - let request = UpdateDigitalTwinRequestBuilder::new() - .build(); - - validate_request_json(request, "metadata", Value::Object(Map::new())); + let request = UpdateDigitalTwinRequestBuilder::new().build(); + + let result = serialize_round_trip(&request); + + assert_ne!(result["metadata"], Value::Null); } #[test] @@ -292,8 +292,10 @@ mod cloud_connector_tests { .add_metadata(metadata.0.into(), metadata.1.into()) .build(); + let result = serialize_round_trip(&request); + let mut map = Map::new(); map.insert(metadata.0.into(), Value::String(metadata.1.into())); - validate_request_json(request, "metadata", Value::Object(map)); + assert_eq!(result["metadata"], Value::Object(map)); } -} \ No newline at end of file +} From 2e58995d7e44631b4bdd718e48235e766ec219b6 Mon Sep 17 00:00:00 2001 From: William Lyles <26171886+wilyle@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:47:26 -0700 Subject: [PATCH 5/5] cleanup --- Cargo.lock | 1 - mocks/mock_cloud_connector/README.md | 2 +- proto/cloud_connector/Cargo.toml | 3 +-- proto/cloud_connector/src/lib.rs | 6 +----- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a54e8d..cd64587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,7 +245,6 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "cloud-connector-proto" version = "0.1.0" dependencies = [ - "freyja-build-common", "prost", "prost-types", "serde", diff --git a/mocks/mock_cloud_connector/README.md b/mocks/mock_cloud_connector/README.md index 359a67b..9dd099b 100644 --- a/mocks/mock_cloud_connector/README.md +++ b/mocks/mock_cloud_connector/README.md @@ -1,6 +1,6 @@ # Mock Cloud Connector -The Mock Cloud Connector mocks the behavior of a Cloud Connector. This enables functionality similar to the [In-Memory Mock Cloud Adapter](../../adapters/cloud/in_memory_mock_cloud_adapter/README.md). +The Mock Cloud Connector mocks the behavior of a Cloud Connector. This enables functionality similar to the [In-Memory Mock Cloud Adapter](../../adapters/cloud/in_memory_mock_cloud_adapter/README.md) while also utilizing the standard Freyja application. The Mock Cloud Connector implements the [Cloud Connector API](../../interfaces/cloud_connector/v1/cloud_connector.proto), making it compatible with the [gRPC Cloud Adapter](../../adapters/cloud/grpc_cloud_adapter/README.md). diff --git a/proto/cloud_connector/Cargo.toml b/proto/cloud_connector/Cargo.toml index 0c6a433..2a98e84 100644 --- a/proto/cloud_connector/Cargo.toml +++ b/proto/cloud_connector/Cargo.toml @@ -17,5 +17,4 @@ time = { workspace = true, features = ["serde-human-readable"] } tonic = { workspace = true } [build-dependencies] -tonic-build = { workspace = true } -freyja-build-common = { workspace = true } \ No newline at end of file +tonic-build = { workspace = true } \ No newline at end of file diff --git a/proto/cloud_connector/src/lib.rs b/proto/cloud_connector/src/lib.rs index b63eaee..c69cede 100644 --- a/proto/cloud_connector/src/lib.rs +++ b/proto/cloud_connector/src/lib.rs @@ -188,11 +188,7 @@ pub mod v1 { mod cloud_connector_tests { use serde_json::{json, Map, Value}; - use self::v1::UpdateDigitalTwinRequestBuilder; - - use super::*; - - use crate::v1::UpdateDigitalTwinRequest; + use crate::v1::{UpdateDigitalTwinRequest, UpdateDigitalTwinRequestBuilder}; fn serialize_round_trip(request: &UpdateDigitalTwinRequest) -> Value { let serialize_result = serde_json::to_string(&request);