From 6e7c4c6ce74cc2c759fecda783aad605b52c5ea1 Mon Sep 17 00:00:00 2001 From: Albin Suresh Date: Mon, 3 Mar 2025 13:50:07 +0000 Subject: [PATCH] fixup! feat: REST API to update entity twin data --- .../tedge_agent/src/entity_manager/server.rs | 19 +++++----- .../tedge_agent/src/entity_manager/tests.rs | 35 +++++++++++++++++++ .../src/http_server/entity_store.rs | 2 +- docs/src/operate/registration/register.md | 11 +++--- .../http_server/entity_store_api.robot | 6 ++++ 5 files changed, 60 insertions(+), 13 deletions(-) diff --git a/crates/core/tedge_agent/src/entity_manager/server.rs b/crates/core/tedge_agent/src/entity_manager/server.rs index f61090445e..88ea9fd631 100644 --- a/crates/core/tedge_agent/src/entity_manager/server.rs +++ b/crates/core/tedge_agent/src/entity_manager/server.rs @@ -56,7 +56,7 @@ impl EntityTwinData { ) -> Result { for key in twin_data.keys() { if key.starts_with('@') { - return Err(InvalidTwinData); + return Err(InvalidTwinData(key.clone())); } } Ok(Self { @@ -67,8 +67,8 @@ impl EntityTwinData { } #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -#[error("Fragment keys starting with '@' are not allowed as twin data")] -pub struct InvalidTwinData; +#[error("Invalid key: '{0}', as fragment keys starting with '@' are not allowed as twin data")] +pub struct InvalidTwinData(String); pub struct EntityStoreServer { entity_store: EntityStore, @@ -258,11 +258,14 @@ impl EntityStoreServer { async fn patch_entity(&mut self, twin_data: EntityTwinData) -> Result<(), entity_store::Error> { for (fragment_key, fragment_value) in twin_data.fragments.into_iter() { - self.entity_store.update_twin_data(EntityTwinMessage::new( - twin_data.topic_id.clone(), - fragment_key, - fragment_value, - ))?; + let twin_message = + EntityTwinMessage::new(twin_data.topic_id.clone(), fragment_key, fragment_value); + let updated = self.entity_store.update_twin_data(twin_message.clone())?; + + if updated { + let message = twin_message.to_mqtt_message(&self.mqtt_schema); + self.publish_message(message).await; + } } Ok(()) diff --git a/crates/core/tedge_agent/src/entity_manager/tests.rs b/crates/core/tedge_agent/src/entity_manager/tests.rs index a1969b5c75..a479d039a6 100644 --- a/crates/core/tedge_agent/src/entity_manager/tests.rs +++ b/crates/core/tedge_agent/src/entity_manager/tests.rs @@ -1,4 +1,5 @@ use crate::entity_manager::server::EntityStoreResponse; +use crate::entity_manager::server::EntityTwinData; use crate::entity_manager::tests::model::Action; use crate::entity_manager::tests::model::Action::AddDevice; use crate::entity_manager::tests::model::Action::AddService; @@ -7,9 +8,12 @@ use crate::entity_manager::tests::model::Commands; use crate::entity_manager::tests::model::Protocol::HTTP; use crate::entity_manager::tests::model::Protocol::MQTT; use proptest::proptest; +use serde_json::json; use std::collections::HashSet; use tedge_actors::Server; use tedge_api::entity::EntityMetadata; +use tedge_api::mqtt_topics::EntityTopicId; +use tedge_mqtt_ext::test_helpers::assert_received_contains_str; #[tokio::test] async fn new_entity_store() { @@ -57,6 +61,23 @@ async fn removing_a_child_using_mqtt() { check_registrations(Commands(registrations)).await } +#[tokio::test] +async fn patched_twin_fragments_published_to_mqtt() { + let (mut entity_store, mut mqtt_box) = entity::server("device-under-test"); + let twin_data = EntityTwinData::try_new( + EntityTopicId::default_main_device(), + json!({"x": 9, "y": true, "z": "foo"}) + .as_object() + .unwrap() + .clone(), + ) + .unwrap(); + entity::patch(&mut entity_store, twin_data).await.unwrap(); + assert_received_contains_str(&mut mqtt_box, [("te/device/main///twin/x", "9")]).await; + assert_received_contains_str(&mut mqtt_box, [("te/device/main///twin/y", "true")]).await; + assert_received_contains_str(&mut mqtt_box, [("te/device/main///twin/z", "foo")]).await; +} + proptest! { //#![proptest_config(proptest::prelude::ProptestConfig::with_cases(1000))] #[test] @@ -165,6 +186,7 @@ mod entity { use crate::entity_manager::server::EntityStoreRequest; use crate::entity_manager::server::EntityStoreResponse; use crate::entity_manager::server::EntityStoreServer; + use crate::entity_manager::server::EntityTwinData; use std::str::FromStr; use tedge_actors::Builder; use tedge_actors::NoMessage; @@ -192,6 +214,19 @@ mod entity { None } + pub async fn patch( + entity_store: &mut EntityStoreServer, + twin_data: EntityTwinData, + ) -> Result<(), anyhow::Error> { + if let EntityStoreResponse::Patch(result) = entity_store + .handle(EntityStoreRequest::Patch(twin_data)) + .await + { + return result.map_err(Into::into); + }; + anyhow::bail!("Unexpected response"); + } + pub fn server( device_id: &str, ) -> (EntityStoreServer, SimpleMessageBox) { diff --git a/crates/core/tedge_agent/src/http_server/entity_store.rs b/crates/core/tedge_agent/src/http_server/entity_store.rs index 851ec6fd90..2ef7a650b2 100644 --- a/crates/core/tedge_agent/src/http_server/entity_store.rs +++ b/crates/core/tedge_agent/src/http_server/entity_store.rs @@ -588,7 +588,7 @@ mod tests { let entity: Value = serde_json::from_slice(&body).unwrap(); assert_json_eq!( entity, - json!({"error":"Fragment keys starting with '@' are not allowed as twin data"}) + json!({"error":"Invalid key: '@id', as fragment keys starting with '@' are not allowed as twin data"}) ); } diff --git a/docs/src/operate/registration/register.md b/docs/src/operate/registration/register.md index dcabf52505..6c1c10ea67 100644 --- a/docs/src/operate/registration/register.md +++ b/docs/src/operate/registration/register.md @@ -445,26 +445,29 @@ PATCH /v1/entities/{topic-id} **Payload** -Update existing fragment: `name`, add new fragment: `hardware` and remove existing fragment: `maintenanceMode` with a `null` value: +Any fragments to be inserted/updated are specified with their desired values. +Fragments to be removed are specified with a `null` value. ```json { - "name": "Child 01", "new-fragment": { "new-key": "new-value" }, - "extra-fragment": null + "fragment-to-update": "updated-value", + "fragment-to-delete": null } ``` **Example** +Update existing fragment: `name`, add new fragment: `hardware` and remove existing fragment: `maintenanceMode` with a `null` value: + ```shell curl http://localhost:8000/tedge/entity-store/v1/entities/device/child01 \ -X PATCH \ -H "Content-Type: application/json" \ -d '{ - "type": "Raspberry Pi 4", + "Child": "Child 01", "hardware": { "serialNo": "98761234" }, diff --git a/tests/RobotFramework/tests/tedge_agent/http_server/entity_store_api.robot b/tests/RobotFramework/tests/tedge_agent/http_server/entity_store_api.robot index 4250c7bee0..92235c779f 100644 --- a/tests/RobotFramework/tests/tedge_agent/http_server/entity_store_api.robot +++ b/tests/RobotFramework/tests/tedge_agent/http_server/entity_store_api.robot @@ -31,6 +31,12 @@ CRUD apis Should Be Equal ... ${patch} ... {"@topic-id":"device/child01//","@parent":"device/main//","@type":"child-device","maintenance_mode":true,"maintenance_window":5} + Should Have MQTT Messages + ... te/device/child01///twin/maintenance_mode + ... message_contains=true + Should Have MQTT Messages + ... te/device/child01///twin/maintenance_window + ... message_contains=5 ${get}= Execute Command curl http://localhost:8000/tedge/entity-store/v1/entities/device/child01// Should Be Equal