Десктопный клиент для Apache Kafka, преобразуем protobuf в json

Kate

Administrator
Команда форума

Чем же неудобен protobuf?​

Если опустить его бинарную природу, то он позволяет писать так

//order.proto
syntax = "proto3";

package order;

import "enums.proto";
import "google/protobuf/timestamp.proto";

message EventOrderEnrichment {
string shipment_uuid = 1;
string order_uuid = 2;
string place_uuid = 3;
enums.ShipmentStatus shipment_status = 4;
string shipment_type = 5;
uint64 weight = 6;
enums.Location client_location = 7;
enums.Location place_location = 8;
uint64 assembly_time_min = 9;
repeated string assembly = 10;
repeated string delivery = 11;
optional DispatchMeta dispatch_meta = 12;
optional Settings settings = 13;
}

message Settings {
uint64 max_order_assign_retry_count = 1;
uint64 avg_parking_min_vehicle = 2;
uint64 max_current_order_assign_queue = 3;
fixed64 order_weight_threshold_to_assign_to_vehicle_gramms = 4;
uint64 average_speed_for_straight_distance_to_client_min = 5;
uint64 additional_factor_for_straight_distance_to_client_min = 6;
uint64 order_transfer_time_from_assembly_to_delivery_min = 7;
uint64 avg_to_place_min_external = 8;
uint64 avg_to_place_min = 9;
bool place_location_center = 10;
uint64 search_radius_transport_pedestrian = 11;
uint64 search_radius_transport_auto = 12;
uint64 search_radius_transport_bike = 13;
uint64 last_position_expire = 14;
}

message DispatchMeta {
uint64 dispatch_count = 1;
google.protobuf.Timestamp dispatch_start = 2;
repeated string dispatch_ids = 3;
string dispatch_id = 4;
optional Tasks decline_task = 5;
optional string decline_performer_uuid = 6;
}

enum Tasks {
DELIVERY = 0;
ASSEMBLY = 1;
ASSEMBLY_AND_DELIVERY = 2;
}

message Location {
double latitude = 1;
double longitude = 2;
}
// enums.proto
syntax = "proto3";

package enums;

enum ShipmentStatus {
NEW = 0;
POSTPONED = 1;
AUTOMATIC_ROUTING = 2;
MANUAL_ROUTING = 3;
OFFERING = 4;
OFFERED = 5;
DECLINED = 6;
CANCELED = 7;
}

message Location {
double latitude = 1;
double longitude = 2;
}
Если вы хотите декодировать protobuf, то нужно указать:

  • где найти фай proto файлы(order.proto, enums.proto, timestamp.proto);
  • тип сообщения.

Реализация на C++​

Какие классы из C++ API потребуются

f072c633b89d8c1dadfa12140d5341d7.png

В Google не используют исключения, поэтому ошибки парсинга proto файла будем ловить наследником от MultiFileErrorCollector

class ProtobufErrorCollector final : public google::protobuf::compiler::MultiFileErrorCollector
{
public:
void AddError(const std::string &filename,
int line,
int column,
const std::string &message) override;

void AddWarning(const std::string &filename,
int line,
int column,
const std::string &message) override;

QStringList errors() const;

bool hasErrors() const;

private:
QStringList m_messages;
};
///
void ProtobufErrorCollector::AddError(const std::string &filename,
int line,
int column,
const std::string &message)
{
m_messages << QString("error file: %1, line: %2, column: %3 %4")
.arg(QString::fromStdString(filename))
.arg(line)
.arg(column)
.arg(QString::fromStdString(message));
}

void ProtobufErrorCollector::AddWarning(const std::string &filename,
int line,
int column,
const std::string &message)
{
m_messages << QString("warning file: %1, line: %2, column: %3 %4")
.arg(QString::fromStdString(filename))
.arg(line)
.arg(column)
.arg(QString::fromStdString(message));
}

QStringList ProtobufErrorCollector::errors() const
{
return m_messages;
}

bool ProtobufErrorCollector::hasErrors() const
{
return !m_messages.isEmpty();
}
SourceTree это абстрактное дерево каталогов. Его наследник DiskSourceTree позволяет нам класть proto файлы в структуру каталогов

dir/
order.proto
enums.proto
google/
protobuf/
timestamp.proto
Каждый раз раскладывать proto файлы от Google неудобно. Поэтому было принято решение таскать эти файлы в самом бинарнике. Так появился ProtobufSourceTree

