1use std::sync::Arc;
14
15use async_trait::async_trait;
16
17use crate::{
18 communication::{CallOptions, RpcClient},
19 core::udiscovery::{
20 TopicInfo, UDiscovery, RESOURCE_ID_FIND_SERVICES, RESOURCE_ID_GET_SERVICE_TOPICS,
21 UDISCOVERY_TYPE_ID, UDISCOVERY_VERSION_MAJOR,
22 },
23 up_core_api::{
24 udiscovery::{
25 FindServicesRequest, FindServicesResponse, GetServiceTopicsRequest,
26 GetServiceTopicsResponse, ServiceTopicInfo,
27 },
28 uri::UUri as UUriProto,
29 },
30 UCode, UStatus, UUri,
31};
32
33fn udiscovery_uri(resource_id: u16) -> UUri {
35 UUri::try_from_parts(
36 "",
37 UDISCOVERY_TYPE_ID,
38 UDISCOVERY_VERSION_MAJOR,
39 resource_id,
40 )
41 .unwrap()
42}
43
44impl TryFrom<&ServiceTopicInfo> for TopicInfo {
45 type Error = UStatus;
46
47 fn try_from(service_topic_info_proto: &ServiceTopicInfo) -> Result<Self, Self::Error> {
48 let Some(topic_proto) = service_topic_info_proto.topic.as_ref() else {
49 return Err(UStatus::fail_with_code(
50 UCode::InvalidArgument,
51 "Service returned invalid ServiceTopicInfo: no topic",
52 ));
53 };
54 let Ok(topic) = UUri::try_from(topic_proto) else {
55 return Err(UStatus::fail_with_code(
56 UCode::InvalidArgument,
57 "Service returned invalid ServiceTopicInfo: malformed topic URI",
58 ));
59 };
60 let Some(uservice_topic) = service_topic_info_proto.info.as_ref() else {
61 return Err(UStatus::fail_with_code(
62 UCode::InvalidArgument,
63 "Service returned invalid ServiceTopicInfo: no info object",
64 ));
65 };
66 Ok(TopicInfo {
67 topic,
68 message_type: uservice_topic.message.clone(),
69 permission_level: uservice_topic.permission_level,
70 ttl: service_topic_info_proto.ttl,
71 })
72 }
73}
74
75pub struct RpcClientUDiscovery {
79 rpc_client: Arc<dyn RpcClient>,
80}
81
82impl core::fmt::Debug for RpcClientUDiscovery {
83 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84 f.debug_struct("RpcClientUDiscovery")
85 .finish_non_exhaustive()
86 }
87}
88
89impl RpcClientUDiscovery {
90 pub fn new(rpc_client: Arc<dyn RpcClient>) -> Self {
96 RpcClientUDiscovery { rpc_client }
97 }
98
99 fn default_call_options() -> CallOptions {
100 CallOptions::for_rpc_request(5_000, None, None, None)
101 }
102}
103
104#[async_trait]
105impl UDiscovery for RpcClientUDiscovery {
106 async fn find_services(
107 &self,
108 uri_pattern: UUri,
109 recursive: bool,
110 ) -> Result<Vec<UUri>, UStatus> {
111 let request_message = FindServicesRequest {
112 uri: Some(UUriProto::from(&uri_pattern)).into(),
113 recursive,
114 ..Default::default()
115 };
116 self.rpc_client
117 .invoke_proto_method::<_, FindServicesResponse>(
118 udiscovery_uri(RESOURCE_ID_FIND_SERVICES),
119 Self::default_call_options(),
120 request_message,
121 )
122 .await
123 .and_then(|response_message| {
124 let mut result = vec![];
125 if let Some(uri_batch) = response_message.uris.as_ref() {
126 for uri in &uri_batch.uris {
127 result.push(UUri::try_from(uri).map_err(|_| {
128 UStatus::fail_with_code(
129 UCode::InvalidArgument,
130 "Service returned invalid URI in FindServicesResponse",
131 )
132 })?);
133 }
134 }
135 Ok(result)
136 })
137 .map_err(UStatus::from)
138 }
139
140 async fn get_service_topics(
141 &self,
142 topic_pattern: UUri,
143 recursive: bool,
144 ) -> Result<Vec<TopicInfo>, UStatus> {
145 let request_message = GetServiceTopicsRequest {
146 topic: Some(UUriProto::from(&topic_pattern)).into(),
147 recursive,
148 ..Default::default()
149 };
150 self.rpc_client
151 .invoke_proto_method::<_, GetServiceTopicsResponse>(
152 udiscovery_uri(RESOURCE_ID_GET_SERVICE_TOPICS),
153 Self::default_call_options(),
154 request_message,
155 )
156 .await
157 .and_then(|response_message| {
158 let mut result = vec![];
159 for topic in response_message.topics {
160 result.push(TopicInfo::try_from(&topic)?);
161 }
162 Ok(result)
163 })
164 .map_err(UStatus::from)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use mockall::Sequence;
171
172 use super::*;
173 use crate::{
174 communication::{MockRpcClient, UPayload},
175 up_core_api::{udiscovery::ServiceTopicInfo, uoptions::UServiceTopic, uri::UUriBatch},
176 UCode, UUri,
177 };
178 use std::sync::Arc;
179
180 #[tokio::test]
181 async fn test_find_services_invokes_rpc_client() {
182 let service_pattern_uri = UUri::try_from_parts("other", 0xFFFF_D5A3, 0x01, 0xFFFF)
183 .expect("failed to create service pattern URI");
184 let request = FindServicesRequest {
185 uri: Some(UUriProto::from(&service_pattern_uri)).into(),
186 ..Default::default()
187 };
188 let expected_request = request.clone();
189 let mut rpc_client = MockRpcClient::new();
190 let mut seq = Sequence::new();
191 rpc_client
192 .expect_invoke_method()
193 .once()
194 .in_sequence(&mut seq)
195 .withf(|method, _options, payload| {
196 method == &udiscovery_uri(RESOURCE_ID_FIND_SERVICES) && payload.is_some()
197 })
198 .return_const(Err(crate::communication::ServiceInvocationError::Internal(
199 "internal error".to_string(),
200 )));
201 rpc_client
202 .expect_invoke_method()
203 .once()
204 .in_sequence(&mut seq)
205 .withf(move |method, _options, payload| {
206 let request = payload
207 .to_owned()
208 .unwrap()
209 .extract_protobuf::<FindServicesRequest>()
210 .unwrap();
211 request == expected_request && method == &udiscovery_uri(RESOURCE_ID_FIND_SERVICES)
212 })
213 .returning(move |_method, _options, _payload| {
214 let response = FindServicesResponse {
215 uris: Some(UUriBatch {
216 uris: vec![UUriProto {
217 authority_name: "other".to_string(),
218 ue_id: 0x0004_D5A3,
219 ue_version_major: 0x01,
220 resource_id: 0xD3FE,
221 ..Default::default()
222 }],
223 ..Default::default()
224 })
225 .into(),
226 ..Default::default()
227 };
228 Ok(Some(UPayload::try_from_protobuf(response).unwrap()))
229 });
230
231 let udiscovery_client = RpcClientUDiscovery::new(Arc::new(rpc_client));
232
233 assert!(udiscovery_client
234 .find_services(service_pattern_uri.clone(), false)
235 .await
236 .is_err_and(|e| e.code() == UCode::Internal));
237 assert!(udiscovery_client
238 .find_services(service_pattern_uri.clone(), false)
239 .await
240 .is_ok_and(
241 |result| result.len() == 1 && service_pattern_uri.matches(result.first().unwrap())
242 ));
243 }
244
245 #[tokio::test]
246 async fn test_get_service_topics_invokes_rpc_client() {
247 let topic_pattern_uri = UUri::try_from_parts("*", 0xFFFF_D5A3, 0x01, 0xFFFF)
248 .expect("failed to create topic pattern URI");
249 let request = GetServiceTopicsRequest {
250 topic: Some((&topic_pattern_uri).into()).into(),
251 ..Default::default()
252 };
253 let expected_request = request.clone();
254 let mut rpc_client = MockRpcClient::new();
255 let mut seq = Sequence::new();
256 rpc_client
257 .expect_invoke_method()
258 .once()
259 .in_sequence(&mut seq)
260 .withf(|method, _options, payload| {
261 method == &udiscovery_uri(RESOURCE_ID_GET_SERVICE_TOPICS) && payload.is_some()
262 })
263 .return_const(Err(crate::communication::ServiceInvocationError::Internal(
264 "internal error".to_string(),
265 )));
266 rpc_client
267 .expect_invoke_method()
268 .once()
269 .in_sequence(&mut seq)
270 .withf(move |method, _options, payload| {
271 let request = payload
272 .to_owned()
273 .unwrap()
274 .extract_protobuf::<GetServiceTopicsRequest>()
275 .unwrap();
276 request == expected_request
277 && method == &udiscovery_uri(RESOURCE_ID_GET_SERVICE_TOPICS)
278 })
279 .returning(move |_method, _options, _payload| {
280 let topic_info = ServiceTopicInfo {
281 topic: Some(UUriProto {
282 authority_name: "other".to_string(),
283 ue_id: 0x0004_D5A3,
284 ue_version_major: 0x01,
285 resource_id: 0xD3FE,
286 ..Default::default()
287 })
288 .into(),
289 info: Some(UServiceTopic {
290 id: 0x9000,
291 name: "TestTopic".to_string(),
292 message: "TestTopicMessage".to_string(),
293 ..Default::default()
294 })
295 .into(),
296 ttl: 600,
297 ..Default::default()
298 };
299 let response = GetServiceTopicsResponse {
300 topics: vec![topic_info],
301 ..Default::default()
302 };
303 Ok(Some(UPayload::try_from_protobuf(response).unwrap()))
304 });
305
306 let udiscovery_client = RpcClientUDiscovery::new(Arc::new(rpc_client));
307
308 assert!(udiscovery_client
309 .get_service_topics(topic_pattern_uri.clone(), false)
310 .await
311 .is_err_and(|e| e.code() == UCode::Internal));
312 assert!(udiscovery_client
313 .get_service_topics(topic_pattern_uri.clone(), false)
314 .await
315 .is_ok_and(|result| result.len() == 1
316 && topic_pattern_uri.matches(result.first().map(|r| r.topic()).unwrap())));
317 }
318}