@aptre/protobuf-es-lite 0.3.0 → 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/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
  }
@@ -146,6 +146,7 @@ function generateMessage(schema, f, message) {
146
146
  f.print("});");
147
147
  }
148
148
  else {
149
+ generateWktFieldWrapper(f, message, reWkt);
149
150
  f.print("}, ", message, "_Wkt);");
150
151
  }
151
152
  f.print();
@@ -549,3 +550,27 @@ function generateWktMethods(schema, f, message, ref) {
549
550
  break;
550
551
  }
551
552
  }
553
+ function generateWktFieldWrapper(f, message, ref) {
554
+ switch (ref?.typeName) {
555
+ case "google.protobuf.DoubleValue":
556
+ case "google.protobuf.FloatValue":
557
+ case "google.protobuf.Int64Value":
558
+ case "google.protobuf.UInt64Value":
559
+ case "google.protobuf.Int32Value":
560
+ case "google.protobuf.UInt32Value":
561
+ case "google.protobuf.BoolValue":
562
+ case "google.protobuf.StringValue":
563
+ case "google.protobuf.BytesValue": {
564
+ const { typing } = getFieldTypeInfo(ref.value);
565
+ f.print(" fieldWrapper: {");
566
+ f.print(" wrapField(value: ", typing, " | null | undefined): ", message, " {");
567
+ f.print(" return ", message, ".createComplete({ value: value ?? undefined });");
568
+ f.print(" },");
569
+ f.print(" unwrapField(msg: ", message, "): ", typing, " | null | undefined {");
570
+ f.print(" return msg.", localName(ref.value), ";");
571
+ f.print(" }");
572
+ f.print(" } as const,");
573
+ break;
574
+ }
575
+ }
576
+ }
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
+ }
@@ -3,7 +3,13 @@
3
3
  /* eslint-disable */
4
4
 
5
5
  import type { MessageType, PartialFieldInfo } from "@aptre/protobuf-es-lite";
6
- import { createEnumType, createMessageType, Message, ScalarType, Timestamp } from "@aptre/protobuf-es-lite";
6
+ import {
7
+ createEnumType,
8
+ createMessageType,
9
+ Message,
10
+ ScalarType,
11
+ Timestamp,
12
+ } from "@aptre/protobuf-es-lite";
7
13
 
8
14
  export const protobufPackage = "example";
9
15
 
@@ -58,35 +64,54 @@ export type EchoMsg = Message<{
58
64
  /**
59
65
  * @generated from oneof example.EchoMsg.demo
60
66
  */
61
- demo?: {
62
- value?: undefined,
63
- case: undefined
64
- } | {
65
- /**
66
- * @generated from field: example.ExampleEnum example_enum = 3;
67
- */
68
- value: ExampleEnum;
69
- case: "exampleEnum";
70
- } | {
71
- /**
72
- * @generated from field: string example_string = 4;
73
- */
74
- value: string;
75
- case: "exampleString";
76
- };
77
-
67
+ demo?:
68
+ | {
69
+ value?: undefined;
70
+ case: undefined;
71
+ }
72
+ | {
73
+ /**
74
+ * @generated from field: example.ExampleEnum example_enum = 3;
75
+ */
76
+ value: ExampleEnum;
77
+ case: "exampleEnum";
78
+ }
79
+ | {
80
+ /**
81
+ * @generated from field: string example_string = 4;
82
+ */
83
+ value: string;
84
+ case: "exampleString";
85
+ };
78
86
  }>;
79
87
 
80
88
  // EchoMsg contains the message type declaration for EchoMsg.
81
89
  export const EchoMsg: MessageType<EchoMsg> = createMessageType({
82
- typeName: "example.EchoMsg",
83
- fields: [
84
- { no: 1, name: "body", kind: "scalar", T: ScalarType.STRING },
85
- { no: 2, name: "ts", kind: "message", T: () => Timestamp },
86
- { no: 3, name: "example_enum", kind: "enum", T: ExampleEnum_Enum, oneof: "demo" },
87
- { no: 4, name: "example_string", kind: "scalar", T: ScalarType.STRING, oneof: "demo" },
88
- { no: 5, name: "timestamps", kind: "message", T: () => Timestamp, repeated: true },
89
- ] as readonly PartialFieldInfo[],
90
- packedByDefault: true,
90
+ typeName: "example.EchoMsg",
91
+ fields: [
92
+ { no: 1, name: "body", kind: "scalar", T: ScalarType.STRING },
93
+ { no: 2, name: "ts", kind: "message", T: () => Timestamp },
94
+ {
95
+ no: 3,
96
+ name: "example_enum",
97
+ kind: "enum",
98
+ T: ExampleEnum_Enum,
99
+ oneof: "demo",
100
+ },
101
+ {
102
+ no: 4,
103
+ name: "example_string",
104
+ kind: "scalar",
105
+ T: ScalarType.STRING,
106
+ oneof: "demo",
107
+ },
108
+ {
109
+ no: 5,
110
+ name: "timestamps",
111
+ kind: "message",
112
+ T: () => Timestamp,
113
+ repeated: true,
114
+ },
115
+ ] as readonly PartialFieldInfo[],
116
+ packedByDefault: true,
91
117
  });
92
-
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.0",
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"