@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.0.0-beta.3

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.
Files changed (45) hide show
  1. package/README.md +42 -0
  2. package/dist/function/GlobalToolFunction.d.ts +1 -0
  3. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  4. package/dist/function/GlobalToolFunction.js +8 -0
  5. package/dist/function/GlobalToolFunction.js.map +1 -1
  6. package/dist/function/GlobalToolFunction.test.js +3 -0
  7. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  8. package/dist/function/ToolFunction.d.ts +7 -1
  9. package/dist/function/ToolFunction.d.ts.map +1 -1
  10. package/dist/function/ToolFunction.js +14 -1
  11. package/dist/function/ToolFunction.js.map +1 -1
  12. package/dist/function/ToolFunction.test.js +3 -0
  13. package/dist/function/ToolFunction.test.js.map +1 -1
  14. package/dist/logging/ToolLogger.d.ts +34 -0
  15. package/dist/logging/ToolLogger.d.ts.map +1 -0
  16. package/dist/logging/ToolLogger.js +153 -0
  17. package/dist/logging/ToolLogger.js.map +1 -0
  18. package/dist/logging/ToolLogger.test.d.ts +2 -0
  19. package/dist/logging/ToolLogger.test.d.ts.map +1 -0
  20. package/dist/logging/ToolLogger.test.js +646 -0
  21. package/dist/logging/ToolLogger.test.js.map +1 -0
  22. package/dist/service/Service.d.ts.map +1 -1
  23. package/dist/service/Service.js +17 -0
  24. package/dist/service/Service.js.map +1 -1
  25. package/dist/service/Service.test.js +114 -6
  26. package/dist/service/Service.test.js.map +1 -1
  27. package/dist/validation/ParameterValidator.d.ts +42 -0
  28. package/dist/validation/ParameterValidator.d.ts.map +1 -0
  29. package/dist/validation/ParameterValidator.js +122 -0
  30. package/dist/validation/ParameterValidator.js.map +1 -0
  31. package/dist/validation/ParameterValidator.test.d.ts +2 -0
  32. package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
  33. package/dist/validation/ParameterValidator.test.js +282 -0
  34. package/dist/validation/ParameterValidator.test.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/function/GlobalToolFunction.test.ts +3 -0
  37. package/src/function/GlobalToolFunction.ts +11 -0
  38. package/src/function/ToolFunction.test.ts +3 -0
  39. package/src/function/ToolFunction.ts +19 -1
  40. package/src/logging/ToolLogger.test.ts +753 -0
  41. package/src/logging/ToolLogger.ts +177 -0
  42. package/src/service/Service.test.ts +155 -14
  43. package/src/service/Service.ts +19 -1
  44. package/src/validation/ParameterValidator.test.ts +341 -0
  45. package/src/validation/ParameterValidator.ts +153 -0
