Skip to content

Commit

Permalink
Move GenericCommandState path substitution in substitution.rs
Browse files Browse the repository at this point in the history
Signed-off-by: Didier Wenzek <[email protected]>
  • Loading branch information
didier-wenzek committed Oct 25, 2024
1 parent b0f62f6 commit b34493a
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 162 deletions.
170 changes: 170 additions & 0 deletions crates/core/tedge_api/src/substitution.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::workflow::GenericCommandState;
use serde_json::json;
use serde_json::Value;

pub trait Record {
Expand Down Expand Up @@ -54,6 +56,36 @@ pub trait Record {
}
}

impl Record for GenericCommandState {
/// Extract the JSON value pointed by a path from this command state
///
/// Return None if the path contains unknown fields,
/// with the exception that the empty string is returned for an unknown path below the `.payload`,
/// the rational being that the payload object represents a free-form value.
fn extract_value(&self, path: &str) -> Option<Value> {
match path {
"." => Some(json!({
"topic": self.topic.name,
"payload": self.payload
})),
".topic" => Some(self.topic.name.clone().into()),
".topic.root_prefix" => self.root_prefix().map(|s| s.into()),
".topic.target" => self.target().map(|s| s.into()),
".topic.operation" => self.operation().map(|s| s.into()),
".topic.cmd_id" => self.cmd_id().map(|s| s.into()),
".payload" => Some(self.payload.clone()),
path if path.contains(['[', ']']) => None,
path => {
let value_path = path.strip_prefix(".payload.")?;
let value = json_excerpt(&self.payload, value_path)
.cloned()
.unwrap_or_else(|| String::new().into());
Some(value)
}
}
}
}

fn json_as_string(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Expand All @@ -63,3 +95,141 @@ fn json_as_string(value: &Value) -> String {
_ => value.to_string(),
}
}

