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 context: Vec::new(),
303 annotations: anns,
304 span: sp(),
305 }
306 }
307
308 fn param(name: &str, attr: ParamAttribute, ty: TypeSpec) -> ParamDecl {
309 ParamDecl {
310 attribute: attr,
311 type_spec: ty,
312 name: ident(name),
313 annotations: Vec::new(),
314 span: sp(),
315 }
316 }
317
318 fn iface(name: &str, exports: Vec<Export>, anns: Vec<Annotation>) -> InterfaceDef {
319 InterfaceDef {
320 kind: InterfaceKind::Plain,
321 name: ident(name),
322 bases: Vec::new(),
323 exports,
324 annotations: anns,
325 span: sp(),
326 }
327 }
328
329 fn ann_simple(name: &str) -> Annotation {
330 Annotation {
331 name: ScopedName {
332 absolute: false,
333 parts: vec![ident(name)],
334 span: sp(),
335 },
336 params: AnnotationParams::None,
337 span: sp(),
338 }
339 }
340
341 #[test]
342 fn calculator_service_with_in_params_lowers() {
343 let add = op(
344 "add",
345 false,
346 Some(long_t()),
347 vec![
348 param("a", ParamAttribute::In, long_t()),
349 param("b", ParamAttribute::In, long_t()),
350 ],
351 Vec::new(),
352 );
353 let i = iface(
354 "Calculator",
355 vec![Export::Op(add)],
356 vec![ann_simple("service")],
357 );
358 let lowered = lower_rpc_annotations(&i.annotations);
359 let svc = lower_service(&i, &lowered).unwrap();
360 assert_eq!(svc.name, "Calculator");
361 assert_eq!(svc.methods.len(), 1);
362 let m = &svc.methods[0];
363 assert_eq!(m.name, "add");
364 assert!(!m.oneway);
365 assert_eq!(m.params.len(), 2);
366 assert_eq!(m.in_params().count(), 2);
367 assert_eq!(m.out_params().count(), 0);
368 assert_eq!(svc.topic_names().unwrap().request, "Calculator_Request");
369 }
370
371 #[test]
372 fn oneway_method_with_return_is_error() {
373 let bad = op(
374 "log",
375 true,
376 Some(long_t()),
377 vec![param("msg", ParamAttribute::In, string_t())],
378 Vec::new(),
379 );
380 let i = iface("Logger", vec![Export::Op(bad)], Vec::new());
381 let lowered = lower_rpc_annotations(&i.annotations);
382 let err = lower_service(&i, &lowered).unwrap_err();
383 assert!(matches!(err, RpcError::OnewayWithReturn(_)));
384 }
385
386 #[test]
387 fn oneway_method_with_out_param_is_error() {
388 let bad = op(
389 "fire",
390 true,
391 None,
392 vec![param("result", ParamAttribute::Out, long_t())],
393 Vec::new(),
394 );
395 let i = iface("Svc", vec![Export::Op(bad)], Vec::new());
396 let lowered = lower_rpc_annotations(&i.annotations);
397 let err = lower_service(&i, &lowered).unwrap_err();
398 assert!(matches!(err, RpcError::OnewayWithOutParam { .. }));
399 }
400
401 #[test]
402 fn oneway_method_with_inout_param_is_error() {
403 let bad = op(
404 "fire",
405 true,
406 None,
407 vec![param("v", ParamAttribute::InOut, long_t())],
408 Vec::new(),
409 );
410 let i = iface("Svc", vec![Export::Op(bad)], Vec::new());
411 let lowered = lower_rpc_annotations(&i.annotations);
412 let err = lower_service(&i, &lowered).unwrap_err();
413 assert!(matches!(err, RpcError::OnewayWithOutParam { .. }));
414 }
415
416 #[test]
417 fn oneway_with_only_in_params_lowers() {
418 let m = op(
419 "log",
420 true,
421 None,
422 vec![param("msg", ParamAttribute::In, string_t())],
423 Vec::new(),
424 );
425 let i = iface("Logger", vec![Export::Op(m)], Vec::new());
426 let lowered = lower_rpc_annotations(&i.annotations);
427 let svc = lower_service(&i, &lowered).unwrap();
428 assert!(svc.methods[0].oneway);
429 assert_eq!(svc.methods[0].in_params().count(), 1);
430 assert_eq!(svc.methods[0].out_params().count(), 0);
431 }
432
433 #[test]
434 fn oneway_via_annotation_recognized() {
435 let m = op("ping", false, None, Vec::new(), vec![ann_simple("oneway")]);
437 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
438 let lowered = lower_rpc_annotations(&i.annotations);
439 let svc = lower_service(&i, &lowered).unwrap();
440 assert!(svc.methods[0].oneway);
441 }
442
443 #[test]
444 fn duplicate_method_detected() {
445 let m1 = op("foo", false, None, Vec::new(), Vec::new());
446 let m2 = op("foo", false, None, Vec::new(), Vec::new());
447 let i = iface("Svc", vec![Export::Op(m1), Export::Op(m2)], Vec::new());
448 let lowered = lower_rpc_annotations(&i.annotations);
449 let err = lower_service(&i, &lowered).unwrap_err();
450 assert_eq!(err, RpcError::DuplicateMethod("foo".into()));
451 }
452
453 #[test]
454 fn duplicate_param_detected() {
455 let m = op(
456 "add",
457 false,
458 Some(long_t()),
459 vec![
460 param("x", ParamAttribute::In, long_t()),
461 param("x", ParamAttribute::In, long_t()),
462 ],
463 Vec::new(),
464 );
465 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
466 let lowered = lower_rpc_annotations(&i.annotations);
467 let err = lower_service(&i, &lowered).unwrap_err();
468 assert!(matches!(err, RpcError::DuplicateParam { .. }));
469 }
470
471 #[test]
472 fn empty_method_name_rejected() {
473 let m = op("", false, None, Vec::new(), Vec::new());
474 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
475 let lowered = lower_rpc_annotations(&i.annotations);
476 let err = lower_service(&i, &lowered).unwrap_err();
477 assert!(matches!(err, RpcError::InvalidMethodName(_)));
478 }
479
480 #[test]
481 fn invalid_service_name_rejected() {
482 let i = iface("Bad-Name", Vec::new(), Vec::new());
483 let lowered = lower_rpc_annotations(&i.annotations);
484 let err = lower_service(&i, &lowered).unwrap_err();
485 assert!(matches!(err, RpcError::InvalidServiceName(_)));
486 }
487
488 #[test]
489 fn service_name_from_annotation_overrides_iface_name() {
490 let i = iface(
492 "InternalIface",
493 Vec::new(),
494 vec![Annotation {
495 name: ScopedName {
496 absolute: false,
497 parts: vec![ident("service")],
498 span: sp(),
499 },
500 params: AnnotationParams::Named(vec![zerodds_idl::ast::NamedParam {
501 name: ident("name"),
502 value: zerodds_idl::ast::ConstExpr::Literal(zerodds_idl::ast::Literal {
503 kind: zerodds_idl::ast::LiteralKind::String,
504 raw: "\"OuterName\"".into(),
505 span: sp(),
506 }),
507 span: sp(),
508 }]),
509 span: sp(),
510 }],
511 );
512 let lowered = lower_rpc_annotations(&i.annotations);
513 let svc = lower_service(&i, &lowered).unwrap();
514 assert_eq!(svc.name, "OuterName");
515 }
516
517 #[test]
518 fn inout_param_appears_in_both_directions() {
519 let m = op(
520 "swap",
521 false,
522 None,
523 vec![param("v", ParamAttribute::InOut, long_t())],
524 Vec::new(),
525 );
526 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
527 let lowered = lower_rpc_annotations(&i.annotations);
528 let svc = lower_service(&i, &lowered).unwrap();
529 let m = &svc.methods[0];
530 assert_eq!(m.in_params().count(), 1);
531 assert_eq!(m.out_params().count(), 1);
532 }
533
534 #[test]
535 fn out_only_param_is_reply_only() {
536 let m = op(
537 "result",
538 false,
539 None,
540 vec![param("v", ParamAttribute::Out, long_t())],
541 Vec::new(),
542 );
543 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
544 let lowered = lower_rpc_annotations(&i.annotations);
545 let svc = lower_service(&i, &lowered).unwrap();
546 let m = &svc.methods[0];
547 assert_eq!(m.in_params().count(), 0);
548 assert_eq!(m.out_params().count(), 1);
549 }
550
551 #[test]
552 fn param_annotation_in_overrides_native_attr() {
553 let mut p = param("v", ParamAttribute::Out, long_t());
555 p.annotations.push(ann_simple("in"));
556 let m = op("foo", false, None, vec![p], Vec::new());
557 let i = iface("Svc", vec![Export::Op(m)], Vec::new());
558 let lowered = lower_rpc_annotations(&i.annotations);
559 let svc = lower_service(&i, &lowered).unwrap();
560 assert_eq!(svc.methods[0].params[0].direction, ParamDirection::In);
561 }
562
563 #[test]
564 fn non_op_exports_are_ignored() {
565 let const_decl = zerodds_idl::ast::ConstDecl {
568 name: ident("MAX"),
569 type_: zerodds_idl::ast::ConstType::Integer(IntegerType::Long),
570 value: zerodds_idl::ast::ConstExpr::Literal(zerodds_idl::ast::Literal {
571 kind: zerodds_idl::ast::LiteralKind::Integer,
572 raw: "10".into(),
573 span: sp(),
574 }),
575 annotations: Vec::new(),
576 span: sp(),
577 };
578 let m = op("foo", false, None, Vec::new(), Vec::new());
579 let i = iface(
580 "Svc",
581 vec![Export::Const(const_decl), Export::Op(m)],
582 Vec::new(),
583 );
584 let lowered = lower_rpc_annotations(&i.annotations);
585 let svc = lower_service(&i, &lowered).unwrap();
586 assert_eq!(svc.methods.len(), 1);
587 }
588
589 #[test]
590 fn param_direction_helpers() {
591 assert!(ParamDirection::In.is_in());
592 assert!(!ParamDirection::In.is_out());
593 assert!(!ParamDirection::Out.is_in());
594 assert!(ParamDirection::Out.is_out());
595 assert!(ParamDirection::InOut.is_in());
596 assert!(ParamDirection::InOut.is_out());
597 }
598
599 #[test]
600 fn param_direction_from_param_attribute() {
601 assert_eq!(ParamDirection::from(ParamAttribute::In), ParamDirection::In);
602 assert_eq!(
603 ParamDirection::from(ParamAttribute::Out),
604 ParamDirection::Out
605 );
606 assert_eq!(
607 ParamDirection::from(ParamAttribute::InOut),
608 ParamDirection::InOut
609 );
610 }
611}