@voxgig/sdkgen 4.2.8 → 4.4.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.
Files changed (63) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/helpers/naming.d.ts +2 -1
  3. package/dist/helpers/naming.js +50 -12
  4. package/dist/helpers/naming.js.map +1 -1
  5. package/dist/sdkgen.d.ts +2 -2
  6. package/dist/sdkgen.js +4 -3
  7. package/dist/sdkgen.js.map +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/package.json +1 -1
  10. package/project/.sdk/src/cmp/zig/Config_zig.ts +1 -1
  11. package/project/.sdk/tm/c/tests/primary_corpus_test.c +565 -0
  12. package/project/.sdk/tm/c/utility/prepare_method.c +3 -1
  13. package/project/.sdk/tm/c/utility/transform_request.c +4 -3
  14. package/project/.sdk/tm/clojure/src/sdk/core.clj +55 -5
  15. package/project/.sdk/tm/clojure/test/sdk/test/primary.clj +171 -1
  16. package/project/.sdk/tm/clojure/test/sdk/test/struct_corpus.clj +13 -2
  17. package/project/.sdk/tm/clojure/test/sdk/test_runner.clj +2 -0
  18. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +104 -0
  19. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +82 -2
  20. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +50 -2
  21. package/project/.sdk/tm/elixir/test/primary_utility_test.exs +18 -200
  22. package/project/.sdk/tm/elixir/test/support/struct_corpus.ex +413 -2
  23. package/project/.sdk/tm/go/test/custom_utility_test.go +103 -0
  24. package/project/.sdk/tm/go/test/feature_corpus_test.go +550 -0
  25. package/project/.sdk/tm/go/utility/make_options.go +7 -1
  26. package/project/.sdk/tm/go/utility/register.go +194 -0
  27. package/project/.sdk/tm/java/test/CustomUtilityTest.java +55 -0
  28. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +463 -0
  29. package/project/.sdk/tm/java/utility/MakeOptions.java +11 -2
  30. package/project/.sdk/tm/java/utility/Register.java +91 -0
  31. package/project/.sdk/tm/js/test/feature/Corpus.test.js +285 -0
  32. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +56 -2
  33. package/project/.sdk/tm/lua/utility/make_options.lua +22 -2
  34. package/project/.sdk/tm/ocaml/Makefile +15 -4
  35. package/project/.sdk/tm/ocaml/sdk_runtime.ml +8 -1
  36. package/project/.sdk/tm/ocaml/test/corpus_runner.ml +185 -0
  37. package/project/.sdk/tm/ocaml/test/primary_utility_test.ml +238 -0
  38. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +5 -160
  39. package/project/.sdk/tm/perl/t/feature_corpus.t +345 -0
  40. package/project/.sdk/tm/perl/utility/make_options.pm +33 -1
  41. package/project/.sdk/tm/php/core/Context.php +3 -0
  42. package/project/.sdk/tm/php/core/Control.php +13 -0
  43. package/project/.sdk/tm/php/core/Error.php +12 -0
  44. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +376 -0
  45. package/project/.sdk/tm/php/utility/MakeOptions.php +30 -1
  46. package/project/.sdk/tm/py/pkg/utility/make_options.py +42 -1
  47. package/project/.sdk/tm/py/test/test_feature_corpus.py +309 -0
  48. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +281 -0
  49. package/project/.sdk/tm/rb/utility/make_options.rb +32 -1
  50. package/project/.sdk/tm/scala/Makefile +1 -0
  51. package/project/.sdk/tm/scala/sdktest/PrimaryCorpus.scala +294 -0
  52. package/project/.sdk/tm/scala/sdktest/StructCorpus.scala +0 -0
  53. package/project/.sdk/tm/scala/utility/Make.scala +49 -2
  54. package/project/.sdk/tm/scala/utility/Prepare.scala +3 -1
  55. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -3
  56. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +13 -2
  57. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +287 -0
  58. package/project/.sdk/tm/zig/core/utility.zig +8 -1
  59. package/project/.sdk/tm/zig/test/primary_utility_test.zig +445 -0
  60. package/project/.sdk/tm/zig/test/struct_runner.zig +238 -0
  61. package/project/sdkgen-package.json +1 -1
  62. package/src/helpers/naming.ts +54 -12
  63. package/src/sdkgen.ts +2 -1
