Skip to content

Commit 7409c18

Browse files
committed
Support protobuf native schema
1 parent 7f6756d commit 7409c18

13 files changed

Lines changed: 1061 additions & 81 deletions

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919

2020
examples
2121
perf
22+
tests/protobuf_schema/generated

index.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ export interface SchemaInfo {
225225
properties?: Record<string, string>;
226226
}
227227

228+
export namespace ProtobufNativeSchema {
229+
interface CreateSchemaInfoFromRootOptions {
230+
root: any;
231+
rootMessageTypeName: string;
232+
rootFileDescriptorName: string;
233+
schemaType?: 'ProtobufNative';
234+
syntax?: 'proto3' | 'proto2' | string;
235+
name?: string;
236+
properties?: Record<string, string>;
237+
}
238+
239+
function createRootFromJson(rootJson: Record<string, unknown>): any;
240+
function createSchemaInfoFromRoot(options: CreateSchemaInfoFromRootOptions): SchemaInfo;
241+
}
242+
228243
export interface DeadLetterPolicy {
229244
deadLetterTopic: string;
230245
maxRedeliverCount?: number;
@@ -385,6 +400,7 @@ export type SchemaType =
385400
'Float32' |
386401
'Float64' |
387402
'KeyValue' |
403+
'ProtobufNative' |
388404
'Bytes' |
389405
'AutoConsume' |
390406
'AutoPublish';

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const AuthenticationToken = require('./src/AuthenticationToken');
2424
const AuthenticationOauth2 = require('./src/AuthenticationOauth2');
2525
const AuthenticationBasic = require('./src/AuthenticationBasic');
2626
const Client = require('./src/Client');
27+
const ProtobufNativeSchema = require('./src/ProtobufNativeSchema');
2728

2829
const LogLevel = {
2930
DEBUG: 0,
@@ -43,6 +44,7 @@ const Pulsar = {
4344
AuthenticationToken,
4445
AuthenticationOauth2,
4546
AuthenticationBasic,
47+
ProtobufNativeSchema,
4648
LogLevel,
4749
};
4850

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"license-checker": "^25.0.1",
6262
"lodash": "^4.17.21",
6363
"typedoc": "^0.23.28",
64-
"typescript": "^4.9.5"
64+
"typescript": "^4.9.5",
65+
"protobufjs": "^8.4.2"
6566
},
6667
"dependencies": {
6768
"@mapbox/node-pre-gyp": "^2.0.3",

src/ProtobufNativeSchema.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
require('protobufjs/ext/descriptor');
21+
22+
const protobuf = require('protobufjs');
23+
const descriptor = require('protobufjs/ext/descriptor');
24+
25+
const normalizeTypeName = (typeName) => typeName.replace(/^\./, '');
26+
27+
const createSchemaInfoFromRoot = ({
28+
root,
29+
rootMessageTypeName,
30+
rootFileDescriptorName,
31+
schemaType = 'ProtobufNative',
32+
syntax = 'proto3',
33+
name = rootMessageTypeName,
34+
properties = {},
35+
}) => {
36+
if (!root) {
37+
throw new Error('root is required');
38+
}
39+
if (!rootMessageTypeName) {
40+
throw new Error('rootMessageTypeName is required');
41+
}
42+
if (!rootFileDescriptorName) {
43+
throw new Error('rootFileDescriptorName is required');
44+
}
45+
46+
const normalizedTypeName = normalizeTypeName(rootMessageTypeName);
47+
const rootMessageType = root.lookupType(normalizedTypeName);
48+
const packageName = normalizedTypeName.split('.').slice(0, -1).join('.');
49+
const namespace = packageName ? root.lookup(packageName) : root;
50+
51+
// protobufjs reflection JSON does not retain the source file name. Set it
52+
// before exporting a FileDescriptorSet, mirroring descriptor->file()->name().
53+
namespace.filename = rootFileDescriptorName;
54+
root.resolveAll();
55+
56+
const fileDescriptorSet = root.toDescriptor(syntax);
57+
const fileDescriptorSetBytes = descriptor.FileDescriptorSet.encode(fileDescriptorSet).finish();
58+
59+
return {
60+
schemaType,
61+
name,
62+
schema: JSON.stringify({
63+
fileDescriptorSet: Buffer.from(fileDescriptorSetBytes).toString('base64'),
64+
rootMessageTypeName: normalizeTypeName(rootMessageType.fullName),
65+
rootFileDescriptorName,
66+
}),
67+
properties,
68+
};
69+
};
70+
71+
const createRootFromJson = (rootJson) => protobuf.Root.fromJSON(rootJson);
72+
73+
module.exports = {
74+
createRootFromJson,
75+
createSchemaInfoFromRoot,
76+
};

src/SchemaInfo.cc

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,47 @@
1717
* under the License.
1818
*/
1919
#include "SchemaInfo.h"
20+
#include <pulsar/ConsumerConfiguration.h>
21+
#include <pulsar/ProducerConfiguration.h>
2022
#include <map>
2123

2224
static const std::string CFG_SCHEMA_TYPE = "schemaType";
2325
static const std::string CFG_NAME = "name";
2426
static const std::string CFG_SCHEMA = "schema";
2527
static const std::string CFG_PROPS = "properties";
2628

27-
static const std::map<std::string, pulsar_schema_type> SCHEMA_TYPE = {{"None", pulsar_None},
28-
{"String", pulsar_String},
29-
{"Json", pulsar_Json},
30-
{"Protobuf", pulsar_Protobuf},
31-
{"Avro", pulsar_Avro},
32-
{"Boolean", pulsar_Boolean},
33-
{"Int8", pulsar_Int8},
34-
{"Int16", pulsar_Int16},
35-
{"Int32", pulsar_Int32},
36-
{"Int64", pulsar_Int64},
37-
{"Float32", pulsar_Float32},
38-
{"Float64", pulsar_Float64},
39-
{"KeyValue", pulsar_KeyValue},
40-
{"Bytes", pulsar_Bytes},
41-
{"AutoConsume", pulsar_AutoConsume},
42-
{"AutoPublish", pulsar_AutoPublish}};
29+
struct _pulsar_producer_configuration {
30+
pulsar::ProducerConfiguration conf;
31+
};
4332

44-
SchemaInfo::SchemaInfo(const Napi::Object &schemaInfo) : cSchemaType(pulsar_Bytes), name("BYTES"), schema() {
45-
this->cProperties = pulsar_string_map_create();
33+
struct _pulsar_consumer_configuration {
34+
pulsar::ConsumerConfiguration consumerConfiguration;
35+
};
36+
37+
static const std::map<std::string, pulsar::SchemaType> SCHEMA_TYPE = {
38+
{"None", static_cast<pulsar::SchemaType>(0)},
39+
{"String", static_cast<pulsar::SchemaType>(1)},
40+
{"Json", static_cast<pulsar::SchemaType>(2)},
41+
{"Protobuf", static_cast<pulsar::SchemaType>(3)},
42+
{"Avro", static_cast<pulsar::SchemaType>(4)},
43+
{"Boolean", static_cast<pulsar::SchemaType>(5)},
44+
{"Int8", static_cast<pulsar::SchemaType>(6)},
45+
{"Int16", static_cast<pulsar::SchemaType>(7)},
46+
{"Int32", static_cast<pulsar::SchemaType>(8)},
47+
{"Int64", static_cast<pulsar::SchemaType>(9)},
48+
{"Float32", static_cast<pulsar::SchemaType>(10)},
49+
{"Float64", static_cast<pulsar::SchemaType>(11)},
50+
{"KeyValue", static_cast<pulsar::SchemaType>(15)},
51+
{"ProtobufNative", static_cast<pulsar::SchemaType>(20)},
52+
{"Bytes", static_cast<pulsar::SchemaType>(-1)},
53+
{"AutoConsume", static_cast<pulsar::SchemaType>(-3)},
54+
{"AutoPublish", static_cast<pulsar::SchemaType>(-4)}};
55+
56+
SchemaInfo::SchemaInfo(const Napi::Object &schemaInfo)
57+
: schemaType(static_cast<pulsar::SchemaType>(-1)), name("BYTES"), schema() {
4658
if (schemaInfo.Has(CFG_SCHEMA_TYPE) && schemaInfo.Get(CFG_SCHEMA_TYPE).IsString()) {
4759
this->name = schemaInfo.Get(CFG_SCHEMA_TYPE).ToString().Utf8Value();
48-
this->cSchemaType = SCHEMA_TYPE.at(schemaInfo.Get(CFG_SCHEMA_TYPE).ToString().Utf8Value());
60+
this->schemaType = SCHEMA_TYPE.at(schemaInfo.Get(CFG_SCHEMA_TYPE).ToString().Utf8Value());
4961
}
5062
if (schemaInfo.Has(CFG_NAME) && schemaInfo.Get(CFG_NAME).IsString()) {
5163
this->name = schemaInfo.Get(CFG_NAME).ToString().Utf8Value();
@@ -60,19 +72,19 @@ SchemaInfo::SchemaInfo(const Napi::Object &schemaInfo) : cSchemaType(pulsar_Byte
6072
for (int i = 0; i < size; i++) {
6173
Napi::String key = arr.Get(i).ToString();
6274
Napi::String value = propObj.Get(key).ToString();
63-
pulsar_string_map_put(this->cProperties, key.Utf8Value().c_str(), value.Utf8Value().c_str());
75+
this->properties[key.Utf8Value()] = value.Utf8Value();
6476
}
6577
}
6678
}
6779

6880
void SchemaInfo::SetProducerSchema(std::shared_ptr<pulsar_producer_configuration_t> cProducerConfiguration) {
69-
pulsar_producer_configuration_set_schema_info(cProducerConfiguration.get(), this->cSchemaType,
70-
this->name.c_str(), this->schema.c_str(), this->cProperties);
81+
cProducerConfiguration->conf.setSchema(
82+
pulsar::SchemaInfo(this->schemaType, this->name, this->schema, this->properties));
7183
}
7284

7385
void SchemaInfo::SetConsumerSchema(std::shared_ptr<pulsar_consumer_configuration_t> cConsumerConfiguration) {
74-
pulsar_consumer_configuration_set_schema_info(cConsumerConfiguration.get(), this->cSchemaType,
75-
this->name.c_str(), this->schema.c_str(), this->cProperties);
86+
cConsumerConfiguration->consumerConfiguration.setSchema(
87+
pulsar::SchemaInfo(this->schemaType, this->name, this->schema, this->properties));
7688
}
7789

78-
SchemaInfo::~SchemaInfo() { pulsar_string_map_free(this->cProperties); }
90+
SchemaInfo::~SchemaInfo() {}

src/SchemaInfo.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
#ifndef SCHEMA_INFO_H
2121
#define SCHEMA_INFO_H
2222

23+
#include <map>
24+
#include <memory>
2325
#include <napi.h>
24-
#include <pulsar/c/producer_configuration.h>
26+
#include <pulsar/Schema.h>
2527
#include <pulsar/c/consumer_configuration.h>
28+
#include <pulsar/c/producer_configuration.h>
29+
#include <string>
2630

2731
class SchemaInfo {
2832
public:
@@ -32,10 +36,10 @@ class SchemaInfo {
3236
void SetConsumerSchema(std::shared_ptr<pulsar_consumer_configuration_t> cConsumerConfiguration);
3337

3438
private:
35-
pulsar_schema_type cSchemaType;
39+
pulsar::SchemaType schemaType;
3640
std::string name;
3741
std::string schema;
38-
pulsar_string_map_t *cProperties;
42+
std::map<std::string, std::string> properties;
3943
};
4044

4145
#endif
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
const Pulsar = require('../index');
21+
const { pulsar } = require('./protobuf_schema/generated/user_event_pb');
22+
const userEventRootJson = require('./protobuf_schema/generated/user_event_root.json');
23+
24+
(() => {
25+
describe('ProtobufNativeSchema', () => {
26+
test('produce and consume protobuf native messages', async () => {
27+
const topic = `persistent://public/default/protobuf-native-schema-${Date.now()}`;
28+
const subscription = `protobuf-native-sub-${Date.now()}`;
29+
const userEventRoot = Pulsar.ProtobufNativeSchema.createRootFromJson(userEventRootJson);
30+
const schema = Pulsar.ProtobufNativeSchema.createSchemaInfoFromRoot({
31+
root: userEventRoot,
32+
rootMessageTypeName: 'pulsar.example.UserEvent',
33+
rootFileDescriptorName: 'user_event.proto',
34+
});
35+
36+
expect(schema.schemaType).toBe('ProtobufNative');
37+
38+
const client = new Pulsar.Client({
39+
serviceUrl: 'pulsar://localhost:6650',
40+
operationTimeoutSeconds: 30,
41+
});
42+
43+
let producer;
44+
let consumer;
45+
46+
try {
47+
consumer = await client.subscribe({
48+
topic,
49+
subscription,
50+
subscriptionType: 'Shared',
51+
ackTimeoutMs: 10000,
52+
schema,
53+
});
54+
55+
producer = await client.createProducer({
56+
topic,
57+
sendTimeoutMs: 30000,
58+
batchingEnabled: true,
59+
schema,
60+
});
61+
62+
const sent = [];
63+
for (let i = 0; i < 5; i += 1) {
64+
const userEvent = pulsar.example.UserEvent.create({
65+
id: `user-${i}`,
66+
name: `User ${i}`,
67+
age: 20 + i,
68+
tags: ['nodejs', 'protobuf'],
69+
});
70+
const data = Buffer.from(pulsar.example.UserEvent.encode(userEvent).finish());
71+
sent.push(pulsar.example.UserEvent.toObject(userEvent));
72+
await producer.send({ data });
73+
}
74+
await producer.flush();
75+
76+
const received = [];
77+
for (let i = 0; i < sent.length; i += 1) {
78+
const msg = await consumer.receive();
79+
const userEvent = pulsar.example.UserEvent.decode(msg.getData());
80+
received.push(pulsar.example.UserEvent.toObject(userEvent));
81+
await consumer.acknowledge(msg);
82+
}
83+
84+
expect(received).toEqual(sent);
85+
} finally {
86+
if (producer) {
87+
await producer.close();
88+
}
89+
if (consumer) {
90+
await consumer.close();
91+
}
92+
await client.close();
93+
}
94+
}, 30000);
95+
});
96+
})();

0 commit comments

Comments
 (0)