1use std::collections::HashMap;
58use std::sync::{Arc, Mutex};
59use std::time::Duration;
60
61use zerodds_ast::{Ast, ExceptionNode, TypeNode};
62use zerodds_ast_core::Id;
63use zerodds_dcps::participant::DomainParticipant;
64use zerodds_lang_rules::adapter::interface::{
65 self, InterfaceError, Operation, Raise, Service, all_operations_flattened,
66 exception_member_name, in_struct_name, operation_hash, out_struct_name, services,
67};
68use zerodds_rpc::codegen::{
69 DUMMY_MEMBER, RESULT_MEMBER, RETCODE_OK, RETURN_MEMBER, UNKNOWN_OP_MEMBER,
70};
71use zerodds_rpc::endpoint_options::RpcEndpointOptions;
72use zerodds_rpc::qos_profile::RpcQos;
73use zerodds_rpc::requester::Requester;
74use zerodds_rpc::topic_naming::{AnnotatedTopics, BindingStyle};
75use zerodds_types::builder::{Extensibility, TypeObjectBuilder};
76use zerodds_types::dynamic::DynamicData;
77use zerodds_types::{CompleteTypeObject, PrimitiveKind, TypeIdentifier};
78
79use crate::dynamic_rpc::{self, DynamicRpcValue};
80use crate::dynxml;
81use crate::types_registry::{ResolvedType, TypeRegistry};
82
83#[derive(Debug)]
85pub enum RpcDispatchError {
86 Model(String),
89 UnknownOperation(String),
91 UnsupportedParameterType(String),
95 BadRequest(String),
97 Rpc(String),
99 RemoteException(u32),
105 OperationException {
109 operation: String,
111 exception: String,
113 fields_xml: String,
116 },
117 Timeout,
119}
120
121impl std::fmt::Display for RpcDispatchError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 match self {
124 Self::Model(m) => write!(f, "interface model: {m}"),
125 Self::UnknownOperation(op) => {
126 write!(f, "no configured interface declares operation `{op}`")
127 }
128 Self::UnsupportedParameterType(m) => write!(f, "unsupported parameter type: {m}"),
129 Self::BadRequest(m) => write!(f, "bad request: {m}"),
130 Self::Rpc(m) => write!(f, "rpc: {m}"),
131 Self::RemoteException(code) => write!(f, "remote exception, code {code}"),
132 Self::OperationException {
133 operation,
134 exception,
135 fields_xml,
136 } => write!(
137 f,
138 "operation `{operation}` raised `{exception}`: {fields_xml}"
139 ),
140 Self::Timeout => f.write_str("timed out waiting for a reply"),
141 }
142 }
143}
144
145impl std::error::Error for RpcDispatchError {}
146
147const RPC_TIMEOUT: Duration = Duration::from_secs(10);
149
150struct OpModel {
153 in_resolved: ResolvedType,
156 #[cfg_attr(not(test), allow(dead_code))]
165 result_resolved: ResolvedType,
166 out_resolved: ResolvedType,
168 exceptions: Vec<(i32, String, String, ResolvedType)>,
173}
174
175struct InterfaceModel {
179 call_resolved: ResolvedType,
182 return_resolved: ResolvedType,
184 operations: HashMap<i32, OpModel>,
188}
189
190pub struct RpcDispatcher {
196 ast: Arc<Ast>,
197 services: Vec<Service>,
198 participant: DomainParticipant,
199 types: Arc<TypeRegistry>,
200 models: Mutex<HashMap<String, Arc<InterfaceModel>>>,
201 requesters: Mutex<HashMap<String, Arc<Requester<DynamicRpcValue, DynamicRpcValue>>>>,
202}
203
204impl RpcDispatcher {
205 pub fn new(ast: Arc<Ast>, participant: DomainParticipant) -> Result<Self, RpcDispatchError> {
212 let services = services(&ast).map_err(interface_error)?;
213 Ok(Self {
214 ast,
215 services,
216 participant,
217 types: Arc::new(TypeRegistry::new()),
218 models: Mutex::new(HashMap::new()),
219 requesters: Mutex::new(HashMap::new()),
220 })
221 }
222
223 fn find_operation(&self, op_name: &str) -> Result<(&Service, Operation), RpcDispatchError> {
228 for service in &self.services {
229 let flattened =
230 all_operations_flattened(&self.ast, service.id).map_err(interface_error)?;
231 if let Some(op) = flattened.into_iter().find(|op| op.name == op_name) {
232 return Ok((service, op));
233 }
234 }
235 Err(RpcDispatchError::UnknownOperation(op_name.to_string()))
236 }
237
238 #[must_use]
242 pub fn knows_operation(&self, op_name: &str) -> bool {
243 self.find_operation(op_name).is_ok()
244 }
245
246 fn model_for(&self, service: &Service) -> Result<Arc<InterfaceModel>, RpcDispatchError> {
249 {
250 let models = self
251 .models
252 .lock()
253 .unwrap_or_else(std::sync::PoisonError::into_inner);
254 if let Some(m) = models.get(&service.qualified_name) {
255 return Ok(Arc::clone(m));
256 }
257 }
258 let model = Arc::new(build_interface_model(&self.ast, service, &self.types)?);
259 let mut models = self
260 .models
261 .lock()
262 .unwrap_or_else(std::sync::PoisonError::into_inner);
263 models.insert(service.qualified_name.clone(), Arc::clone(&model));
264 Ok(model)
265 }
266
267 pub fn dispatch(
276 &self,
277 op_name: &str,
278 request_body_xml: &str,
279 ) -> Result<String, RpcDispatchError> {
280 let (service, operation) = self.find_operation(op_name)?;
281 let model = self.model_for(service)?;
282 let disc = operation_hash(&operation.name);
283 let op_model = model.operations.get(&disc).ok_or_else(|| {
284 RpcDispatchError::Model(format!(
285 "operation `{op_name}`: not found in its own interface's model"
286 ))
287 })?;
288
289 let in_data =
290 dynxml::from_xml_document(request_body_xml, &op_model.in_resolved.dynamic_type)
291 .map_err(|e| RpcDispatchError::BadRequest(e.to_string()))?;
292 let call_ast = Arc::clone(model.call_resolved.dynamic_type.ast());
293 let call_type_name = model.call_resolved.dynamic_type.name().to_string();
294 let call_data = DynamicData::Aggregate(vec![
295 (
296 "_d".to_string(),
297 DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, i128::from(disc)),
298 ),
299 (operation.name.clone(), in_data),
300 ]);
301 let dialect = self
304 .participant
305 .runtime()
306 .map(|rt| rt.codec_dialect())
307 .unwrap_or_default();
308 let call_value =
309 DynamicRpcValue::new(call_ast, call_type_name, call_data).in_dialect(dialect);
310
311 let requester = self.requester_for(service)?;
312 let return_ast = Arc::clone(model.return_resolved.dynamic_type.ast());
313 let return_type_name = model.return_resolved.dynamic_type.name().to_string();
314 let reply =
315 dynamic_rpc::with_decode_context(&return_ast, &return_type_name, dialect, || {
316 requester.send_request_blocking(&call_value, Some(RPC_TIMEOUT))
317 })
318 .map_err(|e| match e {
319 zerodds_rpc::error::RpcError::Timeout => RpcDispatchError::Timeout,
320 zerodds_rpc::error::RpcError::RemoteException(code) => {
321 RpcDispatchError::RemoteException(code)
322 }
323 other => RpcDispatchError::Rpc(other.to_string()),
324 })?;
325
326 let result_data = reply.data.member(&operation.name).ok_or_else(|| {
329 RpcDispatchError::Rpc(format!(
330 "reply named no `{}` case (Return union discriminator mismatch)",
331 operation.name
332 ))
333 })?;
334 let result_disc = match result_data.member("_d") {
335 Some(DynamicData::Integer { value, .. }) => *value as i32,
336 _ => {
337 return Err(RpcDispatchError::Rpc(
338 "Result union reply carried no discriminator".to_string(),
339 ));
340 }
341 };
342 if result_disc == RETCODE_OK {
343 let out_resolved = &op_model.out_resolved;
344 let out_data = result_data.member(RESULT_MEMBER).ok_or_else(|| {
345 RpcDispatchError::Rpc(format!(
346 "Result union's `{RESULT_MEMBER}` case named no `{RESULT_MEMBER}` member"
347 ))
348 })?;
349 return dynxml::to_xml_document("data", out_data, &out_resolved.dynamic_type)
350 .map_err(|e| RpcDispatchError::Rpc(e.to_string()));
351 }
352 let (_, member_name, exception_name, exc_resolved) = op_model
354 .exceptions
355 .iter()
356 .find(|(disc, _, _, _)| *disc == result_disc)
357 .ok_or_else(|| {
358 RpcDispatchError::Rpc(format!(
359 "Result union reply named an unrecognised discriminator {result_disc}"
360 ))
361 })?;
362 let exc_data = result_data.member(member_name).ok_or_else(|| {
363 RpcDispatchError::Rpc(format!("Result union named no `{member_name}` member"))
364 })?;
365 let fields_xml = dynxml::to_xml_document("data", exc_data, &exc_resolved.dynamic_type)
366 .map_err(|e| RpcDispatchError::Rpc(e.to_string()))?;
367 Err(RpcDispatchError::OperationException {
368 operation: operation.name.clone(),
369 exception: exception_name.clone(),
370 fields_xml,
371 })
372 }
373
374 fn requester_for(
379 &self,
380 service: &Service,
381 ) -> Result<Arc<Requester<DynamicRpcValue, DynamicRpcValue>>, RpcDispatchError> {
382 let mut requesters = self
383 .requesters
384 .lock()
385 .unwrap_or_else(std::sync::PoisonError::into_inner);
386 if let Some(r) = requesters.get(&service.qualified_name) {
387 return Ok(Arc::clone(r));
388 }
389 let scope = interface::module_chain(&self.ast, service.module)
392 .map_err(interface_error)?
393 .into_iter()
394 .map(|(_, name)| name)
395 .collect::<Vec<_>>()
396 .join("::");
397 let options = RpcEndpointOptions::for_interface(&scope, &service.name);
398 let requester = Requester::with_options(
399 &self.participant,
400 &service.qualified_name,
401 "",
402 &RpcQos::default(),
403 BindingStyle::FunctionCall,
404 &AnnotatedTopics::none(),
405 &options,
406 )
407 .map_err(|e| RpcDispatchError::Rpc(e.to_string()))?;
408 let requester = Arc::new(requester);
409 requesters.insert(service.qualified_name.clone(), Arc::clone(&requester));
410 Ok(requester)
411 }
412}
413
414fn interface_error(e: InterfaceError) -> RpcDispatchError {
415 RpcDispatchError::Model(format!("{e:?}"))
416}
417
418fn build_interface_model(
423 ast: &Ast,
424 service: &Service,
425 types: &TypeRegistry,
426) -> Result<InterfaceModel, RpcDispatchError> {
427 let qualified = &service.qualified_name;
428 let flattened = all_operations_flattened(ast, service.id).map_err(interface_error)?;
429
430 let mut call_cases: Vec<(i32, String, ResolvedType)> = Vec::new();
431 let mut return_cases: Vec<(i32, String, ResolvedType)> = Vec::new();
432 let mut operations: HashMap<i32, OpModel> = HashMap::new();
433
434 for op in &flattened {
435 let disc = operation_hash(&op.name);
436
437 let in_type_name = in_struct_name(qualified, &op.name);
438 let in_members: Vec<(String, Id<TypeNode>)> =
439 op.inputs().map(|p| (p.name.clone(), p.type_)).collect();
440 let in_resolved = resolve_synthetic_struct(types, ast, &in_type_name, &in_members)?;
441 call_cases.push((disc, op.name.clone(), in_resolved.clone()));
442
443 let out_type_name = out_struct_name(qualified, &op.name);
444 let mut out_members: Vec<(String, Id<TypeNode>)> =
445 op.outputs().map(|p| (p.name.clone(), p.type_)).collect();
446 if let Some(ret) = op.returns {
447 out_members.push((RETURN_MEMBER.to_string(), ret));
448 }
449 let out_resolved = resolve_synthetic_struct(types, ast, &out_type_name, &out_members)?;
450
451 let mut result_cases: Vec<(i32, String, ResolvedType)> =
452 vec![(RETCODE_OK, RESULT_MEMBER.to_string(), out_resolved.clone())];
453 let mut exceptions = Vec::new();
454 for raise in &op.raises {
455 let ex_disc = interface::exception_hash(&raise.scoped_name);
456 let ex_member = exception_member_name(&raise.name);
457 let ex_type_name = format!("{qualified}_{}_{}_Ex", op.name, raise.name);
458 let ex_members = exception_members(ast, raise)
459 .map_err(RpcDispatchError::UnsupportedParameterType)?;
460 let ex_resolved = resolve_synthetic_struct(types, ast, &ex_type_name, &ex_members)?;
461 result_cases.push((ex_disc, ex_member.clone(), ex_resolved.clone()));
462 exceptions.push((ex_disc, ex_member, raise.name.clone(), ex_resolved));
463 }
464 let result_type_name = format!("{qualified}_{}_Result", op.name);
465 let result_resolved =
466 resolve_synthetic_union(types, &result_type_name, &result_cases, None)?;
467 return_cases.push((disc, op.name.clone(), result_resolved.clone()));
468
469 operations.insert(
470 disc,
471 OpModel {
472 in_resolved,
473 result_resolved,
474 out_resolved,
475 exceptions,
476 },
477 );
478 }
479
480 let call_type_name = format!("{qualified}_Call");
481 let call_resolved =
482 resolve_synthetic_union(types, &call_type_name, &call_cases, Some(UNKNOWN_OP_MEMBER))?;
483 let return_type_name = format!("{qualified}_Return");
484 let return_resolved = resolve_synthetic_union(
485 types,
486 &return_type_name,
487 &return_cases,
488 Some(UNKNOWN_OP_MEMBER),
489 )?;
490
491 Ok(InterfaceModel {
492 call_resolved,
493 return_resolved,
494 operations,
495 })
496}
497
498fn exception_members(ast: &Ast, raise: &Raise) -> Result<Vec<(String, Id<TypeNode>)>, String> {
507 let node: &ExceptionNode = ast
508 .exception(raise.id)
509 .map_err(|e| format!("exception `{}`: {e:?}", raise.name))?;
510 node.members
511 .iter()
512 .map(|member_id| {
513 let member = ast
514 .member(*member_id)
515 .map_err(|e| format!("exception `{}`: {e:?}", raise.name))?;
516 let type_id = member.type_.resolved().ok_or_else(|| {
517 format!(
518 "exception `{}`, member `{}`: type not resolved",
519 raise.name, member.name
520 )
521 })?;
522 Ok((member.name.clone(), type_id))
523 })
524 .collect()
525}
526
527fn resolve_synthetic_struct(
532 types: &TypeRegistry,
533 ast: &Ast,
534 type_name: &str,
535 members: &[(String, Id<TypeNode>)],
536) -> Result<ResolvedType, RpcDispatchError> {
537 if let Some(r) = types.get(type_name) {
538 return Ok(r);
539 }
540 let (root, dependencies) = build_synthetic_struct(ast, type_name, members)
541 .map_err(RpcDispatchError::UnsupportedParameterType)?;
542 types
543 .resolve_synthetic(type_name, root, dependencies)
544 .map_err(|e| RpcDispatchError::Model(e.to_string()))
545}
546
547fn build_synthetic_struct(
559 ast: &Ast,
560 type_name: &str,
561 members: &[(String, Id<TypeNode>)],
562) -> Result<(CompleteTypeObject, Vec<CompleteTypeObject>), String> {
563 use zerodds_types::from_ast::{
564 Form, FromAstError, complete_type_object, dependency_type_objects, type_identifier,
565 };
566
567 let mut builder = TypeObjectBuilder::struct_type(type_name).extensibility(Extensibility::Final);
568 let mut dependencies = Vec::new();
569
570 if members.is_empty() {
571 builder = builder.member(
572 DUMMY_MEMBER,
573 TypeIdentifier::Primitive(PrimitiveKind::Byte),
574 |mb| mb,
575 );
576 } else {
577 for (name, type_id) in members {
578 let ti = type_identifier(ast, *type_id, Form::Complete)
579 .map_err(|e| format!("member `{name}`: {e:?}"))?;
580 builder = builder.member(name.clone(), ti, |mb| mb);
581
582 match complete_type_object(ast, *type_id) {
583 Ok(object) => dependencies.push(object),
584 Err(FromAstError::Unrepresentable { .. }) => {
585 }
590 Err(e) => return Err(format!("member `{name}`: {e:?}")),
591 }
592 for dep in dependency_type_objects(ast, *type_id)
593 .map_err(|e| format!("member `{name}`: {e:?}"))?
594 {
595 dependencies.push(dep);
596 }
597 }
598 }
599
600 Ok((
601 CompleteTypeObject::Struct(builder.build_complete()),
602 dependencies,
603 ))
604}
605
606fn resolve_synthetic_union(
620 types: &TypeRegistry,
621 type_name: &str,
622 cases: &[(i32, String, ResolvedType)],
623 default_member: Option<&str>,
624) -> Result<ResolvedType, RpcDispatchError> {
625 use zerodds_types::from_ast::{
626 Form, complete_type_object, dependency_type_objects, type_identifier,
627 };
628
629 if let Some(r) = types.get(type_name) {
630 return Ok(r);
631 }
632 let mut builder =
633 TypeObjectBuilder::union_type(type_name, TypeIdentifier::Primitive(PrimitiveKind::Int32))
634 .extensibility(Extensibility::Final);
635 let mut dependencies = Vec::new();
636 for (label, member_name, case_resolved) in cases {
637 let case_ast = case_resolved.dynamic_type.ast();
638 let case_id = case_resolved.dynamic_type.id();
639 let case_type_id = type_identifier(case_ast, case_id, Form::Complete)
640 .map_err(|e| RpcDispatchError::Model(format!("{member_name}: {e:?}")))?;
641 builder = builder.case(member_name.clone(), case_type_id, vec![*label]);
642 dependencies.push(
643 complete_type_object(case_ast, case_id)
644 .map_err(|e| RpcDispatchError::Model(format!("{member_name}: {e:?}")))?,
645 );
646 for dep in dependency_type_objects(case_ast, case_id)
647 .map_err(|e| RpcDispatchError::Model(format!("{member_name}: {e:?}")))?
648 {
649 dependencies.push(dep);
650 }
651 }
652 if let Some(default_member) = default_member {
653 builder = builder.default_case(
654 default_member,
655 TypeIdentifier::Primitive(PrimitiveKind::Byte),
656 );
657 }
658 let root = CompleteTypeObject::Union(builder.build_complete());
659 types
660 .resolve_synthetic(type_name, root, dependencies)
661 .map_err(|e| RpcDispatchError::Model(e.to_string()))
662}
663
664#[cfg(test)]
665#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
666mod tests {
667 use super::*;
668
669 fn idl_ast(idl: &str) -> Arc<Ast> {
670 Arc::new(zerodds_wire_probe::encode::model_from_idl(idl, "t.idl").unwrap())
671 }
672
673 fn leased_participant() -> (zerodds_test_domains::DomainLease, DomainParticipant) {
674 let lease = zerodds_test_domains::lease_domain();
675 let participant = zerodds_dcps::DomainParticipantFactory::instance()
676 .create_participant(lease.id(), zerodds_dcps::DomainParticipantQos::default())
677 .expect("participant");
678 (lease, participant)
679 }
680
681 #[test]
682 fn finds_a_declared_operation_across_flattened_services() {
683 let ast = idl_ast("interface Calculator { long add(in long a, in long b); };");
684 let (_lease, participant) = leased_participant();
685 let dispatcher = RpcDispatcher::new(ast, participant).expect("new");
686 assert!(dispatcher.knows_operation("add"));
687 assert!(!dispatcher.knows_operation("subtract"));
688 }
689
690 #[test]
691 fn an_operation_with_no_params_gets_a_dummy_member() {
692 let ast = idl_ast("interface Fyi { void ping(); };");
693 let (root, _deps) = build_synthetic_struct(&ast, "Fyi_ping::In", &[]).expect("build");
694 let CompleteTypeObject::Struct(s) = root else {
695 panic!("expected struct")
696 };
697 assert_eq!(s.member_seq.len(), 1);
698 assert_eq!(s.member_seq[0].detail.name, DUMMY_MEMBER);
699 }
700
701 #[test]
702 fn every_synthetic_type_is_final_not_appendable() {
703 let ast = idl_ast("interface Calculator { long add(in long a, in long b); };");
704 let members = vec![("a".to_string(), {
705 let svc = services(&ast).unwrap().into_iter().next().unwrap();
706 let op = all_operations_flattened(&ast, svc.id)
707 .unwrap()
708 .into_iter()
709 .next()
710 .unwrap();
711 op.params[0].type_
712 })];
713 let (root, _deps) = build_synthetic_struct(&ast, "Calculator_add::In", &members).unwrap();
714 let CompleteTypeObject::Struct(s) = root else {
715 panic!("expected struct")
716 };
717 use zerodds_types::type_object::flags::StructTypeFlag;
718 assert!(!s.struct_flags.has(StructTypeFlag::IS_APPENDABLE));
719 assert!(!s.struct_flags.has(StructTypeFlag::IS_MUTABLE));
720 }
721
722 #[test]
723 fn builds_one_interface_wide_call_and_return_union() {
724 let ast = idl_ast(
725 "interface Calculator { long add(in long a, in long b); void log(in string msg); };",
726 );
727 let (_lease, participant) = leased_participant();
728 let dispatcher = RpcDispatcher::new(Arc::clone(&ast), participant).expect("new");
729 let service = services(&ast).unwrap().into_iter().next().unwrap();
730 let model = dispatcher.model_for(&service).expect("model");
731
732 assert_eq!(model.call_resolved.dynamic_type.name(), "Calculator_Call");
735 assert_eq!(model.call_resolved.dynamic_type.name(), "Calculator_Call");
736 assert_eq!(model.operations.len(), 2);
737
738 let add_disc = operation_hash("add");
739 let log_disc = operation_hash("log");
740 assert!(model.operations.contains_key(&add_disc));
741 assert!(model.operations.contains_key(&log_disc));
742 assert_eq!(
744 model.operations[&log_disc]
745 .result_resolved
746 .dynamic_type
747 .name(),
748 "Calculator_log_Result"
749 );
750 assert_eq!(
751 model.operations[&add_disc]
752 .result_resolved
753 .dynamic_type
754 .name(),
755 "Calculator_add_Result"
756 );
757 }
758}