Skip to main content

rpc_gen_cpp/
lib.rs

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(&notification.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(&notification.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(&notification.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(&notification.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}