use std::collections::{BTreeMap, BTreeSet};
use std::fmt::{self, Write};
use camino::Utf8Path;
use rpc_idl_ir::{
CanonicalIr, IrEnumValue, IrField, IrService, IrType, IrTypeKind, IrTypeRef, IrUnionVariant,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GenerateSide {
#[default]
Both,
Client,
Server,
}
impl GenerateSide {
pub fn parse(value: &str) -> Result<Self, GenerateError> {
match value {
"both" => Ok(Self::Both),
"client" => Ok(Self::Client),
"server" => Ok(Self::Server),
_ => Err(GenerateError::new(
"generator option `side` must be one of: both, client, server",
)),
}
}
fn includes_client(self) -> bool {
matches!(self, Self::Both | Self::Client)
}
fn includes_server(self) -> bool {
matches!(self, Self::Both | Self::Server)
}
}
#[derive(Debug, Clone)]
pub struct GenerateOptions {
pub side: GenerateSide,
pub namespace_prefix: Option<String>,
pub include_prefix: String,
pub library_name: String,
}
impl Default for GenerateOptions {
fn default() -> Self {
Self {
side: GenerateSide::Both,
namespace_prefix: None,
include_prefix: "tripley_rpc_generated".to_string(),
library_name: "tripley_rpc_generated".to_string(),
}
}
}
impl GenerateOptions {
pub fn from_cli_opts(opts: &[String]) -> Result<Self, GenerateError> {
let mut parsed = Self::default();
for opt in opts {
let Some((key, value)) = opt.split_once('=') else {
return Err(GenerateError::new(format!(
"generator option `{opt}` must use key=value syntax"
)));
};
match key {
"side" => parsed.side = GenerateSide::parse(value)?,
"namespace_prefix" | "namespace-prefix" => {
parsed.namespace_prefix = if value.trim().is_empty() {
None
} else {
Some(value.trim().to_string())
};
}
"include_prefix" | "include-prefix" => {
if value.trim().is_empty() {
return Err(GenerateError::new(
"generator option `include_prefix` must not be empty",
));
}
parsed.include_prefix = value.trim().trim_matches('/').to_string();
}
"library_name" | "library-name" => {
if value.trim().is_empty() {
return Err(GenerateError::new(
"generator option `library_name` must not be empty",
));
}
parsed.library_name = value.trim().to_string();
}
_ => {}
}
}
Ok(parsed)
}
}
#[derive(Debug)]
pub struct GenerateError {
message: String,
}
impl GenerateError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for GenerateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
impl std::error::Error for GenerateError {}
pub fn generate_to_dir(
ir: &CanonicalIr,
out_dir: &Utf8Path,
options: GenerateOptions,
) -> Result<(), GenerateError> {
std::fs::create_dir_all(out_dir).map_err(|error| {
GenerateError::new(format!(
"failed to create output directory `{out_dir}`: {error}"
))
})?;
for (name, content) in generate(ir, &options)? {
let path = out_dir.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|error| {
GenerateError::new(format!("failed to create `{parent}`: {error}"))
})?;
}
std::fs::write(&path, content)
.map_err(|error| GenerateError::new(format!("failed to write `{path}`: {error}")))?;
}
Ok(())
}
pub fn generate(
ir: &CanonicalIr,
options: &GenerateOptions,
) -> Result<BTreeMap<String, String>, GenerateError> {
let model = Model::new(ir, options)?;
let mut files = BTreeMap::new();
files.insert("CMakeLists.txt".to_string(), generate_cmake(options));
files.insert(
format!("include/{}/types.h", options.include_prefix),
generate_types_h(&model, options)?,
);
files.insert(
format!("include/{}/services.h", options.include_prefix),
generate_services_h(&model, options),
);
files.insert("src/types.cc".to_string(), generate_types_cc(options));
if options.side.includes_client() {
files.insert(
format!("include/{}/client.h", options.include_prefix),
generate_client_h(&model, options),
);
files.insert(
"src/client.cc".to_string(),
generate_client_cc(&model, options),
);
}
if options.side.includes_server() {
files.insert(
format!("include/{}/server.h", options.include_prefix),
generate_server_h(&model, options),
);
files.insert(
format!("include/{}/activation.h", options.include_prefix),
generate_activation_h(&model, options),
);
files.insert(
"src/server.cc".to_string(),
generate_unit_cc(options, "server.h"),
);
files.insert(
"src/activation.cc".to_string(),
generate_unit_cc(options, "activation.h"),
);
}
Ok(files)
}
struct Model<'a> {
types: BTreeMap<&'a str, &'a IrType>,
services: Vec<&'a IrService>,
namespaces: BTreeMap<&'a str, Vec<String>>,
}
impl<'a> Model<'a> {
fn new(ir: &'a CanonicalIr, options: &GenerateOptions) -> Result<Self, GenerateError> {
let mut types = BTreeMap::new();
let mut names = BTreeSet::new();
let mut namespaces = BTreeMap::new();
for ty in &ir.types {
let name = cpp_type_name(&ty.name);
let ns = namespace_parts(options, &ty.package);
let key = format!("{}::{}", ns.join("::"), name);
if !names.insert(key.clone()) {
return Err(GenerateError::new(format!(
"C++ generator cannot disambiguate duplicate type `{key}`"
)));
}
namespaces.insert(ty.fqtn.as_str(), ns);
types.insert(ty.fqtn.as_str(), ty);
}
for service in &ir.services {
namespaces.insert(
service.fqsn.as_str(),
namespace_parts(options, &service.package),
);
}
Ok(Self {
types,
services: ir.services.iter().collect(),
namespaces,
})
}
}
fn generate_cmake(options: &GenerateOptions) -> String {
let mut sources = String::from(" src/types.cc\n");
if options.side.includes_client() {
sources.push_str(" src/client.cc\n");
}
if options.side.includes_server() {
sources.push_str(" src/server.cc\n src/activation.cc\n");
}
format!(
r#"cmake_minimum_required(VERSION 3.24)
project({library} LANGUAGES CXX)
add_library({library}
{sources}
)
target_compile_features({library} PUBLIC cxx_std_20)
target_include_directories({library} PUBLIC ${{CMAKE_CURRENT_SOURCE_DIR}}/include)
target_link_libraries({library} PUBLIC TripleyRpc::runtime)
"#,
library = options.library_name,
sources = sources
)
}
fn generate_types_h(model: &Model<'_>, options: &GenerateOptions) -> Result<String, GenerateError> {
let mut out = header_preamble(options, "types.h");
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");
for ty in model.types.values() {
let ns = namespace_for(model, &ty.fqtn);
open_namespace(&mut out, ns);
match &ty.kind {
IrTypeKind::Enum { base, values, .. } => gen_enum(&mut out, ty, base, values),
IrTypeKind::Flags { base, values, .. } => gen_flags(&mut out, ty, base, values),
IrTypeKind::Struct { .. } | IrTypeKind::Union { .. } => {}
}
close_namespace(&mut out, ns);
}
for ty in model.types.values() {
if matches!(ty.kind, IrTypeKind::Enum { .. }) {
let qualified = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: ty.fqtn.clone(),
},
);
writeln!(out, "MSGPACK_ADD_ENUM({qualified});").unwrap();
}
}
if model
.types
.values()
.any(|ty| matches!(ty.kind, IrTypeKind::Enum { .. }))
{
writeln!(out).unwrap();
}
for ty in model.types.values() {
let ns = namespace_for(model, &ty.fqtn);
open_namespace(&mut out, ns);
match &ty.kind {
IrTypeKind::Struct { fields } => gen_struct(&mut out, model, ty, fields)?,
IrTypeKind::Union {
discriminator_type,
variants,
} => gen_union(&mut out, model, ty, discriminator_type, variants)?,
IrTypeKind::Enum { .. } | IrTypeKind::Flags { .. } => {}
}
close_namespace(&mut out, ns);
}
out.push_str("#endif\n");
Ok(out)
}
fn generate_services_h(model: &Model<'_>, options: &GenerateOptions) -> String {
let mut out = header_preamble(options, "services.h");
out.push_str(
"#include <cstdint>\n#include <string_view>\n\n#include \"tripley/rpc/core.h\"\n\n",
);
for service in &model.services {
let ns = namespace_for(model, &service.fqsn);
open_namespace(&mut out, ns);
let prefix = constant_prefix(&service.name);
writeln!(
out,
"inline constexpr std::string_view k{}ServiceGuid = \"{}\";",
prefix, service.guid
)
.unwrap();
for method in &service.methods {
writeln!(
out,
"inline constexpr std::uint32_t k{}{}MethodId = {};",
prefix,
cpp_type_name(&method.name),
method.id
)
.unwrap();
}
for notification in &service.notifications {
writeln!(
out,
"inline constexpr std::uint32_t k{}{}NotificationId = {};",
prefix,
cpp_type_name(¬ification.name),
notification.id
)
.unwrap();
}
writeln!(out).unwrap();
close_namespace(&mut out, ns);
}
out.push_str("#endif\n");
out
}
fn generate_client_h(model: &Model<'_>, options: &GenerateOptions) -> String {
let mut out = header_preamble(options, "client.h");
writeln!(
out,
"#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",
options.include_prefix, options.include_prefix
)
.unwrap();
for service in &model.services {
let ns = namespace_for(model, &service.fqsn);
open_namespace(&mut out, ns);
let class_name = format!("{}Client", cpp_type_name(&service.name));
writeln!(out, "class {class_name} {{").unwrap();
writeln!(out, " public:").unwrap();
writeln!(
out,
" {class_name}(tripley::rpc::RpcClient& client, tripley::rpc::InstanceId instance_id);"
)
.unwrap();
writeln!(
out,
" static boost::asio::awaitable<{class_name}> ResolveNamed(tripley::rpc::RpcClient& client, std::string name);"
)
.unwrap();
if service
.activation_modes
.iter()
.any(|mode| mode == "instantiable")
{
if let Some(request_type) = &service.create_request_type {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: request_type.clone(),
},
);
writeln!(
out,
" static boost::asio::awaitable<{class_name}> Create(tripley::rpc::RpcClient& client, const {request}& request, std::map<std::string, std::string> options = {{}});"
)
.unwrap();
} else {
writeln!(
out,
" static boost::asio::awaitable<{class_name}> Create(tripley::rpc::RpcClient& client, std::map<std::string, std::string> options = {{}});"
)
.unwrap();
}
}
for method in &service.methods {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.request_type.clone(),
},
);
let response = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.response_type.clone(),
},
);
writeln!(
out,
" boost::asio::awaitable<{response}> {}(const {request}& request);",
cpp_type_name(&method.name)
)
.unwrap();
}
for notification in &service.notifications {
let payload = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: notification.payload_type.clone(),
},
);
writeln!(
out,
" tripley::rpc::NotificationStream<{payload}> Subscribe{}();",
cpp_type_name(¬ification.name)
)
.unwrap();
}
writeln!(
out,
" tripley::rpc::InstanceId instance_id() const {{ return instance_id_; }}"
)
.unwrap();
writeln!(out, "\n private:\n tripley::rpc::RpcClient* client_;\n tripley::rpc::InstanceId instance_id_;\n}};\n").unwrap();
writeln!(
out,
"inline {class_name}::{class_name}(tripley::rpc::RpcClient& client, tripley::rpc::InstanceId instance_id) : client_(&client), instance_id_(instance_id) {{}}"
)
.unwrap();
for method in &service.methods {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.request_type.clone(),
},
);
let response = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.response_type.clone(),
},
);
writeln!(
out,
"inline boost::asio::awaitable<{response}> {class_name}::{}(const {request}& request) {{",
cpp_type_name(&method.name)
)
.unwrap();
writeln!(
out,
" co_return co_await client_->Call<{request}, {response}>(instance_id_, {}, request);",
method.id
)
.unwrap();
writeln!(out, "}}\n").unwrap();
}
for notification in &service.notifications {
let payload = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: notification.payload_type.clone(),
},
);
writeln!(
out,
"inline tripley::rpc::NotificationStream<{payload}> {class_name}::Subscribe{}() {{",
cpp_type_name(¬ification.name)
)
.unwrap();
writeln!(
out,
" return client_->SubscribeNotifications<{payload}>(instance_id_, k{}{}NotificationId);",
constant_prefix(&service.name),
cpp_type_name(¬ification.name)
)
.unwrap();
writeln!(out, "}}\n").unwrap();
}
writeln!(out).unwrap();
close_namespace(&mut out, ns);
}
out.push_str("#endif\n");
out
}
fn generate_client_cc(model: &Model<'_>, options: &GenerateOptions) -> String {
let mut out = generate_unit_cc(options, "client.h");
out.push_str("\n#include <cstdint>\n#include <utility>\n#include <vector>\n\n");
for service in &model.services {
let ns = namespace_for(model, &service.fqsn);
open_namespace(&mut out, ns);
let class_name = format!("{}Client", cpp_type_name(&service.name));
let prefix = constant_prefix(&service.name);
writeln!(
out,
"boost::asio::awaitable<{class_name}> {class_name}::ResolveNamed(tripley::rpc::RpcClient& client, std::string name) {{"
)
.unwrap();
writeln!(out, " std::vector<std::string> names;").unwrap();
writeln!(out, " names.push_back(std::move(name));").unwrap();
writeln!(
out,
" auto ids = co_await client.ResolveInstanceIds(std::move(names));"
)
.unwrap();
writeln!(
out,
" if (ids.empty() || ids[0] == tripley::rpc::kInvalidInstanceId) {{"
)
.unwrap();
writeln!(
out,
" throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kInstanceNotFound, tripley::rpc::ErrorKind::kRuntime, \"named instance was not found\");"
)
.unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " co_return {class_name}(client, ids[0]);").unwrap();
writeln!(out, "}}\n").unwrap();
if service
.activation_modes
.iter()
.any(|mode| mode == "instantiable")
{
if let Some(request_type) = &service.create_request_type {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: request_type.clone(),
},
);
writeln!(
out,
"boost::asio::awaitable<{class_name}> {class_name}::Create(tripley::rpc::RpcClient& client, const {request}& request, std::map<std::string, std::string> options) {{"
)
.unwrap();
writeln!(out, " msgpack::zone zone;").unwrap();
writeln!(
out,
" auto payload_object = tripley::rpc::EncodePayload(request, zone);"
)
.unwrap();
writeln!(out, " msgpack::sbuffer buffer;").unwrap();
writeln!(out, " msgpack::pack(buffer, payload_object);").unwrap();
writeln!(
out,
" auto* begin = reinterpret_cast<const std::uint8_t*>(buffer.data());"
)
.unwrap();
writeln!(
out,
" std::vector<std::uint8_t> payload(begin, begin + buffer.size());"
)
.unwrap();
writeln!(
out,
" auto instance_id = co_await client.CreateInstance(tripley::rpc::ParseServiceGuid(k{prefix}ServiceGuid), std::move(payload), std::move(options));"
)
.unwrap();
} else {
writeln!(
out,
"boost::asio::awaitable<{class_name}> {class_name}::Create(tripley::rpc::RpcClient& client, std::map<std::string, std::string> options) {{"
)
.unwrap();
writeln!(
out,
" auto instance_id = co_await client.CreateInstance(tripley::rpc::ParseServiceGuid(k{prefix}ServiceGuid), std::nullopt, std::move(options));"
)
.unwrap();
}
writeln!(out, " co_return {class_name}(client, instance_id);").unwrap();
writeln!(out, "}}\n").unwrap();
}
close_namespace(&mut out, ns);
}
out
}
fn generate_server_h(model: &Model<'_>, options: &GenerateOptions) -> String {
let mut out = header_preamble(options, "server.h");
writeln!(
out,
"#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",
options.include_prefix, options.include_prefix
)
.unwrap();
for service in &model.services {
let ns = namespace_for(model, &service.fqsn);
open_namespace(&mut out, ns);
let service_name = cpp_type_name(&service.name);
let prefix = constant_prefix(&service.name);
writeln!(out, "class {service_name}Service {{").unwrap();
writeln!(
out,
" public:\n virtual ~{service_name}Service() = default;"
)
.unwrap();
for method in &service.methods {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.request_type.clone(),
},
);
let response = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.response_type.clone(),
},
);
writeln!(
out,
" virtual boost::asio::awaitable<{response}> {}(tripley::rpc::RpcCallContext& ctx, const {request}& request) = 0;",
cpp_type_name(&method.name)
)
.unwrap();
}
writeln!(out, "}};\n").unwrap();
if service
.activation_modes
.iter()
.any(|mode| mode == "instantiable")
{
writeln!(out, "class {service_name}Factory {{").unwrap();
writeln!(
out,
" public:\n virtual ~{service_name}Factory() = default;"
)
.unwrap();
if let Some(request_type) = &service.create_request_type {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: request_type.clone(),
},
);
writeln!(
out,
" 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;"
)
.unwrap();
} else {
writeln!(
out,
" 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;"
)
.unwrap();
}
writeln!(out, "}};\n").unwrap();
}
writeln!(
out,
"inline tripley::rpc::ServiceHandler Make{}Handler(std::shared_ptr<{service_name}Service> service) {{",
service_name
)
.unwrap();
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();
writeln!(out, " msgpack::zone zone;").unwrap();
writeln!(out, " switch (method_id) {{").unwrap();
for method in &service.methods {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: method.request_type.clone(),
},
);
writeln!(out, " case {}: {{", method.id).unwrap();
writeln!(
out,
" auto response = co_await service->{}(ctx, tripley::rpc::DecodePayload<{request}>(payload));",
cpp_type_name(&method.name)
)
.unwrap();
writeln!(
out,
" co_return msgpack::clone(tripley::rpc::EncodePayload(response, zone));"
)
.unwrap();
writeln!(out, " }}").unwrap();
}
writeln!(out, " default: throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kMethodNotFound, tripley::rpc::ErrorKind::kRuntime, \"method not found\");").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " }};").unwrap();
writeln!(out, "}}\n").unwrap();
writeln!(
out,
"inline void Register{}Named(tripley::rpc::RpcServerBuilder& builder, std::string name, std::shared_ptr<{service_name}Service> service) {{",
service_name
)
.unwrap();
write!(
out,
" builder.RegisterNamed(std::move(name), tripley::rpc::ParseServiceGuid(k{}ServiceGuid), std::set<tripley::rpc::MethodId>{{",
constant_prefix(&service.name)
)
.unwrap();
for (index, method) in service.methods.iter().enumerate() {
if index > 0 {
write!(out, ", ").unwrap();
}
write!(out, "{}", method.id).unwrap();
}
writeln!(out, "}},").unwrap();
writeln!(out, " Make{}Handler(std::move(service)));", service_name).unwrap();
writeln!(out, "}}\n").unwrap();
if service
.activation_modes
.iter()
.any(|mode| mode == "instantiable")
{
writeln!(
out,
"inline void Register{}Factory(tripley::rpc::RpcServerBuilder& builder, std::shared_ptr<{service_name}Factory> factory) {{",
service_name
)
.unwrap();
write!(
out,
" builder.RegisterFactory(tripley::rpc::ParseServiceGuid(k{prefix}ServiceGuid), std::set<tripley::rpc::MethodId>{{"
)
.unwrap();
for (index, method) in service.methods.iter().enumerate() {
if index > 0 {
write!(out, ", ").unwrap();
}
write!(out, "{}", method.id).unwrap();
}
writeln!(out, "}},").unwrap();
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();
if let Some(request_type) = &service.create_request_type {
let request = cpp_type_ref(
model,
&IrTypeRef::Named {
fqtn: request_type.clone(),
},
);
writeln!(out, " if (!create_payload.has_value()) {{").unwrap();
writeln!(out, " throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kPayloadDecodeFailed, tripley::rpc::ErrorKind::kRuntime, \"missing constructor payload\");").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, " std::size_t offset = 0;").unwrap();
writeln!(out, " auto handle = msgpack::unpack(reinterpret_cast<const char*>(create_payload->data()), create_payload->size(), offset);").unwrap();
writeln!(out, " if (offset != create_payload->size()) {{").unwrap();
writeln!(out, " throw tripley::rpc::RpcError(tripley::rpc::RuntimeErrorCode::kPayloadDecodeFailed, tripley::rpc::ErrorKind::kRuntime, \"trailing bytes after constructor payload\");").unwrap();
writeln!(out, " }}").unwrap();
writeln!(
out,
" auto request = tripley::rpc::DecodePayload<{request}>(handle.get());"
)
.unwrap();
writeln!(
out,
" auto service = co_await factory->Create(ctx, request, std::move(options));"
)
.unwrap();
} else {
writeln!(
out,
" auto service = co_await factory->Create(ctx, std::move(create_payload), std::move(options));"
)
.unwrap();
}
writeln!(out, " co_return std::make_shared<tripley::rpc::ServiceHandler>(Make{}Handler(std::move(service)));", service_name).unwrap();
writeln!(out, " }});").unwrap();
writeln!(out, "}}\n").unwrap();
}
writeln!(out).unwrap();
close_namespace(&mut out, ns);
}
out.push_str("#endif\n");
out
}
fn generate_activation_h(model: &Model<'_>, options: &GenerateOptions) -> String {
let mut out = header_preamble(options, "activation.h");
writeln!(
out,
"#include \"{}/services.h\"\n#include \"tripley/rpc/activation.h\"\n\n",
options.include_prefix
)
.unwrap();
for service in &model.services {
let ns = namespace_for(model, &service.fqsn);
open_namespace(&mut out, ns);
writeln!(
out,
"inline tripley::rpc::ServiceGuid {}ServiceGuid() {{ return tripley::rpc::ParseServiceGuid(k{}ServiceGuid); }}",
cpp_type_name(&service.name),
constant_prefix(&service.name)
)
.unwrap();
close_namespace(&mut out, ns);
}
out.push_str("#endif\n");
out
}
fn generate_types_cc(options: &GenerateOptions) -> String {
format!("#include \"{}/types.h\"\n", options.include_prefix)
}
fn generate_unit_cc(options: &GenerateOptions, header: &str) -> String {
format!("#include \"{}/{header}\"\n", options.include_prefix)
}
fn gen_struct(
out: &mut String,
model: &Model<'_>,
ty: &IrType,
fields: &[IrField],
) -> Result<(), GenerateError> {
let name = cpp_type_name(&ty.name);
writeln!(out, "struct {name} {{").unwrap();
for field in fields {
writeln!(
out,
" {} {};",
cpp_type_ref(model, &field_type_ref(field)),
cpp_field_name(&field.name)
)
.unwrap();
}
if fields.is_empty() {
writeln!(out, " MSGPACK_DEFINE_ARRAY();").unwrap();
} else {
write!(out, " MSGPACK_DEFINE_ARRAY(").unwrap();
for (index, field) in fields.iter().enumerate() {
if index > 0 {
write!(out, ", ").unwrap();
}
write!(out, "{}", cpp_field_name(&field.name)).unwrap();
}
writeln!(out, ");").unwrap();
}
writeln!(out, "}};\n").unwrap();
writeln!(
out,
"msgpack::object Encode{}(const {name}& value, msgpack::zone& zone);",
name
)
.unwrap();
writeln!(
out,
"{name} Decode{}(const msgpack::object& object);\n",
name
)
.unwrap();
Ok(())
}
fn gen_enum(out: &mut String, ty: &IrType, base: &str, values: &[IrEnumValue]) {
let name = cpp_type_name(&ty.name);
writeln!(out, "enum class {name} : {} {{", cpp_scalar_type(base)).unwrap();
for value in values {
writeln!(
out,
" k{} = {},",
cpp_type_name(&value.name.to_ascii_lowercase()),
value.value
)
.unwrap();
}
writeln!(out, "}};\n").unwrap();
}
fn gen_flags(out: &mut String, ty: &IrType, base: &str, values: &[IrEnumValue]) {
let name = cpp_type_name(&ty.name);
let base = cpp_scalar_type(base);
writeln!(out, "class {name} {{").unwrap();
writeln!(out, " public:").unwrap();
writeln!(out, " using ValueType = {base};").unwrap();
writeln!(out, " constexpr {name}() = default;").unwrap();
writeln!(
out,
" explicit constexpr {name}(ValueType value) : value_(value) {{}}"
)
.unwrap();
writeln!(
out,
" constexpr ValueType value() const {{ return value_; }}"
)
.unwrap();
for value in values {
writeln!(
out,
" static constexpr ValueType k{} = static_cast<ValueType>({});",
cpp_type_name(&value.name.to_ascii_lowercase()),
value.value
)
.unwrap();
}
writeln!(out, "\n private:\n ValueType value_ = 0;\n}};\n").unwrap();
}
fn gen_union(
out: &mut String,
model: &Model<'_>,
ty: &IrType,
_discriminator_type: &str,
variants: &[IrUnionVariant],
) -> Result<(), GenerateError> {
let name = cpp_type_name(&ty.name);
writeln!(out, "class {name} {{").unwrap();
writeln!(out, " public:").unwrap();
writeln!(out, " enum class Tag : std::int64_t {{").unwrap();
for variant in variants {
writeln!(
out,
" k{} = {},",
cpp_type_name(&variant.name),
variant.tag
)
.unwrap();
}
writeln!(out, " }};").unwrap();
write!(out, " using Value = std::variant<").unwrap();
for (index, variant) in variants.iter().enumerate() {
if index > 0 {
write!(out, ", ").unwrap();
}
write!(out, "{}", cpp_type_ref(model, &variant.ty)).unwrap();
}
writeln!(out, ">;").unwrap();
writeln!(
out,
" {name}(Tag tag, Value value) : tag_(tag), value_(std::move(value)) {{}}"
)
.unwrap();
writeln!(out, " Tag tag() const {{ return tag_; }}").unwrap();
writeln!(out, " const Value& value() const {{ return value_; }}").unwrap();
writeln!(out, "\n private:\n Tag tag_;\n Value value_;\n}};\n").unwrap();
Ok(())
}
fn field_type_ref(field: &IrField) -> IrTypeRef {
if field.optional {
if matches!(field.ty, IrTypeRef::Optional { .. }) {
return field.ty.clone();
}
IrTypeRef::Optional {
inner: Box::new(field.ty.clone()),
}
} else {
field.ty.clone()
}
}
fn cpp_type_ref(model: &Model<'_>, ty: &IrTypeRef) -> String {
match ty {
IrTypeRef::Scalar { name } => cpp_scalar_type(name).to_string(),
IrTypeRef::Named { fqtn } => {
let name = model
.types
.get(fqtn.as_str())
.map(|ty| cpp_type_name(&ty.name))
.unwrap_or_else(|| cpp_type_name(fqtn.rsplit('.').next().unwrap_or(fqtn)));
match model.namespaces.get(fqtn.as_str()) {
Some(ns) if !ns.is_empty() => format!("{}::{name}", ns.join("::")),
_ => name,
}
}
IrTypeRef::Optional { inner } => format!("std::optional<{}>", cpp_type_ref(model, inner)),
IrTypeRef::List { inner } => format!("std::vector<{}>", cpp_type_ref(model, inner)),
IrTypeRef::Map { key: _, value } => {
format!("std::map<std::string, {}>", cpp_type_ref(model, value))
}
}
}
fn cpp_scalar_type(name: &str) -> &'static str {
match name {
"bool" => "bool",
"i32" => "std::int32_t",
"i64" => "std::int64_t",
"u32" => "std::uint32_t",
"u64" => "std::uint64_t",
"f32" => "float",
"f64" => "double",
"string" => "std::string",
"bytes" => "std::vector<std::uint8_t>",
"uuid" => "tripley::rpc::Uuid",
_ => "msgpack::object",
}
}
fn namespace_parts(options: &GenerateOptions, package: &str) -> Vec<String> {
let mut parts = Vec::new();
if let Some(prefix) = &options.namespace_prefix {
parts.extend(
prefix
.split("::")
.filter(|part| !part.is_empty())
.map(sanitize_identifier),
);
}
parts.extend(package.split('.').map(sanitize_identifier));
parts
}
fn namespace_for<'a>(model: &'a Model<'_>, fq: &str) -> &'a [String] {
model.namespaces.get(fq).map(Vec::as_slice).unwrap_or(&[])
}
fn open_namespace(out: &mut String, parts: &[String]) {
for part in parts {
writeln!(out, "namespace {part} {{").unwrap();
}
if !parts.is_empty() {
writeln!(out).unwrap();
}
}
fn close_namespace(out: &mut String, parts: &[String]) {
for part in parts.iter().rev() {
writeln!(out, "}} // namespace {part}").unwrap();
}
if !parts.is_empty() {
writeln!(out).unwrap();
}
}
fn header_preamble(options: &GenerateOptions, file: &str) -> String {
let guard = format!(
"TRIPLEY_RPC_GENERATED_{}_{}_",
options
.include_prefix
.replace(['/', '-'], "_")
.to_ascii_uppercase(),
file.replace(['.', '-'], "_").to_ascii_uppercase()
);
format!("// Generated by tripley-rpc. Do not edit.\n#ifndef {guard}\n#define {guard}\n\n")
}
fn cpp_type_name(name: &str) -> String {
let mut out = String::new();
for part in split_words(name) {
let mut chars = part.chars();
if let Some(first) = chars.next() {
out.push(first.to_ascii_uppercase());
out.extend(chars.map(|ch| ch.to_ascii_lowercase()));
}
}
if out.is_empty() {
"Generated".to_string()
} else if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
format!("T{out}")
} else {
out
}
}
fn cpp_field_name(name: &str) -> String {
let mut out = split_words(name).join("_").to_ascii_lowercase();
if out.is_empty() {
out = "field".to_string();
}
if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
out.insert(0, 'f');
}
if is_cpp_keyword(&out) {
out.push('_');
}
out
}
fn constant_prefix(name: &str) -> String {
cpp_type_name(name)
}
fn sanitize_identifier(value: &str) -> String {
let mut out = String::new();
for ch in value.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
out.push(ch.to_ascii_lowercase());
} else {
out.push('_');
}
}
if out.is_empty() {
"generated".to_string()
} else if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
format!("n{out}")
} else if is_cpp_keyword(&out) {
format!("{out}_")
} else {
out
}
}
fn split_words(value: &str) -> Vec<String> {
let raw: Vec<_> = value
.split(|ch: char| ch == '_' || ch == '-' || ch == '.' || ch.is_whitespace())
.filter(|part| !part.is_empty())
.map(ToOwned::to_owned)
.collect();
let mut words = Vec::new();
for part in raw {
let mut current = String::new();
let mut prev_lower_or_digit = false;
for ch in part.chars() {
if ch.is_ascii_uppercase() && prev_lower_or_digit && !current.is_empty() {
words.push(current);
current = String::new();
}
prev_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
current.push(ch);
}
if !current.is_empty() {
words.push(current);
}
}
words
}
fn is_cpp_keyword(value: &str) -> bool {
matches!(
value,
"alignas"
| "alignof"
| "and"
| "asm"
| "auto"
| "bool"
| "break"
| "case"
| "catch"
| "char"
| "class"
| "const"
| "constexpr"
| "continue"
| "default"
| "delete"
| "do"
| "double"
| "else"
| "enum"
| "explicit"
| "export"
| "extern"
| "false"
| "float"
| "for"
| "friend"
| "goto"
| "if"
| "inline"
| "int"
| "long"
| "namespace"
| "new"
| "noexcept"
| "nullptr"
| "operator"
| "private"
| "protected"
| "public"
| "return"
| "short"
| "signed"
| "sizeof"
| "static"
| "struct"
| "switch"
| "template"
| "this"
| "throw"
| "true"
| "try"
| "typedef"
| "typename"
| "union"
| "unsigned"
| "using"
| "virtual"
| "void"
| "volatile"
| "while"
)
}