1use std::{collections::HashMap, sync::Arc};
20
21use async_trait::async_trait;
22use serde_json::Value;
23use symphony::models::{ComponentResultSpec, ComponentSpec, DeploymentSpec};
24use tracing::{debug, error, trace, warn, Level};
25
26use crate::{
27 communication::{RequestHandler, RpcServer, ServiceInvocationError, UPayload},
28 UAttributes, UPayloadFormat,
29};
30
31pub const METHOD_GET_RESOURCE_ID: u16 = 0x0001;
33pub const METHOD_UPDATE_RESOURCE_ID: u16 = 0x0002;
35pub const METHOD_DELETE_RESOURCE_ID: u16 = 0x0003;
37
38pub async fn register_target_provider_endpoints<R: RpcServer, T: DeploymentTarget + 'static>(
53 rpc_server: &R,
54 deployment_target: Arc<T>,
55) -> Result<(), Box<dyn std::error::Error>> {
56 let get_op = Arc::new(GetOperation {
57 target: deployment_target.clone(),
58 });
59 let apply_op = Arc::new(ApplyOperation {
60 target: deployment_target,
61 });
62 rpc_server
63 .register_endpoint(None, METHOD_GET_RESOURCE_ID, get_op)
64 .await
65 .inspect_err(|e| error!("failed to register Get operation on RPC Server: {e}"))?;
66 rpc_server
67 .register_endpoint(None, METHOD_UPDATE_RESOURCE_ID, apply_op.clone())
68 .await
69 .inspect_err(|e| error!("failed to register Update operation on RPC Server: {e}"))?;
70 rpc_server
71 .register_endpoint(None, METHOD_DELETE_RESOURCE_ID, apply_op)
72 .await
73 .inspect_err(|e| error!("failed to register Delete operation on RPC Server: {e}"))?;
74 Ok(())
75}
76
77#[cfg_attr(any(test, feature = "test-util"), mockall::automock)]
78#[async_trait]
79pub trait DeploymentTarget: Send + Sync {
81 async fn get(
93 &self,
94 components: Vec<ComponentSpec>,
95 deployment_spec: DeploymentSpec,
96 ) -> Result<Vec<ComponentSpec>, Box<dyn std::error::Error>>;
97
98 async fn update(
112 &self,
113 components_to_update: Vec<ComponentSpec>,
114 deployment_spec: DeploymentSpec,
115 ) -> Result<HashMap<String, ComponentResultSpec>, Box<dyn std::error::Error>>;
116
117 async fn delete(
131 &self,
132 components_to_delete: Vec<ComponentSpec>,
133 deployment_spec: DeploymentSpec,
134 ) -> Result<HashMap<String, ComponentResultSpec>, Box<dyn std::error::Error>>;
135}
136
137fn extract_request_data(
138 request_payload: Option<UPayload>,
139) -> Result<Value, ServiceInvocationError> {
140 let Some(req_payload) =
141 request_payload.filter(|req_payload| req_payload.payload_format() == UPayloadFormat::Json)
142 else {
143 return Err(ServiceInvocationError::InvalidArgument(
144 "request has no JSON payload".to_string(),
145 ));
146 };
147
148 serde_json::from_slice(req_payload.payload().to_vec().as_slice()).map_err(|err| {
149 debug!("failed to deserialize request payload: {:?}", err);
150 ServiceInvocationError::InvalidArgument(
151 "request payload is not a valid UTF-8 string".to_string(),
152 )
153 })
154}
155
156struct GetOperation<T: DeploymentTarget> {
157 target: Arc<T>,
158}
159
160#[async_trait::async_trait]
161impl<T: DeploymentTarget> RequestHandler for GetOperation<T> {
162 async fn handle_request(
164 &self,
165 _resource_id: u16,
166 message_attributes: &UAttributes,
167 request_payload: Option<UPayload>,
168 ) -> Result<Option<UPayload>, ServiceInvocationError> {
169 let source_uri = message_attributes.source().to_uri(true);
170 if tracing::enabled!(Level::DEBUG) {
171 debug!(source = source_uri, "processing GET request");
172 }
173 let request_data = extract_request_data(request_payload)?;
174 if tracing::enabled!(Level::TRACE) {
175 trace!(
176 source = source_uri,
177 "payload: {}",
178 serde_json::to_string_pretty(&request_data).expect("failed to serialize Value")
179 );
180 }
181 let deployment_spec: DeploymentSpec = request_data
182 .get("deployment")
183 .ok_or_else(|| {
184 debug!(
185 source = source_uri,
186 "request does not contain DeploymentSpec"
187 );
188 ServiceInvocationError::InvalidArgument(
189 "request does not contain DeploymentSpec".to_string(),
190 )
191 })
192 .and_then(|deployment| {
193 serde_json::from_value(deployment.clone()).map_err(|err| {
194 debug!(
195 source = source_uri,
196 "request contains invalid DeploymentSpec: {err}"
197 );
198 ServiceInvocationError::InvalidArgument(
199 "request contains invalid DeploymentSpec".to_string(),
200 )
201 })
202 })?;
203 let component_specs: Vec<ComponentSpec> = request_data
204 .get("components")
205 .ok_or_else(|| {
206 debug!(
207 source = source_uri,
208 "request does not contain ComponentSpec array"
209 );
210 ServiceInvocationError::InvalidArgument(
211 "request does not contain ComponentSpec array".to_string(),
212 )
213 })
214 .and_then(|components| {
215 serde_json::from_value(components.clone()).map_err(|err| {
216 debug!(
217 source = source_uri,
218 "request contains invalid ComponentSpec array: {err}"
219 );
220 ServiceInvocationError::InvalidArgument(
221 "request contains invalid ComponentSpec array".to_string(),
222 )
223 })
224 })?;
225
226 let result = self
227 .target
228 .get(component_specs, deployment_spec)
229 .await
230 .map_err(|err| {
231 warn!(source = source_uri, "error getting component status: {err}");
232 ServiceInvocationError::Internal("failed to get component status".to_string())
233 })?;
234 let serialized_response_data = serde_json::to_vec(&result).map_err(|err| {
235 warn!(
236 source = source_uri,
237 "error serializing ComponentSpec: {err}"
238 );
239 ServiceInvocationError::Internal("failed to create response payload".to_string())
240 })?;
241 if tracing::enabled!(Level::TRACE) {
242 trace!(
243 source = source_uri,
244 "returning response: {}",
245 serde_json::to_string_pretty(&result).expect("failed to serialize Value")
246 );
247 }
248 let response_payload = UPayload::new(serialized_response_data, UPayloadFormat::Json);
249 Ok(Some(response_payload))
250 }
251}
252
253struct ApplyOperation<T: DeploymentTarget> {
254 target: Arc<T>,
255}
256
257#[async_trait::async_trait]
258impl<T: DeploymentTarget> RequestHandler for ApplyOperation<T> {
259 async fn handle_request(
260 &self,
261 resource_id: u16,
262 message_attributes: &UAttributes,
263 request_payload: Option<UPayload>,
264 ) -> Result<Option<UPayload>, ServiceInvocationError> {
265 let source_uri = message_attributes.source().to_uri(true);
266 let sink_uri = message_attributes.sink_unchecked().to_uri(true);
267 if tracing::enabled!(Level::DEBUG) {
268 debug!(source = source_uri, method = sink_uri, "processing request",);
269 }
270 let request_data = extract_request_data(request_payload)?;
271 if tracing::enabled!(Level::TRACE) {
272 let json =
273 serde_json::to_string_pretty(&request_data).expect("failed to serialize Value");
274 trace!("payload: {}", json);
275 }
276
277 let deployment_spec: DeploymentSpec = request_data
278 .get("deployment")
279 .ok_or_else(|| {
280 debug!(
281 source = source_uri,
282 method = sink_uri,
283 "request does not contain DeploymentSpec"
284 );
285 ServiceInvocationError::InvalidArgument(
286 "request does not contain DeploymentSpec".to_string(),
287 )
288 })
289 .and_then(|deployment| {
290 serde_json::from_value(deployment.clone()).map_err(|err| {
291 debug!(
292 source = source_uri,
293 method = sink_uri,
294 "request contains invalid DeploymentSpec: {err}"
295 );
296 ServiceInvocationError::InvalidArgument(
297 "request contains invalid DeploymentSpec".to_string(),
298 )
299 })
300 })?;
301
302 let affected_components: Vec<ComponentSpec> = request_data
303 .get("components")
304 .ok_or_else(|| {
305 debug!(
306 source = source_uri,
307 method = sink_uri,
308 "request does not contain ComponentSpec array"
309 );
310 ServiceInvocationError::InvalidArgument(
311 "request does not contain ComponentSpec array".to_string(),
312 )
313 })
314 .and_then(|components| {
315 serde_json::from_value(components.clone()).map_err(|err| {
316 debug!(
317 source = source_uri,
318 method = sink_uri,
319 "request contains invalid ComponentSpec array: {err}"
320 );
321 ServiceInvocationError::InvalidArgument(
322 "request contains invalid ComponentSpec array".to_string(),
323 )
324 })
325 })?;
326
327 let result = match resource_id {
328 METHOD_UPDATE_RESOURCE_ID => self
329 .target
330 .update(affected_components, deployment_spec)
331 .await
332 .map_err(|err| {
333 warn!(
334 source = source_uri,
335 method = sink_uri,
336 "error updating components: {err}"
337 );
338 ServiceInvocationError::Internal("failed to update components".to_string())
339 }),
340 METHOD_DELETE_RESOURCE_ID => self
341 .target
342 .delete(affected_components, deployment_spec)
343 .await
344 .map_err(|err| {
345 warn!(
346 source = source_uri,
347 method = sink_uri,
348 "error deleting components: {err}"
349 );
350 ServiceInvocationError::Internal("failed to delete components".to_string())
351 }),
352 _ => {
353 return Err(ServiceInvocationError::Unimplemented(
354 "no such operation".to_string(),
355 ));
356 }
357 }?;
358
359 let serialized_response_data = serde_json::to_vec(&result).map_err(|err| {
360 warn!(
361 source = source_uri,
362 method = sink_uri,
363 "error serializing HashMap: {err}"
364 );
365 ServiceInvocationError::Internal("failed to create response payload".to_string())
366 })?;
367
368 let response_payload = UPayload::new(serialized_response_data, UPayloadFormat::Json);
369 Ok(Some(response_payload))
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use serde_json::json;
376
377 use crate::{communication::MockRpcServerImpl, UUri, UUID};
378
379 use super::*;
380
381 #[tokio::test]
382 async fn test_register_target_provider_endpoints_fails() {
383 let mut rpc_server = MockRpcServerImpl::new();
384 rpc_server
385 .expect_do_register_endpoint()
386 .returning(|_, _, _| {
387 Err(crate::communication::RegistrationError::MaxListenersExceeded)
388 });
389 let deployment_target = MockDeploymentTarget::new();
390
391 assert!(
392 register_target_provider_endpoints(&rpc_server, Arc::new(deployment_target))
393 .await
394 .is_err_and(|e| matches!(
395 e.downcast_ref(),
396 Some(crate::communication::RegistrationError::MaxListenersExceeded)
397 ))
398 );
399 }
400
401 #[tokio::test]
402 async fn test_register_target_provider_endpoints_succeeds() {
403 let mut rpc_server = MockRpcServerImpl::new();
404 rpc_server
405 .expect_do_register_endpoint()
406 .returning(|_, _, _| Ok(()));
407 let deployment_target = MockDeploymentTarget::new();
408
409 assert!(
410 register_target_provider_endpoints(&rpc_server, Arc::new(deployment_target))
411 .await
412 .is_ok()
413 );
414 }
415
416 fn create_method_uri(resource_id: u16) -> UUri {
417 UUri::try_from_parts("authority", 0x10AA2, 0x01, resource_id)
418 .expect("failed to create method URI")
419 }
420
421 fn create_request_attributes(resource_id: u16) -> UAttributes {
422 UAttributes {
423 commstatus: None,
424 id: UUID::build(),
425 source: UUri::try_from_parts("authority", 0x10AA1, 0x01, 0x0000)
426 .expect("failed to create source URI"),
427 sink: Some(create_method_uri(resource_id)),
428 payload_format: Some(UPayloadFormat::Json),
429 type_: crate::UMessageType::Request,
430 priority: Some(crate::UPriority::CS4),
431 permission_level: None,
432 ttl: Some(5000),
433 traceparent: None,
434 token: None,
435 reqid: None,
436 payload_encoding_registry_id: None,
437 payload_encoding: None,
438 payload_content_type: None,
439 }
440 }
441
442 #[tokio::test]
443 async fn test_endpoints_delegate_to_deployment_target() {
444 let mut mock_target = MockDeploymentTarget::default();
445 mock_target
446 .expect_get()
447 .once()
448 .returning(move |_, _| Ok(vec![]));
449 mock_target
450 .expect_update()
451 .once()
452 .returning(move |_, _| Ok(HashMap::new()));
453 mock_target
454 .expect_delete()
455 .once()
456 .returning(move |_, _| Ok(HashMap::new()));
457 let target = Arc::new(mock_target);
458 let get_op = Arc::new(GetOperation {
459 target: target.clone(),
460 });
461 let apply_op = Arc::new(ApplyOperation {
462 target: target.clone(),
463 });
464
465 let request_data = json!({
466 "deployment": DeploymentSpec::empty(),
467 "components": []
468 });
469 let payload = UPayload::new(
470 serde_json::to_vec(&request_data).expect("failed to create request payload"),
471 UPayloadFormat::Json,
472 );
473 assert!(get_op
474 .handle_request(
475 METHOD_GET_RESOURCE_ID,
476 &create_request_attributes(METHOD_GET_RESOURCE_ID),
477 Some(payload.clone()),
478 )
479 .await
480 .is_ok());
481 assert!(apply_op
482 .handle_request(
483 METHOD_UPDATE_RESOURCE_ID,
484 &create_request_attributes(METHOD_UPDATE_RESOURCE_ID),
485 Some(payload.clone()),
486 )
487 .await
488 .is_ok());
489 assert!(apply_op
490 .handle_request(
491 METHOD_DELETE_RESOURCE_ID,
492 &create_request_attributes(METHOD_DELETE_RESOURCE_ID),
493 Some(payload.clone()),
494 )
495 .await
496 .is_ok());
497 }
498}