@voxgig/sdkgen 4.2.7 → 4.3.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/helpers/naming.d.ts +2 -1
- package/dist/helpers/naming.js +50 -12
- package/dist/helpers/naming.js.map +1 -1
- package/dist/sdkgen.d.ts +2 -2
- package/dist/sdkgen.js +4 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/tm/go/test/custom_utility_test.go +103 -0
- package/project/.sdk/tm/go/test/feature_corpus_test.go +550 -0
- package/project/.sdk/tm/go/utility/make_options.go +7 -1
- package/project/.sdk/tm/go/utility/register.go +194 -0
- package/project/.sdk/tm/java/test/CustomUtilityTest.java +55 -0
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +463 -0
- package/project/.sdk/tm/java/utility/MakeOptions.java +11 -2
- package/project/.sdk/tm/java/utility/Register.java +91 -0
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +24 -3
- package/project/.sdk/tm/perl/t/feature_corpus.t +345 -0
- package/project/.sdk/tm/perl/utility/make_options.pm +33 -1
- package/project/.sdk/tm/php/core/Context.php +3 -0
- package/project/.sdk/tm/php/core/Control.php +13 -0
- package/project/.sdk/tm/php/core/Error.php +12 -0
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +376 -0
- package/project/.sdk/tm/php/utility/MakeOptions.php +30 -1
- package/project/.sdk/tm/py/pkg/utility/make_options.py +42 -1
- package/project/.sdk/tm/py/test/test_feature_corpus.py +309 -0
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +281 -0
- package/project/.sdk/tm/rb/utility/make_options.rb +32 -1
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +24 -3
- package/project/sdkgen-package.json +1 -1
- package/src/helpers/naming.ts +54 -12
- package/src/sdkgen.ts +2 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!perl
|
|
2
|
+
# ProjectName SDK feature corpus test
|
|
3
|
+
#
|
|
4
|
+
# Feature behaviour, driven by the SHARED corpus.
|
|
5
|
+
#
|
|
6
|
+
# The same route t/primary_utility.t takes for the utilities: language-neutral
|
|
7
|
+
# cases in .sdk/test/test.json, executed against THIS generated SDK. The
|
|
8
|
+
# feature is the ordinary package, built by the generated config, installed by
|
|
9
|
+
# the generated constructor, and driven by a real entity operation. Not a
|
|
10
|
+
# miniature of the pipeline, which can only be as right as the miniature.
|
|
11
|
+
#
|
|
12
|
+
# Everything in a case is data. The one piece perl writes for itself is
|
|
13
|
+
# turning scripted responses into a fetcher, through the documented
|
|
14
|
+
# `utility.fetcher` override.
|
|
15
|
+
|
|
16
|
+
use strict;
|
|
17
|
+
use warnings;
|
|
18
|
+
use Test::More;
|
|
19
|
+
use FindBin;
|
|
20
|
+
use lib "$FindBin::Bin/../lib";
|
|
21
|
+
use Cwd ();
|
|
22
|
+
use Scalar::Util ();
|
|
23
|
+
|
|
24
|
+
use ProjectNameSDK;
|
|
25
|
+
|
|
26
|
+
my $TEST_JSON = Cwd::abs_path("$FindBin::Bin/../../.sdk/test/test.json");
|
|
27
|
+
|
|
28
|
+
unless (defined $TEST_JSON && -e $TEST_JSON) {
|
|
29
|
+
plan skip_all => 'test.json corpus not found';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Features with a corpus section. A name here with no section is a skip, not
|
|
33
|
+
# a failure: an SDK generated without the feature has nothing to run.
|
|
34
|
+
my @FEATURE_CORPUS_NAMES = ('cost');
|
|
35
|
+
|
|
36
|
+
# The standard operation names, in the order the runner prefers them.
|
|
37
|
+
my @FEATURE_CORPUS_OPS = qw(load list create update remove);
|
|
38
|
+
|
|
39
|
+
my $CORPUS = do {
|
|
40
|
+
open my $fh, '<', $TEST_JSON or die "cannot read $TEST_JSON: $!";
|
|
41
|
+
local $/;
|
|
42
|
+
my $raw = <$fh>;
|
|
43
|
+
close $fh;
|
|
44
|
+
Voxgig::Struct::parse_json($raw);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
# A corpus with no `feature` section is a SKIP, not a failure. Each
|
|
48
|
+
# project carries its OWN materialised copy of .sdk/test/test.json, so a
|
|
49
|
+
# project scaffolded before the section existed legitimately has no cases
|
|
50
|
+
# to run - and a hard assertion here turned that into a red suite in every
|
|
51
|
+
# SDK on the fleet, for a corpus the project had simply not re-pulled yet.
|
|
52
|
+
# The strict check belongs where the corpus is CONTROLLED: sdkgen's own
|
|
53
|
+
# end-to-end lane supplies one and requires the cases to actually run.
|
|
54
|
+
unless (defined $CORPUS->{feature}) {
|
|
55
|
+
plan skip_all => "this project's test.json has no `feature` section - "
|
|
56
|
+
. 'recompile the corpus (create-sdkgen .sdk/test/feature/) to run these cases';
|
|
57
|
+
}
|
|
58
|
+
ok(defined $CORPUS->{feature}, 'the corpus carries a feature section');
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# A scripted transport built from a case's `res` list. Responses are consumed
|
|
62
|
+
# in order and the last one repeats, so a case that does not care how many
|
|
63
|
+
# attempts happen need only declare one.
|
|
64
|
+
#
|
|
65
|
+
# Returns the shape the real fetcher returns: a (response, err) PAIR, with the
|
|
66
|
+
# parsed body behind a `json` coderef and `body` as the raw string. A script
|
|
67
|
+
# that only set `body` would look like an empty result, which reads as a
|
|
68
|
+
# feature defect rather than a mis-shaped script.
|
|
69
|
+
sub scripted_fetcher {
|
|
70
|
+
my ($res) = @_;
|
|
71
|
+
my $n = -1;
|
|
72
|
+
return sub {
|
|
73
|
+
my ($ctx, $fullurl, $fetchdef) = @_;
|
|
74
|
+
$n++;
|
|
75
|
+
my $spec = {};
|
|
76
|
+
if (ref $res eq 'ARRAY' && @$res) {
|
|
77
|
+
my $i = $n >= scalar(@$res) ? scalar(@$res) - 1 : $n;
|
|
78
|
+
$spec = $res->[$i] || {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (($spec->{throw} || 0) eq '1' || (defined $spec->{throw} && $spec->{throw})) {
|
|
82
|
+
return (undef, 'scripted transport failure');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
my $status = defined $spec->{status} ? int($spec->{status}) : 200;
|
|
86
|
+
my $body = defined $spec->{body} ? $spec->{body} : {};
|
|
87
|
+
|
|
88
|
+
return ({
|
|
89
|
+
'status' => $status,
|
|
90
|
+
'statusText' => ($status < 400 ? 'OK' : 'ERR'),
|
|
91
|
+
'headers' => { %{ $spec->{headers} || {} } },
|
|
92
|
+
'json' => sub { $body },
|
|
93
|
+
'body' => Voxgig::Struct::stringify($body),
|
|
94
|
+
}, undef);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Build a client the way a caller would.
|
|
100
|
+
#
|
|
101
|
+
# The plain constructor, not the test-mode one: the `test` feature is
|
|
102
|
+
# transport: 'base' and REPLACES the transport, so a client in test mode
|
|
103
|
+
# would shadow the script.
|
|
104
|
+
sub build_client {
|
|
105
|
+
my ($kase) = @_;
|
|
106
|
+
my $opts = { 'utility' => { 'fetcher' => scripted_fetcher($kase->{res}) } };
|
|
107
|
+
$opts->{feature} = $kase->{feature} if defined $kase->{feature};
|
|
108
|
+
return ProjectNameSDK->new($opts);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Every operation this SDK declares, in a stable order.
|
|
113
|
+
#
|
|
114
|
+
# The corpus cannot name an entity - it is shared by SDKs with none in common
|
|
115
|
+
# - so the runner finds them here. An entity accessor is a capitalised client
|
|
116
|
+
# method whose result answers get_name.
|
|
117
|
+
sub candidates {
|
|
118
|
+
my ($client) = @_;
|
|
119
|
+
my %found;
|
|
120
|
+
|
|
121
|
+
my $pkg = ref $client;
|
|
122
|
+
no strict 'refs';
|
|
123
|
+
for my $sym (sort keys %{"${pkg}::"}) {
|
|
124
|
+
next unless $sym =~ /^[A-Z]/;
|
|
125
|
+
next unless defined &{"${pkg}::${sym}"};
|
|
126
|
+
my $ent = eval { $client->$sym() };
|
|
127
|
+
next unless defined $ent && Scalar::Util::blessed($ent);
|
|
128
|
+
next unless $ent->can('get_name');
|
|
129
|
+
my $entname = eval { $ent->get_name };
|
|
130
|
+
next unless defined $entname && length $entname;
|
|
131
|
+
$found{$entname} = [$sym, $ent];
|
|
132
|
+
}
|
|
133
|
+
use strict 'refs';
|
|
134
|
+
|
|
135
|
+
my @out;
|
|
136
|
+
for my $entname (sort keys %found) {
|
|
137
|
+
my ($accessor, $ent) = @{ $found{$entname} };
|
|
138
|
+
for my $opname (@FEATURE_CORPUS_OPS) {
|
|
139
|
+
next unless $ent->can($opname);
|
|
140
|
+
push @out, {
|
|
141
|
+
key => "$entname.$opname",
|
|
142
|
+
accessor => $accessor,
|
|
143
|
+
op => $opname,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return @out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
sub invoke {
|
|
152
|
+
my ($client, $op, $ctrl) = @_;
|
|
153
|
+
my $acc = $op->{accessor};
|
|
154
|
+
my $fn = $op->{op};
|
|
155
|
+
my $ent = $client->$acc();
|
|
156
|
+
return $ent->$fn({}, $ctrl);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Pick operations by DRIVING them: an op is usable when it completes against a
|
|
161
|
+
# plain 200 with no feature active. Declared operations are not all callable
|
|
162
|
+
# with no arguments, and a case failing for that reason would read as a
|
|
163
|
+
# feature defect.
|
|
164
|
+
sub usable_ops {
|
|
165
|
+
my ($want) = @_;
|
|
166
|
+
my @picked;
|
|
167
|
+
for my $cand (candidates(build_client({}))) {
|
|
168
|
+
my $ok = eval { invoke(build_client({}), $cand, {}); 1 };
|
|
169
|
+
next unless $ok;
|
|
170
|
+
push @picked, $cand;
|
|
171
|
+
last if scalar(@picked) >= $want;
|
|
172
|
+
}
|
|
173
|
+
return @picked;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Replace #OPn throughout a case, keys included.
|
|
178
|
+
sub resolve {
|
|
179
|
+
my ($node, $tokens) = @_;
|
|
180
|
+
if (ref $node eq 'ARRAY') {
|
|
181
|
+
return [ map { resolve($_, $tokens) } @$node ];
|
|
182
|
+
}
|
|
183
|
+
if (ref $node eq 'HASH') {
|
|
184
|
+
my %out;
|
|
185
|
+
for my $k (keys %$node) {
|
|
186
|
+
$out{ resolve($k, $tokens) } = resolve($node->{$k}, $tokens);
|
|
187
|
+
}
|
|
188
|
+
return \%out;
|
|
189
|
+
}
|
|
190
|
+
if (!ref $node && defined $node) {
|
|
191
|
+
my $out = $node;
|
|
192
|
+
for my $tok (keys %$tokens) {
|
|
193
|
+
my $q = quotemeta $tok;
|
|
194
|
+
$out =~ s/$q/$tokens->{$tok}/g;
|
|
195
|
+
}
|
|
196
|
+
return $out;
|
|
197
|
+
}
|
|
198
|
+
return $node;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# The highest #OPn a case mentions.
|
|
203
|
+
sub tokens_used {
|
|
204
|
+
my ($kase) = @_;
|
|
205
|
+
my $json = Voxgig::Struct::stringify($kase);
|
|
206
|
+
my $max = 0;
|
|
207
|
+
while ($json =~ /#OP(\d+)/g) {
|
|
208
|
+
$max = $1 if $1 > $max;
|
|
209
|
+
}
|
|
210
|
+
return $max;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
sub member {
|
|
215
|
+
my ($actual, $key) = @_;
|
|
216
|
+
return (undef, 0) unless defined $actual;
|
|
217
|
+
if (ref $actual eq 'HASH' || Scalar::Util::blessed($actual)) {
|
|
218
|
+
return ($actual->{$key}, 1) if exists $actual->{$key};
|
|
219
|
+
}
|
|
220
|
+
return (undef, 0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Assert that `actual` contains `expect`, recursively. Cases assert only the
|
|
225
|
+
# fields they are about, so a full deep comparison would force every case to
|
|
226
|
+
# restate the whole record.
|
|
227
|
+
sub subset {
|
|
228
|
+
my ($actual, $expect, $path) = @_;
|
|
229
|
+
|
|
230
|
+
if (ref $expect eq 'HASH') {
|
|
231
|
+
for my $k (sort keys %$expect) {
|
|
232
|
+
my ($got, $found) = member($actual, $k);
|
|
233
|
+
ok($found, "$path.$k exists") or next;
|
|
234
|
+
subset($got, $expect->{$k}, "$path.$k");
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# JSON true/false parse to a blessed Voxgig::Struct::Bool, and perl's own
|
|
240
|
+
# booleans are a bare 1 or ''. Compare truth, not spelling: stringified,
|
|
241
|
+
# those two are 'true' and '1' and would never match.
|
|
242
|
+
if (Voxgig::Struct::is_jbool($expect)) {
|
|
243
|
+
is(($actual ? 1 : 0), ($expect ? 1 : 0), "$path is $expect");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!ref $expect && defined $expect && Scalar::Util::looks_like_number($expect)) {
|
|
248
|
+
ok(!ref $actual && defined $actual && Scalar::Util::looks_like_number($actual),
|
|
249
|
+
"$path is a number") or return;
|
|
250
|
+
# Money is float arithmetic; compare with a tolerance far below any amount
|
|
251
|
+
# a case states.
|
|
252
|
+
ok(abs($actual - $expect) < 1e-9, "$path == $expect (got "
|
|
253
|
+
. (defined $actual ? $actual : 'undef') . ')');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
is($actual, $expect, $path);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
sub record {
|
|
262
|
+
my ($client, $name) = @_;
|
|
263
|
+
return $client->{"_$name"};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
my @ops = usable_ops(2);
|
|
268
|
+
|
|
269
|
+
# At least one operation, or every case would skip and this file would report
|
|
270
|
+
# green having run nothing.
|
|
271
|
+
ok(scalar(@ops) > 0,
|
|
272
|
+
'this SDK has an operation the corpus can drive') or do {
|
|
273
|
+
done_testing();
|
|
274
|
+
exit 0;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
for my $name (@FEATURE_CORPUS_NAMES) {
|
|
278
|
+
my $section = ($CORPUS->{feature} || {})->{$name};
|
|
279
|
+
next unless defined $section;
|
|
280
|
+
|
|
281
|
+
my $cases = (($section->{basic} || {})->{set}) || [];
|
|
282
|
+
ok(scalar(@$cases) > 0,
|
|
283
|
+
"corpus section feature.$name has cases (an emptied fixture must fail loudly)")
|
|
284
|
+
or next;
|
|
285
|
+
|
|
286
|
+
# Probed by ACTIVATING it: the feature defaults to inactive, so an idle
|
|
287
|
+
# client never builds it and its absence says nothing.
|
|
288
|
+
my $probe = build_client({ feature => [ { name => $name, active => 1 } ] });
|
|
289
|
+
next unless defined record($probe, $name);
|
|
290
|
+
|
|
291
|
+
my %by_key = map { $_->{key} => $_ } @ops;
|
|
292
|
+
|
|
293
|
+
my $ran = 0;
|
|
294
|
+
for my $raw (@$cases) {
|
|
295
|
+
my $need = tokens_used($raw);
|
|
296
|
+
next if $need > scalar(@ops);
|
|
297
|
+
|
|
298
|
+
my %tokens;
|
|
299
|
+
for my $i (0 .. $need - 1) {
|
|
300
|
+
$tokens{ '#OP' . ($i + 1) } = $ops[$i]{key};
|
|
301
|
+
}
|
|
302
|
+
my $kase = resolve($raw, \%tokens);
|
|
303
|
+
|
|
304
|
+
my $client = build_client($kase);
|
|
305
|
+
my $label = $kase->{name} || '';
|
|
306
|
+
|
|
307
|
+
for my $step (@{ $kase->{op} || [] }) {
|
|
308
|
+
my $op = $by_key{ $step->{op} };
|
|
309
|
+
ok(defined $op, "$label: operation $step->{op} is known") or next;
|
|
310
|
+
my $ctrl = $step->{ctrl} || {};
|
|
311
|
+
my $wanterr = $step->{err};
|
|
312
|
+
|
|
313
|
+
my $ok = eval { invoke($client, $op, $ctrl); 1 };
|
|
314
|
+
my $err = $@;
|
|
315
|
+
|
|
316
|
+
if (!defined $wanterr) {
|
|
317
|
+
ok($ok, "$label: $step->{op} succeeded")
|
|
318
|
+
or diag("failed unexpectedly: $err");
|
|
319
|
+
next;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
ok(!$ok, "$label: $step->{op} failed as expected") or next;
|
|
323
|
+
|
|
324
|
+
if (!ref $wanterr) {
|
|
325
|
+
# The CODE, not the message: make_error prefixes and humanises the
|
|
326
|
+
# text, so matching it would pass on any error mentioning the word.
|
|
327
|
+
my $code = (Scalar::Util::blessed($err) && $err->can('code'))
|
|
328
|
+
? $err->code : undef;
|
|
329
|
+
is($code, $wanterr, "$label: error code");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
subset(record($client, $name), $kase->{out}, "$label: _$name");
|
|
334
|
+
$ran++;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
ok($ran > 0, "at least one feature.$name case ran");
|
|
338
|
+
# Say how many ran. A partial run is legitimate (an SDK with one operation
|
|
339
|
+
# skips the cases needing two) but it should be visible rather than
|
|
340
|
+
# inferred from a green tick.
|
|
341
|
+
diag(sprintf('feature.%s: ran %d of %d case(s) against %d operation(s)',
|
|
342
|
+
$name, $ran, scalar(@$cases), scalar(@ops)));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
done_testing();
|
|
@@ -19,10 +19,42 @@ $REGISTRY{make_options} = sub {
|
|
|
19
19
|
my ($ctx) = @_;
|
|
20
20
|
my $options = $ctx->{options} || {};
|
|
21
21
|
|
|
22
|
+
# Merge custom utility overrides.
|
|
23
|
+
#
|
|
24
|
+
# A key naming a real utility member REPLACES it; anything else is attached
|
|
25
|
+
# as a custom extra. This mirrors ts, where the utility is an open object
|
|
26
|
+
# and one setprop does both.
|
|
27
|
+
#
|
|
28
|
+
# Without the replace half this was a no-op: every entry went to
|
|
29
|
+
# `{utility}{custom}`, which nothing reads, so a caller passing
|
|
30
|
+
# `utility => { fetcher => $my_transport }` - the documented way to script
|
|
31
|
+
# the transport, and the seam the shared feature corpus runs on - was
|
|
32
|
+
# silently ignored while ts and js honoured it.
|
|
33
|
+
#
|
|
34
|
+
# Option keys are camelCase, as ts spells them; members here are
|
|
35
|
+
# snake_case. Converting rather than listing keeps the mapping to one rule,
|
|
36
|
+
# so a utility added later is overridable without touching this. The
|
|
37
|
+
# registrar has already populated every member, so `exists` is the test for
|
|
38
|
+
# "is this a real one" - once the key is known to be a PUBLIC name.
|
|
22
39
|
my $custom_utils = ProjectNameHelpers::gp($options, 'utility');
|
|
23
40
|
if (Voxgig::Struct::ismap($custom_utils) && $ctx->{utility}) {
|
|
41
|
+
my $utility = $ctx->{utility};
|
|
24
42
|
for my $k (keys %$custom_utils) {
|
|
25
|
-
|
|
43
|
+
# Public utility names are camelCase and carry no underscore, so an
|
|
44
|
+
# underscore means the caller named something of their own - possibly
|
|
45
|
+
# the INTERNAL spelling of a real member. `make_error` must stay an
|
|
46
|
+
# extension in `custom`; replacing the pipeline function with it (ts,
|
|
47
|
+
# js and go all keep it) would break the error path on the next
|
|
48
|
+
# request, silently.
|
|
49
|
+
my $public_name = ($k !~ /_/);
|
|
50
|
+
my $member = $k;
|
|
51
|
+
$member =~ s/([A-Z])/'_' . lc($1)/ge;
|
|
52
|
+
if ($public_name && 'custom' ne $member && exists $utility->{$member}) {
|
|
53
|
+
$utility->{$member} = $custom_utils->{$k};
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
$utility->{custom}{$k} = $custom_utils->{$k};
|
|
57
|
+
}
|
|
26
58
|
}
|
|
27
59
|
}
|
|
28
60
|
|
|
@@ -52,6 +52,9 @@ class ProjectNameContext
|
|
|
52
52
|
if (isset($ctrl_raw['explain']) && is_array($ctrl_raw['explain'])) {
|
|
53
53
|
$this->ctrl->explain = $ctrl_raw['explain'];
|
|
54
54
|
}
|
|
55
|
+
if (array_key_exists('actor', $ctrl_raw)) {
|
|
56
|
+
$this->ctrl->actor = $ctrl_raw['actor'];
|
|
57
|
+
}
|
|
55
58
|
} elseif ($basectx !== null && $basectx->ctrl !== null) {
|
|
56
59
|
$this->ctrl = $basectx->ctrl;
|
|
57
60
|
}
|
|
@@ -9,10 +9,23 @@ class ProjectNameControl
|
|
|
9
9
|
public mixed $err;
|
|
10
10
|
public mixed $explain;
|
|
11
11
|
|
|
12
|
+
// Per-call actor, read by the `audit` and `cost` features to attribute an
|
|
13
|
+
// operation to whoever asked for it.
|
|
14
|
+
//
|
|
15
|
+
// Declared, because this class is typed. ts carries it without declaring
|
|
16
|
+
// it - its control object is open, so a caller-supplied `actor` simply
|
|
17
|
+
// survives - and both features were written against that, commenting it
|
|
18
|
+
// as "an optional extension property on the control". Here the property
|
|
19
|
+
// did not exist, so `$ctx->ctrl->actor ?? null` was always null and every
|
|
20
|
+
// operation was attributed to 'anonymous' no matter what the caller
|
|
21
|
+
// passed. The shared feature corpus caught it.
|
|
22
|
+
public mixed $actor;
|
|
23
|
+
|
|
12
24
|
public function __construct(array $opts = [])
|
|
13
25
|
{
|
|
14
26
|
$this->throw_err = $opts['throw_err'] ?? null;
|
|
15
27
|
$this->err = null;
|
|
16
28
|
$this->explain = $opts['explain'] ?? null;
|
|
29
|
+
$this->actor = $opts['actor'] ?? null;
|
|
17
30
|
}
|
|
18
31
|
}
|
|
@@ -13,6 +13,17 @@ class ProjectNameError extends \Exception
|
|
|
13
13
|
public mixed $result;
|
|
14
14
|
public mixed $spec;
|
|
15
15
|
|
|
16
|
+
// HTTP status of the response that caused this error, or -1 when the
|
|
17
|
+
// request never got one. Promoted to the top level so a consumer can
|
|
18
|
+
// branch on `err->status` / `err->notFound()` instead of reaching into
|
|
19
|
+
// `err->result`.
|
|
20
|
+
//
|
|
21
|
+
// DECLARED, not created on assignment. makeError set it on an undeclared
|
|
22
|
+
// property, which PHP 8.2 deprecates and PHP 9 makes fatal - so every
|
|
23
|
+
// error path in this SDK was on course to stop working. ts declares the
|
|
24
|
+
// same field; php needs it spelled out because the class is typed.
|
|
25
|
+
public int $status;
|
|
26
|
+
|
|
16
27
|
public function __construct(string $code = '', string $msg = '', mixed $ctx = null)
|
|
17
28
|
{
|
|
18
29
|
parent::__construct($msg);
|
|
@@ -23,6 +34,7 @@ class ProjectNameError extends \Exception
|
|
|
23
34
|
$this->ctx = $ctx;
|
|
24
35
|
$this->result = null;
|
|
25
36
|
$this->spec = null;
|
|
37
|
+
$this->status = -1;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
public function error(): string
|