@voxgig/sdkgen 4.4.0 → 4.5.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/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/Config_ocaml.ts +13 -1
- 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/features.clj +175 -0
- package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
- package/project/.sdk/tm/go/feature/test_feature.go +17 -6
- package/project/.sdk/tm/go/test/feature_corpus_test.go +28 -3
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +13 -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 +19 -4
- 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 +229 -0
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +18 -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/feature_harness.py +6 -0
- package/project/.sdk/tm/py/test/test_feature.py +40 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +20 -3
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +18 -5
- package/project/.sdk/tm/rust/feature/cost.rs +25 -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 +25 -4
- 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
|
@@ -105,6 +105,10 @@ class TestNetsimFeature:
|
|
|
105
105
|
@pytest.mark.skipif(not has_feature("retry"), reason="retry not in this SDK")
|
|
106
106
|
class TestRetryFeature:
|
|
107
107
|
|
|
108
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
109
|
+
# SDK was generated with it (the harness skips absent features).
|
|
110
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
111
|
+
reason="netsim not in this SDK")
|
|
108
112
|
def test_retries_transient_failures_then_succeeds(self):
|
|
109
113
|
clock = make_clock()
|
|
110
114
|
h = make_client([
|
|
@@ -115,6 +119,10 @@ class TestRetryFeature:
|
|
|
115
119
|
assert h.op(op="load")["ok"] is True
|
|
116
120
|
assert h.client._retry["attempts"] == 2
|
|
117
121
|
|
|
122
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
123
|
+
# SDK was generated with it (the harness skips absent features).
|
|
124
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
125
|
+
reason="netsim not in this SDK")
|
|
118
126
|
def test_gives_up_after_the_budget(self):
|
|
119
127
|
clock = make_clock()
|
|
120
128
|
h = make_client([
|
|
@@ -163,6 +171,10 @@ class TestRetryFeature:
|
|
|
163
171
|
assert h.op(op="load")["ok"] is True
|
|
164
172
|
assert state["n"] == 2
|
|
165
173
|
|
|
174
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
175
|
+
# SDK was generated with it (the harness skips absent features).
|
|
176
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
177
|
+
reason="netsim not in this SDK")
|
|
166
178
|
def test_honours_a_server_retry_after(self):
|
|
167
179
|
clock = make_clock()
|
|
168
180
|
h = make_client([
|
|
@@ -174,6 +186,10 @@ class TestRetryFeature:
|
|
|
174
186
|
assert h.op(op="load")["ok"] is True
|
|
175
187
|
assert clock.time == 2000
|
|
176
188
|
|
|
189
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
190
|
+
# SDK was generated with it (the harness skips absent features).
|
|
191
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
192
|
+
reason="netsim not in this SDK")
|
|
177
193
|
def test_default_jitter_path_still_succeeds(self):
|
|
178
194
|
h = make_client([
|
|
179
195
|
{"name": "netsim", "options": {"failTimes": 1}},
|
|
@@ -202,6 +218,10 @@ class TestRetryFeature:
|
|
|
202
218
|
@pytest.mark.skipif(not has_feature("timeout"), reason="timeout not in this SDK")
|
|
203
219
|
class TestTimeoutFeature:
|
|
204
220
|
|
|
221
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
222
|
+
# SDK was generated with it (the harness skips absent features).
|
|
223
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
224
|
+
reason="netsim not in this SDK")
|
|
205
225
|
def test_a_slow_request_times_out(self):
|
|
206
226
|
clock = make_clock()
|
|
207
227
|
h = make_client([
|
|
@@ -438,6 +458,10 @@ class TestRbacFeature:
|
|
|
438
458
|
@pytest.mark.skipif(not has_feature("metrics"), reason="metrics not in this SDK")
|
|
439
459
|
class TestMetricsFeature:
|
|
440
460
|
|
|
461
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
462
|
+
# SDK was generated with it (the harness skips absent features).
|
|
463
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
464
|
+
reason="netsim not in this SDK")
|
|
441
465
|
def test_counts_ok_and_err_per_op(self):
|
|
442
466
|
h = make_client([
|
|
443
467
|
{"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
|
|
@@ -490,6 +514,10 @@ class TestTelemetryFeature:
|
|
|
490
514
|
assert sent["X-Trace-Id"] == h.client._telemetry["spans"][0]["traceId"]
|
|
491
515
|
assert re.match(r"^00-.+-.+-01$", sent["traceparent"])
|
|
492
516
|
|
|
517
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
518
|
+
# SDK was generated with it (the harness skips absent features).
|
|
519
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
520
|
+
reason="netsim not in this SDK")
|
|
493
521
|
def test_records_a_failed_span_on_error(self):
|
|
494
522
|
h = make_client([
|
|
495
523
|
{"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
|
|
@@ -498,6 +526,10 @@ class TestTelemetryFeature:
|
|
|
498
526
|
h.op(op="load")
|
|
499
527
|
assert h.client._telemetry["spans"][0]["ok"] is False
|
|
500
528
|
|
|
529
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
530
|
+
# SDK was generated with it (the harness skips absent features).
|
|
531
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
532
|
+
reason="netsim not in this SDK")
|
|
501
533
|
def test_closes_each_span_exactly_once(self):
|
|
502
534
|
h = make_client([
|
|
503
535
|
{"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
|
|
@@ -542,6 +574,10 @@ class TestDebugFeature:
|
|
|
542
574
|
assert len(seen) == 2
|
|
543
575
|
assert seen[0]["headers"]["authorization"] == "<redacted>"
|
|
544
576
|
|
|
577
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
578
|
+
# SDK was generated with it (the harness skips absent features).
|
|
579
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
580
|
+
reason="netsim not in this SDK")
|
|
545
581
|
def test_captures_failures(self):
|
|
546
582
|
h = make_client([
|
|
547
583
|
{"name": "netsim", "options": {"failTimes": 1, "failStatus": 500}},
|
|
@@ -571,6 +607,10 @@ class TestDebugFeature:
|
|
|
571
607
|
@pytest.mark.skipif(not has_feature("audit"), reason="audit not in this SDK")
|
|
572
608
|
class TestAuditFeature:
|
|
573
609
|
|
|
610
|
+
# Drives netsim as the simulated network: runnable only when this
|
|
611
|
+
# SDK was generated with it (the harness skips absent features).
|
|
612
|
+
@pytest.mark.skipif(not has_feature("netsim"),
|
|
613
|
+
reason="netsim not in this SDK")
|
|
574
614
|
def test_one_record_per_op_with_sink_and_actor(self):
|
|
575
615
|
sink = []
|
|
576
616
|
h = make_client([
|
|
@@ -23,9 +23,20 @@ from projectname_sdk import ProjectNameSDK
|
|
|
23
23
|
|
|
24
24
|
_TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
25
25
|
|
|
26
|
-
# Features with a corpus section
|
|
27
|
-
#
|
|
28
|
-
|
|
26
|
+
# Features with a corpus section — read from the corpus itself, so a
|
|
27
|
+
# project-authored section (a custom feature added under
|
|
28
|
+
# .sdk/test/feature/) runs without editing this file. Read eagerly:
|
|
29
|
+
# parametrize needs the names at collection time. An SDK generated
|
|
30
|
+
# without a listed feature still skips, not fails.
|
|
31
|
+
def _corpus_feature_names():
|
|
32
|
+
try:
|
|
33
|
+
with open(os.path.join(_TEST_DIR, "../../.sdk/test/test.json"), "r") as f:
|
|
34
|
+
return sorted((json.loads(f.read()).get("feature") or {}).keys())
|
|
35
|
+
except OSError:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
FEATURE_CORPUS_NAMES = _corpus_feature_names()
|
|
29
40
|
|
|
30
41
|
# The standard operation names, in the order the runner prefers them.
|
|
31
42
|
FEATURE_CORPUS_OPS = ["load", "list", "create", "update", "remove"]
|
|
@@ -129,6 +140,12 @@ def _candidates(client):
|
|
|
129
140
|
"accessor": accessor,
|
|
130
141
|
"op": opname,
|
|
131
142
|
})
|
|
143
|
+
|
|
144
|
+
# SAFE OPS FIRST - see the ts harness for the reasoning: the cache stores
|
|
145
|
+
# only successful GETs, so an SDK whose first usable op is a `create`
|
|
146
|
+
# (POST) can never satisfy "a hit served from cache costs nothing".
|
|
147
|
+
safe = {"list": 0, "load": 1}
|
|
148
|
+
out.sort(key=lambda o: (safe.get(o["op"], 2), o["key"]))
|
|
132
149
|
return out
|
|
133
150
|
|
|
134
151
|
|
|
@@ -19,9 +19,10 @@ require_relative "../ProjectName_sdk"
|
|
|
19
19
|
|
|
20
20
|
class FeatureCorpusTest < Minitest::Test
|
|
21
21
|
|
|
22
|
-
# Features with a corpus section
|
|
23
|
-
#
|
|
24
|
-
|
|
22
|
+
# Features with a corpus section are read from the corpus itself (see
|
|
23
|
+
# test_feature_corpus), so a project-authored section - a custom feature
|
|
24
|
+
# added under .sdk/test/feature/ - runs without editing this file. An SDK
|
|
25
|
+
# generated without a listed feature still skips, not fails.
|
|
25
26
|
|
|
26
27
|
# The standard operation names, in the order the runner prefers them.
|
|
27
28
|
FEATURE_CORPUS_OPS = %w[load list create update remove].freeze
|
|
@@ -108,7 +109,12 @@ class FeatureCorpusTest < Minitest::Test
|
|
|
108
109
|
out << { "key" => "#{entname}.#{opname}", "accessor" => accessor, "op" => opname }
|
|
109
110
|
end
|
|
110
111
|
end
|
|
111
|
-
|
|
112
|
+
|
|
113
|
+
# SAFE OPS FIRST - see the ts harness for the reasoning: the cache stores
|
|
114
|
+
# only successful GETs, so an SDK whose first usable op is a `create`
|
|
115
|
+
# (POST) can never satisfy "a hit served from cache costs nothing".
|
|
116
|
+
safe = { "list" => 0, "load" => 1 }
|
|
117
|
+
out.sort_by { |o| [safe.fetch(o["op"], 2), o["key"]] }
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
def invoke(client, op, ctrl)
|
|
@@ -187,6 +193,13 @@ class FeatureCorpusTest < Minitest::Test
|
|
|
187
193
|
end
|
|
188
194
|
|
|
189
195
|
def record(client, name)
|
|
196
|
+
# A corpus key is any manifest item name, so it may carry characters
|
|
197
|
+
# (foo-bar, foo.2) that cannot form a Ruby instance-variable name. The
|
|
198
|
+
# probe uses this lookup to SKIP such a section, so answer nil rather
|
|
199
|
+
# than letting instance_variable_get raise NameError for a feature this
|
|
200
|
+
# SDK could never have stored under that name anyway.
|
|
201
|
+
return nil unless name.match?(/\A[A-Za-z0-9_]+\z/)
|
|
202
|
+
|
|
190
203
|
client.instance_variable_get(:"@_#{name}")
|
|
191
204
|
end
|
|
192
205
|
|
|
@@ -213,7 +226,7 @@ class FeatureCorpusTest < Minitest::Test
|
|
|
213
226
|
end
|
|
214
227
|
|
|
215
228
|
def test_feature_corpus
|
|
216
|
-
|
|
229
|
+
(corpus["feature"] || {}).keys.sort.each do |name|
|
|
217
230
|
section = (corpus["feature"] || {})[name]
|
|
218
231
|
next if section.nil?
|
|
219
232
|
|
|
@@ -41,7 +41,10 @@ pub struct CostBucket {
|
|
|
41
41
|
pub amount: f64,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// NOT `#[derive(Default)]`: the struct holds a `Value`, and `Value` has no
|
|
45
|
+
// `Default` impl — deriving it does not compile. This file only ships when the
|
|
46
|
+
// cost feature is active, so no SDK had ever built it. `Value::Noval` is the
|
|
47
|
+
// absent value, which is what a fresh track should carry.
|
|
45
48
|
pub struct CostTrack {
|
|
46
49
|
// Aggregates (mirrors the ts client._cost record).
|
|
47
50
|
pub currency: String,
|
|
@@ -60,6 +63,27 @@ pub struct CostTrack {
|
|
|
60
63
|
pub seq: i64,
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
impl Default for CostTrack {
|
|
67
|
+
fn default() -> Self {
|
|
68
|
+
CostTrack {
|
|
69
|
+
currency: String::new(),
|
|
70
|
+
calls: 0,
|
|
71
|
+
attempts: 0,
|
|
72
|
+
amount: 0.0,
|
|
73
|
+
reported: 0.0,
|
|
74
|
+
estimated: 0.0,
|
|
75
|
+
ops: HashMap::new(),
|
|
76
|
+
actors: HashMap::new(),
|
|
77
|
+
limit: 0.0,
|
|
78
|
+
spent: 0.0,
|
|
79
|
+
remaining: 0.0,
|
|
80
|
+
exceeded: false,
|
|
81
|
+
last: Value::Noval,
|
|
82
|
+
seq: 0,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
pub struct CostFeature {
|
|
64
88
|
pub name: String,
|
|
65
89
|
pub active: bool,
|
|
@@ -43,6 +43,13 @@ function ownIdField(config: any, getpath: any, entityName: string): string {
|
|
|
43
43
|
// key, so move on to the next op rather than guess from it.
|
|
44
44
|
const lastPart = 0 < parts.length ? String(parts[parts.length - 1]) : ''
|
|
45
45
|
if (lastPart.startsWith('{')) return lastPart.slice(1, -1)
|
|
46
|
+
|
|
47
|
+
// No path param at all (or the route ends in a literal, e.g.
|
|
48
|
+
// /orgs/{org}/private-registries/public-key): a single required QUERY
|
|
49
|
+
// param can still be the record's own key (e.g. GET /result?trace_id=).
|
|
50
|
+
const query = (best && best.args && best.args.query) || []
|
|
51
|
+
const reqdQuery = query.filter((q: any) => false !== q.reqd)
|
|
52
|
+
if (1 === reqdQuery.length) return String(reqdQuery[0].name)
|
|
46
53
|
}
|
|
47
54
|
return 'id'
|
|
48
55
|
}
|
|
@@ -332,10 +339,17 @@ class TestFeature extends BaseFeature {
|
|
|
332
339
|
}
|
|
333
340
|
}
|
|
334
341
|
|
|
335
|
-
|
|
342
|
+
// Path AND query: a path-only read misses a query-addressed record
|
|
343
|
+
// (e.g. GET /result?trace_id=), which has no path param at all.
|
|
344
|
+
const reqdParams = transform(
|
|
336
345
|
select(getpath(point, ['args', 'params']), { reqd: true }),
|
|
337
346
|
['`$EACH`', '', '`$KEY.name`']
|
|
338
347
|
)
|
|
348
|
+
const reqdQuery = transform(
|
|
349
|
+
select(getpath(point, ['args', 'query']), { reqd: true }),
|
|
350
|
+
['`$EACH`', '', '`$KEY.name`']
|
|
351
|
+
)
|
|
352
|
+
const reqd = [...(reqdParams || []), ...(reqdQuery || [])]
|
|
339
353
|
|
|
340
354
|
const qand: any[] = []
|
|
341
355
|
const q = { '`$AND`': qand }
|
|
@@ -44,11 +44,13 @@ function makeOptions(ctx: Context) {
|
|
|
44
44
|
// Standard SDK option values.
|
|
45
45
|
const optspec = {
|
|
46
46
|
apikey: '',
|
|
47
|
+
secret: '',
|
|
47
48
|
base: 'http://localhost:8000',
|
|
48
49
|
prefix: '',
|
|
49
50
|
suffix: '',
|
|
50
51
|
auth: {
|
|
51
|
-
prefix: ''
|
|
52
|
+
prefix: '',
|
|
53
|
+
basic: false
|
|
52
54
|
},
|
|
53
55
|
headers: {
|
|
54
56
|
'`$CHILD`': '`$STRING`'
|
|
@@ -5,6 +5,7 @@ import { Context, Spec } from '../types'
|
|
|
5
5
|
const HEADER_auth = 'authorization'
|
|
6
6
|
|
|
7
7
|
const OPTION_apikey = 'apikey'
|
|
8
|
+
const OPTION_secret = 'secret'
|
|
8
9
|
|
|
9
10
|
const NOTFOUND = '__NOTFOUND__'
|
|
10
11
|
|
|
@@ -40,6 +41,25 @@ function prepareAuth(ctx: Context): Spec | Error {
|
|
|
40
41
|
|
|
41
42
|
const apikey = getprop(options, OPTION_apikey, NOTFOUND)
|
|
42
43
|
|
|
44
|
+
// True HTTP Basic Auth needs TWO credentials, base64-joined - a single
|
|
45
|
+
// token in the header (the branch below) can never authenticate against
|
|
46
|
+
// an API that actually checks `Authorization: Basic base64(user:pass)`.
|
|
47
|
+
if (true === options.auth.basic) {
|
|
48
|
+
const secret = getprop(options, OPTION_secret, NOTFOUND)
|
|
49
|
+
const noApikey = NOTFOUND === apikey || null == apikey || '' === apikey
|
|
50
|
+
const noSecret = NOTFOUND === secret || null == secret || '' === secret
|
|
51
|
+
|
|
52
|
+
if (noApikey || noSecret) {
|
|
53
|
+
delprop(headers, HEADER_auth)
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const b64 = Buffer.from(apikey + ':' + secret).toString('base64')
|
|
57
|
+
setprop(headers, HEADER_auth, prefix ? prefix + ' ' + b64 : b64)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return spec
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
if (NOTFOUND === apikey || null == apikey || '' === apikey) {
|
|
44
64
|
delprop(headers, HEADER_auth)
|
|
45
65
|
}
|
|
@@ -25,9 +25,19 @@ import { join } from 'node:path'
|
|
|
25
25
|
import { SDK, TEST_JSON_FILE } from '../utility/index'
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
// Features with a corpus section
|
|
29
|
-
//
|
|
30
|
-
|
|
28
|
+
// Features with a corpus section — read from the corpus itself, so a
|
|
29
|
+
// project-authored section (a custom feature added under
|
|
30
|
+
// .sdk/test/feature/) runs without editing this file. Read eagerly: the
|
|
31
|
+
// node test runner collects the per-feature tests at describe time,
|
|
32
|
+
// before any `before` hook fires. An SDK generated without a listed
|
|
33
|
+
// feature still skips, not fails.
|
|
34
|
+
const FEATURES = Object.keys((() => {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(
|
|
37
|
+
join(__dirname, '..', TEST_JSON_FILE), 'utf8')).feature || {}
|
|
38
|
+
}
|
|
39
|
+
catch (e) { return {} }
|
|
40
|
+
})()).sort()
|
|
31
41
|
|
|
32
42
|
|
|
33
43
|
// One operation this SDK can actually perform.
|
|
@@ -111,7 +121,18 @@ function candidates(client: any): OpRef[] {
|
|
|
111
121
|
out.push({ key: entity + '.' + op, accessor: accessor[entity], entity, op })
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
|
-
|
|
124
|
+
|
|
125
|
+
// SAFE OPS FIRST. The corpus calls #OP1 repeatedly and reasons about what
|
|
126
|
+
// the transport did in between — a cache hit, a retry, a rate-limit wait.
|
|
127
|
+
// `load`/`list` are the GET ops, and the cache stores only successful GETs,
|
|
128
|
+
// so an SDK whose first usable op is a `create` (POST) can never satisfy
|
|
129
|
+
// "a hit served from cache costs nothing": nothing is ever cached, the
|
|
130
|
+
// second call goes to the network, and cost correctly charges twice. That
|
|
131
|
+
// reads as a cost defect and is not one. Ordering here rather than
|
|
132
|
+
// filtering keeps every SDK runnable, including ones with no GET at all.
|
|
133
|
+
const SAFE: Record<string, number> = { list: 0, load: 1 }
|
|
134
|
+
return out.sort((a, b) =>
|
|
135
|
+
(SAFE[a.op] ?? 2) - (SAFE[b.op] ?? 2) || a.key.localeCompare(b.key))
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
|
|
@@ -21,6 +21,16 @@ function recordingServer(reply?: (n: number, fetchdef: any) => any) {
|
|
|
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: string): string | boolean {
|
|
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: any[] = []
|
|
396
406
|
const h = makeClient({ features: [
|
|
397
407
|
{ name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { each } from 'jostraca'
|
|
2
|
+
|
|
3
|
+
import { KIT, getModelPath } from '../types'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// Shared feature facts, derived from the model, for the three doc surfaces
|
|
7
|
+
// that describe features: the root README summary, the per-target README
|
|
8
|
+
// section, and REFERENCE.md.
|
|
9
|
+
//
|
|
10
|
+
// ONE SOURCE. The feature set, its defaults and its ordering constraints all
|
|
11
|
+
// come from `main.kit.feature`, so a feature added to the model documents
|
|
12
|
+
// itself everywhere rather than in whichever file someone remembered.
|
|
13
|
+
|
|
14
|
+
type FeatureDoc = {
|
|
15
|
+
name: string
|
|
16
|
+
Name: string
|
|
17
|
+
title: string
|
|
18
|
+
transport: string
|
|
19
|
+
wraps: boolean
|
|
20
|
+
options: Array<{ name: string, value: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// `transport` says how a feature attaches, and that is the whole of the
|
|
25
|
+
// ordering story:
|
|
26
|
+
// wrap wraps the transport chain — activation order IS nesting order
|
|
27
|
+
// base installs the base transport others wrap (test)
|
|
28
|
+
// none pipeline hooks only; order does not affect it
|
|
29
|
+
function isWrapping(feat: any): boolean {
|
|
30
|
+
return 'wrap' === feat.transport
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function renderValue(v: any): string {
|
|
35
|
+
if (null == v) { return '' }
|
|
36
|
+
if (Array.isArray(v)) { return '[' + v.map((x) => renderValue(x)).join(', ') + ']' }
|
|
37
|
+
if ('object' === typeof v) {
|
|
38
|
+
const keys = Object.keys(v)
|
|
39
|
+
return 0 === keys.length ? '{}' :
|
|
40
|
+
'{' + keys.map((k) => k + ': ' + renderValue(v[k])).join(', ') + '}'
|
|
41
|
+
}
|
|
42
|
+
if ('string' === typeof v) { return `'${v}'` }
|
|
43
|
+
return String(v)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// Every feature the model declares active, in a stable order, with its
|
|
48
|
+
// options and their defaults.
|
|
49
|
+
function featureDocs(model: any): FeatureDoc[] {
|
|
50
|
+
const feature = getModelPath(model, `main.${KIT}.feature`)
|
|
51
|
+
|
|
52
|
+
return each(feature)
|
|
53
|
+
.filter((f: any) => false !== f.active && 'base' !== f.name)
|
|
54
|
+
.map((f: any) => {
|
|
55
|
+
const opts = (f.config && f.config.options) || {}
|
|
56
|
+
const options = Object.keys(opts).sort().map((k) => ({
|
|
57
|
+
name: k,
|
|
58
|
+
value: renderValue(opts[k]),
|
|
59
|
+
}))
|
|
60
|
+
return {
|
|
61
|
+
name: f.name,
|
|
62
|
+
Name: f.Name || f.name,
|
|
63
|
+
title: f.title || '',
|
|
64
|
+
transport: f.transport || 'none',
|
|
65
|
+
wraps: isWrapping(f),
|
|
66
|
+
options,
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.sort((a: FeatureDoc, b: FeatureDoc) => a.name.localeCompare(b.name))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Targets that compose transport features in a FIXED catalog order rather
|
|
76
|
+
// than the order the caller activates them in.
|
|
77
|
+
//
|
|
78
|
+
// Every other target derives `__derived__.featureorder` from the options and
|
|
79
|
+
// adds features in that order, so an ordered activation list is what fixes
|
|
80
|
+
// nesting. lean has no featureorder at all — SdkFeatures.featureNames is a
|
|
81
|
+
// fixed array — and its resolveFeatureOpts accepts only a map, silently
|
|
82
|
+
// replacing a list with an empty one. Telling a lean reader to activate
|
|
83
|
+
// features as an ordered list would therefore disable every feature they
|
|
84
|
+
// asked for.
|
|
85
|
+
//
|
|
86
|
+
// Verify with: grep -rl featureorder tm/<target>
|
|
87
|
+
const FIXED_ORDER_TARGETS = ['lean']
|
|
88
|
+
|
|
89
|
+
function honoursActivationOrder(target: any): boolean {
|
|
90
|
+
return !FIXED_ORDER_TARGETS.includes(target?.name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
featureDocs,
|
|
96
|
+
renderValue,
|
|
97
|
+
honoursActivationOrder,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type { FeatureDoc }
|
package/src/cmp/Readme.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { ReadmeEntity } from './ReadmeEntity'
|
|
|
13
13
|
import { ReadmeUnions } from './ReadmeUnions'
|
|
14
14
|
import { ReadmeHowto } from './ReadmeHowto'
|
|
15
15
|
import { ReadmeExplanation } from './ReadmeExplanation'
|
|
16
|
+
import { ReadmeFeatures } from './ReadmeFeatures'
|
|
16
17
|
import { ReadmeRef } from './ReadmeRef'
|
|
17
18
|
|
|
18
19
|
|
|
@@ -31,6 +32,7 @@ const Readme = cmp(function Readme(props: any) {
|
|
|
31
32
|
ReadmeModel({ target })
|
|
32
33
|
ReadmeOptions({ target })
|
|
33
34
|
ReadmeEntity({ target })
|
|
35
|
+
ReadmeFeatures({ target })
|
|
34
36
|
ReadmeUnions({ target })
|
|
35
37
|
ReadmeExplanation({ target })
|
|
36
38
|
|