@platforma-sdk/workflow-tengo 2.3.1 → 2.4.1

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.
@@ -651,7 +651,7 @@ builder := func() {
651
651
  * @return field - a reference to the file stream resource.
652
652
  */
653
653
  getFileStream: func(fileName) {
654
- return tpl.output("fileStreams", cache).getFutureInputField(fileName)
654
+ return tpl.output("fileStreams", cache).getFutureInputField(fileName, true)
655
655
  },
656
656
 
657
657
  /**
@@ -6,6 +6,13 @@ fmt := import("fmt")
6
6
  log := import("log")
7
7
  text := import("text")
8
8
 
9
+ /**
10
+ * Provides current API version available at runtime
11
+ */
12
+ apiVersion := func() {
13
+ return 0 // stub code. Real API checker will appear here.
14
+ }
15
+
9
16
  /**
10
17
  * Prints message to the pl log
11
18
  */
@@ -74,6 +81,35 @@ fromStrict := func(r) {
74
81
  return plapi.strictToMap(r)
75
82
  }
76
83
 
84
+ /**
85
+ * Safe way to check if method exists in a builtin module or a library
86
+ *
87
+ * @param module: module - builtin module or tengo library
88
+ * @param methodName: string - name of a method to be checked
89
+ *
90
+ * @return bool - true when method exists, false otherwise
91
+ */
92
+ methodExists := func(module, methodName) {
93
+ if isStrict(module) {
94
+ if !is_callable(plapi.mapHasKey) {
95
+ module = fromStrict(module)
96
+
97
+ } else {
98
+ if !plapi.mapHasKey(module, methodName) {
99
+ return false
100
+ }
101
+ }
102
+ }
103
+
104
+ method := module[methodName]
105
+
106
+ if is_undefined(method) {
107
+ return false
108
+ }
109
+
110
+ return is_callable(method)
111
+ }
112
+
77
113
  /**
78
114
  * If input is a strict map, converts it to a normal one, otherwise returns the object as is
79
115
  *
@@ -255,9 +291,11 @@ getCtx := func() {
255
291
  }
256
292
 
257
293
  export toStrict({
294
+ apiVersion: apiVersion,
258
295
  print: print,
259
296
  panic: panic,
260
297
  assert: assert,
298
+ methodExists: methodExists,
261
299
  isStrict: isStrict,
262
300
  isMap: isMap,
263
301
  fromStrict: fromStrict,
@@ -29,6 +29,14 @@ containsKey := func(map, key) {
29
29
  return false
30
30
  }
31
31
 
32
+ // Load effective version of map key checker if it exists in controller's API
33
+ plapi := import("plapi")
34
+ if ll.methodExists(plapi, "mapHasKey") {
35
+ containsKey = func(map, key) {
36
+ return plapi.mapHasKey(map, key)
37
+ }
38
+ }
39
+
32
40
  /**
33
41
  * Transforms all keys of the map with a function and returns a new map with these keys.
34
42
  *
@@ -59,7 +59,7 @@ createUniversal := func(tpl, isEph, inputs) {
59
59
  */
60
60
  output: func(name, ...cache) {
61
61
  ll.assert(len(cache) == 0 || is_int(cache[0]), "cache should be a number (use 'times' tengo library), got: %v", cache)
62
- out := renderer.getFutureOutputField(tplutil.TEMPLATE_OUTPUT_PREFIX + name)
62
+ out := renderer.getFutureOutputField(tplutil.TEMPLATE_OUTPUT_PREFIX + name, isEph)
63
63
  if len(cache) > 0 && cache[0] > 0 {
64
64
  cache = cache[0]
65
65
  tx.cacheSetToField(out.id, cache)
@@ -264,6 +264,7 @@ resource = func(r) {
264
264
  * ID: int64
265
265
  * Data: byte[],
266
266
  * Type: { Name: string, Version: string }
267
+ * Features: { ephemeral: bool }
267
268
  * Inputs: strict map
268
269
  * Outputs: strict map
269
270
  * Dynamic: strict map
@@ -297,6 +298,19 @@ resource = func(r) {
297
298
  return self.info().ResourceReady
298
299
  },
299
300
 
301
+ isEphemeral: func() {
302
+ resourceInfo := self.info()
303
+
304
+ if !maps.containsKey(resourceInfo, "Features") {
305
+ // by default we consider all resources to be ephemeral for backward compatiblity to keep stable logic
306
+ // in platforma backend before v1.16.0.
307
+ // TODO: once we consider this version old enough, we can panic here instead of 'return true'
308
+ return true
309
+ }
310
+
311
+ return resourceInfo.Features.ephemeral
312
+ },
313
+
300
314
  /**
301
315
  * Get raw resource data
302
316
  *
@@ -433,7 +447,7 @@ resource = func(r) {
433
447
  * @return field: smart.field
434
448
  */
435
449
  getFutureField: func(name, fieldType, ...eph) {
436
- isEph := true
450
+ isEph := strictSelf.isEphemeral()
437
451
  if len(eph) > 0 {
438
452
  isEph = eph[0]
439
453
  }
@@ -450,14 +464,19 @@ resource = func(r) {
450
464
  * @return field: smart.field
451
465
  */
452
466
  getFutureInputField: func(nameOrPath, ...eph) {
467
+ isEph := strictSelf.isEphemeral()
468
+ if len(eph) > 0 {
469
+ isEph = eph[0]
470
+ }
471
+
453
472
  if is_array(nameOrPath) {
454
473
  current := strictSelf
455
474
  for name in nameOrPath {
456
- current = current.getFutureField(name, "input", eph...)
475
+ current = current.getFutureField(name, "input", isEph)
457
476
  }
458
477
  return current
459
478
  } else {
460
- return self.getFutureField(nameOrPath, "input", eph...)
479
+ return self.getFutureField(nameOrPath, "input", isEph)
461
480
  }
462
481
  },
463
482
 
@@ -471,7 +490,18 @@ resource = func(r) {
471
490
  * @return field: smart.field
472
491
  */
473
492
  getFutureOutputField: func(name, ...eph) {
474
- return self.getFutureField(name, "output", eph...)
493
+ isEph := strictSelf.isEphemeral()
494
+ if len(eph) > 0 {
495
+ isEph = eph[0]
496
+ }
497
+
498
+ if ll.apiVersion() < 1 && !isEph {
499
+ // Dirty hack that saves from issues with buggy pure getField before v1.16.1
500
+ // We can safely drop it when we consider v1.16.0 old enough.
501
+ return self.getFutureField(name, "output", true).expectNonEphemeral()
502
+ }
503
+
504
+ return self.getFutureField(name, "output", isEph)
475
505
  },
476
506
 
477
507
  /**
@@ -592,10 +622,12 @@ field = func(f) {
592
622
  /**
593
623
  * Field info:
594
624
  *
595
- * ID: { ResourceId: int64, Name: string }
596
- * Value: int64 - value resource id
597
- * Error: int64 - error resource id
598
- * IsSet: bool - whether field is set
625
+ * ID: { ResourceId: int64, Name: string }
626
+ * Type: string - see constants.FTYPE_* for possible values
627
+ * Features: { ephemeral: bool }
628
+ * Value: int64 - value resource id
629
+ * Error: int64 - error resource id
630
+ * IsSet: bool - whether field is set
599
631
  */
600
632
  info: func() {
601
633
  return tx.getField(self.id)
@@ -622,6 +654,37 @@ field = func(f) {
622
654
  return self.info().Error != 0
623
655
  },
624
656
 
657
+ /**
658
+ * Some fields cannot point to ephemeral resources.
659
+ * This method checks, if current field can.
660
+ *
661
+ * @return bool - "true' when field can point to ephemeral resource, 'false' when not
662
+ */
663
+ isEphemeral: func() {
664
+ fieldInfo := self.info()
665
+
666
+ if !maps.containsKey(fieldInfo, "Features") {
667
+ // by default we consider all fields can point to ephemeral resources for backward compatiblity to keep stable logic
668
+ // in platforma backend before v1.16.0.
669
+ // TODO: once we consider this version old enough, we can panic here instead of 'return true'
670
+ return true
671
+ }
672
+
673
+ isParentResourceEphemeral := fieldInfo.Features.ephemeral
674
+ if isParentResourceEphemeral {
675
+ return true
676
+ }
677
+
678
+ return fieldInfo.Type != constants.FTYPE_INPUT &&
679
+ fieldInfo.Type != constants.FTYPE_OUTPUT
680
+ },
681
+
682
+ expectNonEphemeral: func() {
683
+ rr := field(self.id)
684
+ rr.isEphemeral = func() { return false }
685
+ return rr
686
+ },
687
+
625
688
  /**
626
689
  * Use cacheMillis, cacheSeconds, cacheMinutes, cacheHours and cacheDays to not make hidden mistakes in cache time ranges.
627
690
  * Sets the cache time.
@@ -779,7 +842,7 @@ field = func(f) {
779
842
  * @return field: smart.field
780
843
  */
781
844
  getFutureField: func(name, fieldType, ...eph) {
782
- isEph := true
845
+ isEph := strictSelf.isEphemeral()
783
846
  if len(eph) > 0 {
784
847
  isEph = eph[0]
785
848
  }
@@ -796,6 +859,11 @@ field = func(f) {
796
859
  * @return field: smart field
797
860
  */
798
861
  getFutureInputField: func(nameOrPath, ...eph) {
862
+ isEph := strictSelf.isEphemeral()
863
+ if len(eph) > 0 {
864
+ isEph = eph[0]
865
+ }
866
+
799
867
  if is_array(nameOrPath) {
800
868
  current := strictSelf
801
869
  for name in nameOrPath {
@@ -817,7 +885,18 @@ field = func(f) {
817
885
  * @return field: smart field
818
886
  */
819
887
  getFutureOutputField: func(name, ...eph) {
820
- return self.getFutureField(name, "output", eph...)
888
+ isEph := strictSelf.isEphemeral()
889
+ if len(eph) > 0 {
890
+ isEph = eph[0]
891
+ }
892
+
893
+ if ll.apiVersion() < 1 && !isEph {
894
+ // Dirty hack that saves from issues with buggy pure getField before v1.16.1
895
+ // We can safely drop it when we consider v1.16.0 old enough.
896
+ return self.getFutureField(name, "output", true).expectNonEphemeral()
897
+ }
898
+
899
+ return self.getFutureField(name, "output", isEph)
821
900
  }
822
901
  }
823
902
 
@@ -848,7 +927,7 @@ resourceBuilder = func(r) {
848
927
  * Create new field with given name and field type.
849
928
  *
850
929
  * @param name field name
851
- * @param fieldType: field type - input, output, dynamic, service, OTW. Use F_TYPE_* constants.
930
+ * @param fieldType: field type - input, output, dynamic, service, OTW. Use FfTYPE_* constants.
852
931
  */
853
932
  createField: func(name, fieldType) {
854
933
  assertNotBuilt()
@@ -0,0 +1,204 @@
1
+ /*
2
+ * Library for tengo unit tests.
3
+ * Contains collection of helpers, that make tengo unit tests more compact, easier for reading and writing.
4
+ *
5
+ * WARNING: It is designed to be imported ONLY in *.test.tengo files.
6
+ * As it works ONLY in tengo unit tests context, it will break any workflow if imported
7
+ * in .lib.tengo or .tpl.tengo file.
8
+ */
9
+
10
+ test := import("test")
11
+
12
+ //
13
+ // Basic re-export from "test" builtin library with some conveniences
14
+ //
15
+
16
+ /**
17
+ * Skip the test writing given message to the console log.
18
+ *
19
+ * @param args: []any - message to be written.
20
+ * Each argument is converted to string with its default formatter.
21
+ */
22
+ skip := func(...args) {
23
+ test.skip(args...)
24
+ }
25
+
26
+ /**
27
+ * Formatted version of skip(). skipf(msg, ...args) == skip(format(msg, args...))
28
+ *
29
+ * @param msg: string - message with formatting directives.
30
+ * @param args: []any - arguments for directives in <msg>.
31
+ */
32
+ skipf := func(msg, ...args) {
33
+ skip(format(msg, args...))
34
+ }
35
+
36
+ /**
37
+ * Fail the test in place. Interrupts execution of single test and prints a
38
+ * failure message to the console log.
39
+ *
40
+ * @param args: []any - message to be written.
41
+ * Each argument is converted to string with its default formatter.
42
+ */
43
+ fail := func(...args) {
44
+ test.fail(args...)
45
+ }
46
+
47
+ /**
48
+ * Formatted version of fail(). failf(msg, ...args) == fail(format(msg, args...))
49
+ *
50
+ * @param msg: string - message with formatting directives.
51
+ * @param args: []any - arguments for directives in <msg>.
52
+ */
53
+ failf := func(msg, ...args) {
54
+ fail(format(msg, args...))
55
+ }
56
+
57
+ /**
58
+ * Write message to console log.
59
+ *
60
+ * @param args: []any - message to be written.
61
+ * Each argument is converted to string with its default formatter.
62
+ */
63
+ log := func(...args) {
64
+ test.log(args...)
65
+ }
66
+
67
+ /**
68
+ * Formatted version of log(). logf(msg, ...args) == log(format(msg, args...))
69
+ *
70
+ * @param msg: string - message with formatting directives.
71
+ * @param args: []any - arguments for directives in <msg>.
72
+ */
73
+ logf := func(msg, ...args) {
74
+ test.log(format(msg, args...))
75
+ }
76
+
77
+ /**
78
+ * Mark test as executed for tengo testing engine.
79
+ *
80
+ * Regular tengo file with tests contains bunch of fuctions starting with 'Test' prefix, like `TestMyFunction := func() { ... }`.
81
+ * You DO NOT need to call this Done() method if you have file with Test* functions.
82
+ *
83
+ * For some rare cases, you may want to define your own custom testing script, that will call functions on its own instead
84
+ * of using the standard test functions naming. In this case, testing engine needs to know the difference between "nothing was called at all"
85
+ * and "everything was done well". This is what this method for.
86
+ *
87
+ * You don't need it in 99.9% of cases.
88
+ *
89
+ * This method does not interrupt test execution and is intended for use in custom test scripts
90
+ * without declared test functions.
91
+ */
92
+ done := func() {
93
+ test.done()
94
+ }
95
+
96
+ //
97
+ // Boolean assertions
98
+ //
99
+
100
+ /**
101
+ * Require <condition> to be true, failing test when not.
102
+ *
103
+ * @param condition: bool
104
+ * @param msg: []any - message to be printed on check failure
105
+ */
106
+ isTrue := func(condition, ...msg) {
107
+ if !condition {
108
+ test.fail("<condition> != 'true': ", msg...)
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Formatted version of isTrue(). isTruef(condition, msg, ...args) == isTrue(condition, format(msg, args...))
114
+ *
115
+ * @param condition: bool
116
+ * @param msg: string - message with formatting directives to be printed on check failure
117
+ * @param args: []any - arguments for formatting directives in <msg>
118
+ */
119
+ isTruef := func(condition, msg, ...args) {
120
+ isTrue(condition, format(msg, args...))
121
+ }
122
+
123
+ /**
124
+ * Require <condition> to be false, failing test when not.
125
+ *
126
+ * @param condition: bool
127
+ * @param args: []any - message to be printed on check failure
128
+ */
129
+ isFalse := func(condition, ...args) {
130
+ if condition {
131
+ test.fail("<condition> != 'false'", args...)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Formatted version of isFalse(). isFalsef(condition, msg, ..args) == isFalse(condition, format(msg, args...))
137
+ *
138
+ * @param condition: bool
139
+ * @param msg: string - message with formatting directives to be printed on check failure
140
+ * @param args: []any - arguments for formatting directives in <msg>
141
+ */
142
+ isFalsef := func(condition, msg, ...args) {
143
+ isFalse(condition, format(msg, args...))
144
+ }
145
+
146
+ //
147
+ // Equality assertions
148
+ //
149
+
150
+ /**
151
+ * Fails test when <expect> != <value>
152
+ *
153
+ * @param expect: any - expected value for the equality check
154
+ * @param value: any - the value to be checked
155
+ * @param args: []any - the message to be printed on check failure
156
+ */
157
+ isEqual := func(expect, value, ...args) {
158
+ if expect == value {
159
+ return
160
+ }
161
+
162
+ msg := format("%q != %q (expect: %#v, got %#v)", expect, value, expect, value)
163
+
164
+ fail(msg, args...)
165
+ }
166
+
167
+ /**
168
+ * Formatted version of isEqual(). isEqualf(expect, value, msg, ..args) == isEqual(expect, value, format(msg, args...))
169
+ *
170
+ * @param expect: any - expected value for the equality check
171
+ * @param value: any - the value to be checked
172
+ * @param msg: string - message with formatting directives to be printed on check failure
173
+ * @param args: []any - arguments for formatting directives in <msg>
174
+ */
175
+ isEqualf := func(expect, value, msg, ...args) {
176
+ isEqual(expect, value, format(msg, args...))
177
+ }
178
+
179
+ export {
180
+ //
181
+ // Builtin 'test' module method wrappers
182
+ //
183
+ done : done,
184
+ skip : skip,
185
+ skipf : skipf,
186
+ fail : fail,
187
+ failf : failf,
188
+ log : log,
189
+ logf : logf,
190
+
191
+ //
192
+ // Boolean assertions
193
+ //
194
+ isTrue : isTrue,
195
+ isTruef : isTruef,
196
+ isFalse : isFalse,
197
+ isFalsef : isFalsef,
198
+
199
+ //
200
+ // Equality assertions
201
+ //
202
+ isEqual : isEqual,
203
+ isEqualf : isEqualf
204
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/workflow-tengo",
3
- "version": "2.3.1",
3
+ "version": "2.4.1",
4
4
  "description": "Tengo SDK library",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./dist/index.js",