From b34493adbc33a73ecceaef0b9a41dc2b1fb9a8a1 Mon Sep 17 00:00:00 2001 From: Didier Wenzek Date: Fri, 25 Oct 2024 10:03:19 +0200 Subject: [PATCH] Move GenericCommandState path substitution in substitution.rs Signed-off-by: Didier Wenzek --- crates/core/tedge_api/src/substitution.rs | 170 ++++++++++++++++++++ crates/core/tedge_api/src/workflow/state.rs | 162 ------------------- 2 files changed, 170 insertions(+), 162 deletions(-) diff --git a/crates/core/tedge_api/src/substitution.rs b/crates/core/tedge_api/src/substitution.rs index 69e6a4cb30..60a3c6dc2d 100644 --- a/crates/core/tedge_api/src/substitution.rs +++ b/crates/core/tedge_api/src/substitution.rs @@ -1,3 +1,5 @@ +use crate::workflow::GenericCommandState; +use serde_json::json; use serde_json::Value; pub trait Record { @@ -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 { + 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(), @@ -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, + } + } + } +} diff --git a/crates/core/tedge_api/src/workflow/state.rs b/crates/core/tedge_api/src/workflow/state.rs index fd103adc27..8fe8b1d42f 100644 --- a/crates/core/tedge_api/src/workflow/state.rs +++ b/crates/core/tedge_api/src/workflow/state.rs @@ -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 { - 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 @@ -539,14 +507,6 @@ impl From 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")] @@ -779,115 +739,6 @@ 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"); @@ -895,17 +746,4 @@ mod tests { 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, - } - } - } }