@aptre/protobuf-es-lite 0.3.1 → 0.4.0

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/dist/enum.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DescEnum } from "./index.js";
1
2
  /**
2
3
  * Reflection information for a protobuf enumeration.
3
4
  */
@@ -33,8 +34,17 @@ export interface EnumValueInfo {
33
34
  */
34
35
  readonly localName: string;
35
36
  }
36
- export declare function normalizeEnumValue(value: EnumValueInfo | Omit<EnumValueInfo, "localName">): EnumValueInfo;
37
37
  /**
38
38
  * Create a new EnumType with the given values.
39
39
  */
40
40
  export declare function createEnumType(typeName: string, values: (EnumValueInfo | Omit<EnumValueInfo, "localName">)[], _opt?: {}): EnumType;
41
+ export declare function enumInfoZeroValue(values: (EnumValueInfo | Omit<EnumValueInfo, "localName">)[]): number;
42
+ export declare function enumDescZeroValue(info: DescEnum): number;
43
+ export declare function enumZeroValue<T extends EnumType>(info: T): number;
44
+ /**
45
+ * Returns the normalized version of the enum value.
46
+ * Null is cast to the default value.
47
+ * String names are cast to the number enum.
48
+ * If string and the value is unknown, throws an error.
49
+ */
50
+ export declare function normalizeEnumValue(info: EnumType, value: string | number | null | undefined): number;
package/dist/enum.js CHANGED
@@ -1,9 +1,3 @@
1
- export function normalizeEnumValue(value) {
2
- if ("localName" in value) {
3
- return value;
4
- }
5
- return { ...value, localName: value.name };
6
- }
7
1
  /**
8
2
  * Create a new EnumType with the given values.
9
3
  */
@@ -16,7 +10,7 @@ _opt) {
16
10
  for (const value of values) {
17
11
  // We do not surface options at this time
18
12
  // const value: EnumValueInfo = {...v, options: v.options ?? emptyReadonlyObject};
19
- const n = normalizeEnumValue(value);
13
+ const n = "localName" in value ? value : { ...value, localName: value.name };
20
14
  normalValues.push(n);
21
15
  names[value.name] = n;
22
16
  numbers[value.no] = n;
@@ -34,3 +28,58 @@ _opt) {
34
28
  },
35
29
  };
36
30
  }
31
+ // enumInfoZeroValue returns the zero value for an enum info.
32
+ export function enumInfoZeroValue(values) {
33
+ if (!values?.length) {
34
+ return 0;
35
+ }
36
+ // In proto3, the first enum value must be zero.
37
+ // In proto2, protobuf-go returns the first value as the default.
38
+ const zeroValue = values[0];
39
+ return zeroValue.no;
40
+ }
41
+ // enumDescZeroValue returns the zero value for an enum description.
42
+ export function enumDescZeroValue(info) {
43
+ // In proto3, the first enum value must be zero.
44
+ // In proto2, protobuf-go returns the first value as the default.
45
+ if (info.values.length < 1) {
46
+ throw new Error("invalid enum: missing at least one value");
47
+ }
48
+ const zeroValue = info.values[0];
49
+ return zeroValue.number;
50
+ }
51
+ // enumZeroValue returns the zero value for an enum type.
52
+ export function enumZeroValue(info) {
53
+ // In proto3, the first enum value must be zero.
54
+ // In proto2, protobuf-go returns the first value as the default.
55
+ if (info.values.length < 1) {
56
+ throw new Error("invalid enum: missing at least one value");
57
+ }
58
+ const zeroValue = info.values[0];
59
+ return zeroValue.no;
60
+ }
61
+ /**
62
+ * Returns the normalized version of the enum value.
63
+ * Null is cast to the default value.
64
+ * String names are cast to the number enum.
65
+ * If string and the value is unknown, throws an error.
66
+ */
67
+ export function normalizeEnumValue(info, value) {
68
+ const zeroValue = enumZeroValue(info);
69
+ if (value == null) {
70
+ return zeroValue;
71
+ }
72
+ if (value === "" || value === zeroValue) {
73
+ return zeroValue;
74
+ }
75
+ if (typeof value === "string") {
76
+ // TODO: strip the type name prefix as well? MyEnum_VALUE
77
+ const val = info.findName(value);
78
+ if (!val) {
79
+ throw new Error(`enum ${info.typeName}: invalid value: "${value}"`);
80
+ }
81
+ return val.no;
82
+ }
83
+ // return the number value
84
+ return value;
85
+ }
package/dist/field.d.ts CHANGED
@@ -28,7 +28,42 @@ import { protoCamelCase } from "./names.js";
28
28
  * - "default": Only proto2: An explicit default value.
29
29
  * - "delimited": Only proto2: Use the tag-delimited group encoding.
30
30
  */