@@ -0,0 +1,376 @@
1
+ <?php
2
+
3
+ // ProjectName SDK feature corpus test
4
+ //
5
+ // Feature behaviour, driven by the SHARED corpus.
6
+ //
7
+ // The same route PrimaryUtilityTest.php takes for the utilities:
8
+ // language-neutral cases in .sdk/test/test.json, executed against THIS
9
+ // generated SDK. The feature is the ordinary class, built by the generated
10
+ // config, installed by the generated constructor, and driven by a real entity
11
+ // operation. Not a miniature of the pipeline, which can only be as right as
12
+ // the miniature.
13
+ //
14
+ // Everything in a case is data. The one piece php writes for itself is
15
+ // turning scripted responses into a fetcher, through the documented
16
+ // `utility.fetcher` override.
17
+
18
+ declare(strict_types=1);
19
+
20
+ require_once __DIR__ . '/../projectname_sdk.php';
21
+
22
+ use PHPUnit\Framework\TestCase;
23
+
24
+ class FeatureCorpusTest extends TestCase
25
+ {
26
+ // Features with a corpus section. A name here with no section is a skip,
27
+ // not a failure: an SDK generated without the feature has nothing to run.
28
+ private const FEATURE_CORPUS_NAMES = ['cost'];
29
+
30
+ // The standard operation names, in the order the runner prefers them.
31
+ private const FEATURE_CORPUS_OPS = ['load', 'list', 'create', 'update', 'remove'];
32
+
33
+ private static function corpus(): array
34
+ {
35
+ $path = __DIR__ . '/../../.sdk/test/test.json';
36
+ $raw = file_get_contents($path);
37
+ if (false === $raw) {
38
+ throw new RuntimeException("Failed to load test.json: $path");
39
+ }
40
+ return json_decode($raw, true);
41
+ }
42
+
43
+ /**
44
+ * A scripted transport built from a case's `res` list. Responses are
45
+ * consumed in order and the last one repeats, so a case that does not care
46
+ * how many attempts happen need only declare one.
47
+ *
48
+ * Returns the shape the real fetcher returns: a [response, err] PAIR, with
49
+ * the parsed body behind a `json` closure and `body` as the raw string. A
50
+ * script that only set `body` would look like an empty result, which reads
51
+ * as a feature defect rather than a mis-shaped script.
52
+ */
53
+ private static function scriptedFetcher($res): callable
54
+ {
55
+ $n = -1;
56
+ return function ($ctx, $fullurl, $fetchdef) use ($res, &$n) {
57
+ $n++;
58
+ $spec = [];
59
+ if (is_array($res) && count($res) > 0) {
60
+ $i = $n >= count($res) ? count($res) - 1 : $n;
61
+ $spec = $res[$i] ?? [];
62
+ }
63
+
64
+ if (($spec['throw'] ?? null) === true) {
65
+ return [null, new RuntimeException('scripted transport failure')];
66
+ }
67
+
68
+ $status = isset($spec['status']) ? (int)$spec['status'] : 200;
69
+ $body = $spec['body'] ?? [];
70
+
71
+ return [
72
+ [
73
+ 'status' => $status,
74
+ 'statusText' => $status < 400 ? 'OK' : 'ERR',
75
+ 'headers' => (array)($spec['headers'] ?? []),
76
+ 'json' => function () use ($body) { return $body; },
77
+ 'body' => json_encode($body),
78
+ ],
79
+ null,
80
+ ];
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Build a client the way a caller would.
86
+ *
87
+ * The plain constructor, not the test-mode one: the `test` feature is
88
+ * transport: 'base' and REPLACES the transport, so a client in test mode
89
+ * would shadow the script.
90
+ */
91
+ private static function buildClient(array $kase)
92
+ {
93
+ $opts = ['utility' => ['fetcher' => self::scriptedFetcher($kase['res'] ?? null)]];
94
+ if (isset($kase['feature'])) {
95
+ $opts['feature'] = $kase['feature'];
96
+ }
97
+ return new ProjectNameSDK($opts);
98
+ }
99
+
100
+ /**
101
+ * Every operation this SDK declares, in a stable order.
102
+ *
103
+ * The corpus cannot name an entity - it is shared by SDKs with none in
104
+ * common - so the runner finds them here. An entity accessor is a
105
+ * capitalised client method whose result answers get_name().
106
+ */
107
+ private static function candidates($client): array
108
+ {
109
+ $found = [];
110
+ foreach (get_class_methods($client) as $m) {
111
+ if (!preg_match('/^[A-Z]/', $m)) {
112
+ continue;
113
+ }
114
+ try {
115
+ $ent = $client->$m();
116
+ } catch (\Throwable $e) {
117
+ continue;
118
+ }
119
+ if (!is_object($ent) || !method_exists($ent, 'get_name')) {
120
+ continue;
121
+ }
122
+ try {
123
+ $entname = $ent->get_name();
124
+ } catch (\Throwable $e) {
125
+ continue;
126
+ }
127
+ if (!is_string($entname) || '' === $entname) {
128
+ continue;
129
+ }
130
+ $found[$entname] = [$m, $ent];
131
+ }
132
+
133
+ ksort($found);
134
+
135
+ $out = [];
136
+ foreach ($found as $entname => $pair) {
137
+ [$accessor, $ent] = $pair;
138
+ foreach (self::FEATURE_CORPUS_OPS as $opname) {
139
+ if (method_exists($ent, $opname)) {
140
+ $out[] = [
141
+ 'key' => $entname . '.' . $opname,
142
+ 'accessor' => $accessor,
143
+ 'op' => $opname,
144
+ ];
145
+ }
146
+ }
147
+ }
148
+ return $out;
149
+ }
150
+
151
+ private static function invoke($client, array $op, array $ctrl)
152
+ {
153
+ $acc = $op['accessor'];
154
+ $ent = $client->$acc();
155
+ $fn = $op['op'];
156
+ return $ent->$fn([], $ctrl);
157
+ }
158
+
159
+ /**
160
+ * Pick operations by DRIVING them: an op is usable when it completes
161
+ * against a plain 200 with no feature active. Declared operations are not
162
+ * all callable with no arguments, and a case failing for that reason would
163
+ * read as a feature defect.
164
+ */
165
+ private static function usableOps(int $want): array
166
+ {
167
+ $picked = [];
168
+ foreach (self::candidates(self::buildClient([])) as $cand) {
169
+ try {
170
+ self::invoke(self::buildClient([]), $cand, []);
171
+ } catch (\Throwable $e) {
172
+ continue;
173
+ }
174
+ $picked[] = $cand;
175
+ if (count($picked) >= $want) {
176
+ break;
177
+ }
178
+ }
179
+ return $picked;
180
+ }
181
+
182
+ /** Replace #OPn throughout a case, keys included. */
183
+ private static function resolve($node, array $tokens)
184
+ {
185
+ if (is_string($node)) {
186
+ return strtr($node, $tokens);
187
+ }
188
+ if (is_array($node)) {
189
+ $out = [];
190
+ foreach ($node as $k => $v) {
191
+ $key = is_string($k) ? strtr($k, $tokens) : $k;
192
+ $out[$key] = self::resolve($v, $tokens);
193
+ }
194
+ return $out;
195
+ }
196
+ return $node;
197
+ }
198
+
199
+ /** The highest #OPn a case mentions. */
200
+ private static function tokensUsed(array $kase): int
201
+ {
202
+ preg_match_all('/#OP(\d+)/', json_encode($kase), $m);
203
+ $nums = array_map('intval', $m[1] ?? []);
204
+ return count($nums) > 0 ? max($nums) : 0;
205
+ }
206
+
207
+ private static function member($actual, string $key): array
208
+ {
209
+ if (null === $actual) {
210
+ return [null, false];
211
+ }
212
+ if (is_array($actual)) {
213
+ return array_key_exists($key, $actual) ? [$actual[$key], true] : [null, false];
214
+ }
215
+ if (is_object($actual) && property_exists($actual, $key)) {
216
+ return [$actual->$key, true];
217
+ }
218
+ return [null, false];
219
+ }
220
+
221
+ /**
222
+ * Assert that `actual` contains `expect`, recursively. Cases assert only
223
+ * the fields they are about, so a full equality check would force every
224
+ * case to restate the whole record.
225
+ */
226
+ private function subset($actual, $expect, string $path): void
227
+ {
228
+ if (is_array($expect) && (0 === count($expect) || !array_is_list($expect))) {
229
+ foreach ($expect as $k => $want) {
230
+ [$got, $found] = self::member($actual, (string)$k);
231
+ $this->assertTrue($found, "$path.$k: no such member");
232
+ $this->subset($got, $want, "$path.$k");
233
+ }
234
+ return;
235
+ }
236
+
237
+ if (is_int($expect) || is_float($expect)) {
238
+ $this->assertTrue(is_int($actual) || is_float($actual),
239
+ "$path: expected a number, got " . var_export($actual, true));
240
+ // Money is float arithmetic; compare with a tolerance far below
241
+ // any amount a case states.
242
+ $this->assertLessThan(1e-9, abs((float)$actual - (float)$expect), $path);
243
+ return;
244
+ }
245
+
246
+ $this->assertSame($expect, $actual, $path);
247
+ }
248
+
249
+ private static function record($client, string $name)
250
+ {
251
+ $prop = '_' . $name;
252
+ return $client->$prop ?? null;
253
+ }
254
+
255
+ public function testCorpusCarriesAFeatureSection(): void
256
+ {
257
+ // A corpus with no `feature` section is a SKIP, not a failure. Each
258
+ // project carries its OWN materialised copy of .sdk/test/test.json, so a
259
+ // project scaffolded before the section existed legitimately has no cases
260
+ // to run - and a hard assertion here turned that into a red suite in every
261
+ // SDK on the fleet, for a corpus the project had simply not re-pulled yet.
262
+ // The strict check belongs where the corpus is CONTROLLED: sdkgen's own
263
+ // end-to-end lane supplies one and requires the cases to actually run.
264
+ if (null === (self::corpus()['feature'] ?? null)) {
265
+ $this->markTestSkipped(
266
+ "this project's test.json has no `feature` section - recompile "
267
+ . 'the corpus (create-sdkgen .sdk/test/feature/) to run these cases');
268
+ }
269
+ $this->assertNotNull(self::corpus()['feature']);
270
+ }
271
+
272
+ // At least one operation, or every case would skip and this suite would
273
+ // report green having run nothing.
274
+ public function testSdkHasAnOperationTheCorpusCanDrive(): void
275
+ {
276
+ $this->assertNotEmpty(self::usableOps(2),
277
+ 'no declared operation completed against a plain 200 - the corpus '
278
+ . 'cannot exercise a feature without one');
279
+ }
280
+
281
+ public function testFeatureCorpus(): void
282
+ {
283
+ // Skip rather than run vacuously: with no section this asserts
284
+ // nothing, which PHPUnit reports as RISKY - a third state that reads
285
+ // like neither a pass nor a skip.
286
+ if (null === (self::corpus()['feature'] ?? null)) {
287
+ $this->markTestSkipped(
288
+ "this project's test.json has no `feature` section - recompile "
289
+ . 'the corpus (create-sdkgen .sdk/test/feature/) to run these cases');
290
+ }
291
+
292
+ foreach (self::FEATURE_CORPUS_NAMES as $name) {
293
+ $section = self::corpus()['feature'][$name] ?? null;
294
+ if (null === $section) {
295
+ continue;
296
+ }
297
+
298
+ $cases = $section['basic']['set'] ?? [];
299
+ $this->assertNotEmpty($cases,
300
+ "corpus section feature.$name ran ZERO cases - a renamed "
301
+ . 'section or an emptied fixture must fail loudly');
302
+
303
+ // Probed by ACTIVATING it: the feature defaults to inactive, so an
304
+ // idle client never builds it and its absence says nothing.
305
+ $probe = self::buildClient(['feature' => [['name' => $name, 'active' => true]]]);
306
+ if (null === self::record($probe, $name)) {
307
+ continue;
308
+ }
309
+
310
+ $ops = self::usableOps(2);
311
+ $byKey = [];
312
+ foreach ($ops as $o) {
313
+ $byKey[$o['key']] = $o;
314
+ }
315
+
316
+ $ran = 0;
317
+ foreach ($cases as $raw) {
318
+ $need = self::tokensUsed($raw);
319
+ if ($need > count($ops)) {
320
+ continue;
321
+ }
322
+
323
+ $tokens = [];
324
+ for ($i = 0; $i < $need; $i++) {
325
+ $tokens['#OP' . ($i + 1)] = $ops[$i]['key'];
326
+ }
327
+ $kase = self::resolve($raw, $tokens);
328
+
329
+ $client = self::buildClient($kase);
330
+ $label = $kase['name'] ?? '';
331
+
332
+ foreach (($kase['op'] ?? []) as $step) {
333
+ $op = $byKey[$step['op']] ?? null;
334
+ $this->assertNotNull($op, "$label: no operation {$step['op']}");
335
+ $ctrl = $step['ctrl'] ?? [];
336
+ $wanterr = $step['err'] ?? null;
337
+
338
+ try {
339
+ self::invoke($client, $op, $ctrl);
340
+ $this->assertNull($wanterr,
341
+ "$label: {$step['op']} was expected to fail, and did not");
342
+ } catch (\PHPUnit\Framework\AssertionFailedError $e) {
343
+ throw $e;
344
+ } catch (\Throwable $err) {
345
+ $this->assertNotNull($wanterr,
346
+ "$label: {$step['op']} failed unexpectedly: " . $err->getMessage());
347
+ if (is_string($wanterr)) {
348
+ // The CODE, not the message: make_error prefixes
349
+ // and humanises the text, so matching it would
350
+ // pass on any error that mentioned the word.
351
+ //
352
+ // `sdk_code`, not `code`: Exception::$code is a
353
+ // protected int on every PHP throwable, so the
354
+ // SDK carries its own string code beside it.
355
+ $code = property_exists($err, 'sdk_code') ? $err->sdk_code : null;
356
+ $this->assertSame($wanterr, $code,
357
+ "$label: wrong error code (" . $err->getMessage() . ')');
358
+ }
359
+ }
360
+ }
361
+
362
+ $this->subset(self::record($client, $name), $kase['out'] ?? [],
363
+ "$label: _$name");
364
+ $ran++;
365
+ }
366
+
367
+ $this->assertGreaterThan(0, $ran, "every feature.$name case was skipped");
368
+ // Say how many ran. A partial run is legitimate (an SDK with one
369
+ // operation skips the cases needing two) but it should be visible
370
+ // rather than inferred from a green tick.
371
+ fwrite(STDERR, sprintf(
372
+ "feature.%s: ran %d of %d case(s) against %d operation(s)\n",
373
+ $name, $ran, count($cases), count($ops)));
374
+ }
375
+ }
376
+ }
@@ -27,10 +27,39 @@ class ProjectNameMakeOptions
27
27
  {
28
28
  $options = $ctx->options ?? [];
29
29
 
30
+ // Merge custom utility overrides.
31
+ //
32
+ // A key naming a real utility member REPLACES it; anything else is
33
+ // attached as a custom extra. This mirrors ts, where the utility is an
34
+ // open object and one setprop does both.
35
+ //
36
+ // Without the replace half this was a no-op: every entry went to
37
+ // `utility->custom`, which nothing reads, so a caller passing
38
+ // `'utility' => ['fetcher' => $myTransport]` - the documented way to
39
+ // script the transport, and the seam the shared feature corpus runs
40
+ // on - was silently ignored while ts and js honoured it.
41
+ //
42
+ // Option keys are camelCase, as ts spells them; members here are
43
+ // snake_case. Converting rather than listing keeps the mapping to one
44
+ // rule, so a utility added later is overridable without touching this.
30
45
  $custom_utils = \Voxgig\Struct\Struct::getprop($options, 'utility');
31
46
  if ((is_array($custom_utils) || is_object($custom_utils)) && $ctx->utility) {
47
+ $utility = $ctx->utility;
32
48
  foreach ((array)$custom_utils as $k => $v) {
33
- $ctx->utility->custom[$k] = $v;
49
+ // Public utility names are camelCase and carry no underscore,
50
+ // so an underscore means the caller named something of their
51
+ // own - possibly the INTERNAL spelling of a real member.
52
+ // `make_error` must stay an extension in `custom`; replacing
53
+ // the pipeline function with it (ts, js and go all keep it)
54
+ // would break the error path on the next request, silently.
55
+ $public_name = false === strpos((string)$k, '_');
56
+ $member = strtolower(preg_replace('/([A-Z])/', '_$1', (string)$k));
57
+ if ($public_name && 'custom' !== $member
58
+ && property_exists($utility, $member)) {
59
+ $utility->$member = $v;
60
+ } else {
61
+ $utility->custom[$k] = $v;
62
+ }
34
63
  }
35
64
  }
36
65
 
@@ -4,16 +4,57 @@ from __future__ import annotations
4
4
  from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
5
5
 
6
6
 
7
+
8
+ def _util_member(key):
9
+ """Public camelCase option key -> snake_case utility member name.
10
+
11
+ Returns None for a key that is NOT a public name. Public utility names are
12
+ camelCase and contain no underscore, so an underscore means the caller
13
+ named something of their own - possibly the INTERNAL spelling of a real
14
+ member. `make_error` must stay an extension in `custom`; replacing the
15
+ pipeline function with it (ts, js and go all keep it) would break the
16
+ error path on the next request, silently.
17
+ """
18
+ if "_" in key:
19
+ return None
20
+ out = []
21
+ for ch in key:
22
+ if ch.isupper():
23
+ out.append("_")
24
+ out.append(ch.lower())
25
+ else:
26
+ out.append(ch)
27
+ return "".join(out)
28
+
29
+
7
30
  def make_options_util(ctx):
8
31
  options = ctx.options or {}
9
32
 
10
33
  # Merge custom utility overrides.
34
+ #
35
+ # A key naming a real utility member REPLACES it; anything else is
36
+ # attached as a custom extra. This mirrors ts, where the utility is an
37
+ # open object and one setprop does both.
38
+ #
39
+ # Without the replace half this was a no-op: every entry went to
40
+ # `utility.custom`, which nothing reads, so a caller passing
41
+ # `utility={"fetcher": my_transport}` - the documented way to script the
42
+ # transport, and the seam the shared feature corpus runs on - was
43
+ # silently ignored while ts and js honoured it.
44
+ #
45
+ # Option keys are camelCase, as ts spells them; members here are
46
+ # snake_case. Converting rather than listing keeps the mapping to one
47
+ # rule, so a utility added later is overridable without touching this.
11
48
  custom_utils = vs.getprop(options, "utility")
12
49
  if isinstance(custom_utils, dict):
13
50
  utility = ctx.utility
14
51
  if utility is not None:
15
52
  for key, val in custom_utils.items():
16
- utility.custom[key] = val
53
+ member = _util_member(key)
54
+ if member is not None and member != "custom" and hasattr(utility, member):
55
+ setattr(utility, member, val)
56
+ else:
57
+ utility.custom[key] = val
17
58
 
18
59
  # Feature INSTANCES supplied at construction (the station adopt path)
19
60
  # are consumed by the constructor's feature-add loop straight from the