class ProtobufSourceTree final : public google::protobuf::compiler::DiskSourceTree
{
public:
google::protobuf:🇮🇴:ZeroCopyInputStream *Open(const std::string &filename) override;

void Add(const QDir &dir);

private:
static google::protobuf:🇮🇴:ZeroCopyInputStream *openFromResources(const std::string &filename);
};
///
class ByteArrayInputStream final : public google::protobuf:🇮🇴:ArrayInputStream
{
public:
explicit ByteArrayInputStream(QByteArray &&data)
: ArrayInputStream(data.data(), data.size())
, m_data(std::move(data))
{}

private:
QByteArray m_data;
};


google::protobuf:🇮🇴:ZeroCopyInputStream *ProtobufSourceTree::Open(const std::string &filename)
{
static QSet<std::string> inResources = {"google/protobuf/any.proto",
"google/protobuf/api.proto",
"google/protobuf/descriptor.proto",
"google/protobuf/duration.proto",
"google/protobuf/empty.proto",
"google/protobuf/field_mask.proto",
"google/protobuf/source_context.proto",
"google/protobuf/struct.proto",
"google/protobuf/timestamp.proto",
"google/protobuf/type.proto",
"google/protobuf/wrappers.proto"};

if (inResources.contains(filename)) {
return openFromResources(filename);
}

return DiskSourceTree::Open(filename);
}

google::protobuf:🇮🇴:ZeroCopyInputStream *ProtobufSourceTree::eek:penFromResources(
const std::string &filename)
{
using namespace google::protobuf::io;

QString path(QString(":/%1").arg(QString::fromStdString(filename)));
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
spdlog::error("failed open file {} from resources error {}",
path.toStdString(),
file.errorString().toStdString());
return nullptr;
}
auto data = file.readAll();
file.close();

return new ByteArrayInputStream(std::move(data));
}

void ProtobufSourceTree::Add(const QDir &dir)
{
QString path = dir.path();
#ifdef Q_OS_WINDOWS
if (path.front() == '/') {
path = path.remove(0, 1);
}
#endif
MapPath("", path.toStdString());
}
ByteArrayInputStream откровенный костыль, за то не нужно реализовывать все методы ZeroCopyInputStream. Тут стоит обратить внимание на вызов DiskSourceTree::MapPath. Первый параметр пустой, что заставляет второй параметр интерпретировать как путь к каталогу

void DiskSourceTree::MapPath(
const std::string & virtual_path,
const std::string & disk_path)
c913f4695105b9eda1911233ccc7561d.png

Парсим proto файл и получаем список типов, который выведем в UI

using namespace google::protobuf;
using namespace google::protobuf::compiler;

QFileInfo info(m_file.path());

ProtobufErrorCollector errors;
ProtobufSourceTree sources;
sources.Add(info.dir());

SourceTreeDescriptorDatabase database(&sources, nullptr);
database.RecordErrorsTo(&errors);

DescriptorPool pool(&database, database.GetValidationErrorCollector());
pool.EnforceWeakDependencies(true);

const auto *const fileDescriptor = pool.FindFileByName(info.fileName().toStdString());
// обработка ошибок

beginResetModel();
m_messages.clear();
for (int i = 0; i < fileDescriptor->message_type_count(); i++) {
m_messages << QString::fromStdString(fileDescriptor->message_type(i)->name());
}
endResetModel();
Собираем Message. Обратите внимание, что имя включает в себя имя пакета

const auto package = fileDescriptor->package();
const auto messageType = package + "." + message.toStdString();

const auto *const typeDescriptor = pool->FindMessageTypeByName(messageType);
// обработка ошибок
auto factory = std::make_unique<DynamicMessageFactory>(pool.get());
auto *dynamicMessage = factory->GetPrototype(typeDescriptor)->New();
Само преобразование

QByteArray ProtobufConverter::toJSON(QByteArray &&binary)
{
using namespace google::protobuf::util;

m_message->Clear();
if (!m_message->ParseFromArray(binary.data(), binary.size())) {
return errParse;
}

JsonPrintOptions opt;
std::string json;
MessageToJsonString(*m_message, &json, opt);
return QByteArray(json.c_str(), json.size());
}
На этом все. Весь код доступен на GitHub, бинарные сборки на странице релизов

 
Сверху