Skip to content

Commit

Permalink
move parse to common location:
Browse files Browse the repository at this point in the history
  • Loading branch information
wilyle committed Nov 13, 2023
1 parent 4afe8a0 commit 20d682b
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 238 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ freyja-contracts = { workspace = true }
home = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
1 change: 1 addition & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT

pub mod config_utils;
pub mod message_utils;
pub mod retry_utils;
pub mod signal_store;

Expand Down
168 changes: 168 additions & 0 deletions common/src/message_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// SPDX-License-Identifier: MIT

use log::{warn, debug};

const METADATA_KEY: &str = "$metadata";

/// Parses a value published by a provider.
/// The current implementation is a workaround for the current Ibeji sample provider implementation,
/// which uses a non-consistent contract as follows:
///
/// ```ignore
/// {
/// "{propertyName}": "value",
/// "$metadata": {...}
/// }
/// ```
///
/// Note that `{propertyName}` is replaced by the name of the property that the provider published.
/// This function will extract the value from the first property satisfying the following conditions:
/// - The property is not named `$metadata`
/// - The property value is a non-null primitive JSON type (string, bool, or number)
///
/// If any part of parsing fails, a warning is logged and the original value is returned.
///
/// # Arguments
/// - `value`: the value to attempt to parse
pub fn parse_value(value: String) -> String {
match serde_json::from_str::<serde_json::Value>(&value) {
Ok(v) => {
let property_map = match v.as_object() {
Some(o) => o,
None => {
warn!("Could not parse value as JSON object");
return value;
}
};

for property in property_map.iter() {
if property.0 == METADATA_KEY {
continue;
}

let selected_value = match property.1 {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
_ => continue,
};

let metadata_descriptor =
if property_map.contains_key(&METADATA_KEY.to_string()) {
"has"
} else {
"does not have"
};

debug!(
"Value contained {} properties and {metadata_descriptor} a {METADATA_KEY} property. Selecting property with key {} as the signal value",
property_map.len(),
property.0
);

return selected_value;
}

warn!("Could not find a property that was parseable as a value");
value
}
Err(e) => {
warn!("Failed to parse value |{value}|: {e}");
value
}
}
}

