Skip to content

Commit aec6d93

Browse files
committed
feat: Query parameter support for entity store query API
1 parent 93a3bb8 commit aec6d93

File tree

5 files changed

+477
-104
lines changed

5 files changed

+477
-104
lines changed

crates/core/tedge_agent/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ repository = { workspace = true }
1212
[dependencies]
1313
anyhow = { workspace = true }
1414
async-trait = { workspace = true }
15-
axum = { workspace = true }
15+
axum = { workspace = true, features = ["macros"] }
1616
axum-server = { workspace = true }
1717
axum_tls = { workspace = true }
1818
camino = { workspace = true }

crates/core/tedge_agent/src/entity_manager/server.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use tedge_actors::Server;
66
use tedge_api::entity::EntityMetadata;
77
use tedge_api::entity_store;
88
use tedge_api::entity_store::EntityRegistrationMessage;
9+
use tedge_api::entity_store::ListFilters;
910
use tedge_api::mqtt_topics::Channel;
1011
use tedge_api::mqtt_topics::EntityTopicId;
1112
use tedge_api::mqtt_topics::MqttSchema;
@@ -21,7 +22,7 @@ pub enum EntityStoreRequest {
2122
Get(EntityTopicId),
2223
Create(EntityRegistrationMessage),
2324
Delete(EntityTopicId),
24-
List(Option<EntityTopicId>),
25+
List(ListFilters),
2526
MqttMessage(MqttMessage),
2627
}
2728

@@ -30,7 +31,7 @@ pub enum EntityStoreResponse {
3031
Get(Option<EntityMetadata>),
3132
Create(Result<Vec<RegisteredEntityData>, entity_store::Error>),
3233
Delete(Vec<EntityTopicId>),
33-
List(Result<Vec<EntityMetadata>, entity_store::Error>),
34+
List(Vec<EntityMetadata>),
3435
Ok,
3536
}
3637