31
- export type FieldInfo = fiRules<fiScalar> | fiRules<fiEnum> | fiRules<fiMessage> | fiRules<fiMap>;
31
+ export type FieldInfo = ScalarFieldInfo | EnumFieldInfo | MessageFieldInfo | MapFieldInfo;
32
+ /**
33
+ * ScalarFieldInfo represents a scalar field.
34
+ * It includes rules for field presence, repetition, and packing.
35
+ */
36
+ export type ScalarFieldInfo = fiRules<fiScalar>;
37
+ /**
38
+ * EnumFieldInfo represents an enum field.
39
+ * It includes rules for field presence, repetition, and packing.
40
+ */
41
+ export type EnumFieldInfo = fiRules<fiEnum>;
42
+ /**
43
+ * MessageFieldInfo represents a message field.
44
+ * It includes rules for field presence, repetition, and packing.
45
+ */
46
+ export type MessageFieldInfo = fiRules<fiMessage>;
47
+ /**
48
+ * MapFieldInfo represents a map field with a scalar key type and
49
+ * a scalar, enum, or message value type.
50
+ * It includes rules for field presence, repetition, and packing.
51
+ */
52
+ export type MapFieldInfo = fiRules<fiMap>;
53
+ /**
54
+ * MapValueInfo represents the value type of a map field.
55
+ * The value can be a scalar, enum, or message type.
56
+ */
57
+ export type MapValueInfo = {
58
+ readonly kind: "scalar";
59
+ readonly T: ScalarType;
60
+ } | {
61
+ readonly kind: "enum";
62
+ readonly T: EnumType;
63
+ } | {
64
+ readonly kind: "message";
65
+ readonly T: MessageType<any> | (() => MessageType<any>);
66
+ };
32
67
  /**
33
68
  * Provides convenient access to field information of a message type.
34
69
  */
