@@ -12,6 +12,7 @@ use super::server::AgentState;
12
12
use crate :: entity_manager:: server:: EntityStoreRequest ;
13
13
use crate :: entity_manager:: server:: EntityStoreResponse ;
14
14
use axum:: extract:: Path ;
15
+ use axum:: extract:: Query ;
15
16
use axum:: extract:: State ;
16
17
use axum:: response:: IntoResponse ;
17
18
use axum:: response:: Response ;
@@ -20,14 +21,72 @@ use axum::routing::post;
20
21
use axum:: Json ;
21
22
use axum:: Router ;
22
23
use hyper:: StatusCode ;
24
+ use serde:: Deserialize ;
23
25
use serde_json:: json;
24
26
use std:: str:: FromStr ;
25
27
use tedge_api:: entity:: EntityMetadata ;
28
+ use tedge_api:: entity:: InvalidEntityType ;
26
29
use tedge_api:: entity_store;
27
30
use tedge_api:: entity_store:: EntityRegistrationMessage ;
31
+ use tedge_api:: entity_store:: ListFilters ;
28
32
use tedge_api:: mqtt_topics:: EntityTopicId ;
29
33
use tedge_api:: mqtt_topics:: TopicIdError ;
30
34
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
+
31
90
#[ derive( thiserror:: Error , Debug ) ]
32
91
enum Error {
33
92
#[ error( transparent) ]
@@ -46,6 +105,9 @@ enum Error {
46
105
47
106
#[ error( "Received unexpected response from entity store" ) ]
48
107
InvalidEntityStoreResponse ,
108
+
109
+ #[ error( transparent) ]
110
+ InvalidInput ( #[ from] InputValidationError ) ,
49
111
}
50
112
51
113
impl IntoResponse for Error {
@@ -60,10 +122,11 @@ impl IntoResponse for Error {
60
122
Error :: EntityNotFound ( _) => StatusCode :: NOT_FOUND ,
61
123
Error :: ChannelError ( _) => StatusCode :: INTERNAL_SERVER_ERROR ,
62
124
Error :: InvalidEntityStoreResponse => StatusCode :: INTERNAL_SERVER_ERROR ,
125
+ Error :: InvalidInput ( _) => StatusCode :: BAD_REQUEST ,
63
126
} ;
64
127
let error_message = self . to_string ( ) ;
65
128
66
- ( status_code, error_message) . into_response ( )
129
+ ( status_code, Json ( json ! ( { "error" : error_message } ) ) ) . into_response ( )
67
130
}
68
131
}
69
132
@@ -141,18 +204,20 @@ async fn deregister_entity(
141
204
142
205
async fn list_entities (
143
206
State ( state) : State < AgentState > ,
207
+ Query ( params) : Query < ListParams > ,
144
208
) -> Result < Json < Vec < EntityMetadata > > , Error > {
209
+ let filters = params. try_into ( ) ?;
145
210
let response = state
146
211
. entity_store_handle
147
212
. clone ( )
148
- . await_response ( EntityStoreRequest :: List ( None ) )
213
+ . await_response ( EntityStoreRequest :: List ( filters ) )
149
214
. await ?;
150
215
151
216
let EntityStoreResponse :: List ( entities) = response else {
152
217
return Err ( Error :: InvalidEntityStoreResponse ) ;
153
218
} ;
154
219
155
- Ok ( Json ( entities? ) )
220
+ Ok ( Json ( entities) )
156
221
}
157
222
158
223
#[ cfg( test) ]
@@ -254,6 +319,13 @@ mod tests {
254
319
255
320
let response = app. call ( req) . await . unwrap ( ) ;
256
321
assert_eq ! ( response. status( ) , StatusCode :: NOT_FOUND ) ;
322
+
323
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
324
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
325
+ assert_json_eq ! (
326
+ entity,
327
+ json!( { "error" : "Entity not found with topic id: device/test-child//" } )
328
+ ) ;
257
329
}
258
330
259
331
#[ tokio:: test]
@@ -345,6 +417,13 @@ mod tests {
345
417
346
418
let response = app. call ( req) . await . unwrap ( ) ;
347
419
assert_eq ! ( response. status( ) , StatusCode :: CONFLICT ) ;
420
+
421
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
422
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
423
+ assert_json_eq ! (
424
+ entity,
425
+ json!( { "error" : "An entity with topic id: device/test-child// is already registered" } )
426
+ ) ;
348
427
}
349
428
350
429
#[ tokio:: test]
@@ -391,6 +470,13 @@ mod tests {
391
470
392
471
let response = app. call ( req) . await . unwrap ( ) ;
393
472
assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
473
+
474
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
475
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
476
+ assert_json_eq ! (
477
+ entity,
478
+ json!( { "error" : "Specified parent \" test-child\" does not exist in the store" } )
479
+ ) ;
394
480
}
395
481
396
482
#[ tokio:: test]
@@ -478,11 +564,11 @@ mod tests {
478
564
if let Some ( mut req) = entity_store_box. recv ( ) . await {
479
565
if let EntityStoreRequest :: List ( _) = req. request {
480
566
req. reply_to
481
- . send ( EntityStoreResponse :: List ( Ok ( vec ! [
567
+ . send ( EntityStoreResponse :: List ( vec ! [
482
568
EntityMetadata :: main_device( ) ,
483
569
EntityMetadata :: child_device( "child0" . to_string( ) ) . unwrap( ) ,
484
570
EntityMetadata :: child_device( "child1" . to_string( ) ) . unwrap( ) ,
485
- ] ) ) )
571
+ ] ) )
486
572
. await
487
573
. unwrap ( ) ;
488
574
}
@@ -522,9 +608,7 @@ mod tests {
522
608
if let Some ( mut req) = entity_store_box. recv ( ) . await {
523
609
if let EntityStoreRequest :: List ( _) = req. request {
524
610
req. reply_to
525
- . send ( EntityStoreResponse :: List ( Err (
526
- entity_store:: Error :: UnknownEntity ( "unknown" . to_string ( ) ) ,
527
- ) ) )
611
+ . send ( EntityStoreResponse :: List ( vec ! [ ] ) )
528
612
. await
529
613
. unwrap ( ) ;
530
614
}
@@ -538,7 +622,141 @@ mod tests {
538
622
. expect ( "request builder" ) ;
539
623
540
624
let response = app. call ( req) . await . unwrap ( ) ;
541
- assert_eq ! ( response. status( ) , StatusCode :: NOT_FOUND ) ;
625
+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
626
+
627
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
628
+ let entities: Vec < EntityMetadata > = serde_json:: from_slice ( & body) . unwrap ( ) ;
629
+ assert ! ( entities. is_empty( ) ) ;
630
+ }
631
+
632
+ #[ tokio:: test]
633
+ async fn entity_list_query_parameters ( ) {
634
+ let TestHandle {
635
+ mut app,
636
+ mut entity_store_box,
637
+ } = setup ( ) ;
638
+
639
+ // Mock entity store actor response
640
+ tokio:: spawn ( async move {
641
+ if let Some ( mut req) = entity_store_box. recv ( ) . await {
642
+ if let EntityStoreRequest :: List ( _) = req. request {
643
+ req. reply_to
644
+ . send ( EntityStoreResponse :: List ( vec ! [
645
+ EntityMetadata :: child_device( "child00" . to_string( ) ) . unwrap( ) ,
646
+ EntityMetadata :: child_device( "child01" . to_string( ) ) . unwrap( ) ,
647
+ ] ) )
648
+ . await
649
+ . unwrap ( ) ;
650
+ }
651
+ }
652
+ } ) ;
653
+
654
+ let req = Request :: builder ( )
655
+ . method ( Method :: GET )
656
+ . uri ( "/v1/entities?parent=device/child0//&type=child-device" )
657
+ . body ( Body :: empty ( ) )
658
+ . expect ( "request builder" ) ;
659
+
660
+ let response = app. call ( req) . await . unwrap ( ) ;
661
+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
662
+
663
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
664
+ let entities: Vec < EntityMetadata > = serde_json:: from_slice ( & body) . unwrap ( ) ;
665
+
666
+ let entity_set = entities
667
+ . iter ( )
668
+ . map ( |e| e. topic_id . as_str ( ) )
669
+ . collect :: < HashSet < _ > > ( ) ;
670
+ assert ! ( entity_set. contains( "device/child00//" ) ) ;
671
+ assert ! ( entity_set. contains( "device/child01//" ) ) ;
672
+ }
673
+
674
+ #[ tokio:: test]
675
+ async fn entity_list_empty_query_param ( ) {
676
+ let TestHandle {
677
+ mut app,
678
+ mut entity_store_box,
679
+ } = setup ( ) ;
680
+ // Mock entity store actor response
681
+ tokio:: spawn ( async move {
682
+ while let Some ( mut req) = entity_store_box. recv ( ) . await {
683
+ if let EntityStoreRequest :: List ( _) = req. request {
684
+ req. reply_to
685
+ . send ( EntityStoreResponse :: List ( vec ! [ ] ) )
686
+ . await
687
+ . unwrap ( ) ;
688
+ }
689
+ }
690
+ } ) ;
691
+
692
+ for param in [ "root=" , "parent=" , "type=" ] . into_iter ( ) {
693
+ let uri = format ! ( "/v1/entities?{}" , param) ;
694
+ let req = Request :: builder ( )
695
+ . method ( Method :: GET )
696
+ . uri ( uri)
697
+ . body ( Body :: empty ( ) )
698
+ . expect ( "request builder" ) ;
699
+
700
+ let response = app. call ( req) . await . unwrap ( ) ;
701
+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
702
+ }
703
+
704
+ let req = Request :: builder ( )
705
+ . method ( Method :: GET )
706
+ . uri ( "/v1/entities?root=&parent=&type=" )
707
+ . body ( Body :: empty ( ) )
708
+ . expect ( "request builder" ) ;
709
+
710
+ let response = app. call ( req) . await . unwrap ( ) ;
711
+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
712
+ }
713
+
714
+ #[ tokio:: test]
715
+ async fn entity_list_bad_query_param ( ) {
716
+ let TestHandle {
717
+ mut app,
718
+ entity_store_box : _, // Not used
719
+ } = setup ( ) ;
720
+
721
+ let req = Request :: builder ( )
722
+ . method ( Method :: GET )
723
+ . uri ( "/v1/entities?parent=an/invalid/topic/id/" )
724
+ . body ( Body :: empty ( ) )
725
+ . expect ( "request builder" ) ;
726
+
727
+ let response = app. call ( req) . await . unwrap ( ) ;
728
+ assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
729
+
730
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
731
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
732
+ assert_json_eq ! (
733
+ entity,
734
+ json!( { "error" : "An entity topic identifier has at most 4 segments" } )
735
+ ) ;
736
+ }
737
+
738
+ #[ tokio:: test]
739
+ async fn entity_list_bad_query_parameter_combination ( ) {
740
+ let TestHandle {
741
+ mut app,
742
+ entity_store_box : _, // Not used
743
+ } = setup ( ) ;
744
+
745
+ let req = Request :: builder ( )
746
+ . method ( Method :: GET )
747
+ . uri ( "/v1/entities?root=device/some/topic/id&parent=device/another/topic/id" )
748
+ . body ( Body :: empty ( ) )
749
+ . expect ( "request builder" ) ;
750
+
751
+ let response = app. call ( req) . await . unwrap ( ) ;
752
+ assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
753
+
754
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
755
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
756
+ assert_json_eq ! (
757
+ entity,
758
+ json!( { "error" : "The provided parameters: root and parent are mutually exclusive. Use either one." } )
759
+ ) ;
542
760
}
543
761
544
762
struct TestHandle {
0 commit comments