@@ -0,0 +1,341 @@
1
+ import { ParameterValidator } from './ParameterValidator';
2
+ import { Parameter, ParameterType } from '../types/Models';
3
+
4
+ describe('ParameterValidator', () => {
5
+ describe('validate', () => {
6
+ it('should pass validation for valid parameters', () => {
7
+ const paramDefs = [
8
+ new Parameter('name', ParameterType.String, 'User name', true),
9
+ new Parameter('age', ParameterType.Integer, 'User age', false),
10
+ new Parameter('score', ParameterType.Number, 'User score', false),
11
+ new Parameter('active', ParameterType.Boolean, 'Is active', false),
12
+ new Parameter('tags', ParameterType.List, 'User tags', false),
13
+ new Parameter('config', ParameterType.Dictionary, 'User config', false)
14
+ ];
15
+
16
+ const validParams = {
17
+ name: 'John Doe',
18
+ age: 25,
19
+ score: 85.5,
20
+ active: true,
21
+ tags: ['admin', 'user'],
22
+ config: { theme: 'dark', language: 'en' }
23
+ };
24
+
25
+ const result = ParameterValidator.validate(validParams, paramDefs);
26
+
27
+ expect(result.isValid).toBe(true);
28
+ expect(result.errors).toHaveLength(0);
29
+ });
30
+
31
+ it('should fail validation for missing required parameters', () => {
32
+ const paramDefs = [
33
+ new Parameter('name', ParameterType.String, 'User name', true),
34
+ new Parameter('email', ParameterType.String, 'User email', true)
35
+ ];
36
+
37
+ const params = {
38
+ name: 'John Doe'
39
+ // email is missing
40
+ };
41
+
42
+ const result = ParameterValidator.validate(params, paramDefs);
43
+
44
+ expect(result.isValid).toBe(false);
45
+ expect(result.errors).toHaveLength(1);
46
+ expect(result.errors[0]).toEqual({
47
+ field: 'email',
48
+ message: "Required parameter 'email' is missing"
49
+ });
50
+ });
51
+
52
+ it('should fail validation for wrong parameter types', () => {
53
+ const paramDefs = [
54
+ new Parameter('name', ParameterType.String, 'User name', true),
55
+ new Parameter('age', ParameterType.Integer, 'User age', true),
56
+ new Parameter('active', ParameterType.Boolean, 'Is active', true),
57
+ new Parameter('tags', ParameterType.List, 'User tags', true),
58
+ new Parameter('config', ParameterType.Dictionary, 'User config', true)
59
+ ];
60
+
61
+ const invalidParams = {
62
+ name: 123, // should be string
63
+ age: '25', // should be integer
64
+ active: 'true', // should be boolean
65
+ tags: 'tag1,tag2', // should be array
66
+ config: 'invalid' // should be object
67
+ };
68
+
69
+ const result = ParameterValidator.validate(invalidParams, paramDefs);
70
+
71
+ expect(result.isValid).toBe(false);
72
+ expect(result.errors).toHaveLength(5);
73
+
74
+ expect(result.errors[0].field).toBe('name');
75
+ expect(result.errors[0].message).toContain('must be a string');
76
+
77
+ expect(result.errors[1].field).toBe('age');
78
+ expect(result.errors[1].message).toContain('must be an integer');
79
+
80
+ expect(result.errors[2].field).toBe('active');
81
+ expect(result.errors[2].message).toContain('must be a boolean');
82
+
83
+ expect(result.errors[3].field).toBe('tags');
84
+ expect(result.errors[3].message).toContain('must be an array');
85
+
86
+ expect(result.errors[4].field).toBe('config');
87
+ expect(result.errors[4].message).toContain('must be an object');
88
+ });
89
+
90
+ it('should handle null/undefined parameters correctly', () => {
91
+ const paramDefs = [
92
+ new Parameter('required', ParameterType.String, 'Required param', true),
93
+ new Parameter('optional', ParameterType.String, 'Optional param', false)
94
+ ];
95
+
96
+ const result1 = ParameterValidator.validate(null, paramDefs);
97
+ expect(result1.isValid).toBe(false);
98
+ expect(result1.errors).toHaveLength(1);
99
+ expect(result1.errors[0].field).toBe('required');
100
+
101
+ const result2 = ParameterValidator.validate(undefined, paramDefs);
102
+ expect(result2.isValid).toBe(false);
103
+ expect(result2.errors).toHaveLength(1);
104
+ expect(result2.errors[0].field).toBe('required');
105
+ });
106
+
107
+ it('should allow optional parameters to be missing', () => {
108
+ const paramDefs = [
109
+ new Parameter('name', ParameterType.String, 'User name', true),
110
+ new Parameter('age', ParameterType.Integer, 'User age', false)
111
+ ];
112
+
113
+ const params = {
114
+ name: 'John Doe'
115
+ // age is optional and missing
116
+ };
117
+
118
+ const result = ParameterValidator.validate(params, paramDefs);
119
+
120
+ expect(result.isValid).toBe(true);
121
+ expect(result.errors).toHaveLength(0);
122
+ });
123
+
124
+ it('should distinguish between integer and number types', () => {
125
+ const paramDefs = [
126
+ new Parameter('count', ParameterType.Integer, 'Item count', true),
127
+ new Parameter('score', ParameterType.Number, 'Score value', true)
128
+ ];
129
+
130
+ const params1 = {
131
+ count: 25.5, // should be integer, not float
132
+ score: 85.5 // number is fine
133
+ };
134
+
135
+ const result1 = ParameterValidator.validate(params1, paramDefs);
136
+ expect(result1.isValid).toBe(false);
137
+ expect(result1.errors).toHaveLength(1);
138
+ expect(result1.errors[0].field).toBe('count');
139
+ expect(result1.errors[0].message).toContain('must be an integer');
140
+
141
+ const params2 = {
142
+ count: 25, // integer is fine
143
+ score: 85.5 // number is fine
144
+ };
145
+
146
+ const result2 = ParameterValidator.validate(params2, paramDefs);
147
+ expect(result2.isValid).toBe(true);
148
+ expect(result2.errors).toHaveLength(0);
149
+ });
150
+
151
+ it('should handle array vs object distinction', () => {
152
+ const paramDefs = [
153
+ new Parameter('tags', ParameterType.List, 'Tag list', true),
154
+ new Parameter('config', ParameterType.Dictionary, 'Config object', true)
155
+ ];
156
+
157
+ const params = {
158
+ tags: { 0: 'tag1', 1: 'tag2' }, // object that looks like array
159
+ config: ['key1', 'key2'] // array instead of object
160
+ };
161
+
162
+ const result = ParameterValidator.validate(params, paramDefs);
163
+ expect(result.isValid).toBe(false);
164
+ expect(result.errors).toHaveLength(2);
165
+
166
+ expect(result.errors[0].field).toBe('tags');
167
+ expect(result.errors[0].message).toContain('must be an array');
168
+
169
+ expect(result.errors[1].field).toBe('config');
170
+ expect(result.errors[1].message).toContain('must be an object');
171
+ });
172
+
173
+ it('should handle null values for optional parameters', () => {
174
+ const paramDefs = [
175
+ new Parameter('name', ParameterType.String, 'User name', true),
176
+ new Parameter('age', ParameterType.Integer, 'User age', false)
177
+ ];
178
+
179
+ const params = {
180
+ name: 'John Doe',
181
+ age: null // null for optional parameter should be allowed
182
+ };
183
+
184
+ const result = ParameterValidator.validate(params, paramDefs);
185
+ expect(result.isValid).toBe(true);
186
+ expect(result.errors).toHaveLength(0);
187
+ });
188
+
189
+ it('should handle edge cases for number validation', () => {
190
+ const paramDefs = [
191
+ new Parameter('score', ParameterType.Number, 'Score value', true)
192
+ ];
193
+
194
+ // Test NaN
195
+ const params1 = { score: NaN };
196
+ const result1 = ParameterValidator.validate(params1, paramDefs);
197
+ expect(result1.isValid).toBe(false);
198
+ expect(result1.errors[0].message).toContain('must be a number');
199
+
200
+ // Test Infinity
201
+ const params2 = { score: Infinity };
202
+ const result2 = ParameterValidator.validate(params2, paramDefs);
203
+ expect(result2.isValid).toBe(true);
204
+ expect(result2.errors).toHaveLength(0);
205
+
206
+ // Test negative numbers
207
+ const params3 = { score: -42.5 };
208
+ const result3 = ParameterValidator.validate(params3, paramDefs);
209
+ expect(result3.isValid).toBe(true);
210
+ expect(result3.errors).toHaveLength(0);
211
+ });
212
+
213
+ it('should handle edge cases for integer validation', () => {
214
+ const paramDefs = [
215
+ new Parameter('count', ParameterType.Integer, 'Item count', true)
216
+ ];
217
+
218
+ // Test negative integers
219
+ const params1 = { count: -5 };
220
+ const result1 = ParameterValidator.validate(params1, paramDefs);
221
+ expect(result1.isValid).toBe(true);
222
+ expect(result1.errors).toHaveLength(0);
223
+
224
+ // Test zero
225
+ const params2 = { count: 0 };
226
+ const result2 = ParameterValidator.validate(params2, paramDefs);
227
+ expect(result2.isValid).toBe(true);
228
+ expect(result2.errors).toHaveLength(0);
229
+
230
+ // Test very large integers
231
+ const params3 = { count: Number.MAX_SAFE_INTEGER };
232
+ const result3 = ParameterValidator.validate(params3, paramDefs);
233
+ expect(result3.isValid).toBe(true);
234
+ expect(result3.errors).toHaveLength(0);
235
+ });
236
+
237
+ it('should handle empty arrays and objects', () => {
238
+ const paramDefs = [
239
+ new Parameter('tags', ParameterType.List, 'Tag list', true),
240
+ new Parameter('config', ParameterType.Dictionary, 'Config object', true)
241
+ ];
242
+
243
+ const params = {
244
+ tags: [], // empty array should be valid
245
+ config: {} // empty object should be valid
246
+ };
247
+
248
+ const result = ParameterValidator.validate(params, paramDefs);
249
+ expect(result.isValid).toBe(true);
250
+ expect(result.errors).toHaveLength(0);
251
+ });
252
+
253
+ it('should handle special string values', () => {
254
+ const paramDefs = [
255
+ new Parameter('text', ParameterType.String, 'Text value', true)
256
+ ];
257
+
258
+ // Test empty string
259
+ const params1 = { text: '' };
260
+ const result1 = ParameterValidator.validate(params1, paramDefs);
261
+ expect(result1.isValid).toBe(true);
262
+ expect(result1.errors).toHaveLength(0);
263
+
264
+ // Test string with special characters
265
+ const params2 = { text: 'Hello\nWorld\t!' };
266
+ const result2 = ParameterValidator.validate(params2, paramDefs);
267
+ expect(result2.isValid).toBe(true);
268
+ expect(result2.errors).toHaveLength(0);
269
+ });
270
+
271
+ it('should handle multiple validation errors', () => {
272
+ const paramDefs = [
273
+ new Parameter('name', ParameterType.String, 'User name', true),
274
+ new Parameter('age', ParameterType.Integer, 'User age', true),
275
+ new Parameter('email', ParameterType.String, 'User email', true)
276
+ ];
277
+
278
+ const params = {
279
+ name: 123, // wrong type
280
+ age: '25' // wrong type
281
+ // email is missing
282
+ };
283
+
284
+ const result = ParameterValidator.validate(params, paramDefs);
285
+ expect(result.isValid).toBe(false);
286
+ expect(result.errors).toHaveLength(3);
287
+
288
+ expect(result.errors.some((e) => e.field === 'name')).toBe(true);
289
+ expect(result.errors.some((e) => e.field === 'age')).toBe(true);
290
+ expect(result.errors.some((e) => e.field === 'email')).toBe(true);
291
+ });
292
+
293
+ it('should handle tools with no parameter definitions', () => {
294
+ const result = ParameterValidator.validate({ someParam: 'value' }, []);
295
+ expect(result.isValid).toBe(true);
296
+ expect(result.errors).toHaveLength(0);
297
+ });
298
+
299
+ it('should handle extra parameters not in definition', () => {
300
+ const paramDefs = [
301
+ new Parameter('name', ParameterType.String, 'User name', true)
302
+ ];
303
+
304
+ const params = {
305
+ name: 'John Doe',
306
+ extraParam: 'should be ignored'
307
+ };
308
+
309
+ const result = ParameterValidator.validate(params, paramDefs);
310
+ expect(result.isValid).toBe(true);
311
+ expect(result.errors).toHaveLength(0);
312
+ });
313
+
314
+ it('should handle nested objects and arrays', () => {
315
+ const paramDefs = [
316
+ new Parameter('config', ParameterType.Dictionary, 'Config object', true),
317
+ new Parameter('matrix', ParameterType.List, 'Matrix data', true)
318
+ ];
319
+
320
+ const params = {
321
+ config: {
322
+ nested: {
323
+ deep: {
324
+ value: 'test'
325
+ }
326
+ },
327
+ array: [1, 2, 3]
328
+ },
329
+ matrix: [
330
+ [1, 2, 3],
331
+ [4, 5, 6],
332
+ { nested: 'object in array' }
333
+ ]
334
+ };
335
+
336
+ const result = ParameterValidator.validate(params, paramDefs);
337
+ expect(result.isValid).toBe(true);
338
+ expect(result.errors).toHaveLength(0);
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,153 @@
1
+ import { Parameter, ParameterType } from '../types/Models';
2
+
3
+ /**
4
+ * Validation error details
5
+ */
6
+ export interface ValidationError {
7
+ field: string;
8
+ message: string;
9
+ }
10
+
11
+ /**
12
+ * Validation result
13
+ */
14
+ export interface ValidationResult {
15
+ isValid: boolean;
16
+ errors: ValidationError[];
17
+ }
18
+
19
+ /**
20
+ * Parameter validator for tool inputs
21
+ */
22
+ export class ParameterValidator {
23
+ /**
24
+ * Validate parameters against their definitions
25
+ * @param params The actual parameters received
26
+ * @param parameterDefinitions The expected parameter definitions
27
+ * @returns Validation result with any errors found
28
+ */
29
+ public static validate(
30
+ params: any,
31
+ parameterDefinitions: Parameter[]
32
+ ): ValidationResult {
33
+ const errors: ValidationError[] = [];
34
+
35
+ // Validate each defined parameter
36
+ for (const paramDef of parameterDefinitions) {
37
+ const value = params ? params[paramDef.name] : undefined;
38
+
39
+ // Check if required parameter is missing
40
+ if (paramDef.required && (value === undefined || value === null)) {
41
+ errors.push({
42
+ field: paramDef.name,
43
+ message: `Required parameter '${paramDef.name}' is missing`
44
+ });
45
+ continue;
46
+ }
47
+
48
+ // Skip validation if parameter is optional and not provided
49
+ if (!paramDef.required && (value === undefined || value === null)) {
50
+ continue;
51
+ }
52
+
53
+ // Validate parameter type
54
+ const typeError = this.validateParameterType(paramDef.name, value, paramDef.type);
55
+ if (typeError) {
56
+ errors.push(typeError);
57
+ }
58
+ }
59
+
60
+ return { isValid: errors.length === 0, errors };
61
+ }
62
+
63
+ /**
64
+ * Validate a single parameter's type
65
+ * @param paramName Parameter name
66
+ * @param value Parameter value
67
+ * @param expectedType Expected parameter type
68
+ * @returns Validation error if invalid, null if valid
69
+ */
70
+ private static validateParameterType(
71
+ paramName: string,
72
+ value: any,
73
+ expectedType: ParameterType
74
+ ): ValidationError | null {
75
+ const actualType = this.getActualType(value);
76
+
77
+ switch (expectedType) {
78
+ case ParameterType.String:
79
+ if (typeof value !== 'string') {
80
+ return {
81
+ field: paramName,
82
+ message: `Parameter '${paramName}' must be a string, but received ${actualType}`
83
+ };
84
+ }
85
+ break;
86
+
87
+ case ParameterType.Integer:
88
+ if (!Number.isInteger(value)) {
89
+ return {
90
+ field: paramName,
91
+ message: `Parameter '${paramName}' must be an integer, but received ${actualType}`
92
+ };
93
+ }
94
+ break;
95
+
96
+ case ParameterType.Number:
97
+ if (typeof value !== 'number' || isNaN(value)) {
98
+ return {
99
+ field: paramName,
100
+ message: `Parameter '${paramName}' must be a number, but received ${actualType}`
101
+ };
102
+ }
103
+ break;
104
+
105
+ case ParameterType.Boolean:
106
+ if (typeof value !== 'boolean') {
107
+ return {
108
+ field: paramName,
109
+ message: `Parameter '${paramName}' must be a boolean, but received ${actualType}`
110
+ };
111
+ }
112
+ break;
113
+
114
+ case ParameterType.List:
115
+ if (!Array.isArray(value)) {
116
+ return {
117
+ field: paramName,
118
+ message: `Parameter '${paramName}' must be an array, but received ${actualType}`
119
+ };
120
+ }
121
+ break;
122
+
123
+ case ParameterType.Dictionary:
124
+ if (typeof value !== 'object' || Array.isArray(value) || value === null) {
125
+ return {
126
+ field: paramName,
127
+ message: `Parameter '${paramName}' must be an object, but received ${actualType}`
128
+ };
129
+ }
130
+ break;
131
+
132
+ default:
133
+ return {
134
+ field: paramName,
135
+ message: `Parameter '${paramName}' has unknown expected type: ${String(expectedType)}`
136
+ };
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Get a human-readable description of the actual type
144
+ * @param value The value to check
145
+ * @returns String description of the type
146
+ */
147
+ private static getActualType(value: any): string {
148
+ if (value === null) return 'null';
149
+ if (value === undefined) return 'undefined';
150
+ if (Array.isArray(value)) return 'array';
151
+ return typeof value;
152
+ }
153
+ }