userver-framework / userver

Production-ready C++ Asynchronous Framework with rich functionality
https://userver.tech
Apache License 2.0
2.37k stars 275 forks source link

Coverter: Protobuf -> Domain #670

Open root-kidik opened 1 month ago

root-kidik commented 1 month ago

Angle.hpp

namespace sf
{

class Angle
{
public:
    constexpr Angle() = default;
    constexpr Angle(float radians);
    [[nodiscard]] constexpr float asDegrees() const;
    [[nodiscard]] constexpr float asRadians() const;
    [[nodiscard]] constexpr Angle wrapSigned() const;
    // And other really usefull methods

private:
    float m_radians{};
};

}

Vector2.hpp

namespace sf
{

template <typename T>
class Vector2
{
public:
    constexpr Vector2() = default;
    constexpr Vector2(T x, T y);

    [[nodiscard]] constexpr T dot(Vector2 rhs) const;
    [[nodiscard]] constexpr T cross(Vector2 rhs) const;
    // And other really usefull methods

private:
    T x{};
    T y{};
};

using Vector2f = Vector2<float>;
// ...

}

Angle.proto


syntax = "proto3";

package api.sf;

message Angle {
    float radians = 1;
}

Vector2f.proto


syntax = "proto3";

package api.sf;

message Vector2f {
    float x = 1;
    float y = 2;
}

Problem

I really want to use native classes from C++, for this I need to manually transfer each time from classes generated by Protobuf to native ones

Solution

Python plugin

import sys
from google.protobuf.compiler import plugin_pb2 as plugin
from jinja2 import Template

converter_template = Template(
"""
{% for message in proto_file.message_type %}
inline {{cpp_namespace}}::{{message.name}} convert({{api_namespace}}::{{message.name}}&& dto)
{
    return { {% for field in message.field %}std::move(*dto.mutable_{{field.name}}()){{ ", " if not loop.last else "" }}{% endfor %} };
}

inline {{api_namespace}}::{{message.name}} convert({{cpp_namespace}}::{{message.name}}&& object)
{
    return { {% for field in message.field %}std::move(object.{{field.name}}){{ ", " if not loop.last else "" }}{% endfor %} };
}            
{% endfor %}
"""
)

def generate_converter(proto_file, response):

    output_code = ""
    for proto_file in proto_file:
        api_namespace = proto_file.package.replace(".", "::")
        cpp_namespace = api_namespace[5:] 

        output_code += converter_template.render(proto_file=proto_file, api_namespace=api_namespace, cpp_namespace=cpp_namespace) 

        output_file = response.file.add()
        output_file.name = proto_file.name.replace(".proto", ".cpp")
        output_file.content = output_code

def main():
    data = sys.stdin.buffer.read()

    request = plugin.CodeGeneratorRequest.FromString(data)

    response = plugin.CodeGeneratorResponse()

    generate_converter(request.proto_file, response)

    sys.stdout.buffer.write(response.SerializeToString())

if __name__ == "__main__":
    main()

Run

protoc --plugin=protoc-gen-custom=/home/rtkid/Documents/pg_grpc_service_template/proto/handlers/plugin.sh --custom_out=. Angle.proto
protoc --plugin=protoc-gen-custom=/home/rtkid/Documents/pg_grpc_service_template/proto/handlers/plugin.sh --custom_out=. Vector2f.proto

Or inside 1 file.

Output

inline sf::Angle convert(api::sf::Angle&& dto)
{
    return { std::move(*dto.mutable_radians()) };
}

inline api::sf::Angle convert(sf::Angle&& object)
{
    return { std::move(object.radians) };
}            

inline sf::Vector2f convert(api::sf::Vector2f&& dto)
{
    return { std::move(*dto.mutable_x()), std::move(*dto.mutable_y()) };
}

inline api::sf::Vector2f convert(sf::Vector2f&& object)
{
    return { std::move(object.x), std::move(object.y) };
}