1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::{self, Write};
3
4use camino::Utf8Path;
5use rpc_idl_ir::{
6 CanonicalIr, IrEnumValue, IrField, IrService, IrType, IrTypeKind, IrTypeRef, IrUnionVariant,
7};
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub enum GenerateSide {
11 #[default]
12 Both,
13 Client,
14 Server,
15}
16
17impl GenerateSide {
18 pub fn parse(value: &str) -> Result<Self, GenerateError> {
19 match value {
20 "both" => Ok(Self::Both),
21 "client" => Ok(Self::Client),
22 "server" => Ok(Self::Server),
23 _ => Err(GenerateError::new(
24 "generator option `side` must be one of: both, client, server",
25 )),
26 }
27 }
28
29 fn includes_client(self) -> bool {
30 matches!(self, Self::Both | Self::Client)
31 }
32
33 fn includes_server(self) -> bool {
34 matches!(self, Self::Both | Self::Server)
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct GenerateOptions {
40 pub side: GenerateSide,
41 pub namespace_prefix: Option<String>,
42 pub include_prefix: String,
43 pub library_name: String,
44}
45
46impl Default for GenerateOptions {
47 fn default() -> Self {
48 Self {
49 side: GenerateSide::Both,
50 namespace_prefix: None,
51 include_prefix: "tripley_rpc_generated".to_string(),
52 library_name: "tripley_rpc_generated".to_string(),
53 }
54 }
55}
56
57impl GenerateOptions {
58 pub fn from_cli_opts(opts: &[String]) -> Result<Self, GenerateError> {
59 let mut parsed = Self::default();
60 for opt in opts {
61 let Some((key, value)) = opt.split_once('=') else {
62 return Err(GenerateError::new(format!(
63 "generator option `{opt}` must use key=value syntax"
64 )));
65 };
66 match key {
67 "side" => parsed.side = GenerateSide::parse(value)?,
68 "namespace_prefix" | "namespace-prefix" => {
69 parsed.namespace_prefix = if value.trim().is_empty() {
70 None
71 } else {
72 Some(value.trim().to_string())
73 };
74 }
75 "include_prefix" | "include-prefix" => {
76 if value.trim().is_empty() {
77 return Err(GenerateError::new(
78 "generator option `include_prefix` must not be empty",
79 ));
80 }
81 parsed.include_prefix = value.trim().trim_matches('/').to_string();
82 }
83 "library_name" | "library-name" => {
84 if value.trim().is_empty() {
85 return Err(GenerateError::new(
86 "generator option `library_name` must not be empty",
87 ));
88 }
89 parsed.library_name = value.trim().to_string();
90 }
91 _ => {}
92 }
93 }
94 Ok(parsed)
95 }
96}
97
98#[derive(Debug)]
99pub struct GenerateError {
100 message: String,
101}
102
103impl GenerateError {
104 fn new(message: impl Into<String>) -> Self {
105 Self {
106 message: message.into(),
107 }
108 }
109}
110
111impl fmt::Display for GenerateError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 self.message.fmt(f)
114 }
115}
116
117impl std::error::Error for GenerateError {}
118
119pub fn generate_to_dir(
120 ir: &CanonicalIr,
121 out_dir: &Utf8Path,
122 options: GenerateOptions,
123) -> Result<(), GenerateError> {
124 std::fs::create_dir_all(out_dir).map_err(|error| {
125 GenerateError::new(format!(
126 "failed to create output directory `{out_dir}`: {error}"
127 ))
128 })?;
129 for (name, content) in generate(ir, &options)? {
130 let path = out_dir.join(name);
131 if let Some(parent) = path.parent() {
132 std::fs::create_dir_all(parent).map_err(|error| {
133 GenerateError::new(format!("failed to create `{parent}`: {error}"))
134 })?;
135 }
136 std::fs::write(&path, content)
137 .map_err(|error| GenerateError::new(format!("failed to write `{path}`: {error}")))?;
138 }
139 Ok(())
140}
141
142pub fn generate(
143 ir: &CanonicalIr,
144 options: &GenerateOptions,
145) -> Result<BTreeMap<String, String>, GenerateError> {
146 let model = Model::new(ir, options)?;
147 let mut files = BTreeMap::new();
148 files.insert("CMakeLists.txt".to_string(), generate_cmake(options));
149 files.insert(
150 format!("include/{}/types.h", options.include_prefix),
151 generate_types_h(&model, options)?,
152 );
153 files.insert(
154 format!("include/{}/services.h", options.include_prefix),
155 generate_services_h(&model, options),
156 );
157 files.insert("src/types.cc".to_string(), generate_types_cc(options));
158 if options.side.includes_client() {
159 files.insert(
160 format!("include/{}/client.h", options.include_prefix),
161 generate_client_h(&model, options),
162 );
163 files.insert(
164 "src/client.cc".to_string(),
165 generate_client_cc(&model, options),
166 );
167 }
168 if options.side.includes_server() {
169 files.insert(
170 format!("include/{}/server.h", options.include_prefix),
171 generate_server_h(&model, options),
172 );
173 files.insert(
174 format!("include/{}/activation.h", options.include_prefix),
175 generate_activation_h(&model, options),
176 );
177 files.insert(
178 "src/server.cc".to_string(),
179 generate_unit_cc(options, "server.h"),
180 );
181 files.insert(
182 "src/activation.cc".to_string(),
183 generate_unit_cc(options, "activation.h"),
184 );
185 }
186 Ok(files)
187}
188
189struct Model<'a> {
190 types: BTreeMap<&'a str, &'a IrType>,
191 services: Vec<&'a IrService>,
192 namespaces: BTreeMap<&'a str, Vec<String>>,
193}
194
195impl<'a> Model<'a> {
196 fn new(ir: &'a CanonicalIr, options: &GenerateOptions) -> Result<Self, GenerateError> {
197 let mut types = BTreeMap::new();
198 let mut names = BTreeSet::new();
199 let mut namespaces = BTreeMap::new();
200 for ty in &ir.types {
201 let name = cpp_type_name(&ty.name);
202 let ns = namespace_parts(options, &ty.package);
203 let key = format!("{}::{}", ns.join("::"), name);
204 if !names.insert(key.clone()) {
205 return Err(GenerateError::new(format!(
206 "C++ generator cannot disambiguate duplicate type `{key}`"
207 )));
208 }
209 namespaces.insert(ty.fqtn.as_str(), ns);
210 types.insert(ty.fqtn.as_str(), ty);
211 }
212 for service in &ir.services {
213 namespaces.insert(
214 service.fqsn.as_str(),
215 namespace_parts(options, &service.package),
216 );
217 }
218 Ok(Self {
219 types,
220 services: ir.services.iter().collect(),
221 namespaces,
222 })
223 }
224}
225
226fn generate_cmake(options: &GenerateOptions) -> String {
227 let mut sources = String::from(" src/types.cc\n");
228 if options.side.includes_client() {
229 sources.push_str(" src/client.cc\n");
230 }
231 if options.side.includes_server() {
232 sources.push_str(" src/server.cc\n src/activation.cc\n");
233 }
234 format!(
235 r#"cmake_minimum_required(VERSION 3.24)
236project({library} LANGUAGES CXX)
237
238add_library({library}
239{sources}
240)
241target_compile_features({library} PUBLIC cxx_std_20)
242target_include_directories({library} PUBLIC ${{CMAKE_CURRENT_SOURCE_DIR}}/include)
243target_link_libraries({library} PUBLIC TripleyRpc::runtime)
244"#,
245 library = options.library_name,
246 sources = sources
247 )
248}
249
250fn generate_types_h(model: &Model<'_>, options: &GenerateOptions) -> Result<String, GenerateError> {
251 let mut out = header_preamble(options, "types.h");
252 out.push_str("#include <cstdint>\n#include <map>\n#include <optional>\n#include <string>\n#include <variant>\n#include <vector>\n\n#include <msgpack.hpp>\n\n#include \"tripley/rpc/codec.h\"\n\n");
253 for ty in model.types.values() {
254 let ns = namespace_for(model, &ty.fqtn);
255 open_namespace(&mut out, ns);
256 match &ty.kind {
257 IrTypeKind::Enum { base, values, .. } => gen_enum(&mut out, ty, base, values),
258 IrTypeKind::Flags { base, values, .. } => gen_flags(&mut out, ty, base, values),
259 IrTypeKind::Struct { .. } | IrTypeKind::Union { .. } => {}
260 }
261 close_namespace(&mut out, ns);
262 }
263 for ty in model.types.values() {
264 if matches!(ty.kind, IrTypeKind::Enum { .. }) {
265 let qualified = cpp_type_ref(
266 model,
267 &IrTypeRef::Named {
268 fqtn: ty.fqtn.clone(),
269 },
270 );
271 writeln!(out, "MSGPACK_ADD_ENUM({qualified});").unwrap();
272 }
273 }
274 if model
275 .types
276 .values()
277 .any(|ty| matches!(ty.kind, IrTypeKind::Enum { .. }))
278 {
279 writeln!(out).unwrap();
280 }
281 for ty in model.types.values() {
282 let ns = namespace_for(model, &ty.fqtn);
283 open_namespace(&mut out, ns);
284 match &ty.kind {
285 IrTypeKind::Struct { fields } => gen_struct(&mut out, model, ty, fields)?,
286 IrTypeKind::Union {
287 discriminator_type,
288 variants,
289 } => gen_union(&mut out, model, ty, discriminator_type, variants)?,
290 IrTypeKind::Enum { .. } | IrTypeKind::Flags { .. } => {}
291 }
292 close_namespace(&mut out, ns);
293 }
294 out.push_str("#endif\n");
295 Ok(out)
296}
297
298fn generate_services_h(model: &Model<'_>, options: &GenerateOptions) -> String {
299 let mut out = header_preamble(options, "services.h");
300 out.push_str(
301 "#include <cstdint>\n#include <string_view>\n\n#include \"tripley/rpc/core.h\"\n\n",
302 );
303 for service in &model.services {
304 let ns = namespace_for(model, &service.fqsn);
305 open_namespace(&mut out, ns);
306 let prefix = constant_prefix(&service.name);
307 writeln!(
308 out,
309 "inline constexpr std::string_view k{}ServiceGuid = \"{}\";",
310 prefix, service.guid
311 )
312 .unwrap();
313 for method in &service.methods {
314 writeln!(
315 out,
316 "inline constexpr std::uint32_t k{}{}MethodId = {};",
317 prefix,
318 cpp_type_name(&method.name),
319 method.id
320 )
321 .unwrap();
322 }
323 for notification in &service.notifications {
324 writeln!(
325 out,
326 "inline constexpr std::uint32_t k{}{}NotificationId = {};",
327 prefix,
328 cpp_type_name(¬ification.name),
329 notification.id
330 )
331 .unwrap();
332 }
333 writeln!(out).unwrap();
334 close_namespace(&mut out, ns);
335 }
336 out.push_str("#endif\n");
337 out
338}
339
340fn generate_client_h(model: &Model<'_>, options: &GenerateOptions) -> String {
341 let mut out = header_preamble(options, "client.h");
342 writeln!(
343 out,
344 "#include <map>\n#include <string>\n\n#include <boost/asio/awaitable.hpp>\n\n#include \"{}/services.h\"\n#include \"{}/types.h\"\n#include \"tripley/rpc/client.h\"\n\n",
345 options.include_prefix, options.include_prefix
346 )
347 .unwrap();
348 for service in &model.services {
349 let ns = namespace_for(model, &service.fqsn);
350 open_namespace(&mut out, ns);
351 let class_name = format!("{}Client", cpp_type_name(&service.name));
352 writeln!(out, "class {class_name} {{").unwrap();
353 writeln!(out, " public:").unwrap();
354 writeln!(
355 out,
356 " {class_name}(tripley::rpc::RpcClient& client, tripley::rpc::InstanceId instance_id);"
357 )
358 .unwrap();
359 writeln!(
360 out,
361 " static boost::asio::awaitable<{class_name}> ResolveNamed(tripley::rpc::RpcClient& client, std::string name);"
362 )
363 .unwrap();
364 if service
365 .activation_modes
366 .iter()
367 .any(|mode| mode == "instantiable")
368 {
369 if let Some(request_type) = &service.create_request_type {
370 let request = cpp_type_ref(
371 model,
372 &IrTypeRef::Named {
373 fqtn: request_type.clone(),
374 },
375 );
376 writeln!(
377 out,
378 " static boost::asio::awaitable<{class_name}> Create(tripley::rpc::RpcClient& client, const {request}& request, std::map<std::string, std::string> options = {{}});"
379 )
380 .unwrap();
381 } else {
382 writeln!(
383 out,
384 " static boost::asio::awaitable<{class_name}> Create(tripley::rpc::RpcClient& client, std::map<std::string, std::string> options = {{}});"
385 )
386 .unwrap();
387 }
388 }
389 for method in &service.methods {
390 let request = cpp_type_ref(
391 model,
392 &IrTypeRef::Named {
393 fqtn: method.request_type.clone(),
394 },
395 );
396 let response = cpp_type_ref(
397 model,
398 &IrTypeRef::Named {
399 fqtn: method.response_type.clone(),
400 },
401 );
402 writeln!(
403 out,
404 " boost::asio::awaitable<{response}> {}(const {request}& request);",
405 cpp_type_name(&method.name)
406 )
407 .unwrap();
408 }
409 for notification in &service.notifications {
410 let payload = cpp_type_ref(
411 model,
412 &IrTypeRef::Named {
413 fqtn: notification.payload_type.clone(),
414 },
415 );
416 writeln!(
417 out,
418 " tripley::rpc::NotificationStream<{payload}> Subscribe{}();",
419 cpp_type_name(¬ification.name)
420 )
421 .unwrap();
422 }
423 writeln!(
424 out,
425 " tripley::rpc::InstanceId instance_id() const {{ return instance_id_; }}"
426 )
427 .unwrap();
428 writeln!(out, "\n private:\n tripley::rpc::RpcClient* client_;\n tripley::rpc::InstanceId instance_id_;\n}};\n").unwrap();
429 writeln!(
430 out,
431 "inline {class_name}::{class_name}(tripley::rpc::RpcClient& client, tripley::rpc::InstanceId instance_id) : client_(&client), instance_id_(instance_id) {{}}"
432 )
433 .unwrap();
434 for method in &service.methods {
435 let request = cpp_type_ref(
436 model,
437 &IrTypeRef::Named {
438 fqtn: method.request_type.clone(),
439 },
440 );
441 let response = cpp_type_ref(
442 model,
443 &IrTypeRef::Named {
444 fqtn: method.response_type.clone(),
445 },
446 );
447 writeln!(
448 out,
449 "inline boost::asio::awaitable<{response}> {class_name}::{}(const {request}& request) {{",
450 cpp_type_name(&method.name)
451 )
452 .unwrap();
453 writeln!(
454 out,
455 " co_return co_await client_->Call<{request}, {response}>(instance_id_, {}, request);",
456 method.id
457 )
458 .unwrap();
459 writeln!(out, "}}\n").unwrap();
460 }
461 for notification in &service.notifications {
462 let payload = cpp_type_ref(
463 model,
464 &IrTypeRef::Named {
465 fqtn: notification.payload_type.clone(),
466 },
467 );
468 writeln!(
469 out,
470 "inline tripley::rpc::NotificationStream<{payload}> {class_name}::Subscribe{}() {{",
471 cpp_type_name(¬ification.name)
472 )
473 .unwrap();
474 writeln!(
475 out,
476 " return client_->SubscribeNotifications<{payload}>(instance_id_, k{}{}NotificationId);",
477 constant_prefix(&service.name),
478 cpp_type_name(¬ification.name)
479 )
480 .unwrap();
481 writeln!(out, "}}\n").unwrap();
482 }
483 writeln!(out).unwrap();
484 close_namespace(&mut out, ns);
485 }
486 out.push_str("#endif\n");
487 out
488}
489
490fn generate_client_cc(model: &Model<'_>, options: &GenerateOptions) -> String {
491 let mut out = generate_unit_cc(options, "client.h");
492 out.push_str("\n#include <cstdint>\n#include <utility>\n#include <vector>\n\n");
493 for service in &model.services {
494 let ns = namespace_for(model, &service.fqsn);
495 open_namespace(&mut out, ns);
496 let class_name = format!("{}Client", cpp_type_name(&service.name));
497 let prefix = constant_prefix(&service.name);
498 writeln!(
499 out,
500 "boost::asio::awaitable<{class_name}> {class_name}::ResolveNamed(tripley::rpc::RpcClient& client, std::string name) {{"
501 )
502 .unwrap();
503 writeln!(out, " std::vector<std::string> names;").unwrap();
504 writeln!(out, " names.push_back(std::move(name));").unwrap();
505 writeln!(
506 out,
507 " auto ids = co_await client.ResolveInstanceIds(std::move(names));"
508 )
509 .unwrap();
510 writeln!(
511 out,
512 " if (ids.empty() || ids[0] == tripley::rpc::kInvalidInstanceId) {{"
513 )
514 .unwrap();
515 writeln!(
516 out,
517 " throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kInstanceNotFound, tripley::rpc::ErrorKind::kRuntime, \"named instance was not found\");"
518 )
519 .unwrap();
520 writeln!(out, " }}").unwrap();
521 writeln!(out, " co_return {class_name}(client, ids[0]);").unwrap();
522 writeln!(out, "}}\n").unwrap();
523 if service
524 .activation_modes
525 .iter()
526 .any(|mode| mode == "instantiable")
527 {
528 if let Some(request_type) = &service.create_request_type {
529 let request = cpp_type_ref(
530 model,
531 &IrTypeRef::Named {
532 fqtn: request_type.clone(),
533 },
534 );
535 writeln!(
536 out,
537 "boost::asio::awaitable<{class_name}> {class_name}::Create(tripley::rpc::RpcClient& client, const {request}& request, std::map<std::string, std::string> options) {{"
538 )
539 .unwrap();
540 writeln!(out, " msgpack::zone zone;").unwrap();
541 writeln!(
542 out,
543 " auto payload_object = tripley::rpc::EncodePayload(request, zone);"
544 )
545 .unwrap();
546 writeln!(out, " msgpack::sbuffer buffer;").unwrap();
547 writeln!(out, " msgpack::pack(buffer, payload_object);").unwrap();
548 writeln!(
549 out,
550 " auto* begin = reinterpret_cast<const std::uint8_t*>(buffer.data());"
551 )
552 .unwrap();
553 writeln!(
554 out,
555 " std::vector<std::uint8_t> payload(begin, begin + buffer.size());"
556 )
557 .unwrap();
558 writeln!(
559 out,
560 " auto instance_id = co_await client.CreateInstance(tripley::rpc::ParseServiceGuid(k{prefix}ServiceGuid), std::move(payload), std::move(options));"
561 )
562 .unwrap();
563 } else {
564 writeln!(
565 out,
566 "boost::asio::awaitable<{class_name}> {class_name}::Create(tripley::rpc::RpcClient& client, std::map<std::string, std::string> options) {{"
567 )
568 .unwrap();
569 writeln!(
570 out,
571 " auto instance_id = co_await client.CreateInstance(tripley::rpc::ParseServiceGuid(k{prefix}ServiceGuid), std::nullopt, std::move(options));"
572 )
573 .unwrap();
574 }
575 writeln!(out, " co_return {class_name}(client, instance_id);").unwrap();
576 writeln!(out, "}}\n").unwrap();
577 }
578 close_namespace(&mut out, ns);
579 }
580 out
581}
582
583fn generate_server_h(model: &Model<'_>, options: &GenerateOptions) -> String {
584 let mut out = header_preamble(options, "server.h");
585 writeln!(
586 out,
587 "#include <cstddef>\n#include <cstdint>\n#include <map>\n#include <memory>\n#include <optional>\n#include <set>\n#include <string>\n#include <utility>\n#include <vector>\n\n#include <boost/asio/awaitable.hpp>\n\n#include \"{}/services.h\"\n#include \"{}/types.h\"\n#include \"tripley/rpc/server.h\"\n\n",
588 options.include_prefix, options.include_prefix
589 )
590 .unwrap();
591 for service in &model.services {
592 let ns = namespace_for(model, &service.fqsn);
593 open_namespace(&mut out, ns);
594 let service_name = cpp_type_name(&service.name);
595 let prefix = constant_prefix(&service.name);
596 writeln!(out, "class {service_name}Service {{").unwrap();
597 writeln!(
598 out,
599 " public:\n virtual ~{service_name}Service() = default;"
600 )
601 .unwrap();
602 for method in &service.methods {
603 let request = cpp_type_ref(
604 model,
605 &IrTypeRef::Named {
606 fqtn: method.request_type.clone(),
607 },
608 );
609 let response = cpp_type_ref(
610 model,
611 &IrTypeRef::Named {
612 fqtn: method.response_type.clone(),
613 },
614 );
615 writeln!(
616 out,
617 " virtual boost::asio::awaitable<{response}> {}(tripley::rpc::RpcCallContext& ctx, const {request}& request) = 0;",
618 cpp_type_name(&method.name)
619 )
620 .unwrap();
621 }
622 writeln!(out, "}};\n").unwrap();
623 if service
624 .activation_modes
625 .iter()
626 .any(|mode| mode == "instantiable")
627 {
628 writeln!(out, "class {service_name}Factory {{").unwrap();
629 writeln!(
630 out,
631 " public:\n virtual ~{service_name}Factory() = default;"
632 )
633 .unwrap();
634 if let Some(request_type) = &service.create_request_type {
635 let request = cpp_type_ref(
636 model,
637 &IrTypeRef::Named {
638 fqtn: request_type.clone(),
639 },
640 );
641 writeln!(
642 out,
643 " virtual boost::asio::awaitable<std::shared_ptr<{service_name}Service>> Create(tripley::rpc::RpcCallContext& ctx, const {request}& request, std::map<std::string, std::string> options) = 0;"
644 )
645 .unwrap();
646 } else {
647 writeln!(
648 out,
649 " virtual boost::asio::awaitable<std::shared_ptr<{service_name}Service>> Create(tripley::rpc::RpcCallContext& ctx, std::optional<std::vector<std::uint8_t>> create_payload, std::map<std::string, std::string> options) = 0;"
650 )
651 .unwrap();
652 }
653 writeln!(out, "}};\n").unwrap();
654 }
655 writeln!(
656 out,
657 "inline tripley::rpc::ServiceHandler Make{}Handler(std::shared_ptr<{service_name}Service> service) {{",
658 service_name
659 )
660 .unwrap();
661 writeln!(out, " return [service = std::move(service)](tripley::rpc::RpcCallContext& ctx, tripley::rpc::MethodId method_id, const msgpack::object& payload) -> boost::asio::awaitable<tripley::rpc::Payload> {{").unwrap();
662 writeln!(out, " msgpack::zone zone;").unwrap();
663 writeln!(out, " switch (method_id) {{").unwrap();
664 for method in &service.methods {
665 let request = cpp_type_ref(
666 model,
667 &IrTypeRef::Named {
668 fqtn: method.request_type.clone(),
669 },
670 );
671 writeln!(out, " case {}: {{", method.id).unwrap();
672 writeln!(
673 out,
674 " auto response = co_await service->{}(ctx, tripley::rpc::DecodePayload<{request}>(payload));",
675 cpp_type_name(&method.name)
676 )
677 .unwrap();
678 writeln!(
679 out,
680 " co_return msgpack::clone(tripley::rpc::EncodePayload(response, zone));"
681 )
682 .unwrap();
683 writeln!(out, " }}").unwrap();
684 }
685 writeln!(out, " default: throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kMethodNotFound, tripley::rpc::ErrorKind::kRuntime, \"method not found\");").unwrap();
686 writeln!(out, " }}").unwrap();
687 writeln!(out, " }};").unwrap();
688 writeln!(out, "}}\n").unwrap();
689 writeln!(
690 out,
691 "inline void Register{}Named(tripley::rpc::RpcServerBuilder& builder, std::string name, std::shared_ptr<{service_name}Service> service) {{",
692 service_name
693 )
694 .unwrap();
695 write!(
696 out,
697 " builder.RegisterNamed(std::move(name), tripley::rpc::ParseServiceGuid(k{}ServiceGuid), std::set<tripley::rpc::MethodId>{{",
698 constant_prefix(&service.name)
699 )
700 .unwrap();
701 for (index, method) in service.methods.iter().enumerate() {
702 if index > 0 {
703 write!(out, ", ").unwrap();
704 }
705 write!(out, "{}", method.id).unwrap();
706 }
707 writeln!(out, "}},").unwrap();
708 writeln!(out, " Make{}Handler(std::move(service)));", service_name).unwrap();
709 writeln!(out, "}}\n").unwrap();
710 if service
711 .activation_modes
712 .iter()
713 .any(|mode| mode == "instantiable")
714 {
715 writeln!(
716 out,
717 "inline void Register{}Factory(tripley::rpc::RpcServerBuilder& builder, std::shared_ptr<{service_name}Factory> factory) {{",
718 service_name
719 )
720 .unwrap();
721 write!(
722 out,
723 " builder.RegisterFactory(tripley::rpc::ParseServiceGuid(k{prefix}ServiceGuid), std::set<tripley::rpc::MethodId>{{"
724 )
725 .unwrap();
726 for (index, method) in service.methods.iter().enumerate() {
727 if index > 0 {
728 write!(out, ", ").unwrap();
729 }
730 write!(out, "{}", method.id).unwrap();
731 }
732 writeln!(out, "}},").unwrap();
733 writeln!(out, " [factory = std::move(factory)](tripley::rpc::RpcCallContext& ctx, std::optional<std::vector<std::uint8_t>> create_payload, std::map<std::string, std::string> options) -> boost::asio::awaitable<std::shared_ptr<tripley::rpc::ServiceHandler>> {{").unwrap();
734 if let Some(request_type) = &service.create_request_type {
735 let request = cpp_type_ref(
736 model,
737 &IrTypeRef::Named {
738 fqtn: request_type.clone(),
739 },
740 );
741 writeln!(out, " if (!create_payload.has_value()) {{").unwrap();
742 writeln!(out, " throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kPayloadDecodeFailed, tripley::rpc::ErrorKind::kRuntime, \"missing constructor payload\");").unwrap();
743 writeln!(out, " }}").unwrap();
744 writeln!(out, " std::size_t offset = 0;").unwrap();
745 writeln!(out, " auto handle = msgpack::unpack(reinterpret_cast<const char*>(create_payload->data()), create_payload->size(), offset);").unwrap();
746 writeln!(out, " if (offset != create_payload->size()) {{").unwrap();
747 writeln!(out, " throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kPayloadDecodeFailed, tripley::rpc::ErrorKind::kRuntime, \"trailing bytes after constructor payload\");").unwrap();
748 writeln!(out, " }}").unwrap();
749 writeln!(
750 out,
751 " auto request = tripley::rpc::DecodePayload<{request}>(handle.get());"
752 )
753 .unwrap();
754 writeln!(
755 out,
756 " auto service = co_await factory->Create(ctx, request, std::move(options));"
757 )
758 .unwrap();
759 } else {
760 writeln!(
761 out,
762 " auto service = co_await factory->Create(ctx, std::move(create_payload), std::move(options));"
763 )
764 .unwrap();
765 }
766 writeln!(out, " co_return std::make_shared<tripley::rpc::ServiceHandler>(Make{}Handler(std::move(service)));", service_name).unwrap();
767 writeln!(out, " }});").unwrap();
768 writeln!(out, "}}\n").unwrap();
769 }
770 writeln!(out).unwrap();
771 close_namespace(&mut out, ns);
772 }
773 out.push_str("#endif\n");
774 out
775}
776
777fn generate_activation_h(model: &Model<'_>, options: &GenerateOptions) -> String {
778 let mut out = header_preamble(options, "activation.h");
779 writeln!(
780 out,
781 "#include \"{}/services.h\"\n#include \"tripley/rpc/activation.h\"\n\n",
782 options.include_prefix
783 )
784 .unwrap();
785 for service in &model.services {
786 let ns = namespace_for(model, &service.fqsn);
787 open_namespace(&mut out, ns);
788 writeln!(
789 out,
790 "inline tripley::rpc::ServiceGuid {}ServiceGuid() {{ return tripley::rpc::ParseServiceGuid(k{}ServiceGuid); }}",
791 cpp_type_name(&service.name),
792 constant_prefix(&service.name)
793 )
794 .unwrap();
795 close_namespace(&mut out, ns);
796 }
797 out.push_str("#endif\n");
798 out
799}
800
801fn generate_types_cc(options: &GenerateOptions) -> String {
802 format!("#include \"{}/types.h\"\n", options.include_prefix)
803}
804
805fn generate_unit_cc(options: &GenerateOptions, header: &str) -> String {
806 format!("#include \"{}/{header}\"\n", options.include_prefix)
807}
808
809fn gen_struct(
810 out: &mut String,
811 model: &Model<'_>,
812 ty: &IrType,
813 fields: &[IrField],
814) -> Result<(), GenerateError> {
815 let name = cpp_type_name(&ty.name);
816 writeln!(out, "struct {name} {{").unwrap();
817 for field in fields {
818 writeln!(
819 out,
820 " {} {};",
821 cpp_type_ref(model, &field_type_ref(field)),
822 cpp_field_name(&field.name)
823 )
824 .unwrap();
825 }
826 if fields.is_empty() {
827 writeln!(out, " MSGPACK_DEFINE_ARRAY();").unwrap();
828 } else {
829 write!(out, " MSGPACK_DEFINE_ARRAY(").unwrap();
830 for (index, field) in fields.iter().enumerate() {
831 if index > 0 {
832 write!(out, ", ").unwrap();
833 }
834 write!(out, "{}", cpp_field_name(&field.name)).unwrap();
835 }
836 writeln!(out, ");").unwrap();
837 }
838 writeln!(out, "}};\n").unwrap();
839 writeln!(
840 out,
841 "msgpack::object Encode{}(const {name}& value, msgpack::zone& zone);",
842 name
843 )
844 .unwrap();
845 writeln!(
846 out,
847 "{name} Decode{}(const msgpack::object& object);\n",
848 name
849 )
850 .unwrap();
851 Ok(())
852}
853
854fn gen_enum(out: &mut String, ty: &IrType, base: &str, values: &[IrEnumValue]) {
855 let name = cpp_type_name(&ty.name);
856 writeln!(out, "enum class {name} : {} {{", cpp_scalar_type(base)).unwrap();
857 for value in values {
858 writeln!(
859 out,
860 " k{} = {},",
861 cpp_type_name(&value.name.to_ascii_lowercase()),
862 value.value
863 )
864 .unwrap();
865 }
866 writeln!(out, "}};\n").unwrap();
867}
868
869fn gen_flags(out: &mut String, ty: &IrType, base: &str, values: &[IrEnumValue]) {
870 let name = cpp_type_name(&ty.name);
871 let base = cpp_scalar_type(base);
872 writeln!(out, "class {name} {{").unwrap();
873 writeln!(out, " public:").unwrap();
874 writeln!(out, " using ValueType = {base};").unwrap();
875 writeln!(out, " constexpr {name}() = default;").unwrap();
876 writeln!(
877 out,
878 " explicit constexpr {name}(ValueType value) : value_(value) {{}}"
879 )
880 .unwrap();
881 writeln!(
882 out,
883 " constexpr ValueType value() const {{ return value_; }}"
884 )
885 .unwrap();
886 for value in values {
887 writeln!(
888 out,
889 " static constexpr ValueType k{} = static_cast<ValueType>({});",
890 cpp_type_name(&value.name.to_ascii_lowercase()),
891 value.value
892 )
893 .unwrap();
894 }
895 writeln!(out, "\n private:\n ValueType value_ = 0;\n}};\n").unwrap();
896}
897
898fn gen_union(
899 out: &mut String,
900 model: &Model<'_>,
901 ty: &IrType,
902 _discriminator_type: &str,
903 variants: &[IrUnionVariant],
904) -> Result<(), GenerateError> {
905 let name = cpp_type_name(&ty.name);
906 writeln!(out, "class {name} {{").unwrap();
907 writeln!(out, " public:").unwrap();
908 writeln!(out, " enum class Tag : std::int64_t {{").unwrap();
909 for variant in variants {
910 writeln!(
911 out,
912 " k{} = {},",
913 cpp_type_name(&variant.name),
914 variant.tag
915 )
916 .unwrap();
917 }
918 writeln!(out, " }};").unwrap();
919 write!(out, " using Value = std::variant<").unwrap();
920 for (index, variant) in variants.iter().enumerate() {
921 if index > 0 {
922 write!(out, ", ").unwrap();
923 }
924 write!(out, "{}", cpp_type_ref(model, &variant.ty)).unwrap();
925 }
926 writeln!(out, ">;").unwrap();
927 writeln!(
928 out,
929 " {name}(Tag tag, Value value) : tag_(tag), value_(std::move(value)) {{}}"
930 )
931 .unwrap();
932 writeln!(out, " Tag tag() const {{ return tag_; }}").unwrap();
933 writeln!(out, " const Value& value() const {{ return value_; }}").unwrap();
934 writeln!(out, "\n private:\n Tag tag_;\n Value value_;\n}};\n").unwrap();
935 Ok(())
936}
937
938fn field_type_ref(field: &IrField) -> IrTypeRef {
939 if field.optional {
940 if matches!(field.ty, IrTypeRef::Optional { .. }) {
941 return field.ty.clone();
942 }
943 IrTypeRef::Optional {
944 inner: Box::new(field.ty.clone()),
945 }
946 } else {
947 field.ty.clone()
948 }
949}
950
951fn cpp_type_ref(model: &Model<'_>, ty: &IrTypeRef) -> String {
952 match ty {
953 IrTypeRef::Scalar { name } => cpp_scalar_type(name).to_string(),
954 IrTypeRef::Named { fqtn } => {
955 let name = model
956 .types
957 .get(fqtn.as_str())
958 .map(|ty| cpp_type_name(&ty.name))
959 .unwrap_or_else(|| cpp_type_name(fqtn.rsplit('.').next().unwrap_or(fqtn)));
960 match model.namespaces.get(fqtn.as_str()) {
961 Some(ns) if !ns.is_empty() => format!("{}::{name}", ns.join("::")),
962 _ => name,
963 }
964 }
965 IrTypeRef::Optional { inner } => format!("std::optional<{}>", cpp_type_ref(model, inner)),
966 IrTypeRef::List { inner } => format!("std::vector<{}>", cpp_type_ref(model, inner)),
967 IrTypeRef::Map { key: _, value } => {
968 format!("std::map<std::string, {}>", cpp_type_ref(model, value))
969 }
970 }
971}
972
973fn cpp_scalar_type(name: &str) -> &'static str {
974 match name {
975 "bool" => "bool",
976 "i32" => "std::int32_t",
977 "i64" => "std::int64_t",
978 "u32" => "std::uint32_t",
979 "u64" => "std::uint64_t",
980 "f32" => "float",
981 "f64" => "double",
982 "string" => "std::string",
983 "bytes" => "std::vector<std::uint8_t>",
984 "uuid" => "tripley::rpc::Uuid",
985 _ => "msgpack::object",
986 }
987}
988
989fn namespace_parts(options: &GenerateOptions, package: &str) -> Vec<String> {
990 let mut parts = Vec::new();
991 if let Some(prefix) = &options.namespace_prefix {
992 parts.extend(
993 prefix
994 .split("::")
995 .filter(|part| !part.is_empty())
996 .map(sanitize_identifier),
997 );
998 }
999 parts.extend(package.split('.').map(sanitize_identifier));
1000 parts
1001}
1002
1003fn namespace_for<'a>(model: &'a Model<'_>, fq: &str) -> &'a [String] {
1004 model.namespaces.get(fq).map(Vec::as_slice).unwrap_or(&[])
1005}
1006
1007fn open_namespace(out: &mut String, parts: &[String]) {
1008 for part in parts {
1009 writeln!(out, "namespace {part} {{").unwrap();
1010 }
1011 if !parts.is_empty() {
1012 writeln!(out).unwrap();
1013 }
1014}
1015
1016fn close_namespace(out: &mut String, parts: &[String]) {
1017 for part in parts.iter().rev() {
1018 writeln!(out, "}} // namespace {part}").unwrap();
1019 }
1020 if !parts.is_empty() {
1021 writeln!(out).unwrap();
1022 }
1023}
1024
1025fn header_preamble(options: &GenerateOptions, file: &str) -> String {
1026 let guard = format!(
1027 "TRIPLEY_RPC_GENERATED_{}_{}_",
1028 options
1029 .include_prefix
1030 .replace(['/', '-'], "_")
1031 .to_ascii_uppercase(),
1032 file.replace(['.', '-'], "_").to_ascii_uppercase()
1033 );
1034 format!("// Generated by tripley-rpc. Do not edit.\n#ifndef {guard}\n#define {guard}\n\n")
1035}
1036
1037fn cpp_type_name(name: &str) -> String {
1038 let mut out = String::new();
1039 for part in split_words(name) {
1040 let mut chars = part.chars();
1041 if let Some(first) = chars.next() {
1042 out.push(first.to_ascii_uppercase());
1043 out.extend(chars.map(|ch| ch.to_ascii_lowercase()));
1044 }
1045 }
1046 if out.is_empty() {
1047 "Generated".to_string()
1048 } else if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
1049 format!("T{out}")
1050 } else {
1051 out
1052 }
1053}
1054
1055fn cpp_field_name(name: &str) -> String {
1056 let mut out = split_words(name).join("_").to_ascii_lowercase();
1057 if out.is_empty() {
1058 out = "field".to_string();
1059 }
1060 if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
1061 out.insert(0, 'f');
1062 }
1063 if is_cpp_keyword(&out) {
1064 out.push('_');
1065 }
1066 out
1067}
1068
1069fn constant_prefix(name: &str) -> String {
1070 cpp_type_name(name)
1071}
1072
1073fn sanitize_identifier(value: &str) -> String {
1074 let mut out = String::new();
1075 for ch in value.chars() {
1076 if ch.is_ascii_alphanumeric() || ch == '_' {
1077 out.push(ch.to_ascii_lowercase());
1078 } else {
1079 out.push('_');
1080 }
1081 }
1082 if out.is_empty() {
1083 "generated".to_string()
1084 } else if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
1085 format!("n{out}")
1086 } else if is_cpp_keyword(&out) {
1087 format!("{out}_")
1088 } else {
1089 out
1090 }
1091}
1092
1093fn split_words(value: &str) -> Vec<String> {
1094 let raw: Vec<_> = value
1095 .split(|ch: char| ch == '_' || ch == '-' || ch == '.' || ch.is_whitespace())
1096 .filter(|part| !part.is_empty())
1097 .map(ToOwned::to_owned)
1098 .collect();
1099 let mut words = Vec::new();
1100 for part in raw {
1101 let mut current = String::new();
1102 let mut prev_lower_or_digit = false;
1103 for ch in part.chars() {
1104 if ch.is_ascii_uppercase() && prev_lower_or_digit && !current.is_empty() {
1105 words.push(current);
1106 current = String::new();
1107 }
1108 prev_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
1109 current.push(ch);
1110 }
1111 if !current.is_empty() {
1112 words.push(current);
1113 }
1114 }
1115 words
1116}
1117
1118fn is_cpp_keyword(value: &str) -> bool {
1119 matches!(
1120 value,
1121 "alignas"
1122 | "alignof"
1123 | "and"
1124 | "asm"
1125 | "auto"
1126 | "bool"
1127 | "break"
1128 | "case"
1129 | "catch"
1130 | "char"
1131 | "class"
1132 | "const"
1133 | "constexpr"
1134 | "continue"
1135 | "default"
1136 | "delete"
1137 | "do"
1138 | "double"
1139 | "else"
1140 | "enum"
1141 | "explicit"
1142 | "export"
1143 | "extern"
1144 | "false"
1145 | "float"
1146 | "for"
1147 | "friend"
1148 | "goto"
1149 | "if"
1150 | "inline"
1151 | "int"
1152 | "long"
1153 | "namespace"
1154 | "new"
1155 | "noexcept"
1156 | "nullptr"
1157 | "operator"
1158 | "private"
1159 | "protected"
1160 | "public"
1161 | "return"
1162 | "short"
1163 | "signed"
1164 | "sizeof"
1165 | "static"
1166 | "struct"
1167 | "switch"
1168 | "template"
1169 | "this"
1170 | "throw"
1171 | "true"
1172 | "try"
1173 | "typedef"
1174 | "typename"
1175 | "union"
1176 | "unsigned"
1177 | "using"
1178 | "virtual"
1179 | "void"
1180 | "volatile"
1181 | "while"
1182 )
1183}