@pbwire/pbwire 0.1.0 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,36 @@
1
1
  # pbwire Node.js binding
2
2
 
3
- A `FileDescriptorSet` (`protoc --descriptor_set_out`) becomes a schema at
4
- runtime; no code generation, no `protoc` plugin, no npm dependencies. The
5
- runtime is `runtime/addon.c`, an N-API addon in C over `libpbwire.a`; the
6
- `Schema`/`Message` surface is `lib/pbwire.js`.
3
+ The runtime is `runtime/addon.c`, an N-API addon in C over `libpbwire.a`;
4
+ the `Schema`/`Message` surface is `lib/pbwire.js`. No npm dependencies.
5
+
6
+ Most code never touches that surface: generate a protobufjs-static-shaped
7
+ facade once (`make -C codegen gen-node`) and use it like the
8
+ library it mirrors — same `create`/`encode`/`decode`/`verify` calls,
9
+ same observable behavior, but the wire format stays in the C core:
10
+
11
+ ```js
12
+ const { Person } = require('./tests/gen/addressbook.js');
13
+
14
+ const msg = Person.create({name: 'alice', id: 7});
15
+ Person.verify(msg); // null: valid
16
+ const wire = Person.encode(msg).finish(); // Buffer
17
+ const back = Person.decode(wire);
18
+ back.name; // 'alice'
19
+ back.toJSON(); // {name: 'alice', id: 7, ...}
20
+ ```
21
+
22
+ The generated file embeds its own `FileDescriptorSet`, so there is no
23
+ `.desc` to ship: `require` it and go. int64 reads as `BigInt`; `create`/
24
+ `fromObject` also take `number|string|Long-like`. `toObject()` keys are
25
+ camelCase (protobufjs convention); pass `{keepCase: true}` for proto
26
+ names. `fromObject` rejects bad shapes with `TypeError`; `verify`
27
+ returns the same reason strings as protobufjs. The one deliberate
28
+ difference: direct post-hoc writes of an implicit zero (`m.i32 = 0`)
29
+ are not marked present — set values through `create`/`fromObject` to
30
+ write explicit zeros.
31
+
32
+ The dynamic API is still there for gateways and schemas known only at
33
+ runtime:
7
34
 
8
35
  ```js
9
36
  const { Schema } = require('./lib/pbwire');
@@ -11,18 +38,12 @@ const { Schema } = require('./lib/pbwire');
11
38
  const schema = Schema.fromFile('tests/data/demo.desc');
12
39
  const person = schema.message('demo.Person');
13
40
 
14
- const wire = person.encode({name: 'alice', id: 7, inner: {x: 42}});
15
- // or, by hand:
16
- const msg = person.newMessage();
17
- msg.set('name', 'alice').set('inner', {x: 42});
18
- msg.encode(); // Buffer
19
- msg.free();
20
-
21
- const back = person.decode(wire);
22
- back.get('name'); // 'alice'
23
- back.get('inner').x; // 42
24
- back.encode().equals(wire); // true: byte-exact re-encode
25
- back.free();
41
+ const wire2 = person.encode({name: 'alice', id: 7, inner: {x: 42}});
42
+ const back2 = person.decode(wire2);
43
+ back2.get('name'); // 'alice'
44
+ back2.get('inner').x; // 42
45
+ back2.encode().equals(wire2); // true: byte-exact re-encode
46
+ back2.free();
26
47
  ```
27
48
 
28
49
  ## How it works
@@ -187,19 +208,37 @@ per-message finalizer.
187
208
  - **No protoc plugin / code generation.** The schema is always loaded at run
188
209
  time, like the Python binding: a `FileDescriptorSet` is the only schema
189
210
  source, and there is no generated layout to keep in sync.
190
- - **No conformance testee.** The official suite is covered through the C core
191
- and `conformance/`, not through this binding. The differential test above is
192
- what keeps the addon's wire reader honest against the core's.
211
+ Also exported: JSON/text format (`messageToJson`/`jsonToMessage` — the latter
212
+ with an optional ignore-unknown flag, `messageToText`/`textToMessage` — the
213
+ former with an optional print-unknown flag), field presence (`has`), and
214
+ delimited framing (`encodeDelimited`/`decodeDelimited`, returning
215
+ `{message, read}` so concatenated frames decode in a loop).
216
+ See `test/test_pbwire.js`.
217
+
218
+ ## Conformance
219
+
220
+ `conformance/testee.js` drives the official `conformance_test_runner`
221
+ through this binding (`make conformance-node`):
222
+
223
+ ```
224
+ CONFORMANCE SUITE PASSED: 2742 successes, 0 skipped, 2 expected failures,
225
+ 0 unexpected failures.
226
+ CONFORMANCE SUITE PASSED: 445 successes, 0 skipped, 0 expected failures,
227
+ 0 unexpected failures.
228
+ ```
193
229
 
194
- Also out of scope here: JSON/text format (`pbw_json_print`/`pbw_text_print`),
195
- field presence introspection (`pbw_message_field_count`), and delimited framing.
196
- Each is one addon export away when a consumer needs it.
230
+ Binary input decodes to a tree (unknown fields survive, oneof collapses to
231
+ the last member) and every output renders that same tree; JSON input with
232
+ unknown fields only parses when the test category asks to ignore them, and
233
+ text output honors the runner's `print_unknown_fields` flag. The 2 expected
234
+ failures are the shared `conformance/failure_list_pbwire.txt` (tag varints
235
+ the reference C++ testee truncates identically); only JSPB answers `skipped`.
197
236
 