fn json_excerpt<'a>(value: &'a Value, path: &'a str) -> Option<&'a Value> {
match path.split_once('.') {
None if path.is_empty() => Some(value),
None => value.get(path),
Some((key, path)) => value.get(key).and_then(|value| json_excerpt(value, path)),
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::workflow::GenericCommandState;
use mqtt_channel::Topic;
use serde_json::json;

#[test]
fn inject_json_into_parameters() {
let topic = Topic::new_unchecked("te/device/main///cmd/make_it/123");
let payload = r#"{ "status":"init", "foo":42, "bar": { "extra": [1,2,3] }}"#;
let command = mqtt_channel::MqttMessage::new(&topic, payload);
let cmd = GenericCommandState::from_command_message(&command).expect("parsing error");
assert!(cmd.is_init());

// Valid paths
assert_eq!(
cmd.inject_values_into_template("${.}").to_json(),
json!({
"topic": "te/device/main///cmd/make_it/123",
"payload": {
"status":"init",
"foo":42,
"bar": { "extra": [1,2,3] }
}
})
);
assert_eq!(
cmd.inject_values_into_template("${.topic}"),
"te/device/main///cmd/make_it/123"
);
assert_eq!(
cmd.inject_values_into_template("${.topic.target}"),
"device/main//"
);
assert_eq!(
cmd.inject_values_into_template("${.topic.operation}"),
"make_it"
);
assert_eq!(cmd.inject_values_into_template("${.topic.cmd_id}"), "123");
assert_eq!(
cmd.inject_values_into_template("${.payload}").to_json(),
cmd.payload
);
assert_eq!(
cmd.inject_values_into_template("${.payload.status}"),
"init"
);
assert_eq!(cmd.inject_values_into_template("${.payload.foo}"), "42");
assert_eq!(
cmd.inject_values_into_template("prefix-${.payload.foo}"),
"prefix-42"
);
assert_eq!(
cmd.inject_values_into_template("${.payload.foo}-suffix"),
"42-suffix"
);
assert_eq!(
cmd.inject_values_into_template("prefix-${.payload.foo}-suffix"),
"prefix-42-suffix"
);
assert_eq!(
cmd.inject_values_into_template(
"prefix-${.payload.foo}-separator-${.topic.cmd_id}-suffix"
),
"prefix-42-separator-123-suffix"
);
assert_eq!(
cmd.inject_values_into_template("prefix-${.payload.foo}-separator-${invalid-path}"),
"prefix-42-separator-${invalid-path}"
);
assert_eq!(
cmd.inject_values_into_template("not-a-valid-pattern}"),
"not-a-valid-pattern}"
);
assert_eq!(
cmd.inject_values_into_template("${not-a-valid-pattern"),
"${not-a-valid-pattern"
);
assert_eq!(
cmd.inject_values_into_template("${.payload.bar}").to_json(),
json!({
"extra": [1,2,3]
})
);
assert_eq!(
cmd.inject_values_into_template("${.payload.bar.extra}")
.to_json(),
json!([1, 2, 3])
);

// Not supported yet
assert_eq!(
cmd.inject_values_into_template("${.payload.bar.extra[1]}"),
"${.payload.bar.extra[1]}"
);

// Ill formed
assert_eq!(
cmd.inject_values_into_template("not a pattern"),
"not a pattern"
);
assert_eq!(
cmd.inject_values_into_template("${ill-formed}"),
"${ill-formed}"
);
assert_eq!(
cmd.inject_values_into_template("${.unknown_root}"),
"${.unknown_root}"
);
assert_eq!(
cmd.inject_values_into_template("${.payload.bar.unknown}"),
""
);
}

trait JsonContent {
fn to_json(self) -> Value;
}

impl JsonContent for String {
fn to_json(self) -> Value {
match serde_json::from_str(&self) {
Ok(json) => json,
Err(_) => Value::Null,
}
}
}
}
162 changes: 0 additions & 162 deletions crates/core/tedge_api/src/workflow/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,39 +285,7 @@ impl GenericCommandState {
o.insert(property.to_string(), value.into());
}
}
}

impl Record for GenericCommandState {
/// Extract the JSON value pointed by a path from this command state
///
/// Return None if the path contains unknown fields,
/// with the exception that the empty string is returned for an unknown path below the `.payload`,
/// the rational being that the payload object represents a free-form value.
fn extract_value(&self, path: &str) -> Option<Value> {
match path {
"." => Some(json!({
"topic": self.topic.name,
"payload": self.payload
})),
".topic" => Some(self.topic.name.clone().into()),
".topic.root_prefix" => self.root_prefix().map(|s| s.into()),
".topic.target" => self.target().map(|s| s.into()),
".topic.operation" => self.operation().map(|s| s.into()),
".topic.cmd_id" => self.cmd_id().map(|s| s.into()),
".payload" => Some(self.payload.clone()),
path if path.contains(['[', ']']) => None,
path => {
let value_path = path.strip_prefix(".payload.")?;
let value = json_excerpt(&self.payload, value_path)
.cloned()
.unwrap_or_else(|| String::new().into());
Some(value)
}
}
}
}

impl GenericCommandState {
/// Return the topic that uniquely identifies the command
pub fn command_topic(&self) -> &String {
&self.topic.name
Expand Down Expand Up @@ -539,14 +507,6 @@ impl From<GenericStateUpdate> for Value {
}
}

fn json_excerpt<'a>(value: &'a Value, path: &'a str) -> Option<&'a Value> {
match path.split_once('.') {
None if path.is_empty() => Some(value),
None => value.get(path),
Some((key, path)) => value.get(key).and_then(|value| json_excerpt(value, path)),
}
}

/// A set of values to be injected/extracted into/from a [GenericCommandState]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(try_from = "Option<Value>")]
Expand Down Expand Up @@ -779,133 +739,11 @@ mod tests {
);
}

