1extern crate alloc;
23
24use alloc::string::{String, ToString};
25use alloc::vec::Vec;
26
27use zerodds_idl::ast::{Annotation, AnnotationParams, ConstExpr, LiteralKind, NamedParam};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum RpcAnnotation {
32 Service {
34 name: Option<String>,
36 },
37 Oneway,
39 In,
41 Out,
43 InOut,
45 InterfaceQos {
50 profile_ref: String,
52 },
53 DdsRequestTopic(String),
56 DdsReplyTopic(String),
58 RpcRequestType,
61 RpcReplyType,
63 RpcRequest,
66 RpcReply,
68}
69
70#[derive(Debug, Clone, PartialEq, Default)]
73pub struct LoweredRpc {
74 pub builtins: Vec<RpcAnnotation>,
76 pub custom: Vec<Annotation>,
78}
79
80impl LoweredRpc {
81 #[must_use]
83 pub fn is_service(&self) -> bool {
84 self.builtins
85 .iter()
86 .any(|a| matches!(a, RpcAnnotation::Service { .. }))
87 }
88
89 #[must_use]
91 pub fn service_name(&self) -> Option<&str> {
92 self.builtins.iter().find_map(|a| match a {
93 RpcAnnotation::Service { name: Some(n) } => Some(n.as_str()),
94 _ => None,
95 })
96 }
97
98 #[must_use]
100 pub fn has_oneway(&self) -> bool {
101 self.builtins
102 .iter()
103 .any(|a| matches!(a, RpcAnnotation::Oneway))
104 }
105
106 #[must_use]
108 pub fn interface_qos_profile(&self) -> Option<&str> {
109 self.builtins.iter().find_map(|a| match a {
110 RpcAnnotation::InterfaceQos { profile_ref } => Some(profile_ref.as_str()),
111 _ => None,
112 })
113 }
114
115 #[must_use]
117 pub fn dds_request_topic(&self) -> Option<&str> {
118 self.builtins.iter().find_map(|a| match a {
119 RpcAnnotation::DdsRequestTopic(t) => Some(t.as_str()),
120 _ => None,
121 })
122 }
123
124 #[must_use]
126 pub fn dds_reply_topic(&self) -> Option<&str> {
127 self.builtins.iter().find_map(|a| match a {
128 RpcAnnotation::DdsReplyTopic(t) => Some(t.as_str()),
129 _ => None,
130 })
131 }
132
133 #[must_use]
135 pub fn is_rpc_request_type(&self) -> bool {
136 self.builtins
137 .iter()
138 .any(|a| matches!(a, RpcAnnotation::RpcRequestType))
139 }
140
141 #[must_use]
143 pub fn is_rpc_reply_type(&self) -> bool {
144 self.builtins
145 .iter()
146 .any(|a| matches!(a, RpcAnnotation::RpcReplyType))
147 }
148
149 #[must_use]
151 pub fn is_rpc_request(&self) -> bool {
152 self.builtins
153 .iter()
154 .any(|a| matches!(a, RpcAnnotation::RpcRequest))
155 }
156
157 #[must_use]
159 pub fn is_rpc_reply(&self) -> bool {
160 self.builtins
161 .iter()
162 .any(|a| matches!(a, RpcAnnotation::RpcReply))
163 }
164}
165
166fn name_tail(a: &Annotation) -> &str {
167 a.name
168 .parts
169 .last()
170 .map(|p| p.text.as_str())
171 .unwrap_or_default()
172}
173
174fn const_to_string(expr: &ConstExpr) -> Option<String> {
175 if let ConstExpr::Literal(l) = expr {
176 let s = l.raw.as_str();
177 if matches!(l.kind, LiteralKind::String) {
178 return Some(
179 s.strip_prefix('"')
180 .and_then(|s| s.strip_suffix('"'))
181 .unwrap_or(s)
182 .to_string(),
183 );
184 }
185 return Some(s.to_string());
186 }
187 None
188}
189
190fn extract_named<'a>(named: &'a [NamedParam], key: &str) -> Option<&'a ConstExpr> {
191 named.iter().find(|p| p.name.text == key).map(|p| &p.value)
192}
193
194#[must_use]
198pub fn lower_single(ann: &Annotation) -> Option<RpcAnnotation> {
199 let name = name_tail(ann);
200 match name {
201 "service" => {
202 let svc_name = match &ann.params {
203 AnnotationParams::None | AnnotationParams::Empty => None,
204 AnnotationParams::Single(e) => const_to_string(e),
205 AnnotationParams::Named(named) => {
206 extract_named(named, "name").and_then(const_to_string)
207 }
208 };
209 Some(RpcAnnotation::Service { name: svc_name })
210 }
211 "oneway" => Some(RpcAnnotation::Oneway),
212 "in" => Some(RpcAnnotation::In),
213 "out" => Some(RpcAnnotation::Out),
214 "inout" => Some(RpcAnnotation::InOut),
215 "RPCInterfaceQos" => {
216 let profile_ref = match &ann.params {
217 AnnotationParams::Single(e) => const_to_string(e),
218 AnnotationParams::Named(named) => {
219 extract_named(named, "profile").and_then(const_to_string)
220 }
221 _ => None,
222 }?;
223 Some(RpcAnnotation::InterfaceQos { profile_ref })
224 }
225 "DDSRequestTopic" => {
226 let topic = match &ann.params {
227 AnnotationParams::Single(e) => const_to_string(e),
228 AnnotationParams::Named(named) => {
229 extract_named(named, "name").and_then(const_to_string)
230 }
231 _ => None,
232 }?;
233 Some(RpcAnnotation::DdsRequestTopic(topic))
234 }
235 "DDSReplyTopic" => {
236 let topic = match &ann.params {
237 AnnotationParams::Single(e) => const_to_string(e),
238 AnnotationParams::Named(named) => {
239 extract_named(named, "name").and_then(const_to_string)
240 }
241 _ => None,
242 }?;
243 Some(RpcAnnotation::DdsReplyTopic(topic))
244 }
245 "RPCRequestType" => Some(RpcAnnotation::RpcRequestType),
246 "RPCReplyType" => Some(RpcAnnotation::RpcReplyType),
247 "RPCRequest" => Some(RpcAnnotation::RpcRequest),
248 "RPCReply" => Some(RpcAnnotation::RpcReply),
249 _ => None,
250 }
251}
252
253#[must_use]
255pub fn lower_rpc_annotations(anns: &[Annotation]) -> LoweredRpc {
256 let mut out = LoweredRpc::default();
257 for a in anns {
258 match lower_single(a) {
259 Some(b) => out.builtins.push(b),
260 None => out.custom.push(a.clone()),
261 }
262 }
263 out
264}
265
266#[cfg(test)]
267#[allow(clippy::unwrap_used, clippy::expect_used)]
268mod tests {
269 use super::*;
270 use zerodds_idl::ast::{Identifier, Literal, ScopedName};
271 use zerodds_idl::errors::Span;
272
273 fn sp() -> Span {
274 Span::SYNTHETIC
275 }
276
277 fn ident(t: &str) -> Identifier {
278 Identifier::new(t, sp())
279 }
280
281 fn scoped(parts: &[&str]) -> ScopedName {
282 ScopedName {
283 absolute: false,
284 parts: parts.iter().map(|p| ident(p)).collect(),
285 span: sp(),
286 }
287 }
288
289 fn lit(kind: LiteralKind, raw: &str) -> ConstExpr {
290 ConstExpr::Literal(Literal {
291 kind,
292 raw: raw.to_string(),
293 span: sp(),
294 })
295 }
296
297 fn ann(name: &str, params: AnnotationParams) -> Annotation {
298 Annotation {
299 name: scoped(&[name]),
300 params,
301 span: sp(),
302 }
303 }
304
305 #[test]
306 fn service_no_args_lowers_without_name() {
307 let a = lower_single(&ann("service", AnnotationParams::None));
308 assert_eq!(a, Some(RpcAnnotation::Service { name: None }));
309 }
310
311 #[test]
312 fn service_named_arg_lowers_with_name() {
313 let a = lower_single(&ann(
314 "service",
315 AnnotationParams::Named(alloc::vec![NamedParam {
316 name: ident("name"),
317 value: lit(LiteralKind::String, "\"Calculator\""),
318 span: sp(),
319 }]),
320 ));
321 assert_eq!(
322 a,
323 Some(RpcAnnotation::Service {
324 name: Some("Calculator".into())
325 })
326 );
327 }
328
329 #[test]
330 fn service_single_string_arg_lowers_with_name() {
331 let a = lower_single(&ann(
333 "service",
334 AnnotationParams::Single(lit(LiteralKind::String, "\"Foo\"")),
335 ));
336 assert_eq!(
337 a,
338 Some(RpcAnnotation::Service {
339 name: Some("Foo".into())
340 })
341 );
342 }
343
344 #[test]
345 fn service_named_arg_with_unknown_key_yields_none_name() {
346 let a = lower_single(&ann(
347 "service",
348 AnnotationParams::Named(alloc::vec![NamedParam {
349 name: ident("ignored"),
350 value: lit(LiteralKind::String, "\"X\""),
351 span: sp(),
352 }]),
353 ));
354 assert_eq!(a, Some(RpcAnnotation::Service { name: None }));
355 }
356
357 #[test]
358 fn oneway_lowers() {
359 assert_eq!(
360 lower_single(&ann("oneway", AnnotationParams::None)),
361 Some(RpcAnnotation::Oneway)
362 );
363 }
364
365 #[test]
366 fn in_out_inout_lower() {
367 assert_eq!(
368 lower_single(&ann("in", AnnotationParams::None)),
369 Some(RpcAnnotation::In)
370 );
371 assert_eq!(
372 lower_single(&ann("out", AnnotationParams::None)),
373 Some(RpcAnnotation::Out)
374 );
375 assert_eq!(
376 lower_single(&ann("inout", AnnotationParams::None)),
377 Some(RpcAnnotation::InOut)
378 );
379 }
380
381 #[test]
382 fn unknown_annotation_returns_none() {
383 let a = lower_single(&ann("xtypes_builtin", AnnotationParams::None));
384 assert!(a.is_none());
385 }
386
387 #[test]
388 fn lower_rpc_annotations_splits_builtins_and_custom() {
389 let anns = alloc::vec![
390 ann("service", AnnotationParams::None),
391 ann("topic", AnnotationParams::None), ann("oneway", AnnotationParams::None),
393 ];
394 let lowered = lower_rpc_annotations(&anns);
395 assert_eq!(lowered.builtins.len(), 2);
396 assert_eq!(lowered.custom.len(), 1);
397 assert!(lowered.is_service());
398 assert!(lowered.has_oneway());
399 }
400
401 #[test]
402 fn service_name_resolved_via_helper() {
403 let anns = alloc::vec![ann(
404 "service",
405 AnnotationParams::Named(alloc::vec![NamedParam {
406 name: ident("name"),
407 value: lit(LiteralKind::String, "\"Bar\""),
408 span: sp(),
409 }])
410 )];
411 let lowered = lower_rpc_annotations(&anns);
412 assert_eq!(lowered.service_name(), Some("Bar"));
413 }
414
415 #[test]
416 fn service_without_name_yields_none_helper() {
417 let anns = alloc::vec![ann("service", AnnotationParams::None)];
418 let lowered = lower_rpc_annotations(&anns);
419 assert!(lowered.is_service());
420 assert_eq!(lowered.service_name(), None);
421 }
422
423 #[test]
426 fn rpc_interface_qos_with_named_profile_lowers() {
427 let a = lower_single(&ann(
428 "RPCInterfaceQos",
429 AnnotationParams::Named(alloc::vec![NamedParam {
430 name: ident("profile"),
431 value: lit(LiteralKind::String, "\"Lib1::Reliable\""),
432 span: sp(),
433 }]),
434 ));
435 assert_eq!(
436 a,
437 Some(RpcAnnotation::InterfaceQos {
438 profile_ref: "Lib1::Reliable".into()
439 })
440 );
441 }
442
443 #[test]
444 fn rpc_interface_qos_single_string_arg_lowers() {
445 let a = lower_single(&ann(
446 "RPCInterfaceQos",
447 AnnotationParams::Single(lit(LiteralKind::String, "\"Lib::P\"")),
448 ));
449 assert_eq!(
450 a,
451 Some(RpcAnnotation::InterfaceQos {
452 profile_ref: "Lib::P".into()
453 })
454 );
455 }
456
457 #[test]
458 fn interface_qos_profile_resolved_via_helper() {
459 let anns = alloc::vec![ann(
460 "RPCInterfaceQos",
461 AnnotationParams::Single(lit(LiteralKind::String, "\"Lib::Foo\""))
462 )];
463 let lowered = lower_rpc_annotations(&anns);
464 assert_eq!(lowered.interface_qos_profile(), Some("Lib::Foo"));
465 }
466
467 #[test]
470 fn dds_request_topic_lowers_and_resolves() {
471 let anns = alloc::vec![ann(
472 "DDSRequestTopic",
473 AnnotationParams::Single(lit(LiteralKind::String, "\"MyReqTopic\""))
474 )];
475 let lowered = lower_rpc_annotations(&anns);
476 assert_eq!(lowered.dds_request_topic(), Some("MyReqTopic"));
477 }
478
479 #[test]
480 fn dds_reply_topic_lowers_and_resolves() {
481 let anns = alloc::vec![ann(
482 "DDSReplyTopic",
483 AnnotationParams::Single(lit(LiteralKind::String, "\"MyRepTopic\""))
484 )];
485 let lowered = lower_rpc_annotations(&anns);
486 assert_eq!(lowered.dds_reply_topic(), Some("MyRepTopic"));
487 }
488
489 #[test]
492 fn rpc_request_type_lowers_and_resolves() {
493 let anns = alloc::vec![ann("RPCRequestType", AnnotationParams::None)];
494 let lowered = lower_rpc_annotations(&anns);
495 assert!(lowered.is_rpc_request_type());
496 assert!(!lowered.is_rpc_reply_type());
497 }
498
499 #[test]
500 fn rpc_reply_type_lowers_and_resolves() {
501 let anns = alloc::vec![ann("RPCReplyType", AnnotationParams::None)];
502 let lowered = lower_rpc_annotations(&anns);
503 assert!(lowered.is_rpc_reply_type());
504 }
505
506 #[test]
509 fn rpc_request_and_reply_lower_and_resolve() {
510 let anns = alloc::vec![
511 ann("RPCRequest", AnnotationParams::None),
512 ann("RPCReply", AnnotationParams::None),
513 ];
514 let lowered = lower_rpc_annotations(&anns);
515 assert!(lowered.is_rpc_request());
516 assert!(lowered.is_rpc_reply());
517 }
518}