@@ -12,6 +12,7 @@ use super::server::AgentState;
1212use crate :: entity_manager:: server:: EntityStoreRequest ;
1313use crate :: entity_manager:: server:: EntityStoreResponse ;
1414use axum:: extract:: Path ;
15+ use axum:: extract:: Query ;
1516use axum:: extract:: State ;
1617use axum:: response:: IntoResponse ;
1718use axum:: response:: Response ;
@@ -20,14 +21,72 @@ use axum::routing::post;
2021use axum:: Json ;
2122use axum:: Router ;
2223use hyper:: StatusCode ;
24+ use serde:: Deserialize ;
2325use serde_json:: json;
2426use std:: str:: FromStr ;
2527use tedge_api:: entity:: EntityMetadata ;
28+ use tedge_api:: entity:: InvalidEntityType ;
2629use tedge_api:: entity_store;
2730use tedge_api:: entity_store:: EntityRegistrationMessage ;
31+ use tedge_api:: entity_store:: ListFilters ;
2832use tedge_api:: mqtt_topics:: EntityTopicId ;
2933use 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 ) ]
3291enum 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
51113impl 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
142205async 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 {
0 commit comments