198
- ## Typed models
237
+ ## Generated models
199
238
 
200
- `protoc-gen-pbwire-typed` (in `../typed`) generates `matrix3.d.ts` interfaces
239
+ `protoc-gen-pbwire` (in `../../codegen`) generates `matrix3.d.ts` interfaces
201
240
  (for TypeScript) and a JSDoc/`@ts-check` `.js` with the same shapes plus the enum
202
241
  constants, describing exactly the plain objects `MessageType.encode()` accepts
203
242
  and `Message.get()` returns. The committed output is `tests/gen/` and
204
- `examples/gen/`; the typed test type-checks with `tsc --strict --checkJs`.
205
- See `../typed/README.md`.
243
+ `examples/gen/`; the generated test type-checks with `tsc --strict --checkJs`.
244
+ See `../../codegen/README.md`.
package/build.sh CHANGED
@@ -46,4 +46,4 @@ if [ "${1:-all}" = build ]; then
46
46
  fi
47
47
 
48
48
  # --expose-gc so the finalizer test can force the collections it checks.
49
- exec node --expose-gc --test "$here/test/test_pbwire.js" "$here/tests/typed_test.js"
49
+ exec node --expose-gc --test "$here/test/test_pbwire.js" "$here/tests/generated_test.js" "$here/tests/compat_test.js"
@@ -0,0 +1,525 @@
1
+ 'use strict';
2
+
3
+ /* pbwire compat runtime: the verify/fromObject/toObject engine every generated
4
+ * facade delegates to. Schemas are cached per embedded descriptor set, so any
5
+ * number of generated files share this module with no global-state surprises:
6
+ * one entry per distinct FileDescriptorSet, keyed by its own bytes.
7
+ */
8
+
9
+ const { Schema } = require('./pbwire.js');
10
+
11
+ const _schemas = new Map();
12
+ function _type(descB64, full) {
13
+ let byName = _schemas.get(descB64);
14
+ if (byName === undefined) {
15
+ byName = new Map();
16
+ byName.set('', Schema.fromBytes(Buffer.from(descB64, 'base64')));
17
+ _schemas.set(descB64, byName);
18
+ }
19
+ let t = byName.get(full);
20
+ if (t === undefined) {
21
+ t = byName.get('').message(full);
22
+ byName.set(full, t);
23
+ }
24
+ return t;
25
+ }
26
+
27
+ function _isLongLike(v) {
28
+ return typeof v === 'object' && v !== null &&
29
+ Number.isInteger(v.low) && Number.isInteger(v.high);
30
+ }
31
+
32
+ function _longToBigInt(v) {
33
+ return (BigInt(v.high >>> 0) << 32n) | BigInt(v.low >>> 0);
34
+ }
35
+
36
+ // _toBigInt normalizes an int64-ish value for the encoder. BigInt passes
37
+ // through, numbers pass through (the addon casts them, same as plain
38
+ // objects), numeric strings and Long-like {low, high} become BigInt.
39
+ function _toBigInt(v, path, unsigned) {
40
+ if (typeof v === 'bigint') {
41
+ // fall through to the range check below
42
+ } else if (typeof v === 'number') {
43
+ if (!Number.isInteger(v)) {
44
+ throw new TypeError('pbwire: ' + path + ' is not an integer');
45
+ }
46
+ return v;
47
+ } else if (typeof v === 'string') {
48
+ if (!/^-?\d+$/.test(v)) {
49
+ throw new TypeError('pbwire: ' + path + ' is not an integer string');
50
+ }
51
+ v = BigInt(v);
52
+ } else if (_isLongLike(v)) {
53
+ v = _longToBigInt(v);
54
+ } else {
55
+ throw new TypeError('pbwire: ' + path + ' is not an int64 value');
56
+ }
57
+ if (unsigned) {
58
+ if (v < 0n || v > 18446744073709551615n) {
59
+ throw new TypeError('pbwire: ' + path + ' out of uint64 range');
60
+ }
61
+ } else if (v < -9223372036854775808n || v > 9223372036854775807n) {
62
+ throw new TypeError('pbwire: ' + path + ' out of int64 range');
63
+ }
64
+ return v;
65
+ }
66
+
67
+ // Shape check only (any BigInt passes); range is enforced when the value
68
+ // is normalized for the encoder in _toBigInt.
69
+ function _checkInt64(v) {
70
+ if (typeof v === 'bigint') {
71
+ return true;
72
+ }
73
+ if (typeof v === 'number') {
74
+ return Number.isInteger(v);
75
+ }
76
+ if (typeof v === 'string') {
77
+ return /^-?\d+$/.test(v);
78
+ }
79
+ return _isLongLike(v);
80
+ }
81
+
82
+ // _verifyField returns null when v is valid, else a reason string.
83
+ function _verifyField(fd, v, path, enums, classes) {
84
+ if (fd.rep) {
85
+ if (!Array.isArray(v)) {
86
+ return path + ': array expected';
87
+ }
88
+ for (let i = 0; i < v.length; i++) {
89
+ const bad = _verifyOne(fd, v[i], path + '[' + i + ']', enums, classes);
90
+ if (bad !== null) {
91
+ return bad;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ return _verifyOne(fd, v, path, enums, classes);
97
+ }
98
+
99
+ function _verifyOne(fd, v, path, enums, classes) {
100
+ switch (fd.k) {
101
+ // Reason strings mirror protobufjs verify() verbatim so mixed code reads
102
+ // the same errors from either library. Like protobufjs, 32-bit checks are
103
+ // integer-only (range truncation happens in the encoder, identically).
104
+ case 'i32':
105
+ case 's32':
106
+ case 'sfx32':
107
+ case 'u32':
108
+ case 'fx32':
109
+ return Number.isInteger(v) ? null : path + ': integer expected';
110
+ case 'i64':
111
+ case 's64':
112
+ case 'sfx64':
113
+ case 'u64':
114
+ case 'fx64':
115
+ return _checkInt64(v) ? null : path + ': integer|Long expected';
116
+ case 'f32':
117
+ case 'f64':
118
+ return (typeof v === 'number') ? null : path + ': number expected';
119
+ case 'bool':
120
+ return (typeof v === 'boolean') ? null : path + ': boolean expected';
121
+ case 'str':
122
+ return (typeof v === 'string') ? null : path + ': string expected';
123
+ case 'bytes':
124
+ return (typeof v === 'object' && v !== null &&
125
+ (Buffer.isBuffer(v) || v instanceof Uint8Array))
126
+ ? null : path + ': buffer expected';
127
+ case 'enum': {
128
+ const names = enums[fd.en];
129
+ if (typeof v === 'number' && Number.isInteger(v)) {
130
+ // Open enums (proto3) carry unknown values through the round trip.
131
+ if (!fd.closed || names === undefined) {
132
+ return null;
133
+ }
134
+ for (const key in names) {
135
+ if (names[key] === v) {
136
+ return null;
137
+ }
138
+ }
139
+ return path + ': enum value expected';
140
+ }
141
+ return path + ': enum value expected';
142
+ }
143
+ case 'msg':
144
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
145
+ return path + ': object expected';
146
+ }
147
+ return classes[fd.msg].verify(v);
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function _fromOne(fd, v, path, enums, classes) {
154
+ switch (fd.k) {
155
+ case 'i64':
156
+ case 's64':
157
+ case 'sfx64':
158
+ return _toBigInt(v, path, false);
159
+ case 'u64':
160
+ case 'fx64':
161
+ return _toBigInt(v, path, true);
162
+ case 'bytes':
163
+ if (Buffer.isBuffer(v)) {
164
+ return v;
165
+ }
166
+ if (v instanceof Uint8Array) {
167
+ return Buffer.from(v);
168
+ }
169
+ throw new TypeError('pbwire: ' + path + ' is not bytes');
170
+ case 'enum': {
171
+ if (typeof v === 'string') {
172
+ const names = enums[fd.en];
173
+ if (names !== undefined && names[v] !== undefined) {
174
+ return names[v];
175
+ }
176
+ throw new TypeError('pbwire: ' + path + ' unknown enum name ' + v);
177
+ }
178
+ if (typeof v === 'number' && Number.isInteger(v)) {
179
+ return v;
180
+ }
181
+ throw new TypeError('pbwire: ' + path + ' is not an enum value');
182
+ }
183
+ case 'msg':
184
+ if (v !== null && typeof v === 'object' && !(v instanceof classes[fd.msg])) {
185
+ return classes[fd.msg].fromObject(v);
186
+ }
187
+ return v;
188
+ default:
189
+ return v;
190
+ }
191
+ }
192
+
193
+ function _fromValue(fd, v, path, enums, classes) {
194
+ if (v === null || v === undefined) {
195
+ return fd.rep ? [] : null;
196
+ }
197
+ if (fd.rep) {
198
+ if (!Array.isArray(v)) {
199
+ throw new TypeError('pbwire: ' + path + ' is not an array');
200
+ }
201
+ return v.map((item, i) => _fromOne(fd, item, path + '[' + i + ']', enums, classes));
202
+ }
203
+ return _fromOne(fd, v, path, enums, classes);
204
+ }
205
+
206
+ function _toInt64(v, mode) {
207
+ const big = typeof v === 'bigint' ? v
208
+ : typeof v === 'number' ? BigInt(Math.trunc(v))
209
+ : _longToBigInt(v);
210
+ if (mode === 'String') {
211
+ return big.toString();
212
+ }
213
+ if (mode === 'Number') {
214
+ return Number(big);
215
+ }
216
+ return big;
217
+ }
218
+
219
+ function _toOne(fd, v, opts, enums, revEnums) {
220
+ switch (fd.k) {
221
+ case 'i64':
222
+ case 's64':
223
+ case 'sfx64':
224
+ case 'u64':
225
+ case 'fx64':
226
+ return _toInt64(v, opts.longs || 'BigInt');
227
+ case 'enum':
228
+ if ((opts.enums || 'Number') === 'String') {
229
+ const rev = revEnums[fd.en];
230
+ if (rev !== undefined && rev[v] !== undefined) {
231
+ return rev[v];
232
+ }
233
+ }
234
+ return v;
235
+ case 'bytes':
236
+ return (opts.bytes || 'Buffer') === 'base64' ? Buffer.from(v).toString('base64') : v;
237
+ case 'msg':
238
+ return v.toObject(opts);
239
+ default:
240
+ return v;
241
+ }
242
+ }
243
+
244
+ // Output keys are camelCase like protobufjs toObject; keepCase:true keeps
245
+ // the proto names (the dynamic API's convention).
246
+ function _toPlain(cls, msg, opts, enums, revEnums) {
247
+ const out = {};
248
+ const keyOf = (fd) => (opts.keepCase ? fd.n : fd.js);
249
+ const marked = msg._present instanceof Set ? msg._present : null;
250
+ for (const fd of cls._FIELDS) {
251
+ const v = msg[fd.js];
252
+ if (v === null || v === undefined) {
253
+ continue;
254
+ }
255
+ if (fd.rep && v.length === 0) {
256
+ continue;
257
+ }
258
+ // Unmarked fields are constructor defaults or direct post-hoc writes:
259
+ // include non-default values (direct post-hoc writes work), drop default
260
+ // ones (so create({}) encodes empty). Marked fields always go on the wire.
261
+ if (!opts.defaults && !fd.req && marked !== null &&
262
+ !marked.has(fd.js) && _isDefault(fd, v)) {
263
+ continue;
264
+ }
265
+ out[keyOf(fd)] = fd.rep
266
+ ? v.map((item) => _toOne(fd, item, opts, enums, revEnums))
267
+ : _toOne(fd, v, opts, enums, revEnums);
268
+ }
269
+ if (opts.defaults) {
270
+ for (const fd of cls._FIELDS) {
271
+ if (out[keyOf(fd)] !== undefined) {
272
+ continue;
273
+ }
274
+ if (fd.rep) {
275
+ out[keyOf(fd)] = [];
276
+ } else if (fd.k === 'msg') {
277
+ out[keyOf(fd)] = null;
278
+ } else {
279
+ out[keyOf(fd)] = _toOne(fd, _defaultFor(fd), opts, enums, revEnums);
280
+ }
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+
286
+ function _isDefault(fd, v) {
287
+ if (fd.rep) {
288
+ return v.length === 0;
289
+ }
290
+ switch (fd.k) {
291
+ case 'i64':
292
+ case 's64':
293
+ case 'sfx64':
294
+ case 'u64':
295
+ case 'fx64':
296
+ return v === 0 || v === 0n;
297
+ case 'bool':
298
+ return v === false;
299
+ case 'str':
300
+ return v === '';
301
+ case 'bytes':
302
+ return v.length === 0;
303
+ case 'msg':
304
+ return v === null || v === undefined;
305
+ default:
306
+ return v === 0;
307
+ }
308
+ }
309
+
310
+ function _defaultFor(fd, classes) {
311
+ if (fd.rep) {
312
+ return [];
313
+ }
314
+ // Presence fields (explicit presence or sub-message) default to absent.
315
+ if (fd.has) {
316
+ return null;
317
+ }
318
+ switch (fd.k) {
319
+ case 'i64':
320
+ case 's64':
321
+ case 'sfx64':
322
+ case 'u64':
323
+ case 'fx64':
324
+ return 0n;
325
+ case 'f32':
326
+ case 'f64':
327
+ case 'i32':
328
+ case 's32':
329
+ case 'sfx32':
330
+ case 'u32':
331
+ case 'fx32':
332
+ case 'enum':
333
+ return 0;
334
+ case 'bool':
335
+ return false;
336
+ case 'str':
337
+ return '';
338
+ case 'bytes':
339
+ return Buffer.alloc(0);
340
+ case 'msg':
341
+ return null;
342
+ default:
343
+ return null;
344
+ }
345
+ }
346
+
347
+ function _encodeVarint(n) {
348
+ const parts = [];
349
+ while (n >= 128) {
350
+ parts.push((n % 128) | 128);
351
+ n = Math.floor(n / 128);
352
+ }
353
+ parts.push(n);
354
+ return Buffer.from(parts);
355
+ }
356
+
357
+ function _decodeVarint(buf, pos) {
358
+ let n = 0;
359
+ let shift = 0;
360
+ while (pos < buf.length) {
361
+ const b = buf[pos++];
362
+ n += (b & 127) * 2 ** shift;
363
+ shift += 7;
364
+ if ((b & 128) === 0) {
365
+ return [n, pos];
366
+ }
367
+ if (shift >= 64) {
368
+ throw new Error('pbwire: varint too long');
369
+ }
370
+ }
371
+ throw new Error('pbwire: truncated varint');
372
+ }
373
+
374
+ // _makeClass builds one message class over a MessageType. cls._FIELDS is the
375
+ // table the engine above reads; wire bytes always come from the C core.
376
+ // _fromWire converts C-validated wire values into an instance. Unlike
377
+ // fromObject it performs no validation and no coercion: the decoder has
378
+ // already checked every value against the schema, so nested plains only
379
+ // need wrapping and every present field only needs marking. The encode
380
+ // boundary (addon tree build) remains the loudness net for bad data.
381
+ function _fromWire(cls, plain) {
382
+ // new keeps V8's hidden-class shape (measured faster than Object.create
383
+ // + defineProperty here); the constructor's defaults stand for absent
384
+ // fields, present ones are converted and marked below.
385
+ const inst = new cls();
386
+ const classes = cls._CLASSES;
387
+ for (const fd of cls._FIELDS) {
388
+ const v = plain[fd.n];
389
+ if (v === undefined) {
390
+ continue;
391
+ }
392
+ if (fd.k === 'msg') {
393
+ if (fd.rep) {
394
+ inst[fd.js] = v.map((item) => _fromWire(classes[fd.msg], item));
395
+ } else {
396
+ inst[fd.js] = _fromWire(classes[fd.msg], v);
397
+ }
398
+ } else if (fd.rep) {
399
+ inst[fd.js] = v.slice();
400
+ } else {
401
+ inst[fd.js] = v;
402
+ }
403
+ inst._present.add(fd.js);
404
+ }
405
+ return inst;
406
+ }
407
+
408
+ function _makeClass(full, descB64, fields) {
409
+ class Compat {
410
+ constructor(props) {
411
+ props = props || {};
412
+ Object.defineProperty(this, '_present', { value: new Set(), enumerable: false, writable: true });
413
+ for (const fd of fields) {
414
+ const v = props[fd.js] !== undefined ? props[fd.js] : props[fd.n];
415
+ if (v === undefined) {
416
+ this[fd.js] = _defaultFor(fd);
417
+ } else {
418
+ this[fd.js] = v;
419
+ // fromObject normalizes below; direct construction marks as set.
420
+ this._present.add(fd.js);
421
+ }
422
+ }
423
+ }
424
+ static create(props) {
425
+ return this.fromObject(props || {});
426
+ }
427
+ static encode(message) {
428
+ const inst = (message instanceof this) ? message : this.fromObject(message);
429
+ // The dynamic encoder takes proto names; toObject() shows camelCase.
430
+ const plain = _toPlain(this, inst, { keepCase: true }, this._ENUMS, this._REVENUMS);
431
+ const wire = _type(descB64, full).encode(plain);
432
+ return { finish: () => wire };
433
+ }
434
+ static encodeDelimited(message) {
435
+ const wire = this.encode(message).finish();
436
+ const out = Buffer.concat([_encodeVarint(wire.length), wire]);
437
+ return { finish: () => out };
438
+ }
439
+ static decode(bytes) {
440
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
441
+ const m = _type(descB64, full).decode(buf);
442
+ const plain = {};
443
+ for (const fd of fields) {
444
+ const v = m.get(fd.n);
445
+ if (v !== undefined) {
446
+ plain[fd.n] = v;
447
+ }
448
+ }
449
+ return _fromWire(Compat, plain);
450
+ }
451
+ static decodeDelimited(bytes) {
452
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
453
+ const pair = _decodeVarint(buf, 0);
454
+ return this.decode(buf.subarray(pair[1], pair[1] + pair[0]));
455
+ }
456
+ static verify(obj) {
457
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
458
+ return 'object expected';
459
+ }
460
+ const seen = new Map();
461
+ for (const fd of fields) {
462
+ let v = obj[fd.js];
463
+ if (v === undefined) {
464
+ v = obj[fd.n];
465
+ }
466
+ if (v === null || v === undefined) {
467
+ if (fd.req) {
468
+ return fd.n + ': required field missing';
469
+ }
470
+ continue;
471
+ }
472
+ const bad = _verifyField(fd, v, fd.n, this._ENUMS, this._CLASSES);
473
+ if (bad !== null) {
474
+ return bad;
475
+ }
476
+ if (fd.oneof !== null) {
477
+ if (seen.has(fd.oneof)) {
478
+ return fd.oneof + ': multiple values';
479
+ }
480
+ seen.set(fd.oneof, true);
481
+ }
482
+ }
483
+ return null;
484
+ }
485
+ static fromObject(obj) {
486
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
487
+ throw new TypeError('pbwire: fromObject expects an object');
488
+ }
489
+ const inst = new this();
490
+ for (const fd of fields) {
491
+ let v = obj[fd.js] !== undefined ? obj[fd.js] : obj[fd.n];
492
+ if (v === undefined) {
493
+ continue;
494
+ }
495
+ if (v === null) {
496
+ inst[fd.js] = fd.rep ? [] : null;
497
+ continue;
498
+ }
499
+ // One validation logic for verify() and fromObject(): normalize,
500
+ // then reject with the same reason verify() would give.
501
+ const nv = _fromValue(fd, v, fd.n, this._ENUMS, this._CLASSES);
502
+ const bad = _verifyField(fd, nv, fd.n, this._ENUMS, this._CLASSES);
503
+ if (bad !== null) {
504
+ throw new TypeError('pbwire: ' + bad);
505
+ }
506
+ inst[fd.js] = nv;
507
+ inst._present.add(fd.js);
508
+ }
509
+ return inst;
510
+ }
511
+ static toObject(message, opts) {
512
+ const inst = (message instanceof this) ? message : this.fromObject(message);
513
+ return _toPlain(this, inst, opts || {}, this._ENUMS, this._REVENUMS);
514
+ }
515
+ toObject(opts) {
516
+ return this.constructor.toObject(this, opts);
517
+ }
518
+ toJSON() {
519
+ return this.constructor.toObject(this, { enums: 'String', longs: 'String', bytes: 'base64' });
520
+ }
521
+ }
522
+ Compat._FIELDS = fields;
523
+ return Compat;
524
+ }
525
+ module.exports = { _makeClass };
package/lib/pbwire.js CHANGED
@@ -107,6 +107,40 @@ class MessageType {
107
107
  return this._copy(addon.encodeObject(this._handle, value));
108
108
  }
109
109
 
110
+ /** Parse JSON mapping text into a Message; throws on malformed input.
111
+ * A second true argument skips unknown field names instead of failing. */
112
+ fromJson(json, ignore = false) {
113
+ if (typeof json !== 'string') {
114
+ throw new TypeError('pbwire: fromJson expects a string');
115
+ }
116
+ const message = new Message(this);
117
+ message._handle = addon.jsonToMessage(this._handle, json, ignore);
118
+ return message;
119
+ }
120
+
121
+ /** Parse text format into a Message; throws on malformed input. */
122
+ fromText(text) {
123
+ if (typeof text !== 'string') {
124
+ throw new TypeError('pbwire: fromText expects a string');
125
+ }
126
+ const message = new Message(this);
127
+ message._handle = addon.textToMessage(this._handle, text);
128
+ return message;
129
+ }
130
+
131
+ /** Decode one length-delimited frame: {message, read}, where read counts
132
+ * the length prefix plus the frame, so concatenated frames decode in a
133
+ * loop. Throws on a truncated frame. */
134
+ decodeDelimited(bytes) {
135
+ if (!Buffer.isBuffer(bytes)) {
136
+ throw new TypeError('pbwire: decodeDelimited expects a Buffer');
137
+ }
138
+ const res = addon.decodeDelimited(this._handle, bytes);
139
+ const message = new Message(this);
140
+ message._handle = res.message;
141
+ return { message, read: res.read };
142
+ }
143
+
110
144
  /** Decode wire bytes into a Message; throws on malformed input. */
111
145
  decode(bytes) {
112
146
  if (!Buffer.isBuffer(bytes)) {
@@ -152,6 +186,12 @@ class Message {
152
186
 
153
187
  /** Store a field value by name; `undefined`/`null` clears the field. */
154
188
  set(name, value) {
189
+ addon.set(this._tree(), name, value);
190
+ return this;
191
+ }
192
+
193
+ /** The mutable tree, decoding the retained bytes once when needed. */
194
+ _tree() {
155
195
  this._live();
156
196
  if (this._wire !== null) {
157
197
  // Writing needs a mutable tree: decode the retained bytes once.
@@ -159,8 +199,28 @@ class Message {
159
199
  this._wire = null;
160
200
  this._slices = null;
161
201
  }
162
- addon.set(this._handle, name, value);
163
- return this;
202
+ return this._handle;
203
+ }
204
+
205
+ /** JSON mapping text of this message. */
206
+ toJson() {
207
+ return addon.messageToJson(this._tree());
208
+ }
209
+
210
+ /** Protobuf text format of this message; a false argument drops unknown
211
+ * fields instead of printing them numerically. */
212
+ toText(printUnknown = true) {
213
+ return addon.messageToText(this._tree(), printUnknown);
214
+ }
215
+
216
+ /** Field presence by name: set (even to a zero) reads true. */
217
+ has(name) {
218
+ return addon.has(this._tree(), name);
219
+ }
220
+
221
+ /** One length-delimited frame (varint length + message). */
222
+ encodeDelimited() {
223
+ return addon.encodeDelimited(this._tree());
164
224
  }
165
225
 
166
226
  /** Wire bytes of this message. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pbwire/pbwire",
3
- "version": "0.1.0",
3
+ "version": "0.1.8",
4
4
  "description": "Dynamic protobuf over the pbwire C ABI, schema at runtime, no generated code",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/runtime/addon.c CHANGED
@@ -2528,6 +2528,279 @@ message_free(napi_env env, napi_callback_info info)
2528
2528
  return out;
2529
2529
  }
2530
2530
 
2531
+ /* JSON text of a tree (protobuf JSON mapping). */
2532
+ static napi_value
2533
+ message_to_json(napi_env env, napi_callback_info info)
2534
+ {
2535
+ napi_value argv[1];
2536
+ napi_value out = NULL;
2537
+ message_handle *handle;
2538
+ char *text = NULL;
2539
+ size_t len = 0;
2540
+ pbw_status status;
2541
+
2542
+ if (!get_args(env, info, argv, 1)) {
2543
+ return NULL;
2544
+ }
2545
+ if (!unwrap_message(env, argv[0], &handle)) {
2546
+ return NULL;
2547
+ }
2548
+ status = pbw_json_print(handle->msg, &text, &len);
2549
+ if (status != PBW_OK) {
2550
+ return fail_status(env, "messageToJson", status);
2551
+ }
2552
+ if (napi_create_string_utf8(env, text, len, &out) != napi_ok) {
2553
+ pbw_free(text);
2554
+ return NULL;
2555
+ }
2556
+ pbw_free(text);
2557
+ return out;
2558
+ }
2559
+
2560
+ /* Parses JSON mapping text into a new tree; an optional third boolean
2561
+ * argument skips unknown field names instead of failing. */
2562
+ static napi_value
2563
+ json_to_message(napi_env env, napi_callback_info info)
2564
+ {
2565
+ napi_value argv[3];
2566
+ size_t argc = 3;
2567
+ desc_handle *desc;
2568
+ message_handle *handle;
2569
+ pbw_message *msg = NULL;
2570
+ string_buf buf;
2571
+ const char *text;
2572
+ pbw_status status;
2573
+ pbw_json_parse_options opts;
2574
+ bool ignore = false;
2575
+
2576
+ if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok ||
2577
+ argc < 2) {
2578
+ napi_throw_type_error(env, NULL, "pbwire: expected (desc, json)");
2579
+ return NULL;
2580
+ }
2581
+ if (!unwrap(env, argv[0], (void **)&desc, "message descriptor handle")) {
2582
+ return NULL;
2583
+ }
2584
+ if (argc > 2) {
2585
+ napi_valuetype want = napi_undefined;
2586
+
2587
+ if (napi_typeof(env, argv[2], &want) != napi_ok ||
2588
+ want != napi_boolean ||
2589
+ napi_get_value_bool(env, argv[2], &ignore) != napi_ok) {
2590
+ napi_throw_type_error(env, NULL,
2591
+ "pbwire: ignore flag must be a boolean");
2592
+ return NULL;
2593
+ }
2594
+ }
2595
+ text = string_bytes(env, argv[1], &buf);
2596
+ if (text == NULL) {
2597
+ return NULL;
2598
+ }
2599
+ opts.ignore_unknown_fields = ignore;
2600
+ status = pbw_json_parse_opts(desc->table->desc, text, buf.len, &opts,
2601
+ &msg);
2602
+ string_free(&buf, text);
2603
+ if (status != PBW_OK) {
2604
+ return fail_status(env, "jsonToMessage", status);
2605
+ }
2606
+ handle = calloc(1, sizeof(*handle));
2607
+ if (handle == NULL) {
2608
+ pbw_message_free(msg);
2609
+ return fail_memory(env);
2610
+ }
2611
+ handle->msg = msg;
2612
+ return new_message_value(env, desc, argv[0], handle);
2613
+ }
2614
+
2615
+ /* Text format of a tree. */
2616
+ static napi_value
2617
+ message_to_text(napi_env env, napi_callback_info info)
2618
+ {
2619
+ napi_value argv[2];
2620
+ size_t argc = 2;
2621
+ napi_value out = NULL;
2622
+ message_handle *handle;
2623
+ char *text = NULL;
2624
+ size_t len = 0;
2625
+ pbw_status status;
2626
+ /* Absent means print (the binding's standing behavior); the testee
2627
+ * passes the runner's print_unknown_fields flag explicitly. */
2628
+ bool print_unknown = true;
2629
+
2630
+ if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok ||
2631
+ argc < 1) {
2632
+ napi_throw_type_error(env, NULL, "pbwire: expected (message)");
2633
+ return NULL;
2634
+ }
2635
+ if (!unwrap_message(env, argv[0], &handle)) {
2636
+ return NULL;
2637
+ }
2638
+ if (argc > 1) {
2639
+ napi_valuetype want = napi_undefined;
2640
+
2641
+ if (napi_typeof(env, argv[1], &want) != napi_ok ||
2642
+ want != napi_boolean ||
2643
+ napi_get_value_bool(env, argv[1], &print_unknown) != napi_ok) {
2644
+ napi_throw_type_error(env, NULL,
2645
+ "pbwire: print flag must be a boolean");
2646
+ return NULL;
2647
+ }
2648
+ }
2649
+ status = pbw_text_print_opts(handle->msg, print_unknown, &text, &len);
2650
+ if (status != PBW_OK) {
2651
+ return fail_status(env, "messageToText", status);
2652
+ }
2653
+ if (napi_create_string_utf8(env, text, len, &out) != napi_ok) {
2654
+ pbw_free(text);
2655
+ return NULL;
2656
+ }
2657
+ pbw_free(text);
2658
+ return out;
2659
+ }
2660
+
2661
+ /* Parses text format into a new tree. */
2662
+ static napi_value
2663
+ text_to_message(napi_env env, napi_callback_info info)
2664
+ {
2665
+ napi_value argv[2];
2666
+ desc_handle *desc;
2667
+ message_handle *handle;
2668
+ pbw_message *msg = NULL;
2669
+ string_buf buf;
2670
+ const char *text;
2671
+ pbw_status status;
2672
+
2673
+ if (!get_args(env, info, argv, 2)) {
2674
+ return NULL;
2675
+ }
2676
+ if (!unwrap(env, argv[0], (void **)&desc, "message descriptor handle")) {
2677
+ return NULL;
2678
+ }
2679
+ text = string_bytes(env, argv[1], &buf);
2680
+ if (text == NULL) {
2681
+ return NULL;
2682
+ }
2683
+ status = pbw_text_parse(desc->table->desc, text, buf.len, &msg);
2684
+ string_free(&buf, text);
2685
+ if (status != PBW_OK) {
2686
+ return fail_status(env, "textToMessage", status);
2687
+ }
2688
+ handle = calloc(1, sizeof(*handle));
2689
+ if (handle == NULL) {
2690
+ pbw_message_free(msg);
2691
+ return fail_memory(env);
2692
+ }
2693
+ handle->msg = msg;
2694
+ return new_message_value(env, desc, argv[0], handle);
2695
+ }
2696
+
2697
+ /* Presence of one field by name: set (even to a zero) reads true. */
2698
+ static napi_value
2699
+ message_has(napi_env env, napi_callback_info info)
2700
+ {
2701
+ napi_value argv[2];
2702
+ napi_value out = NULL;
2703
+ message_handle *handle;
2704
+ field_entry *entry;
2705
+ int present;
2706
+
2707
+ if (!get_args(env, info, argv, 2)) {
2708
+ return NULL;
2709
+ }
2710
+ if (!unwrap_message(env, argv[0], &handle)) {
2711
+ return NULL;
2712
+ }
2713
+ entry = field_by_name(env, handle, argv[1]);
2714
+ if (entry == NULL) {
2715
+ return NULL;
2716
+ }
2717
+ present = pbw_message_get_field(handle->msg, entry->field) != NULL;
2718
+ if (napi_get_boolean(env, present, &out) != napi_ok) {
2719
+ return NULL;
2720
+ }
2721
+ return out;
2722
+ }
2723
+
2724
+ /* One length-delimited frame (varint length + message) as a Buffer. */
2725
+ static napi_value
2726
+ encode_delimited(napi_env env, napi_callback_info info)
2727
+ {
2728
+ napi_value argv[1];
2729
+ napi_value out = NULL;
2730
+ message_handle *handle;
2731
+ uint8_t *wire = NULL;
2732
+ size_t wire_len = 0;
2733
+ pbw_status status;
2734
+
2735
+ if (!get_args(env, info, argv, 1)) {
2736
+ return NULL;
2737
+ }
2738
+ if (!unwrap_message(env, argv[0], &handle)) {
2739
+ return NULL;
2740
+ }
2741
+ status = pbw_encode_delimited(handle->msg, &wire, &wire_len);
2742
+ if (status != PBW_OK) {
2743
+ return fail_status(env, "encodeDelimited", status);
2744
+ }
2745
+ if (napi_create_buffer_copy(env, wire_len, wire, NULL, &out) != napi_ok) {
2746
+ pbw_free(wire);
2747
+ return NULL;
2748
+ }
2749
+ pbw_free(wire);
2750
+ return out;
2751
+ }
2752
+
2753
+ /* One frame from a Buffer: {message, read}, where read counts the length
2754
+ * prefix plus the frame, so concatenated frames decode in a loop. */
2755
+ static napi_value
2756
+ decode_delimited(napi_env env, napi_callback_info info)
2757
+ {
2758
+ napi_value argv[2];
2759
+ napi_value out = NULL;
2760
+ napi_value message_value = NULL;
2761
+ napi_value read_value = NULL;
2762
+ desc_handle *desc;
2763
+ message_handle *handle;
2764
+ pbw_message *msg = NULL;
2765
+ void *data = NULL;
2766
+ size_t len = 0;
2767
+ size_t consumed = 0;
2768
+ pbw_status status;
2769
+
2770
+ if (!get_args(env, info, argv, 2)) {
2771
+ return NULL;
2772
+ }
2773
+ if (!unwrap(env, argv[0], (void **)&desc, "message descriptor handle")) {
2774
+ return NULL;
2775
+ }
2776
+ if (napi_get_buffer_info(env, argv[1], &data, &len) != napi_ok) {
2777
+ return fail_argument(env, "decodeDelimited expects a Buffer");
2778
+ }
2779
+ status = pbw_decode_delimited(desc->table->desc, data, len, &msg,
2780
+ &consumed);
2781
+ if (status != PBW_OK) {
2782
+ return fail_status(env, "decodeDelimited", status);
2783
+ }
2784
+ handle = calloc(1, sizeof(*handle));
2785
+ if (handle == NULL) {
2786
+ pbw_message_free(msg);
2787
+ return fail_memory(env);
2788
+ }
2789
+ handle->msg = msg;
2790
+ message_value = new_message_value(env, desc, argv[0], handle);
2791
+ if (message_value == NULL) {
2792
+ return NULL;
2793
+ }
2794
+ if (napi_create_object(env, &out) != napi_ok ||
2795
+ napi_set_named_property(env, out, "message", message_value) !=
2796
+ napi_ok ||
2797
+ napi_create_uint32(env, (uint32_t)consumed, &read_value) != napi_ok ||
2798
+ napi_set_named_property(env, out, "read", read_value) != napi_ok) {
2799
+ return NULL;
2800
+ }
2801
+ return out;
2802
+ }
2803
+
2531
2804
  NAPI_MODULE_INIT()
2532
2805
  {
2533
2806
  napi_property_descriptor properties[] = {
@@ -2554,6 +2827,19 @@ NAPI_MODULE_INIT()
2554
2827
  {"copyEncoded", NULL, copy_encoded, NULL, NULL, NULL, napi_enumerable,
2555
2828
  NULL},
2556
2829
  {"free", NULL, message_free, NULL, NULL, NULL, napi_enumerable, NULL},
2830
+ {"messageToJson", NULL, message_to_json, NULL, NULL, NULL,
2831
+ napi_enumerable, NULL},
2832
+ {"jsonToMessage", NULL, json_to_message, NULL, NULL, NULL,
2833
+ napi_enumerable, NULL},
2834
+ {"messageToText", NULL, message_to_text, NULL, NULL, NULL,
2835
+ napi_enumerable, NULL},
2836
+ {"textToMessage", NULL, text_to_message, NULL, NULL, NULL,
2837
+ napi_enumerable, NULL},
2838
+ {"has", NULL, message_has, NULL, NULL, NULL, napi_enumerable, NULL},
2839
+ {"encodeDelimited", NULL, encode_delimited, NULL, NULL, NULL,
2840
+ napi_enumerable, NULL},
2841
+ {"decodeDelimited", NULL, decode_delimited, NULL, NULL, NULL,
2842
+ napi_enumerable, NULL},
2557
2843
  };
2558
2844
 
2559
2845
  if (napi_define_properties(env, exports,