Skip to content

Commit

Permalink
Workaround for current Ibeji provider contract (#78)
Browse files Browse the repository at this point in the history
* it's a start

* complete implementation

* cleanup

* docs

* use valid json in docs

* some robustness

* one more test

* address workflow issues

* more workflow issues

* address comments
  • Loading branch information
wilyle authored Nov 10, 2023
1 parent 91044ec commit e82b561
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .accepted_words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ESDV
EntityConfig
fn
Fólkvangr
foo
FREYJA
Freyja
Freyja's
Expand Down Expand Up @@ -138,6 +139,7 @@ pre
PrivateKeyName
proto
protobuf
PublishRequest
pushd
queryable
quickstart
Expand Down
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 provider_proxies/grpc/v1/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ futures = { workspace = true }
log = { workspace = true }
samples-protobuf-data-access = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
tonic = { workspace = true }
tower = { workspace = true }
Expand Down
21 changes: 20 additions & 1 deletion provider_proxies/grpc/v1/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# GRPC Provider Proxy

The GRPC Provider Proxy interfaces with providers which support GRPC. It acts as a consumer for digital twin providers. This proxy supports the `Get` and `Subscribe` operations as defined for the [Ibeji mixed sample](https://github.com/eclipse-ibeji/ibeji/tree/main/samples/mixed). To use this proxy with other providers, those providers will need to support the same API(s) as the provider in that sample.
The GRPC Provider Proxy interfaces with providers which support GRPC. It acts as a consumer for digital twin providers. This proxy supports the `Get` and `Subscribe` operations as defined for the [Ibeji mixed sample](https://github.com/eclipse-ibeji/ibeji/tree/main/samples/mixed). To use this proxy with other providers, those providers will need to support the same API(s) as the provider in that sample (see [Integrating with this Proxy](#integrating-with-this-proxy) for more information).

## Configuration

Expand All @@ -9,3 +9,22 @@ This proxy supports the following configuration settings:
- `consumer_address`: The address for the proxy's consumer

This adapter supports [config overrides](../../../docs/config-overrides.md). The override filename is `grpc_proxy_config.json`, and the default config is located at `res/grpc_proxy_config.default.json`.

## Integrating with this Proxy

This proxy supports the `Publish` API as [defined by the Ibeji samples](https://github.com/eclipse-ibeji/ibeji/blob/main/samples/interfaces/sample_grpc/v1/digital_twin_consumer.proto). In addition, the `value` property of the `PublishRequest` message that providers publish must conform to one of the following structures in order to properly extract the signal value:

- A raw value as a string. For example, `"42"` or `"\"foo\""`.
<!--alex ignore savage-->
- A serialized JSON object with a property not named `$metadata` containing the signal value as a JSON primitive. If there is more than one property that meets these criteria, the first one will be used. For example:

```json
{
"AmbientAirTemperature": 42,
"$metadata": {
"foo": "bar"
}
}
```

In the above example, this proxy would extract the value `"42"`.
176 changes: 175 additions & 1 deletion provider_proxies/grpc/v1/src/grpc_client_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::sync::Arc;

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

use freyja_contracts::provider_proxy::SignalValue;
Expand All @@ -14,11 +15,95 @@ 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::<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;
}
};

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

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

selected_property = Some((property.0, selected_value));
break;
}

match selected_property {
Some((k, v)) => {
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(),
k
);

v
}
None => {
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 @@ -33,6 +118,8 @@ impl DigitalTwinConsumer for GRPCClientImpl {

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

let value = Self::parse_value(value);

let new_signal_value = SignalValue { entity_id, value };
self.signal_values_queue.push(new_signal_value);
let response = PublishResponse {};
Expand All @@ -54,7 +141,7 @@ impl DigitalTwinConsumer for GRPCClientImpl {
}

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

#[tokio::test]
Expand All @@ -70,4 +157,91 @@ mod consumer_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);
}
}

0 comments on commit e82b561

Please sign in to comment.