@voxgig/sdkgen 3.6.0 → 3.7.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/AgentGuideTop.js +17 -0
- package/dist/cmp/AgentGuideTop.js.map +1 -1
- package/dist/cmp/Readme.js +2 -0
- package/dist/cmp/Readme.js.map +1 -1
- package/dist/cmp/ReadmeStation.d.ts +2 -0
- package/dist/cmp/ReadmeStation.js +57 -0
- package/dist/cmp/ReadmeStation.js.map +1 -0
- package/dist/helpers/naming.d.ts +3 -1
- package/dist/helpers/naming.js +39 -2
- package/dist/helpers/naming.js.map +1 -1
- package/dist/sdkgen.d.ts +2 -2
- package/dist/sdkgen.js +4 -2
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +1 -1
- package/dist/utility.js +16 -2
- package/dist/utility.js.map +1 -1
- package/package.json +1 -1
- package/project/.sdk/src/cmp/c/Config_c.ts +34 -13
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +9 -46
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +7 -1
- package/project/.sdk/src/cmp/dart/Config_dart.ts +14 -2
- package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +5 -0
- package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -0
- package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +13 -4
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +12 -2
- package/project/.sdk/src/cmp/elixir/Package_elixir.ts +41 -2
- package/project/.sdk/src/cmp/go/Config_go.ts +8 -1
- package/project/.sdk/src/cmp/java/Config_java.ts +8 -1
- package/project/.sdk/src/cmp/js/Config_js.ts +9 -1
- package/project/.sdk/src/cmp/js/fragment/Config.data.fragment.js +7 -0
- package/project/.sdk/src/cmp/js/fragment/Config.fragment.js +8 -0
- package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +33 -5
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +8 -1
- package/project/.sdk/src/cmp/lua/Config_lua.ts +11 -2
- package/project/.sdk/src/cmp/lua/Package_lua.ts +24 -1
- package/project/.sdk/src/cmp/perl/Config_perl.ts +8 -60
- package/project/.sdk/src/cmp/php/Config_php.ts +11 -2
- package/project/.sdk/src/cmp/php/Main_php.ts +25 -0
- package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +23 -6
- package/project/.sdk/src/cmp/py/Config_py.ts +11 -2
- package/project/.sdk/src/cmp/py/Main_py.ts +17 -7
- package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +23 -6
- package/project/.sdk/src/cmp/rb/Config_rb.ts +7 -1
- package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -2
- package/project/.sdk/src/cmp/swift/Config_swift.ts +8 -54
- package/project/.sdk/src/cmp/swift/Package_swift.ts +52 -10
- package/project/.sdk/src/cmp/ts/Config_ts.ts +9 -1
- package/project/.sdk/src/cmp/ts/EntityTypes_ts.ts +5 -2
- package/project/.sdk/src/cmp/ts/Entity_ts.ts +8 -2
- package/project/.sdk/src/cmp/ts/fragment/Config.data.fragment.ts +7 -0
- package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +8 -0
- package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +1 -1
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +12 -4
- package/project/.sdk/tm/c/Makefile +6 -1
- package/project/.sdk/tm/c/utility/make_options.c +12 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +9 -1
- package/project/.sdk/tm/csharp/utility/Fetcher.cs +23 -5
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +13 -0
- package/project/.sdk/tm/dart/lib/utility/FetcherUtility.dart +9 -0
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +17 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +39 -3
- package/project/.sdk/tm/elixir/test/feature_test.exs +19 -0
- package/project/.sdk/tm/elixir/test/primary_utility_test.exs +72 -0
- package/project/.sdk/tm/go/test/primary_utility_test.go +42 -20
- package/project/.sdk/tm/go/test/runner_test.go +1 -1
- package/project/.sdk/tm/go/utility/fetcher.go +14 -0
- package/project/.sdk/tm/go/utility/make_options.go +31 -4
- package/project/.sdk/tm/java/utility/Fetcher.java +11 -1
- package/project/.sdk/tm/java/utility/MakeOptions.java +11 -0
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +20 -2
- package/project/.sdk/tm/kotlin/utility/Fetcher.kt +10 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +11 -0
- package/project/.sdk/tm/lean/test/TPrimaryUtility.lean +13 -2
- package/project/.sdk/tm/lua/utility/make_options.lua +44 -0
- package/project/.sdk/tm/perl/utility/fetcher.pm +10 -1
- package/project/.sdk/tm/perl/utility/make_options.pm +36 -0
- package/project/.sdk/tm/php/test/PipelineTest.php +20 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +12 -1
- package/project/.sdk/tm/php/utility/MakeOptions.php +58 -0
- package/project/.sdk/tm/py/pkg/utility/fetcher.py +13 -1
- package/project/.sdk/tm/py/pkg/utility/make_options.py +23 -0
- package/project/.sdk/tm/rb/utility/make_options.rb +18 -0
- package/project/.sdk/tm/rust/tests/common/mod.rs +1 -1
- package/project/.sdk/tm/rust/tests/feature_test.rs +27 -0
- package/project/.sdk/tm/rust/utility/fetcher.rs +8 -0
- package/project/.sdk/tm/rust/utility/make_options.rs +15 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +24 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +12 -0
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +20 -2
- package/project/.sdk/tm/ts/test/utility/Corpus.test.ts +85 -0
- package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +10 -17
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/AgentGuideTop.ts +18 -0
- package/src/cmp/Readme.ts +2 -0
- package/src/cmp/ReadmeStation.ts +69 -0
- package/src/helpers/naming.ts +45 -2
- package/src/sdkgen.ts +3 -1
- package/src/utility.ts +17 -2
|
@@ -68,6 +68,12 @@ function makeOptions(ctx) {
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
utility: {},
|
|
71
|
+
// Feature INSTANCES supplied at construction (the station adopt
|
|
72
|
+
// path): consumed by the constructor's featureAdd loop, so they are
|
|
73
|
+
// class instances, not data - `$ANY` accepts them verbatim. Without
|
|
74
|
+
// this entry the seam is dead: the constructor reads
|
|
75
|
+
// options.extend, but validate rejected the key.
|
|
76
|
+
extend: '`$ANY`',
|
|
71
77
|
system: {
|
|
72
78
|
fetch: undefined
|
|
73
79
|
},
|
|
@@ -131,10 +137,22 @@ function makeOptions(ctx) {
|
|
|
131
137
|
// otherwise order the map test-first, then the remaining names sorted, so
|
|
132
138
|
// the outcome is deterministic and `test` is always the base transport.
|
|
133
139
|
if (0 === featureorder.length) {
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
let names = Object.keys(opts.feature || {}).sort()
|
|
141
|
+
names = names.indexOf('test') < 0
|
|
136
142
|
? names
|
|
137
143
|
: ['test'].concat(names.filter((n) => 'test' !== n))
|
|
144
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
145
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
146
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
147
|
+
// or first, when no test entry exists. Without this the sorted
|
|
148
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
149
|
+
// features, turning its wire-truth events into fiction.
|
|
150
|
+
const si = names.indexOf('station')
|
|
151
|
+
if (0 <= si) {
|
|
152
|
+
names.splice(si, 1)
|
|
153
|
+
names.splice(names.indexOf('test') + 1, 0, 'station')
|
|
154
|
+
}
|
|
155
|
+
featureorder = names
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
opts.__derived__ = {
|
|
@@ -50,9 +50,18 @@ fun defaultHttpFetch(fullurl: String, fetchdef: MutableMap<String, Any?>): Mutab
|
|
|
50
50
|
reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Honour a redirect annotation on the fetch definition (set by the
|
|
54
|
+
// station feature's middleware under a hosts policy): "manual" returns
|
|
55
|
+
// a 3xx like any other response instead of following it — the ts
|
|
56
|
+
// donor's fetch honours the same key natively, and an automatic follow
|
|
57
|
+
// would carry injected credentials to a host no policy approved.
|
|
58
|
+
val redirectPolicy =
|
|
59
|
+
if ("manual" == fetchdef["redirect"]) HttpClient.Redirect.NEVER
|
|
60
|
+
else HttpClient.Redirect.NORMAL
|
|
61
|
+
|
|
53
62
|
// Honour a proxy annotation on the fetch definition (set by the proxy
|
|
54
63
|
// feature): route the request through a proxied HttpClient.
|
|
55
|
-
val clientb = HttpClient.newBuilder().followRedirects(
|
|
64
|
+
val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
|
|
56
65
|
val proxy = fetchdef["proxy"]
|
|
57
66
|
if (proxy is String && "" != proxy) {
|
|
58
67
|
try {
|
|
@@ -141,6 +141,17 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
|
|
|
141
141
|
} else {
|
|
142
142
|
featureorder.addAll(names)
|
|
143
143
|
}
|
|
144
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
145
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
146
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
147
|
+
// or first, when no test entry exists. Without this the sorted
|
|
148
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
149
|
+
// features, turning its wire-truth events into fiction.
|
|
150
|
+
val si = featureorder.indexOf("station")
|
|
151
|
+
if (0 <= si) {
|
|
152
|
+
featureorder.removeAt(si)
|
|
153
|
+
featureorder.add(featureorder.indexOf("test") + 1, "station")
|
|
154
|
+
}
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
val derived = linkedMapOf<String, Any?>()
|
|
@@ -316,8 +316,19 @@ def main : IO UInt32 := do
|
|
|
316
316
|
runset "makeResult" (← getSpec primary #["makeResult", "basic"]) fun entry => do
|
|
317
317
|
pure ((← SdkUtility.makeResult (← entryCtx entry opts config)), none)
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
-- makePoint is NOT driven here, and this is deliberate.
|
|
320
|
+
--
|
|
321
|
+
-- The section used to be empty, so running it asserted nothing. It now
|
|
322
|
+
-- carries seven cases, and every one supplies its own `options` (for
|
|
323
|
+
-- allow.op) and `config` (for the operation's points) — but `entryCtx`
|
|
324
|
+
-- OVERWRITES both with the SDK's, so the cases would not run as authored.
|
|
325
|
+
-- Wiring it up needs entryCtx to prefer a fixture-supplied options/config,
|
|
326
|
+
-- which changes every section here and could not be compiled or run when
|
|
327
|
+
-- this change was made.
|
|
328
|
+
--
|
|
329
|
+
-- Left out rather than left in: an entry that runs with the wrong context
|
|
330
|
+
-- is worse than one that does not run, and no coverage is lost — this
|
|
331
|
+
-- section asserted nothing before either.
|
|
321
332
|
|
|
322
333
|
runset "makeFetchDef" (← getSpec primary #["makeFetchDef", "basic"]) fun entry => do
|
|
323
334
|
pure ((← SdkUtility.makeFetchDef (← entryCtx entry opts config)), none)
|
|
@@ -82,6 +82,12 @@ local function make_options_util(ctx)
|
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
84
|
utility = {},
|
|
85
|
+
-- Feature INSTANCES supplied at construction (the station adopt
|
|
86
|
+
-- path): consumed by the constructor's feature_add loop, so they are
|
|
87
|
+
-- class instances, not data -- `$ANY` accepts them verbatim. Without
|
|
88
|
+
-- this entry the seam is dead: the constructor reads
|
|
89
|
+
-- options.extend, but validate rejected the key.
|
|
90
|
+
extend = "`$ANY`",
|
|
85
91
|
system = {},
|
|
86
92
|
test = {
|
|
87
93
|
active = false,
|
|
@@ -104,6 +110,15 @@ local function make_options_util(ctx)
|
|
|
104
110
|
-- Preserve system.fetch before merge/validate.
|
|
105
111
|
local sys_fetch = vs.getpath(opts, "system.fetch")
|
|
106
112
|
|
|
113
|
+
-- Preserve extend feature INSTANCES before merge/validate, by ORIGINAL
|
|
114
|
+
-- reference (the ts makeOptions keeps the caller's instances too):
|
|
115
|
+
-- merge/validate are data-oriented and would mangle instance tables
|
|
116
|
+
-- carrying functions -- the same reason system.fetch is preserved.
|
|
117
|
+
local extend = nil
|
|
118
|
+
if type(options) == "table" and type(options["extend"]) == "table" then
|
|
119
|
+
extend = options["extend"]
|
|
120
|
+
end
|
|
121
|
+
|
|
107
122
|
-- Clone the config side before merging: `config` is a per-Lua-state
|
|
108
123
|
-- singleton (see config_shared), and merge would otherwise use its nested
|
|
109
124
|
-- tables as merge TARGETS — one instance's options (server, headers, ...)
|
|
@@ -156,6 +171,11 @@ local function make_options_util(ctx)
|
|
|
156
171
|
end
|
|
157
172
|
end
|
|
158
173
|
|
|
174
|
+
-- Restore extend feature instances.
|
|
175
|
+
if extend ~= nil then
|
|
176
|
+
opts["extend"] = extend
|
|
177
|
+
end
|
|
178
|
+
|
|
159
179
|
-- Derived clean config.
|
|
160
180
|
local clean_keys = "key,token,id"
|
|
161
181
|
local ck = vs.getpath(opts, "clean.keys")
|
|
@@ -202,6 +222,30 @@ local function make_options_util(ctx)
|
|
|
202
222
|
else
|
|
203
223
|
featureorder = names
|
|
204
224
|
end
|
|
225
|
+
-- Station special case, mirroring test's: its transport wrap must
|
|
226
|
+
-- sit immediately outside the base transport (inside retry/cache/
|
|
227
|
+
-- netsim), so map-form activation hoists it to just after test -
|
|
228
|
+
-- or first, when no test entry exists. Without this the sorted
|
|
229
|
+
-- default would init station last and wrap OUTSIDE the recording
|
|
230
|
+
-- features, turning its wire-truth events into fiction.
|
|
231
|
+
local si = nil
|
|
232
|
+
for i, n in ipairs(featureorder) do
|
|
233
|
+
if n == "station" then
|
|
234
|
+
si = i
|
|
235
|
+
break
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
if si ~= nil then
|
|
239
|
+
table.remove(featureorder, si)
|
|
240
|
+
local ti = 0
|
|
241
|
+
for i, n in ipairs(featureorder) do
|
|
242
|
+
if n == "test" then
|
|
243
|
+
ti = i
|
|
244
|
+
break
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
table.insert(featureorder, ti + 1, "station")
|
|
248
|
+
end
|
|
205
249
|
end
|
|
206
250
|
|
|
207
251
|
local derived = { clean = {} }
|
|
@@ -39,7 +39,16 @@ our $DefaultHttpFetch = sub {
|
|
|
39
39
|
my $opts = { headers => \%hdrs };
|
|
40
40
|
$opts->{content} = "$body" if defined $body && !ref $body;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
# fetchdef.redirect 'manual' (the ts fetch vocabulary): return a 3xx
|
|
43
|
+
# as-is instead of following it. HTTP::Tiny follows redirects by
|
|
44
|
+
# default; the station feature's hosts policy sets this so a Location
|
|
45
|
+
# off the allowlist can never pull an automatic credentialed follow-up.
|
|
46
|
+
my %new_args;
|
|
47
|
+
$new_args{max_redirect} = 0
|
|
48
|
+
if defined $fetchdef->{redirect} && !ref $fetchdef->{redirect}
|
|
49
|
+
&& 'manual' eq $fetchdef->{redirect};
|
|
50
|
+
|
|
51
|
+
my $res = eval { HTTP::Tiny->new(%new_args)->request($method, $fullurl, $opts) };
|
|
43
52
|
if (!$res) {
|
|
44
53
|
my $e = defined $@ ? "$@" : 'request failed';
|
|
45
54
|
$e =~ s/\s+\z//;
|
|
@@ -26,8 +26,20 @@ $REGISTRY{make_options} = sub {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
# Feature INSTANCES supplied at construction (the station adopt path):
|
|
30
|
+
# consumed by the constructor's extend loop, so they are blessed class
|
|
31
|
+
# instances, not data. This is the perl form of the ts/rb targets'
|
|
32
|
+
# `extend => $ANY` optspec entry: this port deep-clones its options, and
|
|
33
|
+
# Voxgig::Struct::clone would flatten a blessed feature to a plain
|
|
34
|
+
# unblessed hash (and validate rejects the key outright), so the RAW
|
|
35
|
+
# list is captured here and re-attached after validation - the
|
|
36
|
+
# system.fetch idiom. Without this the seam is dead: the constructor
|
|
37
|
+
# reads options.extend, but clone/validate dropped the instances.
|
|
38
|
+
my $extend_raw = ProjectNameHelpers::gp($options, 'extend');
|
|
39
|
+
|
|
29
40
|
my $opts = Voxgig::Struct::clone($options);
|
|
30
41
|
$opts = {} unless Voxgig::Struct::ismap($opts);
|
|
42
|
+
delete $opts->{extend};
|
|
31
43
|
|
|
32
44
|
# Feature add-order. options.feature may be given as an ordered ARRAY of
|
|
33
45
|
# { name, active, ...opts } entries (the array position IS the order in
|
|
@@ -78,6 +90,12 @@ $REGISTRY{make_options} = sub {
|
|
|
78
90
|
'system' => {},
|
|
79
91
|
'test' => { 'active' => $JF, 'entity' => { '`$OPEN`' => $JT } },
|
|
80
92
|
'clean' => { 'keys' => 'key,token,id' },
|
|
93
|
+
# Server-variable values for a templated base URL (OpenAPI server
|
|
94
|
+
# variables). The embedded config (configDefinition) carries the
|
|
95
|
+
# spec defaults; user values override them. This port does not yet
|
|
96
|
+
# substitute {name} placeholders into base - the entry keeps the
|
|
97
|
+
# config's server block valid under this optspec.
|
|
98
|
+
'server' => { '`$CHILD`' => '' },
|
|
81
99
|
};
|
|
82
100
|
|
|
83
101
|
my $sys_fetch = ProjectNameHelpers::gpath($opts, 'system.fetch');
|
|
@@ -95,6 +113,11 @@ $REGISTRY{make_options} = sub {
|
|
|
95
113
|
$opts->{system}{fetch} = $sys_fetch;
|
|
96
114
|
}
|
|
97
115
|
|
|
116
|
+
# Re-attach the raw extend instances captured above.
|
|
117
|
+
if (Voxgig::Struct::islist($extend_raw)) {
|
|
118
|
+
$opts->{extend} = $extend_raw;
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
my $clean_keys = ProjectNameHelpers::gpath($opts, 'clean.keys');
|
|
99
122
|
$clean_keys = 'key,token,id' unless defined $clean_keys && !ref $clean_keys;
|
|
100
123
|
my @parts;
|
|
@@ -114,6 +137,19 @@ $REGISTRY{make_options} = sub {
|
|
|
114
137
|
@featureorder = (grep { 'test' eq $_ } @names)
|
|
115
138
|
? ('test', grep { 'test' ne $_ } @names)
|
|
116
139
|
: @names;
|
|
140
|
+
|
|
141
|
+
# Station special case, mirroring test's: its transport wrap must
|
|
142
|
+
# sit immediately outside the base transport (inside retry/cache/
|
|
143
|
+
# netsim), so map-form activation hoists it to just after test -
|
|
144
|
+
# or first, when no test entry exists. Without this the sorted
|
|
145
|
+
# default would init station last and wrap OUTSIDE the recording
|
|
146
|
+
# features, turning its wire-truth events into fiction.
|
|
147
|
+
my ($si) = grep { 'station' eq $featureorder[$_] } 0 .. $#featureorder;
|
|
148
|
+
if (defined $si) {
|
|
149
|
+
splice @featureorder, $si, 1;
|
|
150
|
+
my ($ti) = grep { 'test' eq $featureorder[$_] } 0 .. $#featureorder;
|
|
151
|
+
splice @featureorder, (defined $ti ? $ti + 1 : 0), 0, 'station';
|
|
152
|
+
}
|
|
117
153
|
}
|
|
118
154
|
|
|
119
155
|
$opts->{__derived__} = {
|
|
@@ -154,6 +154,26 @@ class PipelineTest extends TestCase
|
|
|
154
154
|
]));
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// Station special case, mirroring test's: its transport wrap must sit
|
|
158
|
+
// immediately outside the base transport, so map-form activation hoists
|
|
159
|
+
// it to just after test - or first, when no test entry exists.
|
|
160
|
+
public function test_feature_order_map_hoists_station_after_test(): void
|
|
161
|
+
{
|
|
162
|
+
$this->assertSame('test,station,metrics', self::resolve_order([
|
|
163
|
+
'metrics' => ['active' => true],
|
|
164
|
+
'station' => ['active' => true],
|
|
165
|
+
'test' => ['active' => true],
|
|
166
|
+
]));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public function test_feature_order_map_hoists_station_first_without_test(): void
|
|
170
|
+
{
|
|
171
|
+
$this->assertSame('station,metrics', self::resolve_order([
|
|
172
|
+
'metrics' => ['active' => true],
|
|
173
|
+
'station' => ['active' => true],
|
|
174
|
+
]));
|
|
175
|
+
}
|
|
176
|
+
|
|
157
177
|
public function test_feature_order_no_test_deterministic(): void
|
|
158
178
|
{
|
|
159
179
|
$this->assertSame('cache,retry', self::resolve_order([
|
|
@@ -35,7 +35,8 @@ class ProjectNameFetcher
|
|
|
35
35
|
// Prefer cURL when available — its header capture is reliable across
|
|
36
36
|
// PHP versions, while file_get_contents + custom stream wrappers
|
|
37
37
|
// don't propagate $http_response_header for user-defined wrappers
|
|
38
|
-
// in PHP 8.3+.
|
|
38
|
+
// in PHP 8.3+. (cURL is configured with FOLLOWLOCATION off, so the
|
|
39
|
+
// manual-redirect annotation below holds there by construction.)
|
|
39
40
|
if (function_exists('curl_init')) {
|
|
40
41
|
return self::curlFetch($fullurl, $method_str, $body_str, $header_lines);
|
|
41
42
|
}
|
|
@@ -47,6 +48,16 @@ class ProjectNameFetcher
|
|
|
47
48
|
],
|
|
48
49
|
];
|
|
49
50
|
|
|
51
|
+
// Honour a redirect annotation on the fetch definition (set by the
|
|
52
|
+
// station feature's middleware under a hosts policy): "manual"
|
|
53
|
+
// surfaces a 3xx as an ordinary response instead of auto-following
|
|
54
|
+
// it — the http stream wrapper otherwise replays the request,
|
|
55
|
+
// headers included, against whatever host Location names, carrying
|
|
56
|
+
// injected credentials to a host no policy approved.
|
|
57
|
+
if (($fetchdef['redirect'] ?? null) === 'manual') {
|
|
58
|
+
$opts['http']['follow_location'] = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
if (is_string($body_str)) {
|
|
51
62
|
$opts['http']['content'] = $body_str;
|
|
52
63
|
}
|
|
@@ -37,6 +37,34 @@ class ProjectNameMakeOptions
|
|
|
37
37
|
// Preserve system.fetch before merge/validate.
|
|
38
38
|
$sys_fetch = \Voxgig\Struct\Struct::getpath($options, 'system.fetch');
|
|
39
39
|
|
|
40
|
+
// Feature INSTANCES supplied at construction (the station adopt path)
|
|
41
|
+
// are consumed by the constructor's feature-add loop straight from the
|
|
42
|
+
// RAW construction options - extend is consumed exactly once, at
|
|
43
|
+
// construction. Strip it here so it never enters the cloned option
|
|
44
|
+
// map: Struct::clone flattens arbitrary objects, and options_map()
|
|
45
|
+
// re-clones $this->options on every request.
|
|
46
|
+
if (isset($options['extend'])) {
|
|
47
|
+
unset($options['extend']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The station feature's binding handle and caller opts are live
|
|
51
|
+
// values (connect/adopt/st->options() plant them): hold them aside so
|
|
52
|
+
// the clone below cannot flatten them, and reattach after validate.
|
|
53
|
+
$station_handle = null;
|
|
54
|
+
$station_caller = null;
|
|
55
|
+
$station_has_caller = false;
|
|
56
|
+
if (is_array($options['feature']['station'] ?? null)) {
|
|
57
|
+
if (is_object($options['feature']['station']['station'] ?? null)) {
|
|
58
|
+
$station_handle = $options['feature']['station']['station'];
|
|
59
|
+
unset($options['feature']['station']['station']);
|
|
60
|
+
}
|
|
61
|
+
if (array_key_exists('calleropts', $options['feature']['station'])) {
|
|
62
|
+
$station_caller = $options['feature']['station']['calleropts'];
|
|
63
|
+
$station_has_caller = true;
|
|
64
|
+
unset($options['feature']['station']['calleropts']);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
$opts = \Voxgig\Struct\Struct::clone($options);
|
|
41
69
|
$opts = self::to_array_deep($opts);
|
|
42
70
|
if (!is_array($opts)) {
|
|
@@ -106,6 +134,24 @@ class ProjectNameMakeOptions
|
|
|
106
134
|
$opts = [];
|
|
107
135
|
}
|
|
108
136
|
|
|
137
|
+
// Reattach the station binding handle held aside above (the feature
|
|
138
|
+
// optspec is `$OPEN`, so the extra keys are legal; they were only
|
|
139
|
+
// kept out of the clone/validate round-trip).
|
|
140
|
+
if (null !== $station_handle || $station_has_caller) {
|
|
141
|
+
if (!is_array($opts['feature'] ?? null)) {
|
|
142
|
+
$opts['feature'] = [];
|
|
143
|
+
}
|
|
144
|
+
if (!is_array($opts['feature']['station'] ?? null)) {
|
|
145
|
+
$opts['feature']['station'] = [];
|
|
146
|
+
}
|
|
147
|
+
if (null !== $station_handle) {
|
|
148
|
+
$opts['feature']['station']['station'] = $station_handle;
|
|
149
|
+
}
|
|
150
|
+
if ($station_has_caller) {
|
|
151
|
+
$opts['feature']['station']['calleropts'] = $station_caller;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
// Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
|
|
110
156
|
// Every placeholder must resolve to a non-empty value: from
|
|
111
157
|
// $options['server'] (user), else the config default. A placeholder
|
|
@@ -181,6 +227,18 @@ class ProjectNameMakeOptions
|
|
|
181
227
|
} else {
|
|
182
228
|
$featureorder = $names;
|
|
183
229
|
}
|
|
230
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
231
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
232
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
233
|
+
// or first, when no test entry exists. Without this the sorted
|
|
234
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
235
|
+
// features, turning its wire-truth events into fiction.
|
|
236
|
+
$si = array_search('station', $featureorder, true);
|
|
237
|
+
if (false !== $si) {
|
|
238
|
+
array_splice($featureorder, $si, 1);
|
|
239
|
+
$ti = array_search('test', $featureorder, true);
|
|
240
|
+
array_splice($featureorder, false === $ti ? 0 : $ti + 1, 0, ['station']);
|
|
241
|
+
}
|
|
184
242
|
}
|
|
185
243
|
|
|
186
244
|
$derived = ['clean' => $keyre === '' ? [] : ['keyre' => $keyre]];
|
|
@@ -35,8 +35,20 @@ def _default_http_fetch(fullurl, fetchdef):
|
|
|
35
35
|
if not has_ua:
|
|
36
36
|
req.add_header("User-Agent", _DEFAULT_USER_AGENT)
|
|
37
37
|
|
|
38
|
+
# Manual redirects: fetchdef["redirect"] == "manual" surfaces a 3xx as
|
|
39
|
+
# an ordinary response instead of auto-following it — urllib would
|
|
40
|
+
# otherwise replay the request, headers included, against whatever host
|
|
41
|
+
# Location names. Set by the station feature's transport middleware
|
|
42
|
+
# under an egress hosts policy (mirrors the ts fetch option).
|
|
43
|
+
opener = None
|
|
44
|
+
if fetchdef.get("redirect") == "manual":
|
|
45
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
46
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
47
|
+
return None
|
|
48
|
+
opener = urllib.request.build_opener(_NoRedirect)
|
|
49
|
+
|
|
38
50
|
try:
|
|
39
|
-
resp = urllib.request.urlopen(req)
|
|
51
|
+
resp = opener.open(req) if opener is not None else urllib.request.urlopen(req)
|
|
40
52
|
body = resp.read().decode("utf-8")
|
|
41
53
|
resp_headers = {}
|
|
42
54
|
for k, v in resp.getheaders():
|
|
@@ -15,6 +15,15 @@ def make_options_util(ctx):
|
|
|
15
15
|
for key, val in custom_utils.items():
|
|
16
16
|
utility.custom[key] = val
|
|
17
17
|
|
|
18
|
+
# Feature INSTANCES supplied at construction (the station adopt path)
|
|
19
|
+
# are consumed by the constructor's feature-add loop straight from the
|
|
20
|
+
# RAW construction options - extend is consumed exactly once, at
|
|
21
|
+
# construction. They are stripped here so they never enter the cloned
|
|
22
|
+
# option map: vs.clone flattens arbitrary objects, and options_map()
|
|
23
|
+
# re-clones self.options on every request.
|
|
24
|
+
if isinstance(options, dict) and options.get("extend") is not None:
|
|
25
|
+
options = {k: v for k, v in options.items() if k != "extend"}
|
|
26
|
+
|
|
18
27
|
opts = vs.clone(options)
|
|
19
28
|
if not isinstance(opts, dict):
|
|
20
29
|
opts = {}
|
|
@@ -74,6 +83,10 @@ def make_options_util(ctx):
|
|
|
74
83
|
},
|
|
75
84
|
},
|
|
76
85
|
"utility": {},
|
|
86
|
+
# Extension feature instances (see above) - stripped before the
|
|
87
|
+
# clone, but the key stays legal so a passed-through map cannot
|
|
88
|
+
# fail validation.
|
|
89
|
+
"extend": "`$ANY`",
|
|
77
90
|
"system": {},
|
|
78
91
|
"test": {
|
|
79
92
|
"active": False,
|
|
@@ -171,6 +184,16 @@ def make_options_util(ctx):
|
|
|
171
184
|
featureorder = ["test"] + [n for n in names if n != "test"]
|
|
172
185
|
else:
|
|
173
186
|
featureorder = names
|
|
187
|
+
# Station special case, mirroring test's: its transport wrap must
|
|
188
|
+
# sit immediately outside the base transport (inside retry/cache/
|
|
189
|
+
# netsim), so map-form activation hoists it to just after test -
|
|
190
|
+
# or first, when no test entry exists. Without this the sorted
|
|
191
|
+
# default would init station last and wrap OUTSIDE the recording
|
|
192
|
+
# features, turning its wire-truth events into fiction.
|
|
193
|
+
if "station" in featureorder:
|
|
194
|
+
featureorder = [n for n in featureorder if n != "station"]
|
|
195
|
+
at = featureorder.index("test") + 1 if "test" in featureorder else 0
|
|
196
|
+
featureorder.insert(at, "station")
|
|
174
197
|
|
|
175
198
|
derived = {"clean": {}}
|
|
176
199
|
if keyre != "":
|
|
@@ -49,6 +49,12 @@ module ProjectNameUtilities
|
|
|
49
49
|
"entity" => { "`$CHILD`" => { "`$OPEN`" => true, "active" => false, "alias" => {} } },
|
|
50
50
|
"feature" => { "`$CHILD`" => { "`$OPEN`" => true, "active" => false } },
|
|
51
51
|
"utility" => {},
|
|
52
|
+
# Feature INSTANCES supplied at construction (the station adopt
|
|
53
|
+
# path): consumed by the constructor's feature_add loop, so they are
|
|
54
|
+
# class instances, not data — `$ANY` accepts them verbatim. Without
|
|
55
|
+
# this entry the seam is dead: the constructor reads
|
|
56
|
+
# options["extend"], but validate rejected the key.
|
|
57
|
+
"extend" => "`$ANY`",
|
|
52
58
|
"system" => {},
|
|
53
59
|
"test" => { "active" => false, "entity" => { "`$OPEN`" => true } },
|
|
54
60
|
"clean" => { "keys" => "key,token,id" },
|
|
@@ -122,6 +128,18 @@ module ProjectNameUtilities
|
|
|
122
128
|
else
|
|
123
129
|
featureorder = names
|
|
124
130
|
end
|
|
131
|
+
# Station special case, mirroring test's: its transport wrap must
|
|
132
|
+
# sit immediately outside the base transport (inside retry/cache/
|
|
133
|
+
# netsim), so map-form activation hoists it to just after test -
|
|
134
|
+
# or first, when no test entry exists. Without this the sorted
|
|
135
|
+
# default would init station last and wrap OUTSIDE the recording
|
|
136
|
+
# features, turning its wire-truth events into fiction.
|
|
137
|
+
si = featureorder.index("station")
|
|
138
|
+
unless si.nil?
|
|
139
|
+
featureorder.delete_at(si)
|
|
140
|
+
ti = featureorder.index("test")
|
|
141
|
+
featureorder.insert(ti.nil? ? 0 : ti + 1, "station")
|
|
142
|
+
end
|
|
125
143
|
end
|
|
126
144
|
|
|
127
145
|
derived = { "clean" => keyre.empty? ? {} : { "keyre" => keyre } }
|
|
@@ -295,7 +295,7 @@ pub fn match_deep(check: &Value, base: &Value) -> Option<String> {
|
|
|
295
295
|
// (.sdk/test/primary/<name>.aontu carries a PENDING header). Everything else
|
|
296
296
|
// MUST contribute cases.
|
|
297
297
|
pub const PENDING_SECTIONS: &[&str] = &[
|
|
298
|
-
"fetcher", "makeFetchDef", "
|
|
298
|
+
"fetcher", "makeFetchDef", "makeResult", "featureAdd",
|
|
299
299
|
"featureHook", "featureInit",
|
|
300
300
|
];
|
|
301
301
|
|
|
@@ -1826,6 +1826,33 @@ fn feature_proxy_inactive_does_not_wrap() {
|
|
|
1826
1826
|
);
|
|
1827
1827
|
}
|
|
1828
1828
|
|
|
1829
|
+
// --- station --------------------------------------------------------------------
|
|
1830
|
+
|
|
1831
|
+
// The station adapter (installed by @voxgig/sdkgen-station) with NO open
|
|
1832
|
+
// station must be an inert no-op that emits nothing and fails nothing
|
|
1833
|
+
// (station design 3.1). Constructed through the generated make_feature
|
|
1834
|
+
// factory - never a compile-time module reference - so this file stays
|
|
1835
|
+
// compilable both with and without the station feature installed: absent,
|
|
1836
|
+
// the factory answers with the inert BaseFeature and fh_present skips the
|
|
1837
|
+
// assertions. Bound behaviour (registration, injection, wire events) is
|
|
1838
|
+
// exercised by the station library's own suite and the consumer-side
|
|
1839
|
+
// station validation, not here.
|
|
1840
|
+
#[test]
|
|
1841
|
+
fn feature_station_inert_without_open_station() {
|
|
1842
|
+
if !fh_present(&["station"]) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
let f = RUSTCRATE::make_feature("station");
|
|
1846
|
+
assert_eq!(
|
|
1847
|
+
"station",
|
|
1848
|
+
f.borrow().name(),
|
|
1849
|
+
"generated factory has a station arm"
|
|
1850
|
+
);
|
|
1851
|
+
let h = fh_make(None, vec![(f, Value::Noval)]);
|
|
1852
|
+
let res = h.op(opspec("load"));
|
|
1853
|
+
assert!(res.ok, "expected inert station to pass the op through");
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1829
1856
|
// --- composition ----------------------------------------------------------------
|
|
1830
1857
|
|
|
1831
1858
|
#[test]
|
|
@@ -19,6 +19,14 @@ fn default_http_fetch(fullurl: &str, fetchdef: &Value) -> Result<Value, ProjectN
|
|
|
19
19
|
builder = builder.proxy(p);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
// A `redirect: "manual"` annotation (set by the station feature when a
|
|
23
|
+
// hosts policy is active - the same in-band channel as `proxy` above)
|
|
24
|
+
// disables automatic redirect following, so a 3xx rides back as a normal
|
|
25
|
+
// response: a Location pointing off an egress allowlist must never pull
|
|
26
|
+
// an automatic credentialed follow-up request.
|
|
27
|
+
if get_str(fetchdef, "redirect").as_deref() == Some("manual") {
|
|
28
|
+
builder = builder.redirects(0);
|
|
29
|
+
}
|
|
22
30
|
let agent = builder.build();
|
|
23
31
|
|
|
24
32
|
let mut req = agent.request(&method, fullurl);
|
|
@@ -228,6 +228,21 @@ pub fn make_options_util(ctx: &Rc<Context>) -> Value {
|
|
|
228
228
|
} else {
|
|
229
229
|
feature_order = names;
|
|
230
230
|
}
|
|
231
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
232
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
233
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
234
|
+
// or first, when no test entry exists. Without this the sorted
|
|
235
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
236
|
+
// features, turning its wire-truth events into fiction.
|
|
237
|
+
if let Some(si) = feature_order.iter().position(|n| n == "station") {
|
|
238
|
+
feature_order.remove(si);
|
|
239
|
+
let at = feature_order
|
|
240
|
+
.iter()
|
|
241
|
+
.position(|n| n == "test")
|
|
242
|
+
.map(|ti| ti + 1)
|
|
243
|
+
.unwrap_or(0);
|
|
244
|
+
feature_order.insert(at, "station".to_string());
|
|
245
|
+
}
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
248
|
let order_list =
|
|
@@ -6,6 +6,27 @@ import Foundation
|
|
|
6
6
|
import FoundationNetworking
|
|
7
7
|
#endif
|
|
8
8
|
|
|
9
|
+
// Non-following twin of URLSession.shared. The station feature's
|
|
10
|
+
// middleware sets `redirect: manual` on the fetch definition under a
|
|
11
|
+
// hosts egress policy: a 3xx must come back as a response like any
|
|
12
|
+
// other, because an automatic follow would carry injected credentials
|
|
13
|
+
// to a Location no policy approved (the ts donor's fetch honours the
|
|
14
|
+
// same key natively; redirect policy is per-delegate in URLSession,
|
|
15
|
+
// hence a second session).
|
|
16
|
+
private final class ManualRedirectDelegate: NSObject, URLSessionTaskDelegate {
|
|
17
|
+
func urlSession(
|
|
18
|
+
_ session: URLSession, task: URLSessionTask,
|
|
19
|
+
willPerformHTTPRedirection response: HTTPURLResponse,
|
|
20
|
+
newRequest request: URLRequest,
|
|
21
|
+
completionHandler: @escaping (URLRequest?) -> Void
|
|
22
|
+
) {
|
|
23
|
+
completionHandler(nil)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private let manualRedirectSession = URLSession(
|
|
28
|
+
configuration: .default, delegate: ManualRedirectDelegate(), delegateQueue: nil)
|
|
29
|
+
|
|
9
30
|
func defaultHttpFetch(_ fullurl: String, _ fetchdef: VMap) throws -> Value {
|
|
10
31
|
guard let url = URL(string: fullurl) else {
|
|
11
32
|
let m = VMap()
|
|
@@ -45,7 +66,9 @@ func defaultHttpFetch(_ fullurl: String, _ fetchdef: VMap) throws -> Value {
|
|
|
45
66
|
}
|
|
46
67
|
let box = FetchBox()
|
|
47
68
|
let sem = DispatchSemaphore(value: 0)
|
|
48
|
-
let
|
|
69
|
+
let session = gp(fetchdef, "redirect").asString == "manual"
|
|
70
|
+
? manualRedirectSession : URLSession.shared
|
|
71
|
+
let task = session.dataTask(with: req) { d, r, e in
|
|
49
72
|
box.data = d; box.resp = r; box.err = e; sem.signal()
|
|
50
73
|
}
|
|
51
74
|
task.resume()
|
|
@@ -139,6 +139,18 @@ func makeOptionsUtil(_ ctx: Context) -> VMap {
|
|
|
139
139
|
} else {
|
|
140
140
|
for n in names { featureorder.append(.string(n)) }
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
144
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
145
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
146
|
+
// or first, when no test entry exists. Without this the sorted
|
|
147
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
148
|
+
// features, turning its wire-truth events into fiction.
|
|
149
|
+
if let si = featureorder.firstIndex(where: { $0.asString == "station" }) {
|
|
150
|
+
featureorder.remove(at: si)
|
|
151
|
+
let ti = featureorder.firstIndex(where: { $0.asString == "test" })
|
|
152
|
+
featureorder.insert(.string("station"), at: (ti ?? -1) + 1)
|
|
153
|
+
}
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
let derived = VMap()
|