@voxgig/sdkgen 3.6.0 → 3.7.2
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/cmp/ReadmeTop.js +11 -4
- package/dist/cmp/ReadmeTop.js.map +1 -1
- 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/go/ReadmeIntro_go.ts +19 -4
- 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/Package_ts.ts +3 -1
- package/project/.sdk/src/cmp/ts/ReadmeIntro_ts.ts +19 -4
- 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/exists.test.ts +14 -3
- 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/cmp/ReadmeTop.ts +12 -3
- package/src/helpers/naming.ts +45 -2
- package/src/sdkgen.ts +3 -1
- package/src/utility.ts +17 -2
|
@@ -4,7 +4,7 @@ import * as Path from 'node:path'
|
|
|
4
4
|
import {
|
|
5
5
|
cmp, each, camelify, names,
|
|
6
6
|
File, Content, Folder, Fragment, Line, FeatureHook, Slot,
|
|
7
|
-
opTypeName, entityClassName, entityCollection,
|
|
7
|
+
opTypeName, entityClassName, entityCollection, tsSafeTypeName,
|
|
8
8
|
} from '@voxgig/sdkgen'
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -33,9 +33,14 @@ const Entity = cmp(function Entity(props: any) {
|
|
|
33
33
|
|
|
34
34
|
names(entrep, entity.Name, 'EntityName')
|
|
35
35
|
|
|
36
|
+
// A TS/JS global (Record, Array, Promise, ...) would shadow itself for
|
|
37
|
+
// the rest of the file — see tsSafeTypeName. The runtime `this.Name`
|
|
38
|
+
// string stays entity.Name; only the TYPE reference is ever renamed.
|
|
39
|
+
const dataType = tsSafeTypeName(entity.Name)
|
|
40
|
+
|
|
36
41
|
// Import exactly the typed models this entity references: its data type plus
|
|
37
42
|
// one request type per ACTIVE op (matches what EntityTypes_ts.ts emits).
|
|
38
|
-
const typeNames = [
|
|
43
|
+
const typeNames = [dataType]
|
|
39
44
|
const opnamesAll = Object.keys(entity.op || {})
|
|
40
45
|
;['load', 'list', 'create', 'update', 'remove'].forEach((opname: string) => {
|
|
41
46
|
if (opnamesAll.includes(opname)) {
|
|
@@ -69,6 +74,7 @@ const Entity = cmp(function Entity(props: any) {
|
|
|
69
74
|
entityname: entity.name,
|
|
70
75
|
SdkName: model.const.Name,
|
|
71
76
|
EntityName: entity.Name,
|
|
77
|
+
EntityDataType: dataType,
|
|
72
78
|
|
|
73
79
|
// Class token decoupled from the EntityName data-type token in
|
|
74
80
|
// Entity.fragment.ts so the class can be renamed independently.
|
|
@@ -124,7 +124,9 @@ const Package = cmp(async function Package(props: any) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
File({ name: 'package.json' }, () => {
|
|
127
|
-
|
|
127
|
+
// Trailing newline: POSIX wants one, and without it every diff of a
|
|
128
|
+
// regenerated package.json reports "".
|
|
129
|
+
Content(JSON.stringify(pkg, null, 2) + '\n')
|
|
128
130
|
})
|
|
129
131
|
})
|
|
130
132
|
|
|
@@ -33,6 +33,24 @@ const ReadmeIntro = cmp(function ReadmeIntro(props: any) {
|
|
|
33
33
|
const opList = (opNames.length ? opNames : ['list', 'load'])
|
|
34
34
|
.map((o) => '`' + o + '`').join(', ')
|
|
35
35
|
|
|
36
|
+
// Sibling surfaces, MODEL-DRIVEN. This line used to hardcode "the CLI, and
|
|
37
|
+
// MCP server" — those are the `go-cli` and `go-mcp` targets, and this project
|
|
38
|
+
// has neither. So both per-language READMEs advertised surfaces nothing here
|
|
39
|
+
// generates, while the top-level Surfaces table (which does gate on active
|
|
40
|
+
// targets) listed only ts/ and go/. Name what is actually active, and drop
|
|
41
|
+
// the sentence entirely when this is the only surface, so it cannot outrun
|
|
42
|
+
// the model again.
|
|
43
|
+
const targets = getModelPath(model, `main.${KIT}.target`) || {}
|
|
44
|
+
const siblings = Object.entries(targets)
|
|
45
|
+
.filter(([name, t]: any) => name !== target.name && false !== t.active)
|
|
46
|
+
.map(([name]: any) => name)
|
|
47
|
+
.sort()
|
|
48
|
+
|
|
49
|
+
const siblingNote = 0 === siblings.length ? '' : `
|
|
50
|
+
> Also generated from this model: ${siblings.map((s: string) => '`' + s + '`').join(', ')} — see
|
|
51
|
+
> the [top-level README](../README.md).
|
|
52
|
+
`
|
|
53
|
+
|
|
36
54
|
Content(`# ${model.Name} ${target.title} SDK
|
|
37
55
|
|
|
38
56
|
${tagline}
|
|
@@ -43,10 +61,7 @@ The API is exposed as capitalised, semantic **Entities** — e.g.
|
|
|
43
61
|
\`client.${eName}()\` — each with a small set of operations (${opList})
|
|
44
62
|
instead of raw URL paths and query parameters. This keeps the surface
|
|
45
63
|
predictable and low-friction for both humans and AI agents.
|
|
46
|
-
|
|
47
|
-
> Other languages, the CLI, and MCP server live alongside this one — see
|
|
48
|
-
> the [top-level README](../README.md).
|
|
49
|
-
|
|
64
|
+
${siblingNote}
|
|
50
65
|
`)
|
|
51
66
|
})
|
|
52
67
|
|
|
@@ -34,6 +34,13 @@ class Config {
|
|
|
34
34
|
return fi
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// False for a feature added at runtime via options.extend (station's
|
|
38
|
+
// adopt path) - the constructor uses this to skip makeFeature for names
|
|
39
|
+
// no generated class backs.
|
|
40
|
+
hasFeature(this: any, fn: string) {
|
|
41
|
+
return null != FEATURE_CLASS[fn]
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
|
|
@@ -17,9 +17,17 @@ class Config {
|
|
|
17
17
|
return fi
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// False for a feature added at runtime via options.extend (station's
|
|
21
|
+
// adopt path) - the constructor uses this to skip makeFeature for names
|
|
22
|
+
// no generated class backs.
|
|
23
|
+
hasFeature(this: any, fn: string) {
|
|
24
|
+
return null != FEATURE_CLASS[fn]
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
|
|
21
28
|
main = {
|
|
22
29
|
name: '$$const.Name$$',
|
|
30
|
+
// #MainMeta
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
// TODO: needs Entity superclass
|
|
21
|
-
class EntyClass extends ProjectNameEntityBase<
|
|
21
|
+
class EntyClass extends ProjectNameEntityBase<EntityDataType> {
|
|
22
22
|
|
|
23
23
|
constructor(client: ProjectNameSDK, entopts: any) {
|
|
24
24
|
super(client, entopts)
|
|
@@ -52,18 +52,26 @@ class ProjectNameSDK {
|
|
|
52
52
|
// the `test` feature installs the base mock transport and the transport
|
|
53
53
|
// features (retry/cache/netsim/proxy/ratelimit) wrap whatever is current,
|
|
54
54
|
// so `test` must be added before them to sit at the base of the chain.
|
|
55
|
+
const extend = this._options.extend || []
|
|
56
|
+
|
|
55
57
|
const featureorder = getpath(this._options, '__derived__.featureorder') || []
|
|
56
58
|
for (const fname of featureorder) {
|
|
57
59
|
const fopts = this._options.feature[fname] || {}
|
|
58
60
|
if (fopts.active) {
|
|
61
|
+
// An active name with no generated class is legal when an
|
|
62
|
+
// extend-supplied instance carries that name (station's adopt
|
|
63
|
+
// path): the instance is added below, positioned by its own
|
|
64
|
+
// __after__ entry, so skip it here rather than fail construction.
|
|
65
|
+
if (!this._rootctx.config.hasFeature(fname) &&
|
|
66
|
+
extend.some((f: any) => fname === f.name)) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
59
69
|
featureAdd(this._rootctx, this._rootctx.config.makeFeature(fname))
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
featureAdd(this._rootctx, f)
|
|
66
|
-
}
|
|
73
|
+
for (let f of extend) {
|
|
74
|
+
featureAdd(this._rootctx, f)
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
for (let f of this._features) {
|
|
@@ -7,7 +7,12 @@ CFLAGS ?= -std=c11 -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -O0 -g
|
|
|
7
7
|
INC := -I core -I utility/struct
|
|
8
8
|
LDLIBS := -lm
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# feature/*/*.c: a feature may carry a vendored library payload in its own
|
|
11
|
+
# subdirectory (the station feature vendors voxgig_station there - C has no
|
|
12
|
+
# package registry, so tier-C libraries ride the feature source; see
|
|
13
|
+
# utility/struct/ for the same precedent at the utility level). Empty when
|
|
14
|
+
# no feature ships one.
|
|
15
|
+
LIB_SRCS := $(wildcard core/*.c utility/*.c utility/struct/*.c feature/*.c feature/*/*.c entity/*.c)
|
|
11
16
|
LIB_OBJS := $(LIB_SRCS:.c=.o)
|
|
12
17
|
|
|
13
18
|
TEST_SRCS := $(wildcard tests/*.c)
|
|
@@ -166,8 +166,19 @@ voxgig_value* make_options_util(Context* ctx) {
|
|
|
166
166
|
if (has_test) {
|
|
167
167
|
voxgig_list_push(voxgig_as_list(feature_order), v_str("test"));
|
|
168
168
|
}
|
|
169
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
170
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
171
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
172
|
+
// or first, when no test entry exists. Without this the sorted
|
|
173
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
174
|
+
// features, turning its wire-truth events into fiction.
|
|
169
175
|
for (size_t i = 0; i < fn; i++) {
|
|
170
|
-
if (strcmp(names[i], "
|
|
176
|
+
if (strcmp(names[i], "station") == 0) {
|
|
177
|
+
voxgig_list_push(voxgig_as_list(feature_order), v_str("station"));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (size_t i = 0; i < fn; i++) {
|
|
181
|
+
if (strcmp(names[i], "test") != 0 && strcmp(names[i], "station") != 0) {
|
|
171
182
|
voxgig_list_push(voxgig_as_list(feature_order), v_str(names[i]));
|
|
172
183
|
}
|
|
173
184
|
}
|
|
@@ -1211,8 +1211,16 @@ inline Value makeOptions(CtxPtr ctx) {
|
|
|
1211
1211
|
std::sort(names.begin(), names.end());
|
|
1212
1212
|
bool hasTest = std::find(names.begin(), names.end(), "test") != names.end();
|
|
1213
1213
|
if (hasTest) featureOrder.push_back("test");
|
|
1214
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
1215
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
1216
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
1217
|
+
// or first, when no test entry exists. Without this the sorted
|
|
1218
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
1219
|
+
// features, turning its wire-truth events into fiction.
|
|
1220
|
+
bool hasStation = std::find(names.begin(), names.end(), "station") != names.end();
|
|
1221
|
+
if (hasStation) featureOrder.push_back("station");
|
|
1214
1222
|
for (const auto& n : names) {
|
|
1215
|
-
if (n != "test") featureOrder.push_back(n);
|
|
1223
|
+
if (n != "test" && n != "station") featureOrder.push_back(n);
|
|
1216
1224
|
}
|
|
1217
1225
|
}
|
|
1218
1226
|
}
|
|
@@ -13,30 +13,48 @@ public static partial class SdkUtility
|
|
|
13
13
|
{
|
|
14
14
|
private static readonly HttpClient DefaultHttpClient = new();
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
16
|
+
// Non-following twin of DefaultHttpClient. The station feature's
|
|
17
|
+
// middleware sets `redirect: manual` on the fetch definition under a
|
|
18
|
+
// hosts egress policy: a 3xx must come back as a response like any
|
|
19
|
+
// other, because an automatic follow would carry injected credentials
|
|
20
|
+
// to a Location no policy approved (the ts donor's fetch honours the
|
|
21
|
+
// same key natively; redirect policy is per-handler in .NET, hence a
|
|
22
|
+
// second client).
|
|
23
|
+
private static readonly HttpClient ManualRedirectHttpClient = new(
|
|
24
|
+
new HttpClientHandler
|
|
25
|
+
{
|
|
26
|
+
AllowAutoRedirect = false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Proxy-routed clients, cached per proxy URL and redirect policy (see
|
|
30
|
+
// the proxy feature's fetchdef annotation).
|
|
18
31
|
private static readonly Dictionary<string, HttpClient> ProxyClients = new();
|
|
19
32
|
private static readonly object ProxyClientsLock = new();
|
|
20
33
|
|
|
21
34
|
private static HttpClient ClientFor(Dictionary<string, object?> fetchdef)
|
|
22
35
|
{
|
|
36
|
+
var manual = fetchdef.TryGetValue("redirect", out var rraw) &&
|
|
37
|
+
rraw is string redirect && redirect == "manual";
|
|
38
|
+
|
|
23
39
|
if (fetchdef.TryGetValue("proxy", out var raw) && raw is string proxy && proxy != "")
|
|
24
40
|
{
|
|
25
41
|
lock (ProxyClientsLock)
|
|
26
42
|
{
|
|
27
|
-
|
|
43
|
+
var key = (manual ? "manual|" : "auto|") + proxy;
|
|
44
|
+
if (!ProxyClients.TryGetValue(key, out var client))
|
|
28
45
|
{
|
|
29
46
|
client = new HttpClient(new HttpClientHandler
|
|
30
47
|
{
|
|
31
48
|
Proxy = new System.Net.WebProxy(proxy),
|
|
32
49
|
UseProxy = true,
|
|
50
|
+
AllowAutoRedirect = !manual,
|
|
33
51
|
});
|
|
34
|
-
ProxyClients[
|
|
52
|
+
ProxyClients[key] = client;
|
|
35
53
|
}
|
|
36
54
|
return client;
|
|
37
55
|
}
|
|
38
56
|
}
|
|
39
|
-
return DefaultHttpClient;
|
|
57
|
+
return manual ? ManualRedirectHttpClient : DefaultHttpClient;
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
internal static Dictionary<string, object?> DefaultHttpFetch(
|
|
@@ -248,6 +248,19 @@ public static partial class SdkUtility
|
|
|
248
248
|
featureorder.Add(n);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
+
|
|
252
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
253
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
254
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
255
|
+
// or first, when no test entry exists. Without this the sorted
|
|
256
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
257
|
+
// features, turning its wire-truth events into fiction.
|
|
258
|
+
var si = featureorder.IndexOf("station");
|
|
259
|
+
if (si >= 0)
|
|
260
|
+
{
|
|
261
|
+
featureorder.RemoveAt(si);
|
|
262
|
+
featureorder.Insert(featureorder.IndexOf("test") + 1, "station");
|
|
263
|
+
}
|
|
251
264
|
}
|
|
252
265
|
|
|
253
266
|
var derived = new Dictionary<string, object?>
|
|
@@ -46,6 +46,15 @@ Future<dynamic> httpFetch(dynamic fullurl, dynamic fetchdef) async {
|
|
|
46
46
|
|
|
47
47
|
final req = await client.openUrl(method, uri);
|
|
48
48
|
|
|
49
|
+
// Manual redirects: fetchdef['redirect'] == 'manual' surfaces a 3xx as
|
|
50
|
+
// an ordinary response instead of auto-following it — HttpClient would
|
|
51
|
+
// otherwise replay the request, headers included, against whatever host
|
|
52
|
+
// Location names. Set by the station feature's transport middleware
|
|
53
|
+
// under an egress hosts policy (mirrors the ts fetch option).
|
|
54
|
+
if ('manual' == vs.getprop(fetchdef, 'redirect')) {
|
|
55
|
+
req.followRedirects = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
final headers = vs.getprop(fetchdef, 'headers');
|
|
50
59
|
if (headers is Map) {
|
|
51
60
|
headers.forEach((k, v) {
|
|
@@ -47,6 +47,12 @@ dynamic makeOptions(dynamic ctx) {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
'utility': {},
|
|
50
|
+
// Feature INSTANCES supplied at construction (the station adopt
|
|
51
|
+
// path): consumed by the constructor's featureAdd loop, so they are
|
|
52
|
+
// class instances, not data - `$ANY` accepts them verbatim. Without
|
|
53
|
+
// this entry the seam is dead: the constructor reads
|
|
54
|
+
// options.extend, but validate rejected the key.
|
|
55
|
+
'extend': '`\$ANY`',
|
|
50
56
|
'system': {},
|
|
51
57
|
'test': {
|
|
52
58
|
'active': false,
|
|
@@ -177,6 +183,17 @@ dynamic makeOptions(dynamic ctx) {
|
|
|
177
183
|
} else {
|
|
178
184
|
featureorder.addAll(names);
|
|
179
185
|
}
|
|
186
|
+
// Station special case, mirroring test's: its transport wrap must
|
|
187
|
+
// sit immediately outside the base transport (inside retry/cache/
|
|
188
|
+
// netsim), so map-form activation hoists it to just after test -
|
|
189
|
+
// or first, when no test entry exists. Without this the sorted
|
|
190
|
+
// default would init station last and wrap OUTSIDE the recording
|
|
191
|
+
// features, turning its wire-truth events into fiction.
|
|
192
|
+
if (featureorder.contains('station')) {
|
|
193
|
+
featureorder.remove('station');
|
|
194
|
+
final ti = featureorder.indexOf('test');
|
|
195
|
+
featureorder.insert(-1 == ti ? 0 : ti + 1, 'station');
|
|
196
|
+
}
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
opts['__derived__'] = {
|
|
@@ -261,6 +261,12 @@ defmodule ProjectName.Utility do
|
|
|
261
261
|
"entity" => %{"`$CHILD`" => %{"`$OPEN`" => true, "active" => false, "alias" => %{}}},
|
|
262
262
|
"feature" => %{"`$CHILD`" => %{"`$OPEN`" => true, "active" => false}},
|
|
263
263
|
"utility" => %{},
|
|
264
|
+
# Feature INSTANCES supplied at construction (the station adopt
|
|
265
|
+
# path): consumed by the constructor's extend loop, so they are
|
|
266
|
+
# struct feature nodes, not data - `$ANY` accepts them verbatim.
|
|
267
|
+
# Without this entry the seam is dead: the constructor reads
|
|
268
|
+
# options.extend, but validate rejected the key.
|
|
269
|
+
"extend" => "`$ANY`",
|
|
264
270
|
"system" => %{},
|
|
265
271
|
"test" => %{"active" => false, "entity" => %{"`$OPEN`" => true}},
|
|
266
272
|
"clean" => %{"keys" => "key,token,id"},
|
|
@@ -359,8 +365,29 @@ defmodule ProjectName.Utility do
|
|
|
359
365
|
fmap = if S.ismap(fmap), do: fmap, else: S.jm([])
|
|
360
366
|
names = S.keysof(fmap)
|
|
361
367
|
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
names =
|
|
369
|
+
if Enum.member?(names, "test") do
|
|
370
|
+
["test" | Enum.reject(names, &(&1 == "test"))]
|
|
371
|
+
else
|
|
372
|
+
names
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Station special case, mirroring test's: its transport wrap must
|
|
376
|
+
# sit immediately outside the base transport (inside retry/cache/
|
|
377
|
+
# netsim), so map-form activation hoists it to just after test -
|
|
378
|
+
# or first, when no test entry exists. Without this the sorted
|
|
379
|
+
# default would init station last and wrap OUTSIDE the recording
|
|
380
|
+
# features, turning its wire-truth events into fiction.
|
|
381
|
+
if Enum.member?(names, "station") do
|
|
382
|
+
rest = Enum.reject(names, &(&1 == "station"))
|
|
383
|
+
|
|
384
|
+
at =
|
|
385
|
+
case Enum.find_index(rest, &(&1 == "test")) do
|
|
386
|
+
nil -> 0
|
|
387
|
+
ti -> ti + 1
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
List.insert_at(rest, at, "station")
|
|
364
391
|
else
|
|
365
392
|
names
|
|
366
393
|
end
|
|
@@ -1518,7 +1545,16 @@ defmodule ProjectName.Utility do
|
|
|
1518
1545
|
{url, hlist}
|
|
1519
1546
|
end
|
|
1520
1547
|
|
|
1521
|
-
|
|
1548
|
+
# A `redirect: "manual"` annotation (set by the station feature when a
|
|
1549
|
+
# hosts egress policy is active - the same in-band channel go and rust
|
|
1550
|
+
# use) disables :httpc's automatic redirect following, so a 3xx rides
|
|
1551
|
+
# back as a normal response: a Location pointing off an egress
|
|
1552
|
+
# allowlist must never pull an automatic credentialed follow-up
|
|
1553
|
+
# request.
|
|
1554
|
+
http_opts =
|
|
1555
|
+
if S.getprop(fetchdef, "redirect") == "manual", do: [autoredirect: false], else: []
|
|
1556
|
+
|
|
1557
|
+
case :httpc.request(method, request, http_opts, body_format: :binary) do
|
|
1522
1558
|
{:ok, {{_v, status, _reason}, resp_headers, resp_body}} ->
|
|
1523
1559
|
rh =
|
|
1524
1560
|
Enum.reduce(resp_headers, S.jm([]), fn {k, v}, acc ->
|
|
@@ -50,6 +50,25 @@ defmodule ProjectName.FeatureTest do
|
|
|
50
50
|
assert has?("test")
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# === station ===
|
|
54
|
+
|
|
55
|
+
# The station adapter (installed by @voxgig/sdkgen-station) with NO open
|
|
56
|
+
# station must be an inert no-op that emits nothing and fails nothing
|
|
57
|
+
# (station design 3.1). Driven through the generated factory - never a
|
|
58
|
+
# compile-time module reference - so this file stays green both with and
|
|
59
|
+
# without the feature installed (absent, the harness simply adds no
|
|
60
|
+
# feature). Bound behaviour (registration, injection, wire events) is
|
|
61
|
+
# exercised by the station library's own suite and the consumer-side
|
|
62
|
+
# station validation, not here.
|
|
63
|
+
test "station adapter with no open station is an inert no-op" do
|
|
64
|
+
h = harness([fspec("station")])
|
|
65
|
+
res = FH.op(h, %{})
|
|
66
|
+
assert res.ok == true
|
|
67
|
+
|
|
68
|
+
# Inert: the transport is neither wrapped nor marked.
|
|
69
|
+
assert S.getprop(h.utility, "__station_wrap__") == nil
|
|
70
|
+
end
|
|
71
|
+
|
|
53
72
|
# === netsim ===
|
|
54
73
|
|
|
55
74
|
test "netsim fixed latency then delegate" do
|
|
@@ -112,6 +112,78 @@ defmodule ProjectName.PrimaryUtilityTest do
|
|
|
112
112
|
assert S.getpath(opts, "__derived__.clean.keyre") == "key|token|id"
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# --- feature add-order ----------------------------------------------------
|
|
116
|
+
# options.feature accepts an ordered LIST (developer add-order) or a map
|
|
117
|
+
# (defaults test-first); make_options records the resolved order in
|
|
118
|
+
# __derived__.featureorder. Driven with a synthetic ctx (empty config
|
|
119
|
+
# options) so the assertions do not depend on this SDK's own features.
|
|
120
|
+
|
|
121
|
+
defp resolve_order(feature) do
|
|
122
|
+
ctx =
|
|
123
|
+
S.jm([
|
|
124
|
+
"utility", Utility.new(),
|
|
125
|
+
"options", H.deep(%{"feature" => feature}),
|
|
126
|
+
"config", H.deep(%{"options" => %{}})
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
fo = S.getpath(Utility.make_options(ctx), "__derived__.featureorder")
|
|
130
|
+
n = S.size(fo)
|
|
131
|
+
order = if n == 0, do: [], else: Enum.map(0..(n - 1), fn i -> S.getelem(fo, i) end)
|
|
132
|
+
Enum.join(order, ",")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
test "feature order: map form is test-first" do
|
|
136
|
+
assert "test,metrics" ==
|
|
137
|
+
resolve_order(%{"metrics" => %{"active" => true}, "test" => %{"active" => true}})
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
test "feature order: an explicit list preserves its order" do
|
|
141
|
+
assert "metrics,test" ==
|
|
142
|
+
resolve_order([
|
|
143
|
+
%{"name" => "metrics", "active" => true},
|
|
144
|
+
%{"name" => "test", "active" => true}
|
|
145
|
+
])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Station special case, mirroring test's: its transport wrap must sit
|
|
149
|
+
# immediately outside the base transport, so map-form activation hoists
|
|
150
|
+
# it to just after test - or first, when no test entry exists.
|
|
151
|
+
test "feature order: map form hoists station after test" do
|
|
152
|
+
assert "test,station,metrics" ==
|
|
153
|
+
resolve_order(%{
|
|
154
|
+
"metrics" => %{"active" => true},
|
|
155
|
+
"station" => %{"active" => true},
|
|
156
|
+
"test" => %{"active" => true}
|
|
157
|
+
})
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
test "feature order: station first when no test entry exists" do
|
|
161
|
+
assert "station,metrics" ==
|
|
162
|
+
resolve_order(%{
|
|
163
|
+
"metrics" => %{"active" => true},
|
|
164
|
+
"station" => %{"active" => true}
|
|
165
|
+
})
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The extend seam (feature INSTANCES supplied at construction - the
|
|
169
|
+
# station adopt path): validate must accept the key verbatim, or the
|
|
170
|
+
# constructor's extend loop reads a stripped option and the seam is dead.
|
|
171
|
+
test "make_options preserves options.extend through validation" do
|
|
172
|
+
marker = S.jm(["name", "station"])
|
|
173
|
+
|
|
174
|
+
ctx =
|
|
175
|
+
S.jm([
|
|
176
|
+
"utility", Utility.new(),
|
|
177
|
+
"options", S.jm(["extend", S.jt([marker])]),
|
|
178
|
+
"config", H.deep(%{"options" => %{}})
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
extend = S.getprop(Utility.make_options(ctx), "extend")
|
|
182
|
+
assert S.islist(extend)
|
|
183
|
+
assert S.size(extend) == 1
|
|
184
|
+
assert S.getprop(S.getelem(extend, 0), "name") == "station"
|
|
185
|
+
end
|
|
186
|
+
|
|
115
187
|
test "make_error formats a namespaced message and raises by default" do
|
|
116
188
|
c = client()
|
|
117
189
|
ctx = ctx(c, "load")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package sdktest
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"errors"
|
|
4
5
|
"fmt"
|
|
5
6
|
"strings"
|
|
6
7
|
"testing"
|
|
@@ -556,27 +557,48 @@ func TestPrimaryUtility(t *testing.T) {
|
|
|
556
557
|
})
|
|
557
558
|
})
|
|
558
559
|
|
|
560
|
+
// Was one hand-written case (the single-point path) covering one of this
|
|
561
|
+
// utility's seven branches, which is how the corpus fixture came to be
|
|
562
|
+
// marked deferred as "needs a real client". It does not: NewContext
|
|
563
|
+
// rebuilds Op from opname + entity + config, and Options can be supplied
|
|
564
|
+
// literally. Driven from the corpus now, so TS and Go assert the same
|
|
565
|
+
// branches.
|
|
566
|
+
//
|
|
567
|
+
// TS returns the error AS the value; Go returns it as a second result. The
|
|
568
|
+
// corpus says `match: out: code` for both, so the error is normalised to a
|
|
569
|
+
// map carrying its code here rather than forking the fixture per language.
|
|
559
570
|
t.Run("makePoint-basic", func(t *testing.T) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
"
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
571
|
+
runsetNamed(t, "makePoint", getSpec(primary, "makePoint", "basic"), func(entry map[string]any) (any, error) {
|
|
572
|
+
ctxmap, _ := entry["ctx"].(map[string]any)
|
|
573
|
+
|
|
574
|
+
// NewContext resolves Op from the ENTITY NAME, and reaches it
|
|
575
|
+
// through the Entity interface — a literal {name:...} map from the
|
|
576
|
+
// fixture is not one, so entname would be "" and every lookup would
|
|
577
|
+
// miss, reporting point_no_points for all seven cases. TS reads the
|
|
578
|
+
// same field with getprop and accepts the plain map. Swap in the
|
|
579
|
+
// package's minimal Entity so both ports resolve the same op.
|
|
580
|
+
if em, ok := ctxmap["entity"].(map[string]any); ok {
|
|
581
|
+
name, _ := em["name"].(string)
|
|
582
|
+
made := []any{}
|
|
583
|
+
swapped := map[string]any{}
|
|
584
|
+
for k, v := range ctxmap {
|
|
585
|
+
swapped[k] = v
|
|
586
|
+
}
|
|
587
|
+
swapped["entity"] = &plEntity{name: name, made: &made}
|
|
588
|
+
ctxmap = swapped
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
ctx := makeCtxFromMap(ctxmap, client, utility)
|
|
592
|
+
point, err := utility.MakePoint(ctx)
|
|
593
|
+
if err != nil {
|
|
594
|
+
var sdkErr *sdk.ProjectNameError
|
|
595
|
+
if errors.As(err, &sdkErr) {
|
|
596
|
+
return map[string]any{"code": sdkErr.Code}, nil
|
|
597
|
+
}
|
|
598
|
+
return nil, err
|
|
599
|
+
}
|
|
600
|
+
return point, nil
|
|
601
|
+
})
|
|
580
602
|
})
|
|
581
603
|
|
|
582
604
|
t.Run("makeUrl-basic", func(t *testing.T) {
|
|
@@ -218,7 +218,7 @@ type RunSubject func(entry map[string]any) (any, error)
|
|
|
218
218
|
// PENDING sections are the ones deliberately left empty in the shared corpus
|
|
219
219
|
// (.sdk/test/primary/<name>.aontu). Everything else MUST contribute cases.
|
|
220
220
|
var pendingSections = map[string]bool{
|
|
221
|
-
"fetcher": true, "makeFetchDef": true, "
|
|
221
|
+
"fetcher": true, "makeFetchDef": true, "makeResult": true,
|
|
222
222
|
"featureAdd": true, "featureHook": true, "featureInit": true,
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -57,6 +57,20 @@ func defaultHTTPFetch(fullurl string, fetchdef map[string]any) (map[string]any,
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Honour a redirect annotation: "manual" surfaces a 3xx as an ordinary
|
|
61
|
+
// response instead of auto-following it. In the ts/js targets fetchdef
|
|
62
|
+
// rides straight into fetch(), where `redirect` is a native option -
|
|
63
|
+
// this is the same seam for Go's auto-following http.Client. The
|
|
64
|
+
// station feature sets it under a hosts egress policy, so a Location
|
|
65
|
+
// off the allowlist cannot pull an automatic credentialed follow-up.
|
|
66
|
+
if redirect, ok := fetchdef["redirect"].(string); ok && redirect == "manual" {
|
|
67
|
+
manual := *client
|
|
68
|
+
manual.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
69
|
+
return http.ErrUseLastResponse
|
|
70
|
+
}
|
|
71
|
+
client = &manual
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
resp, err := client.Do(req)
|
|
61
75
|
if err != nil {
|
|
62
76
|
return nil, err
|