1extern crate alloc;
36
37use alloc::string::{String, ToString};
38use alloc::vec::Vec;
39
40use zerodds_idl::ast::{Export, InterfaceDef, OpDecl, ParamAttribute, TypeSpec};
41
42use crate::annotations::{LoweredRpc, lower_rpc_annotations};
43use crate::error::{RpcError, RpcResult};
44use crate::topic_naming::{ServiceTopicNames, validate_service_name};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ParamDirection {
49 In,
51 Out,
53 InOut,
55}
56
57impl From<ParamAttribute> for ParamDirection {
58 fn from(value: ParamAttribute) -> Self {
59 match value {
60 ParamAttribute::In => Self::In,
61 ParamAttribute::Out => Self::Out,
62 ParamAttribute::InOut => Self::InOut,
63 }
64 }
65}
66
67impl ParamDirection {
68 #[must_use]
70 pub const fn is_in(self) -> bool {
71 matches!(self, Self::In | Self::InOut)
72 }
73
74 #[must_use]
76 pub const fn is_out(self) -> bool {
77 matches!(self, Self::Out | Self::InOut)
78 }
79}
80
81pub type TypeRef = TypeSpec;
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct ParamDef {
89 pub name: String,
91 pub direction: ParamDirection,
93 pub type_ref: TypeRef,
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub struct MethodDef {
100 pub name: String,
102 pub params: Vec<ParamDef>,
104 pub return_type: Option<TypeRef>,
106 pub oneway: bool,
108}
109
110impl MethodDef {
111 pub fn in_params(&self) -> impl Iterator<Item = &ParamDef> {
113 self.params.iter().filter(|p| p.direction.is_in())
114 }
115
116 pub fn out_params(&self) -> impl Iterator<Item = &ParamDef> {
118 self.params.iter().filter(|p| p.direction.is_out())
119 }
120}
121
122#[derive(Debug, Clone, PartialEq)]
124pub struct ServiceDef {
125 pub name: String,
127 pub methods: Vec<MethodDef>,
129}
130
131impl ServiceDef {
132 pub fn topic_names(&self) -> RpcResult<ServiceTopicNames> {
138 ServiceTopicNames::new(&self.name)
139 }
140}
141
142pub fn lower_service(iface: &InterfaceDef, lowered: &LoweredRpc) -> RpcResult<ServiceDef> {
155 let svc_name = lowered
157 .service_name()
158 .map(ToString::to_string)
159 .unwrap_or_else(|| iface.name.text.clone());
160 validate_service_name(&svc_name)?;
161
162 let mut methods = Vec::new();
163 for export in &iface.exports {
164 if let Export::Op(op) = export {
165 methods.push(lower_method(op)?);
166 }
167 }
171
172 for i in 0..methods.len() {
174 for j in (i + 1)..methods.len() {
175 if methods[i].name == methods[j].name {
176 return Err(RpcError::DuplicateMethod(methods[i].name.clone()));
177 }
178 }
179 }
180
181 Ok(ServiceDef {
182 name: svc_name,
183 methods,
184 })
185}
186
187fn lower_method(op: &OpDecl) -> RpcResult<MethodDef> {
188 let name = op.name.text.clone();
189 if name.is_empty() {
190 return Err(RpcError::InvalidMethodName(name));
191 }
192
193 let method_anns = lower_rpc_annotations(&op.annotations);
197 let oneway = op.oneway || method_anns.has_oneway();
198
199 let return_type = op.return_type.clone();
200
201 if oneway && return_type.is_some() {
202 return Err(RpcError::OnewayWithReturn(name));
203 }
204
205 let mut params = Vec::with_capacity(op.params.len());
206 for p in &op.params {
207 let p_anns = lower_rpc_annotations(&p.annotations);
210 let direction = override_direction(p.attribute, &p_anns);
211
212 if oneway && direction.is_out() {
213 return Err(RpcError::OnewayWithOutParam {
214 method: name.clone(),
215 param: p.name.text.clone(),
216 });
217 }
218
219 params.push(ParamDef {
220 name: p.name.text.clone(),
221 direction,
222 type_ref: p.type_spec.clone(),
223 });
224 }
225
226 for i in 0..params.len() {
228 for j in (i + 1)..params.len() {
229 if params[i].name == params[j].name {
230 return Err(RpcError::DuplicateParam {
231 method: name,
232 param: params[i].name.clone(),
233 });
234 }
235 }
236 }
237
238 Ok(MethodDef {
239 name,
240 params,
241 return_type,
242 oneway,
243 })
244}
245
246fn override_direction(native: ParamAttribute, anns: &LoweredRpc) -> ParamDirection {
247 use crate::annotations::RpcAnnotation;
248 for a in &anns.builtins {
249 match a {
250 RpcAnnotation::In => return ParamDirection::In,
251 RpcAnnotation::Out => return ParamDirection::Out,
252 RpcAnnotation::InOut => return ParamDirection::InOut,
253 _ => {}
254 }
255 }
256 native.into()
257}
258
259#[cfg(test)]
260#[allow(clippy::unwrap_used, clippy::expect_used)]
261mod tests {
262 use super::*;
263 use zerodds_idl::ast::{
264 Annotation, AnnotationParams, Identifier, IntegerType, InterfaceKind, OpDecl, ParamDecl,
265 PrimitiveType, ScopedName, StringType, TypeSpec,
266 };
267 use zerodds_idl::errors::Span;
268
269 fn sp() -> Span {
270 Span::SYNTHETIC
271 }
272
273 fn ident(t: &str) -> Identifier {
274 Identifier::new(t, sp())
275 }
276
277 fn long_t() -> TypeSpec {
278 TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long))
279 }
280
281 fn string_t() -> TypeSpec {
282 TypeSpec::String(StringType {
283 wide: false,
284 bound: None,
285 span: sp(),
286 })
287 }
288
289 fn op(
290 name: &str,
291 oneway: bool,
292 ret: Option<TypeSpec>,
293 params: Vec<ParamDecl>,
294 anns: Vec<Annotation>,
295 ) -> OpDecl {
296 OpDecl {
297 name: ident(name),
298 oneway,
299 return_type: ret,
300 params,
301 raises: Vec::new(),
302 annotations: anns,
303 span: sp(),
304 }
305 }
306
307 fn param(name: &str, attr: ParamAttribute, ty: TypeSpec) -> ParamDecl {
308 ParamDecl {
309 attribute: attr,
310 type_spec: ty,
311 name: ident(name),
312 annotations: Vec::new(),
313 span: sp(),
314 }
315 }
316
317 fn iface(name: &str, exports: Vec<Export>, anns: Vec<Annotation>) -> InterfaceDef {
318 InterfaceDef {
319 kind: InterfaceKind::Plain,
320 name: ident(name),
321 bases: Vec::new(),
322 exports,
323 annotations: anns,
324 span: sp(),
325 }
326 }
327
328 fn ann_simple(name: &str) -> Annotation {
329 Annotation {
330 name: ScopedName {
331 absolute: false,
332 parts: vec![ident(name)],
333 span: sp(),
334 },
335 params: AnnotationParams::None,
336 span: sp(),
337 }
338 }
339
340 #[test]
341 fn calculator_service_with_in_params_lowers() {
342 let add = op(
343 "add",
344 false,
345 Some(long_t()),
346 vec![
347 param("a", ParamAttribute::In, long_t()),
348 param("b", ParamAttribute::In, long_t()),
349 ],
350 Vec::new(),
351 );
352 let i = iface(
353 "Calculator",
354 vec![Export::Op(add)],
355 vec![ann_simple("service")],
356 );
357 let lowered = lower_rpc_annotations(&i.annotations);
358 let svc = lower_service(&i, &lowered).unwrap();
359 assert_eq!(svc.name, "Calculator");
360 assert_eq!(svc.methods.len(), 1);
361 let m = &svc.methods[0];
362 assert_eq!(m.name, "add");
363 assert!(!m.oneway);
364 assert_eq!(m.params.len(), 2);
365 assert_eq!(m.in_params().count(), 2);
366 assert_eq!(m.out_params().count(), 0);
367 assert_eq!(svc.topic_names().unwrap().request, "Calculator_Request");
368 }
369
370 #[test]
371 fn oneway_method_with_return_is_error() {
372 let bad = op(
373 "log",
374 true,
375 Some(long_t()),
376 vec![param("msg", ParamAttribute::In, string_t())],
377 Vec::new(),
378 );
379 let i = iface("Logger", vec![Export::Op(bad)], Vec::new());
380 let lowered = lower_rpc_annotations(&i.annotations);
381 let err = lower_service(&i, &lowered).unwrap_err();
382 assert!(matches!(err, RpcError::OnewayWithReturn(_)));
383 }
384
385 #[test]
386 fn oneway_method_with_out_param_is_error() {
387 let bad = op(
388 "fire",
389 true,
390 None,
391 vec![param("result", ParamAttribute::Out, long_t())],
392 Vec::new(),
393 );
394 let i = iface("Svc", vec![Export::Op(bad)], Vec::new());
395 let lowered = lower_rpc_annotations(&i.annotations);
396 let err = lower_service(&i, &lowered).unwrap_err();
397 assert!(matches!(err, RpcError::OnewayWithOutParam { .. }));
398 }
399
400 #[test]
401 fn oneway_method_with_inout_param_is_error() {
402 let bad = op(
403 "fire",
404 true,
405 None,
406 vec![param("v", ParamAttribute::InOut, long_t())],
407 Vec::new(),
408 );
409 let i = iface("Svc", vec![Export::Op(bad)], Vec::new());
410 let lowered = lower_rpc_annotations(&i.annotations);
411 let err = lower_service(&i, &lowered).unwrap_err();
412 assert!(matches!(err, RpcError::OnewayWithOutParam { .. }));
413 }
414
415 #[test]
416 fn oneway_with_only_in_params_lowers() {
417 let m = op(
418 "log",
419 true,
420 None,
421 vec![param("msg", ParamAttribute::In, string_t())],
422 Vec::new(),
423 );
424 let i = iface("Logger", vec![Export::Op(m)], Vec::new());
425 let lowered = lower_rpc_annotations(&i.annotations);
426 let svc = lower_service(&i, &lowered).unwrap();
427 assert!(svc.methods[0].oneway);
428 assert_eq!(svc.methods[0].in_params().count(), 1);
429 assert_eq!(svc.methods[0].out_params().count(), 0);
430 }
431
432 #[test]
433 fn oneway_via_annotation_recognized() {
434 let m = op("ping", false, None, Vec::new(), vec![ann_simple("oneway")]);
436 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
437 let lowered = lower_rpc_annotations(&i.annotations);
438 let svc = lower_service(&i, &lowered).unwrap();
439 assert!(svc.methods[0].oneway);
440 }
441
442 #[test]
443 fn duplicate_method_detected() {
444 let m1 = op("foo", false, None, Vec::new(), Vec::new());
445 let m2 = op("foo", false, None, Vec::new(), Vec::new());
446 let i = iface("Svc", vec![Export::Op(m1), Export::Op(m2)], Vec::new());
447 let lowered = lower_rpc_annotations(&i.annotations);
448 let err = lower_service(&i, &lowered).unwrap_err();
449 assert_eq!(err, RpcError::DuplicateMethod("foo".into()));
450 }
451
452 #[test]
453 fn duplicate_param_detected() {
454 let m = op(
455 "add",
456 false,
457 Some(long_t()),
458 vec![
459 param("x", ParamAttribute::In, long_t()),
460 param("x", ParamAttribute::In, long_t()),
461 ],
462 Vec::new(),
463 );
464 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
465 let lowered = lower_rpc_annotations(&i.annotations);
466 let err = lower_service(&i, &lowered).unwrap_err();
467 assert!(matches!(err, RpcError::DuplicateParam { .. }));
468 }
469
470 #[test]
471 fn empty_method_name_rejected() {
472 let m = op("", false, None, Vec::new(), Vec::new());
473 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
474 let lowered = lower_rpc_annotations(&i.annotations);
475 let err = lower_service(&i, &lowered).unwrap_err();
476 assert!(matches!(err, RpcError::InvalidMethodName(_)));
477 }
478
479 #[test]
480 fn invalid_service_name_rejected() {
481 let i = iface("Bad-Name", Vec::new(), Vec::new());
482 let lowered = lower_rpc_annotations(&i.annotations);
483 let err = lower_service(&i, &lowered).unwrap_err();
484 assert!(matches!(err, RpcError::InvalidServiceName(_)));
485 }
486
487 #[test]
488 fn service_name_from_annotation_overrides_iface_name() {
489 let i = iface(
491 "InternalIface",
492 Vec::new(),
493 vec![Annotation {
494 name: ScopedName {
495 absolute: false,
496 parts: vec![ident("service")],
497 span: sp(),
498 },
499 params: AnnotationParams::Named(vec![zerodds_idl::ast::NamedParam {
500 name: ident("name"),
501 value: zerodds_idl::ast::ConstExpr::Literal(zerodds_idl::ast::Literal {
502 kind: zerodds_idl::ast::LiteralKind::String,
503 raw: "\"OuterName\"".into(),
504 span: sp(),
505 }),
506 span: sp(),
507 }]),
508 span: sp(),
509 }],
510 );
511 let lowered = lower_rpc_annotations(&i.annotations);
512 let svc = lower_service(&i, &lowered).unwrap();
513 assert_eq!(svc.name, "OuterName");
514 }
515
516 #[test]
517 fn inout_param_appears_in_both_directions() {
518 let m = op(
519 "swap",
520 false,
521 None,
522 vec![param("v", ParamAttribute::InOut, long_t())],
523 Vec::new(),
524 );
525 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
526 let lowered = lower_rpc_annotations(&i.annotations);
527 let svc = lower_service(&i, &lowered).unwrap();
528 let m = &svc.methods[0];
529 assert_eq!(m.in_params().count(), 1);
530 assert_eq!(m.out_params().count(), 1);
531 }
532
533 #[test]
534 fn out_only_param_is_reply_only() {
535 let m = op(
536 "result",
537 false,
538 None,
539 vec![param("v", ParamAttribute::Out, long_t())],
540 Vec::new(),
541 );
542 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
543 let lowered = lower_rpc_annotations(&i.annotations);
544 let svc = lower_service(&i, &lowered).unwrap();
545 let m = &svc.methods[0];
546 assert_eq!(m.in_params().count(), 0);
547 assert_eq!(m.out_params().count(), 1);
548 }
549
550 #[test]
551 fn param_annotation_in_overrides_native_attr() {
552 let mut p = param("v", ParamAttribute::Out, long_t());
554 p.annotations.push(ann_simple("in"));
555 let m = op("foo", false, None, vec![p], Vec::new());
556 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
557 let lowered = lower_rpc_annotations(&i.annotations);
558 let svc = lower_service(&i, &lowered).unwrap();
559 assert_eq!(svc.methods[0].params[0].direction, ParamDirection::In);
560 }
561
562 #[test]
563 fn non_op_exports_are_ignored() {
564 let const_decl = zerodds_idl::ast::ConstDecl {
567 name: ident("MAX"),
568 type_: zerodds_idl::ast::ConstType::Integer(IntegerType::Long),
569 value: zerodds_idl::ast::ConstExpr::Literal(zerodds_idl::ast::Literal {
570 kind: zerodds_idl::ast::LiteralKind::Integer,
571 raw: "10".into(),
572 span: sp(),
573 }),
574 annotations: Vec::new(),
575 span: sp(),
576 };
577 let m = op("foo", false, None, Vec::new(), Vec::new());
578 let i = iface(
579 "Svc",
580 vec![Export::Const(const_decl), Export::Op(m)],
581 Vec::new(),
582 );
583 let lowered = lower_rpc_annotations(&i.annotations);
584 let svc = lower_service(&i, &lowered).unwrap();
585 assert_eq!(svc.methods.len(), 1);
586 }
587
588 #[test]
589 fn param_direction_helpers() {
590 assert!(ParamDirection::In.is_in());
591 assert!(!ParamDirection::In.is_out());
592 assert!(!ParamDirection::Out.is_in());
593 assert!(ParamDirection::Out.is_out());
594 assert!(ParamDirection::InOut.is_in());
595 assert!(ParamDirection::InOut.is_out());
596 }
597
598 #[test]
599 fn param_direction_from_param_attribute() {
600 assert_eq!(ParamDirection::from(ParamAttribute::In), ParamDirection::In);
601 assert_eq!(
602 ParamDirection::from(ParamAttribute::Out),
603 ParamDirection::Out
604 );
605 assert_eq!(
606 ParamDirection::from(ParamAttribute::InOut),
607 ParamDirection::InOut
608 );
609 }
610}