@voxgig/sdkgen 4.4.1 → 4.5.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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/FeatureDocs.d.ts +16 -0
- package/dist/cmp/FeatureDocs.js +72 -0
- package/dist/cmp/FeatureDocs.js.map +1 -0
- package/dist/cmp/Readme.js +2 -0
- package/dist/cmp/Readme.js.map +1 -1
- package/dist/cmp/ReadmeFeatures.d.ts +2 -0
- package/dist/cmp/ReadmeFeatures.js +95 -0
- package/dist/cmp/ReadmeFeatures.js.map +1 -0
- package/dist/cmp/ReadmeRef.js +4 -0
- package/dist/cmp/ReadmeRef.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
- package/dist/cmp/ReadmeRefFeatures.js +130 -0
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
- package/dist/cmp/ReadmeTop.js +46 -0
- package/dist/cmp/ReadmeTop.js.map +1 -1
- package/dist/helpers/opShape.js +5 -1
- package/dist/helpers/opShape.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -2
- package/dist/sdkgen.js +30 -10
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +17 -1
- package/dist/utility.js.map +1 -1
- package/package.json +1 -1
- package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
- package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
- package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
- package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
- package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
- package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
- package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
- package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
- package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
- package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
- package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
- package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
- package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
- package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
- package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
- package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
- package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
- package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
- package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
- package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
- package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
- package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
- package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
- package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
- package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
- package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
- package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
- package/project/.sdk/tm/clojure/src/sdk/core.clj +6 -2
- package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +5 -0
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +4 -0
- package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +4 -1
- package/project/.sdk/tm/go/feature/test_feature.go +17 -6
- package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
- package/project/.sdk/tm/js/test/feature.test.js +19 -9
- package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
- package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
- package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
- package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
- package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +4 -1
- package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
- package/project/.sdk/tm/perl/utility/make_options.pm +4 -1
- package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
- package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
- package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
- package/project/.sdk/tm/py/test/test_feature.py +40 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
- package/project/.sdk/tm/rb/utility/make_options.rb +4 -1
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
- package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
- package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/FeatureDocs.ts +100 -0
- package/src/cmp/Readme.ts +2 -0
- package/src/cmp/ReadmeFeatures.ts +113 -0
- package/src/cmp/ReadmeRef.ts +4 -0
- package/src/cmp/ReadmeRefFeatures.ts +149 -0
- package/src/cmp/ReadmeTop.ts +48 -0
- package/src/helpers/opShape.ts +5 -1
- package/src/sdkgen.ts +28 -7
- package/src/utility.ts +21 -1
|
@@ -134,6 +134,12 @@ void tests() {
|
|
|
134
134
|
if (hasFeature('retry')) {
|
|
135
135
|
describe('retry', () {
|
|
136
136
|
test('retries transient failures then succeeds', (t) async {
|
|
137
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
138
|
+
// SDK was generated with it (the harness skips absent features).
|
|
139
|
+
if (!hasFeature('netsim')) {
|
|
140
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
137
143
|
final clock = makeClock();
|
|
138
144
|
final h = makeClient(features: [
|
|
139
145
|
{'name': 'netsim', 'options': {'failTimes': 2, 'failStatus': 503}},
|
|
@@ -152,6 +158,12 @@ void tests() {
|
|
|
152
158
|
});
|
|
153
159
|
|
|
154
160
|
test('gives up after the budget', (t) async {
|
|
161
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
162
|
+
// SDK was generated with it (the harness skips absent features).
|
|
163
|
+
if (!hasFeature('netsim')) {
|
|
164
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
155
167
|
final clock = makeClock();
|
|
156
168
|
final h = makeClient(features: [
|
|
157
169
|
{'name': 'netsim', 'options': {'failTimes': 9, 'failStatus': 500}},
|
|
@@ -203,6 +215,12 @@ void tests() {
|
|
|
203
215
|
});
|
|
204
216
|
|
|
205
217
|
test('honours a server Retry-After', (t) async {
|
|
218
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
219
|
+
// SDK was generated with it (the harness skips absent features).
|
|
220
|
+
if (!hasFeature('netsim')) {
|
|
221
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
206
224
|
final clock = makeClock();
|
|
207
225
|
final h = makeClient(features: [
|
|
208
226
|
{'name': 'netsim', 'options': {'rateLimitTimes': 1, 'retryAfter': 2}},
|
|
@@ -222,6 +240,12 @@ void tests() {
|
|
|
222
240
|
});
|
|
223
241
|
|
|
224
242
|
test('default jitter path still succeeds', (t) async {
|
|
243
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
244
|
+
// SDK was generated with it (the harness skips absent features).
|
|
245
|
+
if (!hasFeature('netsim')) {
|
|
246
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
225
249
|
final h = makeClient(features: [
|
|
226
250
|
{'name': 'netsim', 'options': {'failTimes': 1}},
|
|
227
251
|
{'name': 'retry', 'options': {'retries': 2, 'minDelay': 0}},
|
|
@@ -235,6 +259,12 @@ void tests() {
|
|
|
235
259
|
if (hasFeature('timeout')) {
|
|
236
260
|
describe('timeout', () {
|
|
237
261
|
test('a slow request times out', (t) async {
|
|
262
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
263
|
+
// SDK was generated with it (the harness skips absent features).
|
|
264
|
+
if (!hasFeature('netsim')) {
|
|
265
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
238
268
|
final h = makeClient(features: [
|
|
239
269
|
{'name': 'netsim', 'options': {'latency': 80}},
|
|
240
270
|
{'name': 'timeout', 'options': {'ms': 10}},
|
|
@@ -470,6 +500,12 @@ void tests() {
|
|
|
470
500
|
if (hasFeature('metrics')) {
|
|
471
501
|
describe('metrics', () {
|
|
472
502
|
test('counts ok and err per op', (t) async {
|
|
503
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
504
|
+
// SDK was generated with it (the harness skips absent features).
|
|
505
|
+
if (!hasFeature('netsim')) {
|
|
506
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
473
509
|
final h = makeClient(features: [
|
|
474
510
|
{'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
|
|
475
511
|
{'name': 'metrics', 'options': {}},
|
|
@@ -509,6 +545,12 @@ void tests() {
|
|
|
509
545
|
});
|
|
510
546
|
|
|
511
547
|
test('records a failed span on error', (t) async {
|
|
548
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
549
|
+
// SDK was generated with it (the harness skips absent features).
|
|
550
|
+
if (!hasFeature('netsim')) {
|
|
551
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
512
554
|
final h = makeClient(features: [
|
|
513
555
|
{'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
|
|
514
556
|
{'name': 'telemetry', 'options': {}},
|
|
@@ -542,6 +584,12 @@ void tests() {
|
|
|
542
584
|
});
|
|
543
585
|
|
|
544
586
|
test('captures failures', (t) async {
|
|
587
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
588
|
+
// SDK was generated with it (the harness skips absent features).
|
|
589
|
+
if (!hasFeature('netsim')) {
|
|
590
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
545
593
|
final h = makeClient(features: [
|
|
546
594
|
{'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
|
|
547
595
|
{'name': 'debug', 'options': {}},
|
|
@@ -556,6 +604,12 @@ void tests() {
|
|
|
556
604
|
if (hasFeature('audit')) {
|
|
557
605
|
describe('audit', () {
|
|
558
606
|
test('one record per op with sink + actor', (t) async {
|
|
607
|
+
// Drives netsim as the simulated network: runnable only when this
|
|
608
|
+
// SDK was generated with it (the harness skips absent features).
|
|
609
|
+
if (!hasFeature('netsim')) {
|
|
610
|
+
t.skip('this SDK was generated without the netsim feature');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
559
613
|
final sink = [];
|
|
560
614
|
final h = makeClient(features: [
|
|
561
615
|
{'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
|
|
@@ -285,10 +285,13 @@ defmodule ProjectName.Utility do
|
|
|
285
285
|
optspec =
|
|
286
286
|
H.deep(%{
|
|
287
287
|
"apikey" => "",
|
|
288
|
+
"secret" => "",
|
|
288
289
|
"base" => "http://localhost:8000",
|
|
289
290
|
"prefix" => "",
|
|
290
291
|
"suffix" => "",
|
|
291
|
-
|
|
292
|
+
# `basic` and `secret`: HTTP Basic Auth needs a second credential and
|
|
293
|
+
# a flag to say the pair is Basic rather than a single bearer token.
|
|
294
|
+
"auth" => %{"prefix" => "", "basic" => false},
|
|
292
295
|
"headers" => %{"`$CHILD`" => "`$STRING`"},
|
|
293
296
|
"allow" => %{
|
|
294
297
|
"method" => "GET,PUT,POST,PATCH,DELETE,OPTIONS",
|
|
@@ -13,7 +13,7 @@ import (
|
|
|
13
13
|
|
|
14
14
|
// The `body.<key>` form of an op's response transform: the mock wraps its
|
|
15
15
|
// payload in <key> so the transform can unwrap it again.
|
|
16
|
-
var envelopeResRe = regexp.MustCompile("^`body\\.(
|
|
16
|
+
var envelopeResRe = regexp.MustCompile("^`body\\.(.+)`$")
|
|
17
17
|
|
|
18
18
|
type TestFeature struct {
|
|
19
19
|
BaseFeature
|
|
@@ -78,7 +78,14 @@ func (f *TestFeature) Init(ctx *core.Context, options map[string]any) {
|
|
|
78
78
|
if m == nil {
|
|
79
79
|
return data
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
// Multi-segment on purpose: GraphQL ops unwrap body.data.<field>
|
|
82
|
+
// (and body.data.<field>.<entity> for mutations), not just one level.
|
|
83
|
+
segs := strings.Split(m[1], ".")
|
|
84
|
+
out := data
|
|
85
|
+
for i := len(segs) - 1; 0 <= i; i-- {
|
|
86
|
+
out = map[string]any{segs[i]: out}
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
respond := func(status int, data any, extra map[string]any) map[string]any {
|
|
@@ -346,10 +353,14 @@ func (f *TestFeature) buildArgs(ctx *core.Context, op *core.Operation, args map[
|
|
|
346
353
|
}
|
|
347
354
|
}
|
|
348
355
|
|
|
349
|
-
//
|
|
356
|
+
// Path AND query: a path-only read misses a query-addressed record
|
|
357
|
+
// (e.g. GET /result?trace_id=), which has no path param at all.
|
|
350
358
|
paramsPath := vs.GetPath([]any{"args", "params"}, point)
|
|
351
359
|
reqdParams := vs.Select(paramsPath, map[string]any{"reqd": true})
|
|
352
|
-
|
|
360
|
+
reqdFromParams := vs.Transform(reqdParams, []any{"`$EACH`", "", "`$KEY.name`"})
|
|
361
|
+
queryPath := vs.GetPath([]any{"args", "query"}, point)
|
|
362
|
+
reqdQuery := vs.Select(queryPath, map[string]any{"reqd": true})
|
|
363
|
+
reqdFromQuery := vs.Transform(reqdQuery, []any{"`$EACH`", "", "`$KEY.name`"})
|
|
353
364
|
|
|
354
365
|
qand := []any{}
|
|
355
366
|
q := map[string]any{"`$AND`": &qand}
|
|
@@ -357,8 +368,8 @@ func (f *TestFeature) buildArgs(ctx *core.Context, op *core.Operation, args map[
|
|
|
357
368
|
if args != nil {
|
|
358
369
|
for _, key := range vs.KeysOf(args) {
|
|
359
370
|
isId := key == "id"
|
|
360
|
-
|
|
361
|
-
|
|
371
|
+
isReqd := !vs.IsEmpty(vs.Select(reqdFromParams, key)) ||
|
|
372
|
+
!vs.IsEmpty(vs.Select(reqdFromQuery, key))
|
|
362
373
|
|
|
363
374
|
if isId || isReqd {
|
|
364
375
|
v := ctx.Utility.Param(ctx, key)
|
|
@@ -27,9 +27,10 @@ import (
|
|
|
27
27
|
sdk "GOMODULE"
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
// Features with a corpus section
|
|
31
|
-
// a
|
|
32
|
-
|
|
30
|
+
// Features with a corpus section are read from the corpus itself (see
|
|
31
|
+
// TestFeatureCorpus), so a project-authored section — a custom feature
|
|
32
|
+
// added under .sdk/test/feature/ — runs without editing this file. An
|
|
33
|
+
// SDK generated without a listed feature still skips, not fails.
|
|
33
34
|
|
|
34
35
|
// The standard operation names, in the order the runner prefers them. Every
|
|
35
36
|
// entity declares every CRUD method, so an op that the API does not define
|
|
@@ -450,6 +451,12 @@ func TestFeatureCorpus(t *testing.T) {
|
|
|
450
451
|
"corpus cannot exercise a feature without one")
|
|
451
452
|
}
|
|
452
453
|
|
|
454
|
+
featureCorpusNames := make([]string, 0, len(featureSection))
|
|
455
|
+
for name := range featureSection {
|
|
456
|
+
featureCorpusNames = append(featureCorpusNames, name)
|
|
457
|
+
}
|
|
458
|
+
sort.Strings(featureCorpusNames)
|
|
459
|
+
|
|
453
460
|
for _, name := range featureCorpusNames {
|
|
454
461
|
name := name
|
|
455
462
|
t.Run(name, func(t *testing.T) {
|
|
@@ -43,9 +43,10 @@ import JAVAPACKAGE.core.Utility;
|
|
|
43
43
|
@SuppressWarnings({"unchecked"})
|
|
44
44
|
public class FeatureCorpusTest {
|
|
45
45
|
|
|
46
|
-
// Features with a corpus section
|
|
47
|
-
//
|
|
48
|
-
|
|
46
|
+
// Features with a corpus section are read from the corpus itself (see
|
|
47
|
+
// featureCorpus), so a project-authored section - a custom feature added
|
|
48
|
+
// under .sdk/test/feature/ - runs without editing this file. An SDK
|
|
49
|
+
// generated without a listed feature still skips, not fails.
|
|
49
50
|
|
|
50
51
|
// The standard operation names, in the order the runner prefers them.
|
|
51
52
|
private static final List<String> FEATURE_CORPUS_OPS =
|
|
@@ -377,7 +378,7 @@ public class FeatureCorpusTest {
|
|
|
377
378
|
"this project's test.json has no `feature` section - recompile the "
|
|
378
379
|
+ "corpus (create-sdkgen .sdk/test/feature/) to run these cases");
|
|
379
380
|
|
|
380
|
-
for (String name :
|
|
381
|
+
for (String name : new TreeMap<>(features).keySet()) {
|
|
381
382
|
Object sectionRaw = features.get(name);
|
|
382
383
|
if (!(sectionRaw instanceof Map)) {
|
|
383
384
|
continue;
|
|
@@ -274,10 +274,17 @@ class TestFeature extends BaseFeature {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
// Path AND query: a path-only read misses a query-addressed record
|
|
278
|
+
// (e.g. GET /result?trace_id=), which has no path param at all.
|
|
279
|
+
const reqdParams = transform(
|
|
278
280
|
select(getpath(point, ['args', 'params']), { reqd: true }),
|
|
279
281
|
['`$EACH`', '', '`$KEY.name`']
|
|
280
282
|
)
|
|
283
|
+
const reqdQuery = transform(
|
|
284
|
+
select(getpath(point, ['args', 'query']), { reqd: true }),
|
|
285
|
+
['`$EACH`', '', '`$KEY.name`']
|
|
286
|
+
)
|
|
287
|
+
const reqd = [...(reqdParams || []), ...(reqdQuery || [])]
|
|
281
288
|
|
|
282
289
|
const qand = []
|
|
283
290
|
const q = { '`$AND`': qand }
|
|
@@ -41,11 +41,13 @@ function makeOptions(ctx) {
|
|
|
41
41
|
// Standard SDK option values.
|
|
42
42
|
const optspec = {
|
|
43
43
|
apikey: '',
|
|
44
|
+
secret: '',
|
|
44
45
|
base: 'http://localhost:8000',
|
|
45
46
|
prefix: '',
|
|
46
47
|
suffix: '',
|
|
47
48
|
auth: {
|
|
48
|
-
prefix: ''
|
|
49
|
+
prefix: '',
|
|
50
|
+
basic: false
|
|
49
51
|
},
|
|
50
52
|
headers: {
|
|
51
53
|
'`$CHILD`': '`$STRING`'
|
|
@@ -27,9 +27,19 @@ const { join } = require('node:path')
|
|
|
27
27
|
const { SDK, TEST_JSON_FILE } = require('../utility/index')
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
// Features with a corpus section
|
|
31
|
-
//
|
|
32
|
-
|
|
30
|
+
// Features with a corpus section — read from the corpus itself, so a
|
|
31
|
+
// project-authored section (a custom feature added under
|
|
32
|
+
// .sdk/test/feature/) runs without editing this file. Read eagerly: the
|
|
33
|
+
// node test runner collects the per-feature tests at describe time,
|
|
34
|
+
// before any `before` hook fires. An SDK generated without a listed
|
|
35
|
+
// feature still skips, not fails.
|
|
36
|
+
const FEATURES = Object.keys((() => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(
|
|
39
|
+
join(__dirname, '..', TEST_JSON_FILE), 'utf8')).feature || {}
|
|
40
|
+
}
|
|
41
|
+
catch (e) { return {} }
|
|
42
|
+
})()).sort()
|
|
33
43
|
|
|
34
44
|
|
|
35
45
|
// One operation this SDK can actually perform is described by
|
|
@@ -21,6 +21,16 @@ function recordingServer(reply) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
// A subtest that drives a second feature (netsim as the simulated network)
|
|
25
|
+
// can only run when this SDK was generated with it: the harness skips
|
|
26
|
+
// absent features, which would leave the scenario unsimulated and the
|
|
27
|
+
// assertions meaningless. Same convention as the other targets.
|
|
28
|
+
function skipWithout(name) {
|
|
29
|
+
return hasFeature(name) ? false
|
|
30
|
+
: ('this SDK was generated without the ' + name + ' feature')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
describe('feature', () => {
|
|
25
35
|
|
|
26
36
|
test('at least the test feature is present', () => {
|
|
@@ -95,7 +105,7 @@ describe('feature', () => {
|
|
|
95
105
|
// --- retry ----------------------------------------------------------------
|
|
96
106
|
if (hasFeature('retry')) describe('retry', () => {
|
|
97
107
|
|
|
98
|
-
test('retries transient failures then succeeds', async () => {
|
|
108
|
+
test('retries transient failures then succeeds', { skip: skipWithout('netsim') }, async () => {
|
|
99
109
|
const clock = makeClock()
|
|
100
110
|
const h = makeClient({ features: [
|
|
101
111
|
{ name: 'netsim', options: { failTimes: 2, failStatus: 503 } },
|
|
@@ -105,7 +115,7 @@ describe('feature', () => {
|
|
|
105
115
|
strictEqual(h.client._retry.attempts, 2)
|
|
106
116
|
})
|
|
107
117
|
|
|
108
|
-
test('gives up after the budget', async () => {
|
|
118
|
+
test('gives up after the budget', { skip: skipWithout('netsim') }, async () => {
|
|
109
119
|
const clock = makeClock()
|
|
110
120
|
const h = makeClient({ features: [
|
|
111
121
|
{ name: 'netsim', options: { failTimes: 9, failStatus: 500 } },
|
|
@@ -131,7 +141,7 @@ describe('feature', () => {
|
|
|
131
141
|
strictEqual(n, 3)
|
|
132
142
|
})
|
|
133
143
|
|
|
134
|
-
test('honours a server Retry-After', async () => {
|
|
144
|
+
test('honours a server Retry-After', { skip: skipWithout('netsim') }, async () => {
|
|
135
145
|
const clock = makeClock()
|
|
136
146
|
const h = makeClient({ features: [
|
|
137
147
|
{ name: 'netsim', options: { rateLimitTimes: 1, retryAfter: 2 } },
|
|
@@ -141,7 +151,7 @@ describe('feature', () => {
|
|
|
141
151
|
strictEqual(clock.time, 2000)
|
|
142
152
|
})
|
|
143
153
|
|
|
144
|
-
test('default jitter path still succeeds', async () => {
|
|
154
|
+
test('default jitter path still succeeds', { skip: skipWithout('netsim') }, async () => {
|
|
145
155
|
const h = makeClient({ features: [
|
|
146
156
|
{ name: 'netsim', options: { failTimes: 1 } },
|
|
147
157
|
{ name: 'retry', options: { retries: 2, minDelay: 0 } },
|
|
@@ -154,7 +164,7 @@ describe('feature', () => {
|
|
|
154
164
|
// --- timeout --------------------------------------------------------------
|
|
155
165
|
if (hasFeature('timeout')) describe('timeout', () => {
|
|
156
166
|
|
|
157
|
-
test('a slow request times out', async () => {
|
|
167
|
+
test('a slow request times out', { skip: skipWithout('netsim') }, async () => {
|
|
158
168
|
const h = makeClient({ features: [
|
|
159
169
|
{ name: 'netsim', options: { latency: 80 } },
|
|
160
170
|
{ name: 'timeout', options: { ms: 10 } },
|
|
@@ -319,7 +329,7 @@ describe('feature', () => {
|
|
|
319
329
|
// --- metrics --------------------------------------------------------------
|
|
320
330
|
if (hasFeature('metrics')) describe('metrics', () => {
|
|
321
331
|
|
|
322
|
-
test('counts ok and err per op', async () => {
|
|
332
|
+
test('counts ok and err per op', { skip: skipWithout('netsim') }, async () => {
|
|
323
333
|
const h = makeClient({ features: [
|
|
324
334
|
{ name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
|
|
325
335
|
{ name: 'metrics', options: {} },
|
|
@@ -352,7 +362,7 @@ describe('feature', () => {
|
|
|
352
362
|
ok(/^00-.+-.+-01$/.test(sent['traceparent']))
|
|
353
363
|
})
|
|
354
364
|
|
|
355
|
-
test('records a failed span on error', async () => {
|
|
365
|
+
test('records a failed span on error', { skip: skipWithout('netsim') }, async () => {
|
|
356
366
|
const h = makeClient({ features: [
|
|
357
367
|
{ name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
|
|
358
368
|
{ name: 'telemetry', options: {} },
|
|
@@ -377,7 +387,7 @@ describe('feature', () => {
|
|
|
377
387
|
strictEqual(seen[0].headers.authorization, '<redacted>')
|
|
378
388
|
})
|
|
379
389
|
|
|
380
|
-
test('captures failures', async () => {
|
|
390
|
+
test('captures failures', { skip: skipWithout('netsim') }, async () => {
|
|
381
391
|
const h = makeClient({ features: [
|
|
382
392
|
{ name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
|
|
383
393
|
{ name: 'debug', options: {} },
|
|
@@ -391,7 +401,7 @@ describe('feature', () => {
|
|
|
391
401
|
// --- audit ----------------------------------------------------------------
|
|
392
402
|
if (hasFeature('audit')) describe('audit', () => {
|
|
393
403
|
|
|
394
|
-
test('one record per op with sink + actor', async () => {
|
|
404
|
+
test('one record per op with sink + actor', { skip: skipWithout('netsim') }, async () => {
|
|
395
405
|
const sink = []
|
|
396
406
|
const h = makeClient({ features: [
|
|
397
407
|
{ name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
|
|
@@ -479,6 +479,190 @@ def pagingFeature : SIO Feature := do
|
|
|
479
479
|
(newMap #[("pages", .num 0.0), ("items", .num 0.0)])
|
|
480
480
|
bumpNum b "items" n.toFloat }
|
|
481
481
|
|
|
482
|
+
-- ---------------------------------------------------------------------------
|
|
483
|
+
-- cost
|
|
484
|
+
-- ---------------------------------------------------------------------------
|
|
485
|
+
-- Prices every transport ATTEMPT and commits the spend once per OPERATION.
|
|
486
|
+
-- Mirrors tm/ts/src/feature/cost/CostFeature.ts.
|
|
487
|
+
--
|
|
488
|
+
-- ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
489
|
+
-- cache is charged for money that was never spent.
|
|
490
|
+
|
|
491
|
+
def costFeature : SIO Feature := do
|
|
492
|
+
let optsR ← IO.mkRef (← emptyMap)
|
|
493
|
+
-- Accumulated per call and committed once: adding each attempt to the
|
|
494
|
+
-- running total and subtracting it again loses precision.
|
|
495
|
+
let pendR ← IO.mkRef (← newMap #[("attempts", Value.num 0.0), ("amount", Value.num 0.0),
|
|
496
|
+
("reported", Value.num 0.0), ("estimated", Value.num 0.0),
|
|
497
|
+
("source", Value.str "none")])
|
|
498
|
+
let seqR ← IO.mkRef 0
|
|
499
|
+
|
|
500
|
+
let record : Value → SIO Value := fun ctx => do
|
|
501
|
+
let o ← optsR.get
|
|
502
|
+
let cur ← optStr o "currency" "USD"
|
|
503
|
+
let lim ← optNum o "budget" 0.0
|
|
504
|
+
let total ← newMap #[("calls", Value.num 0.0), ("attempts", Value.num 0.0),
|
|
505
|
+
("amount", Value.num 0.0), ("reported", Value.num 0.0),
|
|
506
|
+
("estimated", Value.num 0.0)]
|
|
507
|
+
let ops ← emptyMap
|
|
508
|
+
let actors ← emptyMap
|
|
509
|
+
let budget ← newMap #[("limit", Value.num lim), ("spent", Value.num 0.0),
|
|
510
|
+
("remaining", Value.num lim), ("exceeded", Value.bool false)]
|
|
511
|
+
trackCtx ctx "cost" (newMap #[("currency", Value.str cur), ("total", total),
|
|
512
|
+
("ops", ops), ("actors", actors), ("budget", budget),
|
|
513
|
+
("last", Value.noval)])
|
|
514
|
+
|
|
515
|
+
let resetPending : SIO Unit := do
|
|
516
|
+
let p ← newMap #[("attempts", Value.num 0.0), ("amount", Value.num 0.0),
|
|
517
|
+
("reported", Value.num 0.0), ("estimated", Value.num 0.0),
|
|
518
|
+
("source", Value.str "none")]
|
|
519
|
+
pendR.set p
|
|
520
|
+
|
|
521
|
+
-- Pricing precedence: a server-stated header beats the rate table, which
|
|
522
|
+
-- beats the flat unit.
|
|
523
|
+
let priceOf : Value → Value → SIO (Float × String) := fun ctx res => do
|
|
524
|
+
let o ← optsR.get
|
|
525
|
+
let perUnit ← optNum o "perUnit" 0.0
|
|
526
|
+
let hname ← optStr o "header" ""
|
|
527
|
+
let hv ← if hname == "" then pure Value.noval else headerCI (← gp res "headers") hname
|
|
528
|
+
let hn := numOf hv (-1.0)
|
|
529
|
+
if hname != "" && hn >= 0.0 then
|
|
530
|
+
pure (hn * perUnit, "header")
|
|
531
|
+
else do
|
|
532
|
+
let rates ← gp o "rates"
|
|
533
|
+
let opname ← SdkUtility.opnameOf ctx
|
|
534
|
+
-- The documented grammar is '<entity>.<op>', then '<op>', then '*'.
|
|
535
|
+
-- Checking only the bare name charged `{'widget.load': 0.25, '*': 0.01}`
|
|
536
|
+
-- a widget load at 0.01, unlike every other port.
|
|
537
|
+
let ent ← gpS (← gp ctx "op") "entity"
|
|
538
|
+
let byEntOp := numOf (← gp rates (ent ++ "." ++ opname)) (-1.0)
|
|
539
|
+
let byOp := numOf (← gp rates opname) (-1.0)
|
|
540
|
+
let byAny := numOf (← gp rates "*") (-1.0)
|
|
541
|
+
if byEntOp >= 0.0 then pure (byEntOp, "table")
|
|
542
|
+
else if byOp >= 0.0 then pure (byOp, "table")
|
|
543
|
+
else if byAny >= 0.0 then pure (byAny, "table")
|
|
544
|
+
else do
|
|
545
|
+
let unit ← optNum o "unit" 0.0
|
|
546
|
+
if unit != 0.0 then pure (unit, "unit") else pure (0.0, "none")
|
|
547
|
+
|
|
548
|
+
let bucketOf : Value → String → SIO Value := fun bucket key => do
|
|
549
|
+
let b ← gp bucket key
|
|
550
|
+
match b with
|
|
551
|
+
| Value.map _ => pure b
|
|
552
|
+
| _ => do
|
|
553
|
+
let m ← newMap #[("calls", Value.num 0.0), ("amount", Value.num 0.0)]
|
|
554
|
+
sp bucket key m
|
|
555
|
+
pure m
|
|
556
|
+
|
|
557
|
+
let commit : Value → SIO Unit := fun ctx => do
|
|
558
|
+
let o ← optsR.get
|
|
559
|
+
let p ← pendR.get
|
|
560
|
+
let rec_ ← record ctx
|
|
561
|
+
|
|
562
|
+
-- A body usage figure prices the WHOLE call, so it replaces the
|
|
563
|
+
-- per-attempt estimate rather than adding to it - and, being
|
|
564
|
+
-- server-stated, the whole amount counts as reported. Read here, not at
|
|
565
|
+
-- the transport seam, because the body is one-shot.
|
|
566
|
+
let path ← optStr o "path" ""
|
|
567
|
+
let perUnit ← optNum o "perUnit" 0.0
|
|
568
|
+
let bodyAmt ← if path == "" then pure (-1.0) else do
|
|
569
|
+
let body ← gp (← gp ctx "result") "body"
|
|
570
|
+
pure (numOf (← getpath body (Value.str path)) (-1.0))
|
|
571
|
+
|
|
572
|
+
-- Bind the pending figures first: `←` cannot be nested inside an
|
|
573
|
+
-- if-then-else that is not itself a `do` block.
|
|
574
|
+
let pendAmount := numOf (← gp p "amount") 0.0
|
|
575
|
+
let pendReported := numOf (← gp p "reported") 0.0
|
|
576
|
+
let pendEstimated := numOf (← gp p "estimated") 0.0
|
|
577
|
+
let amount := if bodyAmt >= 0.0 then bodyAmt * perUnit else pendAmount
|
|
578
|
+
let reported := if bodyAmt >= 0.0 then amount else pendReported
|
|
579
|
+
let estimated := if bodyAmt >= 0.0 then 0.0 else pendEstimated
|
|
580
|
+
if bodyAmt >= 0.0 then sp p "source" (Value.str "body")
|
|
581
|
+
|
|
582
|
+
let total ← gp rec_ "total"
|
|
583
|
+
bumpNum total "amount" amount
|
|
584
|
+
bumpNum total "reported" reported
|
|
585
|
+
bumpNum total "estimated" estimated
|
|
586
|
+
bumpNum total "calls" 1.0
|
|
587
|
+
|
|
588
|
+
let budget ← gp rec_ "budget"
|
|
589
|
+
let lim := numOf (← gp budget "limit") 0.0
|
|
590
|
+
let spent := numOf (← gp total "amount") 0.0
|
|
591
|
+
sp budget "spent" (Value.num spent)
|
|
592
|
+
sp budget "remaining"
|
|
593
|
+
(Value.num (if lim > 0.0 then (if lim - spent > 0.0 then lim - spent else 0.0) else 0.0))
|
|
594
|
+
if lim > 0.0 && spent >= lim then sp budget "exceeded" (Value.bool true)
|
|
595
|
+
|
|
596
|
+
let opname ← SdkUtility.opnameOf ctx
|
|
597
|
+
let actorV ← gp (← gp ctx "ctrl") "actor"
|
|
598
|
+
let actorS := match actorV with | Value.str a => a | _ => ""
|
|
599
|
+
let actor ← if actorS != "" then pure actorS else optStr o "actor" "anonymous"
|
|
600
|
+
|
|
601
|
+
let opB ← bucketOf (← gp rec_ "ops") ("_." ++ opname)
|
|
602
|
+
bumpNum opB "calls" 1.0
|
|
603
|
+
bumpNum opB "amount" amount
|
|
604
|
+
let acB ← bucketOf (← gp rec_ "actors") actor
|
|
605
|
+
bumpNum acB "calls" 1.0
|
|
606
|
+
bumpNum acB "amount" amount
|
|
607
|
+
|
|
608
|
+
let n ← seqR.modifyGet fun n => (n + 1, n + 1)
|
|
609
|
+
let last ← newMap #[("seq", Value.num n.toFloat), ("op", Value.str opname),
|
|
610
|
+
("actor", Value.str actor), ("amount", Value.num amount),
|
|
611
|
+
("currency", ← gp rec_ "currency"), ("source", ← gp p "source"),
|
|
612
|
+
("attempts", ← gp p "attempts")]
|
|
613
|
+
sp rec_ "last" last
|
|
614
|
+
resetPending
|
|
615
|
+
|
|
616
|
+
pure { name := "cost"
|
|
617
|
+
, init := fun ctx opts => do
|
|
618
|
+
let om ← toOptsMap opts
|
|
619
|
+
optsR.set om
|
|
620
|
+
resetPending
|
|
621
|
+
if (← optActive opts) then do
|
|
622
|
+
let _ ← record ctx
|
|
623
|
+
let client ← clientOf ctx
|
|
624
|
+
let inner ← getFetcher client
|
|
625
|
+
setFetcher client fun c u f => do
|
|
626
|
+
-- The transport answers (response, error): a rejecting attempt
|
|
627
|
+
-- still costs, or a run of failures under retry would be free.
|
|
628
|
+
let (res, rerr) ← inner c u f
|
|
629
|
+
let (amount, source) ← priceOf c res
|
|
630
|
+
let p ← pendR.get
|
|
631
|
+
bumpNum p "attempts" 1.0
|
|
632
|
+
bumpNum p "amount" amount
|
|
633
|
+
bumpNum p (if source == "header" then "reported" else "estimated") amount
|
|
634
|
+
sp p "source" (Value.str source)
|
|
635
|
+
let rec_ ← record c
|
|
636
|
+
bumpNum (← gp rec_ "total") "attempts" 1.0
|
|
637
|
+
pure (res, rerr)
|
|
638
|
+
, hook := fun stage ctx => do
|
|
639
|
+
let o ← optsR.get
|
|
640
|
+
if stage == "PrePoint" then do
|
|
641
|
+
let lim ← optNum o "budget" 0.0
|
|
642
|
+
if lim > 0.0 then do
|
|
643
|
+
let rec_ ← record ctx
|
|
644
|
+
let spent := numOf (← gp (← gp rec_ "total") "amount") 0.0
|
|
645
|
+
if spent >= lim then do
|
|
646
|
+
sp (← gp rec_ "budget") "exceeded" (Value.bool true)
|
|
647
|
+
if (← optStr o "onBudget" "warn") == "deny" then do
|
|
648
|
+
let e ← SdkUtility.mkErr "cost_budget"
|
|
649
|
+
("Cost budget of " ++ toString lim ++ " is spent")
|
|
650
|
+
let out ← SdkUtility.gpMap ctx "out"
|
|
651
|
+
sp out "point" e
|
|
652
|
+
else if stage == "PreDone" then do
|
|
653
|
+
-- A cache hit is a real call that cost nothing, so it commits
|
|
654
|
+
-- too: that is the point of ordering cost inside the cache.
|
|
655
|
+
commit ctx
|
|
656
|
+
else if stage == "PreUnexpected" then do
|
|
657
|
+
-- A failed operation never reaches PreDone, so without this its
|
|
658
|
+
-- attempts are priced and then discarded: repeated connection
|
|
659
|
+
-- failures would slip past an onBudget "deny" ceiling, and the
|
|
660
|
+
-- shared pending value would survive to be attributed to the
|
|
661
|
+
-- next successful call. A call that made NO attempt was refused
|
|
662
|
+
-- before the network and must not be counted.
|
|
663
|
+
let p ← pendR.get
|
|
664
|
+
if numOf (← gp p "attempts") 0.0 > 0.0 then commit ctx else resetPending }
|
|
665
|
+
|
|
482
666
|
def streamingFeature : SIO Feature := do
|
|
483
667
|
let optsR ← IO.mkRef (← emptyMap)
|
|
484
668
|
pure { name := "streaming"
|
|
@@ -591,12 +775,13 @@ def makeFeature (name : String) : SIO Feature :=
|
|
|
591
775
|
| "streaming" => streamingFeature
|
|
592
776
|
| "proxy" => proxyFeature
|
|
593
777
|
| "netsim" => netsimFeature
|
|
778
|
+
| "cost" => costFeature
|
|
594
779
|
| _ => baseFeature
|
|
595
780
|
|
|
596
781
|
/-- The catalog order: transport wrappers compose in this order. -/
|
|
597
782
|
def featureNames : Array String :=
|
|
598
783
|
#["log", "rbac", "idempotency", "clienttrack", "paging", "streaming",
|
|
599
784
|
"metrics", "telemetry", "debug", "audit",
|
|
600
|
-
"cache", "ratelimit", "timeout", "retry", "proxy", "netsim"]
|
|
785
|
+
"cost", "cache", "ratelimit", "timeout", "retry", "proxy", "netsim"]
|
|
601
786
|
|
|
602
787
|
end SdkFeatures
|
|
@@ -61,11 +61,21 @@ function TestFeature:init(ctx, options)
|
|
|
61
61
|
if type(restf) ~= "string" then
|
|
62
62
|
return data
|
|
63
63
|
end
|
|
64
|
-
local
|
|
65
|
-
if
|
|
64
|
+
local path = string.match(restf, "^`body%.(.+)`$")
|
|
65
|
+
if path == nil then
|
|
66
66
|
return data
|
|
67
67
|
end
|
|
68
|
-
|
|
68
|
+
-- Multi-segment on purpose: GraphQL ops unwrap body.data.<field> (and
|
|
69
|
+
-- body.data.<field>.<entity> for mutations), not just one level.
|
|
70
|
+
local segs = {}
|
|
71
|
+
for seg in string.gmatch(path, "[^.]+") do
|
|
72
|
+
table.insert(segs, seg)
|
|
73
|
+
end
|
|
74
|
+
local out = data
|
|
75
|
+
for i = #segs, 1, -1 do
|
|
76
|
+
out = { [segs[i]] = out }
|
|
77
|
+
end
|
|
78
|
+
return out
|
|
69
79
|
end
|
|
70
80
|
|
|
71
81
|
local function respond(status, data, extra)
|
|
@@ -309,10 +319,14 @@ function TestFeature:build_args(ctx, op, args)
|
|
|
309
319
|
local points = vs.getpath(ctx.config, "entity." .. ctx.entity:get_name() .. ".op." .. opname .. ".points")
|
|
310
320
|
local point = vs.getelem(points, -1)
|
|
311
321
|
|
|
312
|
-
--
|
|
322
|
+
-- Path AND query: a path-only read misses a query-addressed record
|
|
323
|
+
-- (e.g. GET /result?trace_id=), which has no path param at all.
|
|
313
324
|
local params_path = vs.getpath(point, "args.params")
|
|
314
325
|
local reqd_params = vs.select(params_path, { reqd = true })
|
|
315
|
-
local
|
|
326
|
+
local reqd_from_params = vs.transform(reqd_params, { "`$EACH`", "", "`$KEY.name`" })
|
|
327
|
+
local query_path = vs.getpath(point, "args.query")
|
|
328
|
+
local reqd_query = vs.select(query_path, { reqd = true })
|
|
329
|
+
local reqd_from_query = vs.transform(reqd_query, { "`$EACH`", "", "`$KEY.name`" })
|
|
316
330
|
|
|
317
331
|
local qand = {}
|
|
318
332
|
local q = { ["`$AND`"] = qand }
|
|
@@ -322,8 +336,8 @@ function TestFeature:build_args(ctx, op, args)
|
|
|
322
336
|
if keys ~= nil then
|
|
323
337
|
for _, key in ipairs(keys) do
|
|
324
338
|
local is_id = (key == "id")
|
|
325
|
-
local
|
|
326
|
-
|
|
339
|
+
local is_reqd = not vs.isempty(vs.select(reqd_from_params, key))
|
|
340
|
+
or not vs.isempty(vs.select(reqd_from_query, key))
|
|
327
341
|
|
|
328
342
|
if is_id or is_reqd then
|
|
329
343
|
local v = ctx.utility.param(ctx, key)
|