#[test]
fn inject_json_into_parameters() {
let topic = Topic::new_unchecked("te/device/main///cmd/make_it/123");
let payload = r#"{ "status":"init", "foo":42, "bar": { "extra": [1,2,3] }}"#;
let command = mqtt_channel::MqttMessage::new(&topic, payload);
let cmd = GenericCommandState::from_command_message(&command).expect("parsing error");
assert!(cmd.is_init());

// Valid paths
assert_eq!(
cmd.inject_values_into_template("${.}").to_json(),
json!({
"topic": "te/device/main///cmd/make_it/123",
"payload": {
"status":"init",
"foo":42,
"bar": { "extra": [1,2,3] }
}
})
);
assert_eq!(
cmd.inject_values_into_template("${.topic}"),
"te/device/main///cmd/make_it/123"
);
assert_eq!(
cmd.inject_values_into_template("${.topic.target}"),
"device/main//"
);
assert_eq!(
cmd.inject_values_into_template("${.topic.operation}"),
"make_it"
);
assert_eq!(cmd.inject_values_into_template("${.topic.cmd_id}"), "123");
assert_eq!(
cmd.inject_values_into_template("${.payload}").to_json(),
cmd.payload
);
assert_eq!(
cmd.inject_values_into_template("${.payload.status}"),
"init"
);
assert_eq!(cmd.inject_values_into_template("${.payload.foo}"), "42");
assert_eq!(
cmd.inject_values_into_template("prefix-${.payload.foo}"),
"prefix-42"
);
assert_eq!(
cmd.inject_values_into_template("${.payload.foo}-suffix"),
"42-suffix"
);
assert_eq!(
cmd.inject_values_into_template("prefix-${.payload.foo}-suffix"),
"prefix-42-suffix"
);
assert_eq!(
cmd.inject_values_into_template(
"prefix-${.payload.foo}-separator-${.topic.cmd_id}-suffix"
),
"prefix-42-separator-123-suffix"
);
assert_eq!(
cmd.inject_values_into_template("prefix-${.payload.foo}-separator-${invalid-path}"),
"prefix-42-separator-${invalid-path}"
);
assert_eq!(
cmd.inject_values_into_template("not-a-valid-pattern}"),
"not-a-valid-pattern}"
);
assert_eq!(
cmd.inject_values_into_template("${not-a-valid-pattern"),
"${not-a-valid-pattern"
);
assert_eq!(
cmd.inject_values_into_template("${.payload.bar}").to_json(),
json!({
"extra": [1,2,3]
})
);
assert_eq!(
cmd.inject_values_into_template("${.payload.bar.extra}")
.to_json(),
json!([1, 2, 3])
);

// Not supported yet
assert_eq!(
cmd.inject_values_into_template("${.payload.bar.extra[1]}"),
"${.payload.bar.extra[1]}"
);

// Ill formed
assert_eq!(
cmd.inject_values_into_template("not a pattern"),
"not a pattern"
);
assert_eq!(
cmd.inject_values_into_template("${ill-formed}"),
"${ill-formed}"
);
assert_eq!(
cmd.inject_values_into_template("${.unknown_root}"),
"${.unknown_root}"
);
assert_eq!(
cmd.inject_values_into_template("${.payload.bar.unknown}"),
""
);
}

#[test]
fn parse_empty_payload() {
let topic = Topic::new_unchecked("te/device/main///cmd/make_it/123");
let command = mqtt_channel::MqttMessage::new(&topic, "".to_string());
let cmd = GenericCommandState::from_command_message(&command).expect("parsing error");
assert!(cmd.is_cleared())
}

trait JsonContent {
fn to_json(self) -> Value;
}

impl JsonContent for String {
fn to_json(self) -> Value {
match serde_json::from_str(&self) {
Ok(json) => json,
Err(_) => Value::Null,
}
}
}
}

0 comments on commit b34493a

Please sign in to comment.