11
11
use super :: server:: AgentState ;
12
12
use crate :: entity_manager:: server:: EntityStoreRequest ;
13
13
use crate :: entity_manager:: server:: EntityStoreResponse ;
14
+ use crate :: entity_manager:: server:: EntityTwinData ;
15
+ use crate :: entity_manager:: server:: InvalidTwinData ;
14
16
use axum:: extract:: Path ;
15
17
use axum:: extract:: Query ;
16
18
use axum:: extract:: State ;
@@ -23,6 +25,8 @@ use axum::Router;
23
25
use hyper:: StatusCode ;
24
26
use serde:: Deserialize ;
25
27
use serde_json:: json;
28
+ use serde_json:: Map ;
29
+ use serde_json:: Value ;
26
30
use std:: str:: FromStr ;
27
31
use tedge_api:: entity:: EntityMetadata ;
28
32
use tedge_api:: entity:: InvalidEntityType ;
@@ -96,7 +100,7 @@ enum Error {
96
100
#[ error( transparent) ]
97
101
EntityStoreError ( #[ from] entity_store:: Error ) ,
98
102
99
- #[ error( "Entity not found with topic id: {0}" ) ]
103
+ #[ error( "Entity with topic id: {0} not found " ) ]
100
104
EntityNotFound ( EntityTopicId ) ,
101
105
102
106
#[ allow( clippy:: enum_variant_names) ]
@@ -108,6 +112,9 @@ enum Error {
108
112
109
113
#[ error( transparent) ]
110
114
InvalidInput ( #[ from] InputValidationError ) ,
115
+
116
+ #[ error( transparent) ]
117
+ InvalidTwinData ( #[ from] InvalidTwinData ) ,
111
118
}
112
119
113
120
impl IntoResponse for Error {
@@ -123,6 +130,7 @@ impl IntoResponse for Error {
123
130
Error :: ChannelError ( _) => StatusCode :: INTERNAL_SERVER_ERROR ,
124
131
Error :: InvalidEntityStoreResponse => StatusCode :: INTERNAL_SERVER_ERROR ,
125
132
Error :: InvalidInput ( _) => StatusCode :: BAD_REQUEST ,
133
+ Error :: InvalidTwinData ( _) => StatusCode :: BAD_REQUEST ,
126
134
} ;
127
135
let error_message = self . to_string ( ) ;
128
136
@@ -135,7 +143,9 @@ pub(crate) fn entity_store_router(state: AgentState) -> Router {
135
143
. route ( "/v1/entities" , post ( register_entity) . get ( list_entities) )
136
144
. route (
137
145
"/v1/entities/{*path}" ,
138
- get ( get_entity) . delete ( deregister_entity) ,
146
+ get ( get_entity)
147
+ . patch ( patch_entity)
148
+ . delete ( deregister_entity) ,
139
149
)
140
150
. with_state ( state)
141
151
}
@@ -160,6 +170,29 @@ async fn register_entity(
160
170
) )
161
171
}
162
172
173
+ async fn patch_entity (
174
+ State ( state) : State < AgentState > ,
175
+ Path ( path) : Path < String > ,
176
+ Json ( twin_fragments) : Json < Map < String , Value > > ,
177
+ ) -> impl IntoResponse {
178
+ let topic_id = EntityTopicId :: from_str ( & path) ?;
179
+ let twin_data = EntityTwinData :: try_new ( topic_id, twin_fragments) ?;
180
+
181
+ let response = state
182
+ . entity_store_handle
183
+ . clone ( )
184
+ . await_response ( EntityStoreRequest :: Patch ( twin_data) )
185
+ . await ?;
186
+ let EntityStoreResponse :: Patch ( res) = response else {
187
+ return Err ( Error :: InvalidEntityStoreResponse ) ;
188
+ } ;
189
+ res?;
190
+
191
+ let entity = get_entity ( State ( state) , Path ( path) ) . await ?;
192
+
193
+ Ok ( entity)
194
+ }
195
+
163
196
async fn get_entity (
164
197
State ( state) : State < AgentState > ,
165
198
Path ( path) : Path < String > ,
@@ -328,7 +361,7 @@ mod tests {
328
361
let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
329
362
assert_json_eq ! (
330
363
entity,
331
- json!( { "error" : "Entity not found with topic id: device/test-child//" } )
364
+ json!( { "error" : "Entity with topic id: device/test-child// not found " } )
332
365
) ;
333
366
}
334
367
@@ -483,6 +516,127 @@ mod tests {
483
516
) ;
484
517
}
485
518
519
+ #[ tokio:: test]
520
+ async fn entity_patch ( ) {
521
+ let TestHandle {
522
+ mut app,
523
+ mut entity_store_box,
524
+ } = setup ( ) ;
525
+
526
+ // Mock entity store actor response for patch
527
+ tokio:: spawn ( async move {
528
+ while let Some ( mut req) = entity_store_box. recv ( ) . await {
529
+ if let EntityStoreRequest :: Patch ( twin_data) = req. request {
530
+ if twin_data. topic_id
531
+ == EntityTopicId :: default_child_device ( "test-child" ) . unwrap ( )
532
+ {
533
+ req. reply_to
534
+ . send ( EntityStoreResponse :: Patch ( Ok ( ( ) ) ) )
535
+ . await
536
+ . unwrap ( ) ;
537
+ }
538
+ } else if let EntityStoreRequest :: Get ( topic_id) = req. request {
539
+ if topic_id == EntityTopicId :: default_child_device ( "test-child" ) . unwrap ( ) {
540
+ let mut entity =
541
+ EntityMetadata :: child_device ( "test-child" . to_string ( ) ) . unwrap ( ) ;
542
+ entity. twin_data . insert ( "foo" . to_string ( ) , json ! ( "bar" ) ) ;
543
+
544
+ req. reply_to
545
+ . send ( EntityStoreResponse :: Get ( Some ( entity) ) )
546
+ . await
547
+ . unwrap ( ) ;
548
+ }
549
+ }
550
+ }
551
+ } ) ;
552
+
553
+ let twin_payload = json ! ( { "foo" : "bar" } ) . to_string ( ) ;
554
+
555
+ let req = Request :: builder ( )
556
+ . method ( Method :: PATCH )
557
+ . uri ( "/v1/entities/device/test-child//" )
558
+ . header ( "Content-Type" , "application/json" )
559
+ . body ( Body :: from ( twin_payload) )
560
+ . expect ( "request builder" ) ;
561
+
562
+ let response = app. call ( req) . await . unwrap ( ) ;
563
+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
564
+
565
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
566
+ let entity: EntityMetadata = serde_json:: from_slice ( & body) . unwrap ( ) ;
567
+ assert_eq ! ( entity. twin_data. get( "foo" ) , Some ( & json!( "bar" ) ) ) ;
568
+ }
569
+
570
+ #[ tokio:: test]
571
+ async fn entity_patch_invalid_key ( ) {
572
+ let TestHandle {
573
+ mut app,
574
+ entity_store_box : _, // Not used
575
+ } = setup ( ) ;
576
+
577
+ let req = Request :: builder ( )
578
+ . method ( Method :: PATCH )
579
+ . uri ( "/v1/entities/device/test-child//" )
580
+ . header ( "Content-Type" , "application/json" )
581
+ . body ( Body :: from ( r#"{"@id": "new-id"}"# ) )
582
+ . expect ( "request builder" ) ;
583
+
584
+ let response = app. call ( req) . await . unwrap ( ) ;
585
+ assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
586
+
587
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
588
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
589
+ assert_json_eq ! (
590
+ entity,
591
+ json!( { "error" : "Fragment keys starting with '@' are not allowed as twin data" } )
592
+ ) ;
593
+ }
594
+
595
+ #[ tokio:: test]
596
+ async fn patch_unknown_entity ( ) {
597
+ let TestHandle {
598
+ mut app,
599
+ mut entity_store_box,
600
+ } = setup ( ) ;
601
+
602
+ // Mock entity store actor response
603
+ tokio:: spawn ( async move {
604
+ if let Some ( mut req) = entity_store_box. recv ( ) . await {
605
+ if let EntityStoreRequest :: Patch ( twin_data) = req. request {
606
+ if twin_data. topic_id
607
+ == EntityTopicId :: default_child_device ( "test-child" ) . unwrap ( )
608
+ {
609
+ req. reply_to
610
+ . send ( EntityStoreResponse :: Patch ( Err (
611
+ entity_store:: Error :: UnknownEntity (
612
+ "device/test-child//" . to_string ( ) ,
613
+ ) ,
614
+ ) ) )
615
+ . await
616
+ . unwrap ( ) ;
617
+ }
618
+ }
619
+ }
620
+ } ) ;
621
+
622
+ let req = Request :: builder ( )
623
+ . method ( Method :: PATCH )
624
+ . uri ( "/v1/entities/device/test-child//" )
625
+ . header ( "Content-Type" , "application/json" )
626
+ . body ( Body :: from ( r#"{"foo": "bar"}"# ) )
627
+ . expect ( "request builder" ) ;
628
+
629
+ let response = app. call ( req) . await . unwrap ( ) ;
630
+ assert_eq ! ( response. status( ) , StatusCode :: NOT_FOUND ) ;
631
+
632
+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
633
+ let entity: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
634
+ assert_json_eq ! (
635
+ entity,
636
+ json!( { "error" : "The specified entity: device/test-child// does not exist in the store" } )
637
+ ) ;
638
+ }
639
+
486
640
#[ tokio:: test]
487
641
async fn entity_delete ( ) {
488
642
let TestHandle {
0 commit comments