#[cfg(test)]
mod message_utils_tests {
use super::*;

#[test]
fn parse_value_returns_input_when_parse_fails() {
let input = r#"invalid json"#;
let result = parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_input_is_plain_string() {
let input = r#""value""#;
let result = parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_input_has_zero_properties() {
let input = r#"{}"#;
let result = parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_input_has_no_valid_properties() {
let input = format!(r#"{{"{METADATA_KEY}": "foo"}}"#);
let result = parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_property_value_is_not_string() {
let input = r#"{"property": ["value"]}"#;
let result = parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_correct_value_for_strings() {
let expected_value = "value";
let input = format!(r#"{{"property": "{expected_value}", "{METADATA_KEY}": "foo"}}"#);
let result = parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_returns_correct_value_for_bools() {
let expected_value = "true";
let input = format!(r#"{{"property": {expected_value}, "{METADATA_KEY}": "foo"}}"#);
let result = parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_returns_correct_value_for_numbers() {
let expected_value = "123.456";
let input = format!(r#"{{"property": {expected_value}, "{METADATA_KEY}": "foo"}}"#);
let result = parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_skips_metadata_property() {
let expected_value = "value";
let input = format!(r#"{{"{METADATA_KEY}": "foo", "property": "{expected_value}"}}"#);
let result = parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_skips_non_primitive_properties() {
let expected_value = "value";
let input = format!(
r#"{{"foo": ["bar"], "property": "{expected_value}", "{METADATA_KEY}": "foo"}}"#
);
let result = parse_value(input.to_string());

assert_eq!(result, expected_value);
}
}
163 changes: 2 additions & 161 deletions provider_proxies/grpc/v1/src/grpc_client_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use std::sync::Arc;

use crossbeam::queue::SegQueue;
use freyja_common::message_utils;
use log::{debug, warn};
use tonic::{Request, Response, Status};

Expand All @@ -14,86 +15,13 @@ use samples_protobuf_data_access::sample_grpc::v1::digital_twin_consumer::{
RespondRequest, RespondResponse,
};

const METADATA_KEY: &str = "$metadata";

/// Struct which implements the DigitalTwinConsumer trait for gRPC clients
#[derive(Debug, Default)]
pub struct GRPCClientImpl {
/// The queue on which incoming signal values should be published
pub signal_values_queue: Arc<SegQueue<SignalValue>>,
}

impl GRPCClientImpl {
/// Parses the value published by a provider.
/// The current implementation is a workaround for the current Ibeji sample provider implementation,
/// which uses a non-consistent contract as follows:
///
/// ```ignore
/// {
/// "{propertyName}": "value",
/// "$metadata": {...}
/// }
/// ```
///
/// Note that `{propertyName}` is replaced by the name of the property that the provider published.
/// This function will extract the value from the first property satisfying the following conditions:
/// - The property is not named `$metadata`
/// - The property value is a non-null primitive JSON type (string, bool, or number)
///
/// If any part of parsing fails, a warning is logged and the original value is returned.
///
/// # Arguments
/// - `value`: the value to attempt to parse
fn parse_value(value: String) -> String {
match serde_json::from_str::<serde_json::Value>(&value) {
Ok(v) => {
let property_map = match v.as_object() {
Some(o) => o,
None => {
warn!("Could not parse value as JSON object");
return value;
}
};

for property in property_map.iter() {
if property.0 == METADATA_KEY {
continue;
}

let selected_value = match property.1 {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
_ => continue,
};

let metadata_descriptor =
if property_map.contains_key(&METADATA_KEY.to_string()) {
"has"
} else {
"does not have"
};

debug!(
"Value contained {} properties and {metadata_descriptor} a {METADATA_KEY} property. Selecting property with key {} as the signal value",
property_map.len(),
property.0
);

return selected_value;
}

warn!("Could not find a property that was parseable as a value");
value
}
Err(e) => {
warn!("Failed to parse value: {e}");
value
}
}
}
}

#[tonic::async_trait]
impl DigitalTwinConsumer for GRPCClientImpl {
/// Publish implementation.
Expand All @@ -108,7 +36,7 @@ impl DigitalTwinConsumer for GRPCClientImpl {

debug!("Received a publish for entity id {entity_id} with the value {value}");

let value = Self::parse_value(value);
let value = message_utils::parse_value(value);

let new_signal_value = SignalValue { entity_id, value };
self.signal_values_queue.push(new_signal_value);
Expand Down Expand Up @@ -147,91 +75,4 @@ mod grpc_client_impl_tests {
let result = consumer_impl.publish(request).await;
assert!(result.is_ok());
}

#[test]
fn parse_value_returns_input_when_parse_fails() {
let input = r#"invalid json"#;
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_input_is_plain_string() {
let input = r#""value""#;
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_input_has_zero_properties() {
let input = r#"{}"#;
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_input_has_no_valid_properties() {
let input = format!(r#"{{"{METADATA_KEY}": "foo"}}"#);
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_input_when_property_value_is_not_string() {
let input = r#"{"property": ["value"]}"#;
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, input);
}

#[test]
fn parse_value_returns_correct_value_for_strings() {
let expected_value = "value";
let input = format!(r#"{{"property": "{expected_value}", "{METADATA_KEY}": "foo"}}"#);
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_returns_correct_value_for_bools() {
let expected_value = "true";
let input = format!(r#"{{"property": {expected_value}, "{METADATA_KEY}": "foo"}}"#);
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_returns_correct_value_for_numbers() {
let expected_value = "123.456";
let input = format!(r#"{{"property": {expected_value}, "{METADATA_KEY}": "foo"}}"#);
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_skips_metadata_property() {
let expected_value = "value";
let input = format!(r#"{{"{METADATA_KEY}": "foo", "property": "{expected_value}"}}"#);
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, expected_value);
}

#[test]
fn parse_value_skips_non_primitive_properties() {
let expected_value = "value";
let input = format!(
r#"{{"foo": ["bar"], "property": "{expected_value}", "{METADATA_KEY}": "foo"}}"#
);
let result = GRPCClientImpl::parse_value(input.to_string());

assert_eq!(result, expected_value);
}
}
Loading

0 comments on commit 20d682b

Please sign in to comment.