@@ -283,16 +318,7 @@ interface fiMap extends fiShared {
283
318
  /**
284
319
  * Map value type. Can be scalar, enum, or message.
285
320
  */
286
- readonly V: {
287
- readonly kind: "scalar";
288
- readonly T: ScalarType;
289
- } | {
290
- readonly kind: "enum";
291
- readonly T: EnumType;
292
- } | {
293
- readonly kind: "message";
294
- readonly T: MessageType<any> | (() => MessageType<any>);
295
- };
321
+ readonly V: MapValueInfo;
296
322
  /**
297
323
  * Is the field repeated? Never true for maps.
298
324
  */
@@ -68,7 +68,7 @@ export const DoubleValue = createMessageType({
68
68
  packedByDefault: true,
69
69
  fieldWrapper: {
70
70
  wrapField(value) {
71
- return DoubleValue.create({ value: value ?? undefined });
71
+ return DoubleValue.createComplete({ value: value ?? undefined });
72
72
  },
73
73
  unwrapField(msg) {
74
74
  return msg.value;
@@ -104,7 +104,7 @@ export const FloatValue = createMessageType({
104
104
  packedByDefault: true,
105
105
  fieldWrapper: {
106
106
  wrapField(value) {
107
- return FloatValue.create({ value: value ?? undefined });
107
+ return FloatValue.createComplete({ value: value ?? undefined });
108
108
  },
109
109
  unwrapField(msg) {
110
110
  return msg.value;
@@ -140,7 +140,7 @@ export const Int64Value = createMessageType({
140
140
  packedByDefault: true,
141
141
  fieldWrapper: {
142
142
  wrapField(value) {
143
- return Int64Value.create({ value: value ?? undefined });
143
+ return Int64Value.createComplete({ value: value ?? undefined });
144
144
  },
145
145
  unwrapField(msg) {
146
146
  return msg.value;
@@ -176,7 +176,7 @@ export const UInt64Value = createMessageType({
176
176
  packedByDefault: true,
177
177
  fieldWrapper: {
178
178
  wrapField(value) {
179
- return UInt64Value.create({ value: value ?? undefined });
179
+ return UInt64Value.createComplete({ value: value ?? undefined });
180
180
  },
181
181
  unwrapField(msg) {
182
182
  return msg.value;
@@ -212,7 +212,7 @@ export const Int32Value = createMessageType({
212
212
  packedByDefault: true,
213
213
  fieldWrapper: {
214
214
  wrapField(value) {
215
- return Int32Value.create({ value: value ?? undefined });
215
+ return Int32Value.createComplete({ value: value ?? undefined });
216
216
  },
217
217
  unwrapField(msg) {
218
218
  return msg.value;
@@ -248,7 +248,7 @@ export const UInt32Value = createMessageType({
248
248
  packedByDefault: true,
249
249
  fieldWrapper: {
250
250
  wrapField(value) {
251
- return UInt32Value.create({ value: value ?? undefined });
251
+ return UInt32Value.createComplete({ value: value ?? undefined });
252
252
  },
253
253
  unwrapField(msg) {
254
254
  return msg.value;
@@ -284,7 +284,7 @@ export const BoolValue = createMessageType({
284
284
  packedByDefault: true,
285
285
  fieldWrapper: {
286
286
  wrapField(value) {
287
- return BoolValue.create({ value: value ?? undefined });
287
+ return BoolValue.createComplete({ value: value ?? undefined });
288
288
  },
289
289
  unwrapField(msg) {
290
290
  return msg.value;
@@ -320,7 +320,7 @@ export const StringValue = createMessageType({
320
320
  packedByDefault: true,
321
321
  fieldWrapper: {
322
322
  wrapField(value) {
323
- return StringValue.create({ value: value ?? undefined });
323
+ return StringValue.createComplete({ value: value ?? undefined });
324
324
  },
325
325
  unwrapField(msg) {
326
326
  return msg.value;
@@ -356,7 +356,7 @@ export const BytesValue = createMessageType({
356
356
  packedByDefault: true,
357
357
  fieldWrapper: {
358
358
  wrapField(value) {
359
- return BytesValue.create({ value: value ?? undefined });
359
+ return BytesValue.createComplete({ value: value ?? undefined });
360
360
  },
361
361
  unwrapField(msg) {
362
362
  return msg.value;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { Message, CompleteMessage, AnyMessage, MessageType, Field, compareMessages, createMessageType, } from "./message.js";
2
2
  export { ServiceType, MethodInfo, MethodInfoUnary, MethodInfoServerStreaming, MethodInfoClientStreaming, MethodInfoBiDiStreaming, MethodKind, MethodIdempotency, } from "./service-type.js";
3
3
  export { isCompleteMessage, isCompleteField } from "./is-message.js";
4
- export { newFieldList, FieldList, PartialFieldInfo, FieldInfo, OneofInfo, fieldJsonName, } from "./field.js";
4
+ export { FieldList, PartialFieldInfo, FieldInfo, ScalarFieldInfo, EnumFieldInfo, MessageFieldInfo, MapFieldInfo, OneofInfo, newFieldList, fieldJsonName, } from "./field.js";
5
5
  export { applyPartialMessage } from "./partial.js";
6
6
  export { scalarEquals, scalarZeroValue, isScalarZeroValue, ScalarType, ScalarValue, LongType, } from "./scalar.js";
7
- export { createEnumType, normalizeEnumValue } from "./enum.js";
7
+ export { EnumType, EnumValueInfo, createEnumType, enumInfoZeroValue, enumZeroValue, enumDescZeroValue, normalizeEnumValue, } from "./enum.js";
8
8
  export { localName, localFieldName, localOneofName, findEnumSharedPrefix, camelToSnakeCase, protoCamelCase, safeObjectProperty, safeIdentifier, } from "./names.js";
9
9
  export { int64FromString, int64ToString, uInt64ToString } from "./varint.js";
10
10
  export { protoInt64 } from "./proto-int64.js";
@@ -15,3 +15,4 @@ export { DescComments, AnyDesc, DescEnum, DescEnumValue, DescExtension, DescFiel
15
15
  export { jsonReadEnum, jsonReadField, jsonReadMapKey, jsonReadScalar, jsonReadMessage, jsonWriteEnum, jsonWriteField, jsonWriteScalar, jsonWriteMessage, jsonDebugValue, JsonValue, JsonObject, JsonReadOptions, jsonMakeReadOptions, JsonWriteOptions, JsonWriteStringOptions, jsonMakeWriteOptions, } from "./json.js";
16
16
  export { binaryReadField, binaryReadMapEntry, binaryReadScalar, binaryReadScalarLTString, binaryReadMessage, binaryWriteField, binaryWriteScalar, binaryWritePacked, binaryWriteMapEntry, binaryWriteMessage, binaryMakeReadOptions, binaryMakeWriteOptions, BinaryReadOptions, BinaryWriteOptions, } from "./binary.js";
17
17
  export type { IMessageTypeRegistry, IServiceTypeRegistry, IEnumTypeRegistry, } from "./type-registry.js";
18
+ export { compareFieldZeroValue, isMessageZeroValue, getFieldZeroValue, } from "./zero-value.js";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  export { compareMessages, createMessageType, } from "./message.js";
2
2
  export { MethodKind, MethodIdempotency, } from "./service-type.js";
3
3
  export { isCompleteMessage, isCompleteField } from "./is-message.js";
4
- export { newFieldList, FieldList, fieldJsonName, } from "./field.js";
4
+ export { FieldList, newFieldList, fieldJsonName, } from "./field.js";
5
5
  export { applyPartialMessage } from "./partial.js";
6
6
  export { scalarEquals, scalarZeroValue, isScalarZeroValue, ScalarType, LongType, } from "./scalar.js";
7
- export { createEnumType, normalizeEnumValue } from "./enum.js";
7
+ export { createEnumType, enumInfoZeroValue, enumZeroValue, enumDescZeroValue, normalizeEnumValue, } from "./enum.js";
8
8
  export { localName, localFieldName, localOneofName, findEnumSharedPrefix, camelToSnakeCase, protoCamelCase, safeObjectProperty, safeIdentifier, } from "./names.js";
9
9
  export { int64FromString, int64ToString, uInt64ToString } from "./varint.js";
10
10
  export { protoInt64 } from "./proto-int64.js";
@@ -13,3 +13,4 @@ export { protoDouble } from "./proto-double.js";
13
13
  export { Timestamp, Duration, Any, Empty, DoubleValue, FloatValue, Int64Value, UInt64Value, Int32Value, UInt32Value, BoolValue, StringValue, BytesValue, Value, NullValue, ListValue, Struct, } from "./google/index.js";
14
14
  export { jsonReadEnum, jsonReadField, jsonReadMapKey, jsonReadScalar, jsonReadMessage, jsonWriteEnum, jsonWriteField, jsonWriteScalar, jsonWriteMessage, jsonDebugValue, jsonMakeReadOptions, jsonMakeWriteOptions, } from "./json.js";
15
15
  export { binaryReadField, binaryReadMapEntry, binaryReadScalar, binaryReadScalarLTString, binaryReadMessage, binaryWriteField, binaryWriteScalar, binaryWritePacked, binaryWriteMapEntry, binaryWriteMessage, binaryMakeReadOptions, binaryMakeWriteOptions, } from "./binary.js";
16
+ export { compareFieldZeroValue, isMessageZeroValue, getFieldZeroValue, } from "./zero-value.js";
package/dist/message.d.ts CHANGED
@@ -38,10 +38,14 @@ export interface MessageType<T extends Message<T> = AnyMessage> {
38
38
  * When used as a field, unwrap this message to a simple value.
39
39
  */
40
40
  readonly fieldWrapper?: FieldWrapper<T>;
41
+ /**
42
+ * Create a new empty instance of this message applying the partial message.
43
+ */
44
+ create(partial?: Message<T>): Message<T>;
41
45
  /**
42
46
  * Create a new instance of this message with zero values for fields.
43
47
  */
44
- create(partial?: Message<T>): CompleteMessage<T>;
48
+ createComplete(partial?: Message<T>): CompleteMessage<T>;
45
49
  /**
46
50
  * Create a deep copy.
47
51
  */
@@ -109,6 +113,6 @@ export declare function createMessageType<T extends Message<T>, E extends Record
109
113
  export declare function compareMessages<T extends Message<T>>(fields: FieldList, a: T | undefined | null, b: T | undefined | null): boolean;
110
114
  export declare function cloneMessage<T extends Message<T>>(message: T, fields: FieldList): T;
111
115
  /**
112
- * createMessage recursively builds a message filled with zero values based on the given FieldList.
116
+ * createCompleteMessage recursively builds a message filled with zero values based on the given FieldList.
113
117
  */
114
- export declare function createMessage<T extends Message<T>>(fields: FieldList): CompleteMessage<T>;
118
+ export declare function createCompleteMessage<T extends Message<T>>(fields: FieldList): CompleteMessage<T>;
package/dist/message.js CHANGED
@@ -30,7 +30,12 @@ export function createMessageType(params, exts) {
30
30
  fields,
31
31
  fieldWrapper,
32
32
  create(partial) {
33
- const message = createMessage(fields);
33
+ const message = Object.create(null);
34
+ applyPartialMessage(partial, message, fields);
35
+ return message;
36
+ },
37
+ createComplete(partial) {
38
+ const message = createCompleteMessage(fields);
34
39
  applyPartialMessage(partial, message, fields);
35
40
  return message;
36
41
  },
@@ -218,9 +223,9 @@ export function cloneMessage(message, fields) {
218
223
  return clone;
219
224
  }
220
225
  /**
221
- * createMessage recursively builds a message filled with zero values based on the given FieldList.
226
+ * createCompleteMessage recursively builds a message filled with zero values based on the given FieldList.
222
227
  */
223
- export function createMessage(fields) {
228
+ export function createCompleteMessage(fields) {
224
229
  const message = {};
225
230
  for (const field of fields.list()) {
226
231
  const fieldKind = field.kind;
@@ -278,7 +283,7 @@ export function createMessage(fields) {
278
283
  message[field.localName] = [];
279
284
  }
280
285
  else {
281
- message[field.localName] = createMessage(messageType.fields);
286
+ message[field.localName] = createCompleteMessage(messageType.fields);
282
287
  }
283
288
  break;
284
289
  case "map":
package/dist/names.d.ts CHANGED
@@ -41,3 +41,5 @@ export declare const safeObjectProperty: (name: string) => string;
41
41
  * Names that can be used for identifiers or class properties
42
42
  */
43
43
  export declare const safeIdentifier: (name: string) => string;
44
+ export declare function checkSanitizeKey(key: string): boolean;
45
+ export declare function throwSanitizeKey(key: string): void;
package/dist/names.js CHANGED
@@ -32,12 +32,7 @@ export function localName(desc) {
32
32
  const pkg = desc.file.proto.package;
33
33
  const offset = pkg === undefined ? 0 : pkg.length + 1;
34
34
  const name = desc.typeName.substring(offset).replace(/\./g, "_");
35
- // For services, we only care about safe identifiers, not safe object properties,
36
- // but we have shipped v1 with a bug that respected object properties, and we
37
- // do not want to introduce a breaking change, so we continue to escape for
38
- // safe object properties.
39
- // See https://github.com/bufbuild/protobuf-es/pull/391
40
- return safeObjectProperty(safeIdentifier(name));
35
+ return safeIdentifier(name);
41
36
  }
42
37
  case "enum_value": {
43
38
  let name = desc.name;
@@ -219,20 +214,7 @@ const reservedObjectProperties = new Set([
219
214
  * Names that cannot be used for object properties because they are reserved
220
215
  * by the runtime.
221
216
  */
222
- const reservedMessageProperties = new Set([
223
- // names reserved by the runtime
224
- "getType",
225
- "clone",
226
- "equals",
227
- "fromBinary",
228
- "fromJson",
229
- "fromJsonString",
230
- "toBinary",
231
- "toJson",
232
- "toJsonString",
233
- // names reserved by the runtime for the future
234
- "toObject",
235
- ]);
217
+ const reservedMessageProperties = new Set(["__proto__"]);
236
218
  const fallback = (name) => `${name}$`;
237
219
  /**
238
220
  * Will wrap names that are Object prototype properties or names reserved
@@ -263,3 +245,14 @@ export const safeIdentifier = (name) => {
263
245
  }
264
246
  return name;
265
247
  };
248
+ export function checkSanitizeKey(key) {
249
+ return typeof key === "string" && key !== "__proto__";
250
+ }
251
+ export function throwSanitizeKey(key) {
252
+ if (typeof key !== "string") {
253
+ throw new Error("illegal non-string object key: " + typeof key);
254
+ }
255
+ if (!checkSanitizeKey(key)) {
256
+ throw new Error("illegal object key: " + key);
257
+ }
258
+ }
package/dist/partial.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- import { FieldList } from "./field.js";
1
+ import { FieldList, MapValueInfo } from "./field.js";
2
2
  import { Message } from "./message.js";
3
3
  export declare function applyPartialMessage<T extends Message<T>>(source: Message<T> | undefined, target: Message<T>, fields: FieldList): void;
4
+ export declare function applyPartialMap(sourceMap: Record<string, any> | undefined, targetMap: Record<string, any>, value: MapValueInfo): void;
package/dist/partial.js CHANGED
@@ -1,99 +1,194 @@
1
+ import { normalizeEnumValue } from "./enum.js";
1
2
  import { resolveMessageType } from "./field.js";
2
- import { ScalarType } from "./scalar.js";
3
- // applyPartialMessage applies a partial message to a message.
3
+ import { createCompleteMessage } from "./message.js";
4
+ import { throwSanitizeKey } from "./names.js";
5
+ import { normalizeScalarValue } from "./scalar.js";
6
+ // applyPartialMessage applies a partial source message to a target message.
4
7
  export function applyPartialMessage(source, target, fields) {
5
- if (source === undefined) {
8
+ if (source == null || target == null) {
6
9
  return;
7
10
  }
11
+ const t = target, s = source;
8
12
  for (const member of fields.byMember()) {
9
- const localName = member.localName, t = target, s = source;
10
- if (s[localName] === undefined) {
13
+ const localName = member.localName;
14
+ throwSanitizeKey(localName);
15
+ if (!(localName in s) || s[localName] === undefined) {
16
+ continue;
17
+ }
18
+ const sourceValue = s[localName];
19
+ if (sourceValue === null) {
20
+ delete t[localName];
11
21
  continue;
12
22
  }
13
23
  switch (member.kind) {
14
24
  case "oneof":
15
- const sk = s[localName].case;
16
- if (sk === undefined) {
17
- continue;
25
+ if (typeof sourceValue !== "object") {
26
+ throw new Error(`field ${localName}: invalid oneof: must be an object with case and value`);
27
+ }
28
+ // sk, sk are the source case and value
29
+ const { case: sk, value: sv } = sourceValue;
30
+ // sourceField is the field set by the source case, if any.
31
+ const sourceField = sk != null ? member.findField(sk) : null;
32
+ // dv is the destination oneof object
33
+ let dv = localName in t ? t[localName] : undefined;
34
+ if (typeof dv !== "object") {
35
+ dv = Object.create(null);
36
+ }
37
+ // check the case is valid and throw if not
38
+ if (sk != null && sourceField == null) {
39
+ throw new Error(`field ${localName}: invalid oneof case: ${sk}`);
40
+ }
41
+ // update the case
42
+ dv.case = sk;
43
+ // if the case was different or null, clear the value.
44
+ if (dv.case !== sk || sk == null) {
45
+ delete dv.value;
18
46
  }
19
- const sourceField = member.findField(sk);
20
- let val = s[localName].value;
21
- if (sourceField?.kind == "message") {
22
- if (val === undefined) {
23
- val = {};
47
+ t[localName] = dv;
48
+ // stop here if there was no valid case selected
49
+ if (!sourceField) {
50
+ break;
51
+ }
52
+ if (sourceField.kind === "message") {
53
+ // apply the partial to the value
54
+ let dest = dv.value;
55
+ if (typeof dest !== "object") {
56
+ dest = dv.value = Object.create(null);
57
+ }
58
+ // skip zero or null value
59
+ if (sv != null) {
60
+ const sourceFieldMt = resolveMessageType(sourceField.T);
61
+ applyPartialMessage(sv, dest, sourceFieldMt.fields);
24
62
  }
25
63
  }
26
- else if (sourceField &&
27
- sourceField.kind === "scalar" &&
28
- sourceField.T === ScalarType.BYTES) {
29
- val = toU8Arr(val);
64
+ else if (sourceField.kind === "scalar") {
65
+ dv.value = normalizeScalarValue(sourceField.T, sv);
66
+ }
67
+ else {
68
+ dv.value = sv;
30
69
  }
31
- t[localName] = { case: sk, value: val };
32
70
  break;
33
71
  case "scalar":
72
+ if (member.repeated) {
73
+ if (!Array.isArray(sourceValue)) {
74
+ throw new Error(`field ${localName}: invalid value: must be array`);
75
+ }
76
+ let dst = localName in t ? t[localName] : null;
77
+ if (dst == null || !Array.isArray(dst)) {
78
+ dst = t[localName] = [];
79
+ }
80
+ dst.push(...sourceValue.map((v) => normalizeScalarValue(member.T, v)));
81
+ break;
82
+ }
83
+ t[localName] = normalizeScalarValue(member.T, sourceValue);
84
+ break;
34
85
  case "enum":
35
- let copy = s[localName];
36
- if (member.T === ScalarType.BYTES) {
37
- copy =
38
- member.repeated ?
39
- copy.map(toU8Arr)
40
- : toU8Arr(copy);
41
- }
42
- t[localName] = copy;
86
+ t[localName] = normalizeEnumValue(member.T, sourceValue);
43
87
  break;
44
88
  case "map":
45
- switch (member.V.kind) {
46
- case "scalar":
47
- case "enum":
48
- if (member.V.T === ScalarType.BYTES) {
49
- for (const [k, v] of Object.entries(s[localName])) {
50
- t[localName][k] = toU8Arr(v);
51
- }
52
- }
53
- else {
54
- Object.assign(t[localName], s[localName]);
55
- }
56
- break;
57
- case "message":
58
- const messageType = resolveMessageType(member.V.T);
59
- for (const k of Object.keys(s[localName])) {
60
- let val = s[localName][k];
61
- if (!messageType.fieldWrapper) {
62
- if (val === undefined) {
63
- val = {};
64
- }
65
- }
66
- t[localName][k] = val;
67
- }
68
- break;
89
+ if (typeof sourceValue !== "object") {
90
+ throw new Error(`field ${member.localName}: invalid value: must be object`);
91
+ }
92
+ let tMap = t[localName];
93
+ if (typeof tMap !== "object") {
94
+ tMap = t[localName] = Object.create(null);
69
95
  }
96
+ applyPartialMap(sourceValue, tMap, member.V);
70
97
  break;
71
98
  case "message":
72
99
  const mt = resolveMessageType(member.T);
73
100
  if (member.repeated) {
74
- t[localName] = s[localName].map((val) => val ?? {});
101
+ // skip null or undefined values
102
+ if (!Array.isArray(sourceValue)) {
103
+ throw new Error(`field ${localName}: invalid value: must be array`);
104
+ }
105
+ let tArr = t[localName];
106
+ if (!Array.isArray(tArr)) {
107
+ tArr = t[localName] = [];
108
+ }
109
+ for (const v of sourceValue) {
110
+ // skip null or undefined values
111
+ if (v != null) {
112
+ if (mt.fieldWrapper) {
113
+ tArr.push(mt.fieldWrapper.unwrapField(mt.fieldWrapper.wrapField(v)));
114
+ }
115
+ else {
116
+ tArr.push(mt.create(v));
117
+ }
118
+ }
119
+ }
120
+ break;
121
+ }
122
+ if (mt.fieldWrapper) {
123
+ t[localName] = mt.fieldWrapper.unwrapField(mt.fieldWrapper.wrapField(sourceValue));
75
124
  }
76
125
  else {
77
- const val = s[localName];
78
- if (mt.fieldWrapper) {
79
- if (
80
- // We can't use BytesValue.typeName as that will create a circular import
81
- mt.typeName === "google.protobuf.BytesValue") {
82
- t[localName] = toU8Arr(val);
83
- }
84
- else {
85
- t[localName] = val;
86
- }
126
+ if (typeof sourceValue !== "object") {
127
+ throw new Error(`field ${member.localName}: invalid value: must be object`);
87
128
  }
88
- else {
89
- t[localName] = val ?? {};
129
+ let destMsg = t[localName];
130
+ if (typeof destMsg !== "object") {
131
+ destMsg = t[localName] = Object.create(null);
90
132
  }
133
+ applyPartialMessage(sourceValue, destMsg, mt.fields);
91
134
  }
92
135
  break;
93
136
  }
94
137
  }
95
138
  }
96
- // converts any ArrayLike<number> to Uint8Array if necessary.
97
- function toU8Arr(input) {
98
- return input instanceof Uint8Array ? input : new Uint8Array(input);
139
+ // applyPartialMap applies a partial source map to a target map.
140
+ export function applyPartialMap(sourceMap, targetMap, value) {
141
+ if (sourceMap == null) {
142
+ return;
143
+ }
144
+ if (typeof sourceMap !== "object") {
145
+ throw new Error(`invalid map: must be object`);
146
+ }
147
+ switch (value.kind) {
148
+ case "scalar":
149
+ for (const [k, v] of Object.entries(sourceMap)) {
150
+ throwSanitizeKey(k);
151
+ if (v !== undefined) {
152
+ targetMap[k] = normalizeScalarValue(value.T, v);
153
+ }
154
+ else {
155
+ delete targetMap[k];
156
+ }
157
+ }
158
+ break;
159
+ case "enum":
160
+ for (const [k, v] of Object.entries(sourceMap)) {
161
+ throwSanitizeKey(k);
162
+ if (v !== undefined) {
163
+ targetMap[k] = normalizeEnumValue(value.T, v);
164
+ }
165
+ else {
166
+ delete targetMap[k];
167
+ }
168
+ }
169
+ break;
170
+ case "message":
171
+ const messageType = resolveMessageType(value.T);
172
+ for (const [k, v] of Object.entries(sourceMap)) {
173
+ throwSanitizeKey(k);
174
+ if (v === undefined) {
175
+ delete targetMap[k];
176
+ continue;
177
+ }
178
+ if (typeof v !== "object") {
179
+ throw new Error(`invalid value: must be object`);
180
+ }
181
+ let val = targetMap[k];
182
+ if (!!messageType.fieldWrapper) {
183
+ // For wrapper type messages, call createCompleteMessage.
184
+ val = targetMap[k] = createCompleteMessage(messageType.fields);
185
+ }
186
+ else if (typeof val !== "object") {
187
+ // Otherwise apply the partial to the existing value, if any.
188
+ val = targetMap[k] = Object.create(null);
189
+ }
190
+ applyPartialMessage(v, val, messageType.fields);
191
+ }
192
+ break;
193
+ }
99
194
  }
@@ -564,7 +564,7 @@ function generateWktFieldWrapper(f, message, ref) {
564
564
  const { typing } = getFieldTypeInfo(ref.value);
565
565
  f.print(" fieldWrapper: {");
566
566
  f.print(" wrapField(value: ", typing, " | null | undefined): ", message, " {");
567
- f.print(" return ", message, ".create({ value: value ?? undefined });");
567
+ f.print(" return ", message, ".createComplete({ value: value ?? undefined });");
568
568
  f.print(" },");
569
569
  f.print(" unwrapField(msg: ", message, "): ", typing, " | null | undefined {");
570
570
  f.print(" return msg.", localName(ref.value), ";");
package/dist/scalar.d.ts CHANGED
@@ -67,3 +67,11 @@ export declare function scalarZeroValue<T extends ScalarType, L extends LongType
67
67
  * optional or repeated.
68
68
  */
69
69
  export declare function isScalarZeroValue(type: ScalarType, value: unknown): boolean;
70
+ /**
71
+ * Returns the normalized version of the scalar value.
72
+ * Zero or null is cast to the zero value.
73
+ * Bytes is cast to a Uint8Array.
74
+ * The BigInt long type is used.
75
+ */
76
+ export declare function normalizeScalarValue<T>(type: ScalarType, value: T | null | undefined, longType?: LongType): T;
77
+ export declare function toU8Arr(input: ArrayLike<number>): Uint8Array;
package/dist/scalar.js CHANGED
@@ -161,3 +161,26 @@ export function isScalarZeroValue(type, value) {
161
161
  return value == 0; // Loose comparison matches 0n, 0 and "0"
162
162
  }
163
163
  }
164
+ /**
165
+ * Returns the normalized version of the scalar value.
166
+ * Zero or null is cast to the zero value.
167
+ * Bytes is cast to a Uint8Array.
168
+ * The BigInt long type is used.
169
+ */
170
+ export function normalizeScalarValue(type, value, longType = LongType.BIGINT) {
171
+ if (value == null) {
172
+ return scalarZeroValue(type, longType);
173
+ }
174
+ if (type === ScalarType.BYTES) {
175
+ return toU8Arr(value);
176
+ }
177
+ if (isScalarZeroValue(type, value)) {
178
+ return scalarZeroValue(type, longType);
179
+ }
180
+ // TODO: enforce correct type for other values as well.
181
+ return value;
182
+ }
183
+ // converts any ArrayLike<number> to Uint8Array if necessary.
184
+ export function toU8Arr(input) {
185
+ return input instanceof Uint8Array ? input : new Uint8Array(input);
186
+ }
package/dist/util.d.ts CHANGED
@@ -15,7 +15,7 @@ export declare function getFieldDefaultValueExpression(field: DescField | DescEx
15
15
  *
16
16
  * Returns either:
17
17
  * - empty array literal for repeated fields
18
- * - empty object literal for maps
18
+ * - Object.create(null) for maps
19
19
  * - undefined for message fields
20
20
  * - an enums first value
21
21
  * - scalar zero value
package/dist/util.js CHANGED
@@ -134,7 +134,7 @@ export function getFieldDefaultValueExpression(field, enumAs = "enum_value_as_is
134
134
  *
135
135
  * Returns either:
136
136
  * - empty array literal for repeated fields
137
- * - empty object literal for maps
137
+ * - Object.create(null) for maps
138
138
  * - undefined for message fields
139
139
  * - an enums first value
140
140
  * - scalar zero value
@@ -147,7 +147,7 @@ export function getFieldZeroValueExpression(field, enumAs = "enum_value_as_is")
147
147
  case "message":
148
148
  return undefined;
149
149
  case "map":
150
- return "{}";
150
+ return "Object.create(null)";
151
151
  case "enum": {
152
152
  // In proto3, the first enum value must be zero.
153
153
  // In proto2, protobuf-go returns the first value as the default.
@@ -0,0 +1,23 @@
1
+ import { DescExtension, DescField } from "./descriptor-set.js";
2
+ /**
3
+ * Returns the zero value for a field.
4
+ *
5
+ * Returns either:
6
+ * - empty array literal for repeated fields
7
+ * - Object.create(null) for maps
8
+ * - undefined for message fields
9
+ * - an enums first value
10
+ * - scalar zero value
11
+ */
12
+ export declare function getFieldZeroValue(field: DescField | DescExtension): any;
13
+ /**
14
+ * Returns true for a zero-value (all fields are zero).
15
+ *
16
+ * In proto3, zero-values are not written to the wire, unless the field is
17
+ * optional or repeated.
18
+ */
19
+ export declare function isMessageZeroValue<T extends Record<string, any>>(value: T | null | undefined, fields: (DescField | DescExtension)[]): boolean;
20
+ /**
21
+ * Compares if the value is considered a zero value for the field.
22
+ */
23
+ export declare function compareFieldZeroValue<T>(field: DescField | DescExtension, value: T | null | undefined): boolean;
@@ -0,0 +1,88 @@
1
+ import { enumDescZeroValue } from "./enum.js";
2
+ import { localName } from "./names.js";
3
+ import { scalarZeroValue } from "./scalar.js";
4
+ /**
5
+ * Returns the zero value for a field.
6
+ *
7
+ * Returns either:
8
+ * - empty array literal for repeated fields
9
+ * - Object.create(null) for maps
10
+ * - undefined for message fields
11
+ * - an enums first value
12
+ * - scalar zero value
13
+ */
14
+ export function getFieldZeroValue(field) {
15
+ if (field.repeated) {
16
+ return [];
17
+ }
18
+ switch (field.fieldKind) {
19
+ case "message":
20
+ return undefined;
21
+ case "map":
22
+ return Object.create(null);
23
+ case "enum": {
24
+ return enumDescZeroValue(field.enum);
25
+ }
26
+ case "scalar": {
27
+ return scalarZeroValue(field.scalar, field.longType);
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * Returns true for a zero-value (all fields are zero).
33
+ *
34
+ * In proto3, zero-values are not written to the wire, unless the field is
35
+ * optional or repeated.
36
+ */
37
+ export function isMessageZeroValue(value, fields) {
38
+ if (value == null) {
39
+ return true;
40
+ }
41
+ if (typeof value !== "object") {
42
+ return false;
43
+ }
44
+ for (const field of fields) {
45
+ const fieldLocalName = localName(field);
46
+ if (fieldLocalName in value &&
47
+ !compareFieldZeroValue(field, value[fieldLocalName])) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+ /**
54
+ * Compares if the value is considered a zero value for the field.
55
+ */
56
+ export function compareFieldZeroValue(field, value) {
57
+ if (value == null || value == 0) {
58
+ return true;
59
+ }
60
+ if (field.repeated) {
61
+ if (!Array.isArray(value) || typeof value.length !== "number") {
62
+ throw new Error("invalid repeated field: must be an array");
63
+ }
64
+ return value.length === 0;
65
+ }
66
+ switch (field.fieldKind) {
67
+ case "message":
68
+ if (typeof value !== "object") {
69
+ throw new Error("invalid message: must be an object");
70
+ }
71
+ // We need to check if all the fields are empty.
72
+ return isMessageZeroValue(value, field.message.fields);
73
+ case "map":
74
+ return Object.create(null);
75
+ case "enum": {
76
+ // In proto3, the first enum value must be zero.
77
+ // In proto2, protobuf-go returns the first value as the default.
78
+ if (field.enum.values.length < 1) {
79
+ throw new Error("invalid enum: missing at least one value");
80
+ }
81
+ const zeroValue = field.enum.values[0];
82
+ return zeroValue.number === value;
83
+ }
84
+ case "scalar": {
85
+ return scalarZeroValue(field.scalar, field.longType) === value;
86
+ }
87
+ }
88
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aptre/protobuf-es-lite",
3
3
  "description": "Lightweight Protobuf codegen for TypeScript and JavaScript.",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "url": "git+ssh://git@github.com/aperturerobotics/protobuf-es-lite.git"