rclnodejs 1.6.0 → 1.8.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/binding.gyp +2 -0
- package/index.js +152 -0
- package/lib/action/client.js +109 -10
- package/lib/action/deferred.js +8 -2
- package/lib/action/server.js +10 -1
- package/lib/action/uuid.js +4 -1
- package/lib/client.js +218 -4
- package/lib/clock.js +182 -1
- package/lib/clock_change.js +49 -0
- package/lib/clock_event.js +88 -0
- package/lib/context.js +12 -2
- package/lib/duration.js +37 -12
- package/lib/errors.js +621 -0
- package/lib/event_handler.js +21 -4
- package/lib/interface_loader.js +52 -12
- package/lib/lifecycle.js +8 -2
- package/lib/logging.js +90 -3
- package/lib/message_introspector.js +123 -0
- package/lib/message_serialization.js +10 -2
- package/lib/message_validation.js +512 -0
- package/lib/native_loader.js +9 -4
- package/lib/node.js +403 -50
- package/lib/node_options.js +40 -1
- package/lib/observable_subscription.js +105 -0
- package/lib/parameter.js +172 -35
- package/lib/parameter_client.js +506 -0
- package/lib/parameter_watcher.js +309 -0
- package/lib/publisher.js +56 -1
- package/lib/qos.js +79 -5
- package/lib/rate.js +6 -1
- package/lib/serialization.js +7 -2
- package/lib/subscription.js +8 -0
- package/lib/time.js +136 -21
- package/lib/time_source.js +13 -4
- package/lib/timer.js +42 -0
- package/lib/utils.js +27 -1
- package/lib/validator.js +74 -19
- package/package.json +4 -2
- package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
- package/rosidl_gen/message_translator.js +0 -61
- package/scripts/config.js +1 -0
- package/src/addon.cpp +2 -0
- package/src/clock_event.cpp +268 -0
- package/src/clock_event.hpp +62 -0
- package/src/macros.h +2 -4
- package/src/rcl_action_server_bindings.cpp +21 -3
- package/src/rcl_bindings.cpp +59 -0
- package/src/rcl_context_bindings.cpp +5 -0
- package/src/rcl_graph_bindings.cpp +73 -0
- package/src/rcl_logging_bindings.cpp +158 -0
- package/src/rcl_node_bindings.cpp +14 -2
- package/src/rcl_publisher_bindings.cpp +12 -0
- package/src/rcl_service_bindings.cpp +7 -6
- package/src/rcl_subscription_bindings.cpp +51 -14
- package/src/rcl_time_point_bindings.cpp +135 -0
- package/src/rcl_timer_bindings.cpp +140 -0
- package/src/rcl_utilities.cpp +103 -2
- package/src/rcl_utilities.h +7 -1
- package/types/action_client.d.ts +27 -2
- package/types/base.d.ts +6 -0
- package/types/client.d.ts +65 -1
- package/types/clock.d.ts +86 -0
- package/types/clock_change.d.ts +27 -0
- package/types/clock_event.d.ts +51 -0
- package/types/errors.d.ts +496 -0
- package/types/index.d.ts +10 -0
- package/types/logging.d.ts +32 -0
- package/types/message_introspector.d.ts +75 -0
- package/types/message_validation.d.ts +183 -0
- package/types/node.d.ts +107 -0
- package/types/node_options.d.ts +13 -0
- package/types/observable_subscription.d.ts +39 -0
- package/types/parameter_client.d.ts +252 -0
- package/types/parameter_watcher.d.ts +104 -0
- package/types/publisher.d.ts +28 -1
- package/types/qos.d.ts +18 -0
- package/types/subscription.d.ts +6 -0
- package/types/timer.d.ts +18 -0
- package/types/validator.d.ts +86 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { MessageValidationError, TypeValidationError } = require('./errors.js');
|
|
18
|
+
const interfaceLoader = require('./interface_loader.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validation issue problem types
|
|
22
|
+
* @enum {string}
|
|
23
|
+
*/
|
|
24
|
+
const ValidationProblem = {
|
|
25
|
+
/** Field exists in object but not in message schema */
|
|
26
|
+
UNKNOWN_FIELD: 'UNKNOWN_FIELD',
|
|
27
|
+
/** Field type doesn't match expected type */
|
|
28
|
+
TYPE_MISMATCH: 'TYPE_MISMATCH',
|
|
29
|
+
/** Required field is missing */
|
|
30
|
+
MISSING_FIELD: 'MISSING_FIELD',
|
|
31
|
+
/** Array length constraint violated */
|
|
32
|
+
ARRAY_LENGTH: 'ARRAY_LENGTH',
|
|
33
|
+
/** Value is out of valid range */
|
|
34
|
+
OUT_OF_RANGE: 'OUT_OF_RANGE',
|
|
35
|
+
/** Nested message validation failed */
|
|
36
|
+
NESTED_ERROR: 'NESTED_ERROR',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Map ROS primitive types to JavaScript types
|
|
41
|
+
*/
|
|
42
|
+
const PRIMITIVE_TYPE_MAP = {
|
|
43
|
+
bool: 'boolean',
|
|
44
|
+
int8: 'number',
|
|
45
|
+
uint8: 'number',
|
|
46
|
+
int16: 'number',
|
|
47
|
+
uint16: 'number',
|
|
48
|
+
int32: 'number',
|
|
49
|
+
uint32: 'number',
|
|
50
|
+
int64: 'bigint',
|
|
51
|
+
uint64: 'bigint',
|
|
52
|
+
float32: 'number',
|
|
53
|
+
float64: 'number',
|
|
54
|
+
char: 'number',
|
|
55
|
+
byte: 'number',
|
|
56
|
+
string: 'string',
|
|
57
|
+
wstring: 'string',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if value is a TypedArray
|
|
62
|
+
* @param {any} value - Value to check
|
|
63
|
+
* @returns {boolean} True if TypedArray
|
|
64
|
+
*/
|
|
65
|
+
function isTypedArray(value) {
|
|
66
|
+
return ArrayBuffer.isView(value) && !(value instanceof DataView);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the JavaScript type string for a value
|
|
71
|
+
* @param {any} value - Value to get type of
|
|
72
|
+
* @returns {string} Type description
|
|
73
|
+
*/
|
|
74
|
+
function getValueType(value) {
|
|
75
|
+
if (value === null) return 'null';
|
|
76
|
+
if (value === undefined) return 'undefined';
|
|
77
|
+
if (Array.isArray(value)) return 'array';
|
|
78
|
+
if (isTypedArray(value)) return 'TypedArray';
|
|
79
|
+
return typeof value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a type class from various input formats
|
|
84
|
+
* @param {string|object|function} typeClass - Type identifier
|
|
85
|
+
* @returns {function|null} The resolved type class or null
|
|
86
|
+
*/
|
|
87
|
+
function resolveTypeClass(typeClass) {
|
|
88
|
+
if (typeof typeClass === 'function') {
|
|
89
|
+
return typeClass;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return interfaceLoader.loadInterface(typeClass);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get message type string from type class
|
|
101
|
+
* @param {function} typeClass - Message type class
|
|
102
|
+
* @returns {string} Message type string (e.g., 'std_msgs/msg/String')
|
|
103
|
+
*/
|
|
104
|
+
function getMessageTypeString(typeClass) {
|
|
105
|
+
if (typeof typeClass.type === 'function') {
|
|
106
|
+
const t = typeClass.type();
|
|
107
|
+
return `${t.pkgName}/${t.subFolder}/${t.interfaceName}`;
|
|
108
|
+
}
|
|
109
|
+
return 'unknown';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the schema definition for a message type
|
|
114
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
115
|
+
* @returns {object|null} Schema definition with fields and constants, or null if not found
|
|
116
|
+
* @example
|
|
117
|
+
* const schema = getMessageSchema(StringClass);
|
|
118
|
+
* // Returns: {
|
|
119
|
+
* // fields: [{name: 'data', type: {type: 'string', isPrimitiveType: true, ...}}],
|
|
120
|
+
* // constants: [],
|
|
121
|
+
* // messageType: 'std_msgs/msg/String'
|
|
122
|
+
* // }
|
|
123
|
+
*/
|
|
124
|
+
function getMessageSchema(typeClass) {
|
|
125
|
+
const resolved = resolveTypeClass(typeClass);
|
|
126
|
+
if (!resolved || !resolved.ROSMessageDef) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const def = resolved.ROSMessageDef;
|
|
131
|
+
return {
|
|
132
|
+
fields: def.fields || [],
|
|
133
|
+
constants: def.constants || [],
|
|
134
|
+
messageType: getMessageTypeString(resolved),
|
|
135
|
+
baseType: def.baseType,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get field names for a message type
|
|
141
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
142
|
+
* @returns {string[]} Array of field names
|
|
143
|
+
*/
|
|
144
|
+
function getFieldNames(typeClass) {
|
|
145
|
+
const schema = getMessageSchema(typeClass);
|
|
146
|
+
if (!schema) return [];
|
|
147
|
+
return schema.fields.map((f) => f.name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get type information for a specific field
|
|
152
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
153
|
+
* @param {string} fieldName - Name of the field
|
|
154
|
+
* @returns {object|null} Field type information or null if not found
|
|
155
|
+
*/
|
|
156
|
+
function getFieldType(typeClass, fieldName) {
|
|
157
|
+
const schema = getMessageSchema(typeClass);
|
|
158
|
+
if (!schema) return null;
|
|
159
|
+
|
|
160
|
+
const field = schema.fields.find((f) => f.name === fieldName);
|
|
161
|
+
return field ? field.type : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate a primitive value against its expected type
|
|
166
|
+
* @param {any} value - Value to validate
|
|
167
|
+
* @param {object} fieldType - Field type definition
|
|
168
|
+
* @returns {object|null} Validation issue or null if valid
|
|
169
|
+
*/
|
|
170
|
+
function validatePrimitiveValue(value, fieldType) {
|
|
171
|
+
const expectedJsType = PRIMITIVE_TYPE_MAP[fieldType.type];
|
|
172
|
+
const actualType = typeof value;
|
|
173
|
+
|
|
174
|
+
if (!expectedJsType) {
|
|
175
|
+
return null; // Unknown primitive type, skip validation
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Allow number for bigint fields (will be converted)
|
|
179
|
+
if (expectedJsType === 'bigint' && actualType === 'number') {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (actualType !== expectedJsType) {
|
|
184
|
+
return {
|
|
185
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
186
|
+
expected: expectedJsType,
|
|
187
|
+
received: actualType,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validate array constraints
|
|
196
|
+
* @param {any} value - Array value to validate
|
|
197
|
+
* @param {object} fieldType - Field type definition
|
|
198
|
+
* @returns {object|null} Validation issue or null if valid
|
|
199
|
+
*/
|
|
200
|
+
function validateArrayConstraints(value, fieldType) {
|
|
201
|
+
if (!Array.isArray(value) && !isTypedArray(value)) {
|
|
202
|
+
return {
|
|
203
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
204
|
+
expected: 'array',
|
|
205
|
+
received: getValueType(value),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const length = value.length;
|
|
210
|
+
|
|
211
|
+
// Fixed size array
|
|
212
|
+
if (fieldType.isFixedSizeArray && length !== fieldType.arraySize) {
|
|
213
|
+
return {
|
|
214
|
+
problem: ValidationProblem.ARRAY_LENGTH,
|
|
215
|
+
expected: `exactly ${fieldType.arraySize} elements`,
|
|
216
|
+
received: `${length} elements`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Upper bound array
|
|
221
|
+
if (fieldType.isUpperBound && length > fieldType.arraySize) {
|
|
222
|
+
return {
|
|
223
|
+
problem: ValidationProblem.ARRAY_LENGTH,
|
|
224
|
+
expected: `at most ${fieldType.arraySize} elements`,
|
|
225
|
+
received: `${length} elements`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Validate a message object against its schema
|
|
234
|
+
* @param {object} obj - Plain object to validate
|
|
235
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
236
|
+
* @param {object} [options] - Validation options
|
|
237
|
+
* @param {boolean} [options.strict=false] - If true, unknown fields cause validation failure
|
|
238
|
+
* @param {boolean} [options.checkTypes=true] - If true, validate field types
|
|
239
|
+
* @param {boolean} [options.checkRequired=false] - If true, check for missing fields
|
|
240
|
+
* @param {string} [options.path=''] - Current path for nested validation (internal use)
|
|
241
|
+
* @returns {{valid: boolean, issues: Array<object>}} Validation result
|
|
242
|
+
*/
|
|
243
|
+
function validateMessage(obj, typeClass, options = {}) {
|
|
244
|
+
const {
|
|
245
|
+
strict = false,
|
|
246
|
+
checkTypes = true,
|
|
247
|
+
checkRequired = false,
|
|
248
|
+
path = '',
|
|
249
|
+
} = options;
|
|
250
|
+
|
|
251
|
+
const issues = [];
|
|
252
|
+
const resolved = resolveTypeClass(typeClass);
|
|
253
|
+
|
|
254
|
+
if (!resolved) {
|
|
255
|
+
issues.push({
|
|
256
|
+
field: path || '(root)',
|
|
257
|
+
problem: 'INVALID_TYPE_CLASS',
|
|
258
|
+
expected: 'valid message type class',
|
|
259
|
+
received: typeof typeClass,
|
|
260
|
+
});
|
|
261
|
+
return { valid: false, issues };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const schema = getMessageSchema(resolved);
|
|
265
|
+
if (!schema) {
|
|
266
|
+
issues.push({
|
|
267
|
+
field: path || '(root)',
|
|
268
|
+
problem: 'NO_SCHEMA',
|
|
269
|
+
expected: 'message with ROSMessageDef',
|
|
270
|
+
received: 'class without schema',
|
|
271
|
+
});
|
|
272
|
+
return { valid: false, issues };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (obj === null || obj === undefined) {
|
|
276
|
+
issues.push({
|
|
277
|
+
field: path || '(root)',
|
|
278
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
279
|
+
expected: 'object',
|
|
280
|
+
received: String(obj),
|
|
281
|
+
});
|
|
282
|
+
return { valid: false, issues };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const type = typeof obj;
|
|
286
|
+
if (
|
|
287
|
+
type === 'string' ||
|
|
288
|
+
type === 'number' ||
|
|
289
|
+
type === 'boolean' ||
|
|
290
|
+
type === 'bigint'
|
|
291
|
+
) {
|
|
292
|
+
if (schema.fields.length === 1 && schema.fields[0].name === 'data') {
|
|
293
|
+
const fieldType = schema.fields[0].type;
|
|
294
|
+
if (checkTypes && fieldType.isPrimitiveType) {
|
|
295
|
+
const typeIssue = validatePrimitiveValue(obj, fieldType);
|
|
296
|
+
if (typeIssue) {
|
|
297
|
+
issues.push({
|
|
298
|
+
field: path ? `${path}.data` : 'data',
|
|
299
|
+
...typeIssue,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { valid: issues.length === 0, issues };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (type !== 'object') {
|
|
308
|
+
issues.push({
|
|
309
|
+
field: path || '(root)',
|
|
310
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
311
|
+
expected: 'object',
|
|
312
|
+
received: type,
|
|
313
|
+
});
|
|
314
|
+
return { valid: false, issues };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const fieldNames = new Set(schema.fields.map((f) => f.name));
|
|
318
|
+
const objKeys = Object.keys(obj);
|
|
319
|
+
|
|
320
|
+
if (strict) {
|
|
321
|
+
for (const key of objKeys) {
|
|
322
|
+
if (!fieldNames.has(key)) {
|
|
323
|
+
issues.push({
|
|
324
|
+
field: path ? `${path}.${key}` : key,
|
|
325
|
+
problem: ValidationProblem.UNKNOWN_FIELD,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const field of schema.fields) {
|
|
332
|
+
const fieldPath = path ? `${path}.${field.name}` : field.name;
|
|
333
|
+
const value = obj[field.name];
|
|
334
|
+
const fieldType = field.type;
|
|
335
|
+
|
|
336
|
+
if (field.name.startsWith('_')) continue;
|
|
337
|
+
|
|
338
|
+
if (value === undefined) {
|
|
339
|
+
if (checkRequired) {
|
|
340
|
+
issues.push({
|
|
341
|
+
field: fieldPath,
|
|
342
|
+
problem: ValidationProblem.MISSING_FIELD,
|
|
343
|
+
expected: fieldType.type,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (fieldType.isArray) {
|
|
350
|
+
const arrayIssue = validateArrayConstraints(value, fieldType);
|
|
351
|
+
if (arrayIssue) {
|
|
352
|
+
issues.push({ field: fieldPath, ...arrayIssue });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (checkTypes && Array.isArray(value) && value.length > 0) {
|
|
357
|
+
if (fieldType.isPrimitiveType) {
|
|
358
|
+
for (let i = 0; i < value.length; i++) {
|
|
359
|
+
const elemIssue = validatePrimitiveValue(value[i], fieldType);
|
|
360
|
+
if (elemIssue) {
|
|
361
|
+
issues.push({
|
|
362
|
+
field: `${fieldPath}[${i}]`,
|
|
363
|
+
...elemIssue,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
for (let i = 0; i < value.length; i++) {
|
|
369
|
+
const nestedResult = validateMessage(
|
|
370
|
+
value[i],
|
|
371
|
+
getNestedTypeClass(resolved, field.name),
|
|
372
|
+
{
|
|
373
|
+
strict,
|
|
374
|
+
checkTypes,
|
|
375
|
+
checkRequired,
|
|
376
|
+
path: `${fieldPath}[${i}]`,
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
if (!nestedResult.valid) {
|
|
380
|
+
issues.push(...nestedResult.issues);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} else if (fieldType.isPrimitiveType) {
|
|
386
|
+
if (checkTypes) {
|
|
387
|
+
const typeIssue = validatePrimitiveValue(value, fieldType);
|
|
388
|
+
if (typeIssue) {
|
|
389
|
+
issues.push({ field: fieldPath, ...typeIssue });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
if (value !== null && typeof value === 'object') {
|
|
394
|
+
const nestedTypeClass = getNestedTypeClass(resolved, field.name);
|
|
395
|
+
if (nestedTypeClass) {
|
|
396
|
+
const nestedResult = validateMessage(value, nestedTypeClass, {
|
|
397
|
+
strict,
|
|
398
|
+
checkTypes,
|
|
399
|
+
checkRequired,
|
|
400
|
+
path: fieldPath,
|
|
401
|
+
});
|
|
402
|
+
if (!nestedResult.valid) {
|
|
403
|
+
issues.push(...nestedResult.issues);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else if (checkTypes && value !== null) {
|
|
407
|
+
issues.push({
|
|
408
|
+
field: fieldPath,
|
|
409
|
+
problem: ValidationProblem.TYPE_MISMATCH,
|
|
410
|
+
expected: 'object',
|
|
411
|
+
received: getValueType(value),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { valid: issues.length === 0, issues };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get the type class for a nested field
|
|
422
|
+
* @param {function} parentTypeClass - Parent message type class
|
|
423
|
+
* @param {string} fieldName - Field name
|
|
424
|
+
* @returns {function|null} Nested type class or null
|
|
425
|
+
*/
|
|
426
|
+
function getNestedTypeClass(parentTypeClass, fieldName) {
|
|
427
|
+
try {
|
|
428
|
+
const instance = new parentTypeClass();
|
|
429
|
+
const fieldValue = instance[fieldName];
|
|
430
|
+
|
|
431
|
+
if (
|
|
432
|
+
fieldValue &&
|
|
433
|
+
fieldValue.constructor &&
|
|
434
|
+
fieldValue.constructor.ROSMessageDef
|
|
435
|
+
) {
|
|
436
|
+
return fieldValue.constructor;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
fieldValue &&
|
|
441
|
+
fieldValue.classType &&
|
|
442
|
+
fieldValue.classType.elementType
|
|
443
|
+
) {
|
|
444
|
+
return fieldValue.classType.elementType;
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
const schema = getMessageSchema(parentTypeClass);
|
|
448
|
+
if (schema) {
|
|
449
|
+
const field = schema.fields.find((f) => f.name === fieldName);
|
|
450
|
+
if (field && !field.type.isPrimitiveType) {
|
|
451
|
+
const typeName = `${field.type.pkgName}/msg/${field.type.type}`;
|
|
452
|
+
return resolveTypeClass(typeName);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Validate a message and throw if invalid
|
|
461
|
+
* @param {object} obj - Plain object to validate
|
|
462
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
463
|
+
* @param {object} [options] - Validation options (same as validateMessage)
|
|
464
|
+
* @throws {MessageValidationError} If validation fails
|
|
465
|
+
* @returns {void}
|
|
466
|
+
*/
|
|
467
|
+
function assertValidMessage(obj, typeClass, options = {}) {
|
|
468
|
+
const result = validateMessage(obj, typeClass, options);
|
|
469
|
+
|
|
470
|
+
if (!result.valid) {
|
|
471
|
+
const resolved = resolveTypeClass(typeClass);
|
|
472
|
+
const messageType = resolved
|
|
473
|
+
? getMessageTypeString(resolved)
|
|
474
|
+
: String(typeClass);
|
|
475
|
+
throw new MessageValidationError(messageType, result.issues);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create a validator function for a specific message type
|
|
481
|
+
* @param {function|string|object} typeClass - Message type class or identifier
|
|
482
|
+
* @param {object} [defaultOptions] - Default validation options
|
|
483
|
+
* @returns {function} Validator function that takes (obj, options?) and returns validation result
|
|
484
|
+
*/
|
|
485
|
+
function createMessageValidator(typeClass, defaultOptions = {}) {
|
|
486
|
+
const resolved = resolveTypeClass(typeClass);
|
|
487
|
+
if (!resolved) {
|
|
488
|
+
throw new TypeValidationError(
|
|
489
|
+
'typeClass',
|
|
490
|
+
typeClass,
|
|
491
|
+
'valid message type class'
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return function validator(obj, options = {}) {
|
|
496
|
+
return validateMessage(obj, resolved, {
|
|
497
|
+
...defaultOptions,
|
|
498
|
+
...options,
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = {
|
|
504
|
+
ValidationProblem,
|
|
505
|
+
getMessageSchema,
|
|
506
|
+
getFieldNames,
|
|
507
|
+
getFieldType,
|
|
508
|
+
validateMessage,
|
|
509
|
+
assertValidMessage,
|
|
510
|
+
createMessageValidator,
|
|
511
|
+
getMessageTypeString,
|
|
512
|
+
};
|
package/lib/native_loader.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { execSync } = require('child_process');
|
|
20
|
+
const { NativeError } = require('./errors.js');
|
|
20
21
|
const bindings = require('bindings');
|
|
21
22
|
const debug = require('debug')('rclnodejs');
|
|
22
23
|
const { detectUbuntuCodename } = require('./utils');
|
|
@@ -100,8 +101,10 @@ function loadNativeAddon() {
|
|
|
100
101
|
return nativeModule;
|
|
101
102
|
} catch (compileError) {
|
|
102
103
|
debug('Forced compilation failed:', compileError.message);
|
|
103
|
-
throw new
|
|
104
|
-
`Failed to force build rclnodejs from source: ${compileError.message}
|
|
104
|
+
throw new NativeError(
|
|
105
|
+
`Failed to force build rclnodejs from source: ${compileError.message}`,
|
|
106
|
+
'Forced compilation',
|
|
107
|
+
{ cause: compileError }
|
|
105
108
|
);
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -163,8 +166,10 @@ function loadNativeAddon() {
|
|
|
163
166
|
return nativeModule;
|
|
164
167
|
} catch (compileError) {
|
|
165
168
|
debug('Compilation failed:', compileError.message);
|
|
166
|
-
throw new
|
|
167
|
-
`Failed to build rclnodejs from source: ${compileError.message}
|
|
169
|
+
throw new NativeError(
|
|
170
|
+
`Failed to build rclnodejs from source: ${compileError.message}`,
|
|
171
|
+
'Compilation',
|
|
172
|
+
{ cause: compileError }
|
|
168
173
|
);
|
|
169
174
|
}
|
|
170
175
|
}
|