@@ -92,11 +93,9 @@ impl Server for EntityStoreServer {
9293
let deleted_entities = self.deregister_entity(topic_id).await;
9394
EntityStoreResponse::Delete(deleted_entities)
9495
}
95-
EntityStoreRequest::List(topic_id) => {
96-
let entities = self.entity_store.list_entity_tree(topic_id.as_ref());
97-
EntityStoreResponse::List(
98-
entities.map(|entities| entities.into_iter().cloned().collect()),
99-
)
96+
EntityStoreRequest::List(filters) => {
97+
let entities = self.entity_store.list_entity_tree(filters);
98+
EntityStoreResponse::List(entities.into_iter().cloned().collect())
10099
}
101100
EntityStoreRequest::MqttMessage(mqtt_message) => {
102101
self.process_mqtt_message(mqtt_message).await;

crates/core/tedge_agent/src/http_server/entity_store.rs

Lines changed: 191 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use super::server::AgentState;
1212
use crate::entity_manager::server::EntityStoreRequest;
1313
use crate::entity_manager::server::EntityStoreResponse;
1414
use axum::extract::Path;
15+
use axum::extract::Query;
1516
use axum::extract::State;
1617
use axum::response::IntoResponse;
1718
use axum::response::Response;
@@ -20,14 +21,72 @@ use axum::routing::post;
2021
use axum::Json;
2122
use axum::Router;
2223
use hyper::StatusCode;
24+
use serde::Deserialize;
2325
use serde_json::json;
2426
use std::str::FromStr;
2527
use tedge_api::entity::EntityMetadata;
28+
use tedge_api::entity::InvalidEntityType;
2629
use tedge_api::entity_store;
2730
use tedge_api::entity_store::EntityRegistrationMessage;
31+
use tedge_api::entity_store::ListFilters;
2832
use tedge_api::mqtt_topics::EntityTopicId;
2933
use tedge_api::mqtt_topics::TopicIdError;
3034

35+
#[derive(Debug, Default, Deserialize)]
36+
pub struct ListParams {
37+
#[serde(default)]
38+
root: Option<String>,
39+
#[serde(default)]
40+
parent: Option<String>,
41+
#[serde(default)]
42+
r#type: Option<String>,
43+
}
44+
45+
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
46+
pub enum InputValidationError {
47+
#[error(transparent)]
48+
InvalidEntityType(#[from] InvalidEntityType),
49+
#[error(transparent)]
50+
InvalidEntityTopic(#[from] TopicIdError),
51+
#[error("The provided parameters: {0} and {1} are mutually exclusive. Use either one.")]
52+
IncompatibleParams(String, String),
53+
}
54+
55+
impl TryFrom<ListParams> for ListFilters {
56+
type Error = InputValidationError;
57+
58+
fn try_from(params: ListParams) -> Result<Self, Self::Error> {
59+
let root = params
60+
.root
61+
.filter(|v| !v.is_empty())
62+
.map(|val| val.parse())
63+
.transpose()?;
64+
let parent = params
65+
.parent
66+
.filter(|v| !v.is_empty())
67+
.map(|val| val.parse())
68+
.transpose()?;
69+
let r#type = params
70+
.r#type
71+
.filter(|v| !v.is_empty())
72+
.map(|val| val.parse())
73+
.transpose()?;
74+
75+
if root.is_some() && parent.is_some() {
76+
return Err(InputValidationError::IncompatibleParams(
77+
"root".to_string(),
78+
"parent".to_string(),
79+
));
80+
}
81+
82+
Ok(Self {
83+
root,
84+
parent,
85+
r#type,
86+
})
87+
}
88+
}
89+
3190
#[derive(thiserror::Error, Debug)]
3291
enum Error {
3392
#[error(transparent)]
@@ -46,6 +105,9 @@ enum Error {
46105

47106
#[error("Received unexpected response from entity store")]
48107
InvalidEntityStoreResponse,
108+
109+
#[error(transparent)]
110+
InvalidInput(#[from] InputValidationError),
49111
}
50112

51113
impl IntoResponse for Error {
@@ -60,6 +122,7 @@ impl IntoResponse for Error {
60122
Error::EntityNotFound(_) => StatusCode::NOT_FOUND,
61123
Error::ChannelError(_) => StatusCode::INTERNAL_SERVER_ERROR,
62124
Error::InvalidEntityStoreResponse => StatusCode::INTERNAL_SERVER_ERROR,
125+
Error::InvalidInput(_) => StatusCode::BAD_REQUEST,
63126
};
64127
let error_message = self.to_string();
65128

@@ -141,18 +204,20 @@ async fn deregister_entity(
141204

142205
async fn list_entities(
143206
State(state): State<AgentState>,
207+
Query(params): Query<ListParams>,
144208
) -> Result<Json<Vec<EntityMetadata>>, Error> {
209+
let filters = params.try_into()?;
145210
let response = state
146211
.entity_store_handle
147212
.clone()
148-
.await_response(EntityStoreRequest::List(None))
213+
.await_response(EntityStoreRequest::List(filters))
149214
.await?;
150215

151216
let EntityStoreResponse::List(entities) = response else {
152217
return Err(Error::InvalidEntityStoreResponse);
153218
};
154219

155-
Ok(Json(entities?))
220+
Ok(Json(entities))
156221
}
157222

158223
#[cfg(test)]
@@ -478,11 +543,11 @@ mod tests {
478543
if let Some(mut req) = entity_store_box.recv().await {
479544
if let EntityStoreRequest::List(_) = req.request {
480545
req.reply_to
481-
.send(EntityStoreResponse::List(Ok(vec![
546+
.send(EntityStoreResponse::List(vec![
482547
EntityMetadata::main_device(),
483548
EntityMetadata::child_device("child0".to_string()).unwrap(),
484549
EntityMetadata::child_device("child1".to_string()).unwrap(),
485-
])))
550+
]))
486551
.await
487552
.unwrap();
488553
}
@@ -522,9 +587,7 @@ mod tests {
522587
if let Some(mut req) = entity_store_box.recv().await {
523588
if let EntityStoreRequest::List(_) = req.request {
524589
req.reply_to
525-
.send(EntityStoreResponse::List(Err(
526-
entity_store::Error::UnknownEntity("unknown".to_string()),
527-
)))
590+
.send(EntityStoreResponse::List(vec![]))
528591
.await
529592
.unwrap();
530593
}
@@ -538,7 +601,127 @@ mod tests {
538601
.expect("request builder");
539602

540603
let response = app.call(req).await.unwrap();
541-
assert_eq!(response.status(), StatusCode::NOT_FOUND);
604+
assert_eq!(response.status(), StatusCode::OK);
605+
606+
let body = response.into_body().collect().await.unwrap().to_bytes();
607+
let entities: Vec<EntityMetadata> = serde_json::from_slice(&body).unwrap();
608+
assert!(entities.is_empty());
609+
}
610+
611+
#[tokio::test]
612+
async fn entity_list_query_parameters() {
613+
let TestHandle {
614+
mut app,
615+
mut entity_store_box,
616+
} = setup();
617+
618+
// Mock entity store actor response
619+
tokio::spawn(async move {
620+
if let Some(mut req) = entity_store_box.recv().await {
621+
if let EntityStoreRequest::List(_) = req.request {
622+
req.reply_to
623+
.send(EntityStoreResponse::List(vec![
624+
EntityMetadata::child_device("child00".to_string()).unwrap(),
625+
EntityMetadata::child_device("child01".to_string()).unwrap(),
626+
]))
627+
.await
628+
.unwrap();
629+
}
630+
}
631+
});
632+
633+
let req = Request::builder()
634+
.method(Method::GET)
635+
.uri("/v1/entities?parent=device/child0//&type=child-device")
636+
.body(Body::empty())
637+
.expect("request builder");
638+
639+
let response = app.call(req).await.unwrap();
640+
assert_eq!(response.status(), StatusCode::OK);
641+
642+
let body = response.into_body().collect().await.unwrap().to_bytes();
643+
let entities: Vec<EntityMetadata> = serde_json::from_slice(&body).unwrap();
644+
645+
let entity_set = entities
646+
.iter()
647+
.map(|e| e.topic_id.as_str())
648+
.collect::<HashSet<_>>();
649+
assert!(entity_set.contains("device/child00//"));
650+
assert!(entity_set.contains("device/child01//"));
651+
}
652+
653+
#[tokio::test]
654+
async fn entity_list_empty_query_param() {
655+
let TestHandle {
656+
mut app,
657+
mut entity_store_box,
658+
} = setup();
659+
// Mock entity store actor response
660+
tokio::spawn(async move {
661+
while let Some(mut req) = entity_store_box.recv().await {
662+
if let EntityStoreRequest::List(_) = req.request {
663+
req.reply_to
664+
.send(EntityStoreResponse::List(vec![]))
665+
.await
666+
.unwrap();
667+
}
668+
}
669+
});
670+
671+
for param in ["root=", "parent=", "type="].into_iter() {
672+
let uri = format!("/v1/entities?{}", param);
673+
let req = Request::builder()
674+
.method(Method::GET)
675+
.uri(uri)
676+
.body(Body::empty())
677+
.expect("request builder");
678+
679+
let response = app.call(req).await.unwrap();
680+
assert_eq!(response.status(), StatusCode::OK);
681+
}
682+
683+
let req = Request::builder()
684+
.method(Method::GET)
685+
.uri("/v1/entities?root=&parent=&type=")
686+
.body(Body::empty())
687+
.expect("request builder");
688+
689+
let response = app.call(req).await.unwrap();
690+
assert_eq!(response.status(), StatusCode::OK);
691+
}
692+
693+
#[tokio::test]
694+
async fn entity_list_bad_query_param() {
695+
let TestHandle {
696+
mut app,
697+
entity_store_box: _, // Not used
698+
} = setup();
699+
700+
let req = Request::builder()
701+
.method(Method::GET)
702+
.uri("/v1/entities?parent=an/invalid/topic/id/")
703+
.body(Body::empty())
704+
.expect("request builder");
705+
706+
let response = app.call(req).await.unwrap();
707+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
708+
}
709+
710+
#[tokio::test]
711+
async fn entity_list_bad_query_parameter_combination() {
712+
let TestHandle {
713+
mut app,
714+
entity_store_box: _, // Not used
715+
} = setup();
716+
717+
let req = Request::builder()
718+
.method(Method::GET)
719+
.uri("/v1/entities?root=device/some/topic/id&parent=device/another/topic/id")
720+
.body(Body::empty())
721+
.expect("request builder");
722+
723+
let response = app.call(req).await.unwrap();
724+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
542725
}
543726

544727
struct TestHandle {

crates/core/tedge_api/src/entity.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ impl Display for EntityType {
136136
}
137137
}
138138

139-
#[derive(Debug, Error)]
139+
#[derive(Debug, Error, PartialEq, Eq, Clone)]
140140
#[error("Invalid entity type: {0}")]
141141
pub struct InvalidEntityType(String);
142142

0 commit comments

Comments
 (0)