@voxgig/sdkgen 4.2.8 → 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.
Files changed (33) 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/tm/go/test/custom_utility_test.go +103 -0
  11. package/project/.sdk/tm/go/test/feature_corpus_test.go +550 -0
  12. package/project/.sdk/tm/go/utility/make_options.go +7 -1
  13. package/project/.sdk/tm/go/utility/register.go +194 -0
  14. package/project/.sdk/tm/java/test/CustomUtilityTest.java +55 -0
  15. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +463 -0
  16. package/project/.sdk/tm/java/utility/MakeOptions.java +11 -2
  17. package/project/.sdk/tm/java/utility/Register.java +91 -0
  18. package/project/.sdk/tm/js/test/feature/Corpus.test.js +285 -0
  19. package/project/.sdk/tm/perl/t/feature_corpus.t +345 -0
  20. package/project/.sdk/tm/perl/utility/make_options.pm +33 -1
  21. package/project/.sdk/tm/php/core/Context.php +3 -0
  22. package/project/.sdk/tm/php/core/Control.php +13 -0
  23. package/project/.sdk/tm/php/core/Error.php +12 -0
  24. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +376 -0
  25. package/project/.sdk/tm/php/utility/MakeOptions.php +30 -1
  26. package/project/.sdk/tm/py/pkg/utility/make_options.py +42 -1
  27. package/project/.sdk/tm/py/test/test_feature_corpus.py +309 -0
  28. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +281 -0
  29. package/project/.sdk/tm/rb/utility/make_options.rb +32 -1
  30. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +287 -0
  31. package/project/sdkgen-package.json +1 -1
  32. package/src/helpers/naming.ts +54 -12
  33. package/src/sdkgen.ts +2 -1
@@ -0,0 +1,550 @@
1
+ package sdktest
2
+
3
+ // Feature behaviour, driven by the SHARED corpus.
4
+ //
5
+ // The same route primary_utility_test.go takes for the utilities:
6
+ // language-neutral cases in .sdk/test/test.json, executed against THIS
7
+ // generated SDK. The feature is the ordinary compiled type, built by the
8
+ // generated config, installed by the generated constructor, and driven by a
9
+ // real entity operation. Not a miniature of the pipeline - that is what
10
+ // feature_harness_test.go does, and a miniature can only be as right as the
11
+ // miniature.
12
+ //
13
+ // Everything in a case is data. The two pieces go writes for itself are
14
+ // turning scripted responses into a FetcherFunc, and reading the record back
15
+ // off the feature (go keeps the aggregates on the feature value, where ts
16
+ // keeps them on the client).
17
+
18
+ import (
19
+ "encoding/json"
20
+ "errors"
21
+ "fmt"
22
+ "reflect"
23
+ "strings"
24
+ "testing"
25
+
26
+ sdk "GOMODULE"
27
+ )
28
+
29
+ // Features with a corpus section. A name here with no section is a skip, not
30
+ // a failure: an SDK generated without the feature has nothing to run.
31
+ var featureCorpusNames = []string{"cost"}
32
+
33
+ // The standard operation names, in the order the runner prefers them. Every
34
+ // entity declares every CRUD method, so an op that the API does not define
35
+ // errors at runtime rather than failing to compile - which is why usable
36
+ // operations are found by DRIVING them, below.
37
+ var featureCorpusOps = []string{"Load", "List", "Create", "Update", "Remove"}
38
+
39
+ // One operation this SDK can actually perform.
40
+ type fcOp struct {
41
+ key string // "<entity>.<op>", how features attribute it
42
+ accessor string // the client method returning the entity
43
+ method string // the op method on that entity
44
+ }
45
+
46
+ // fcFetcher builds a scripted transport from a case's `res` list. Responses
47
+ // are consumed in order and the last one repeats, so a case that does not
48
+ // care how many attempts happen need only declare one.
49
+ func fcFetcher(res []any) sdk.FetcherFunc {
50
+ n := -1
51
+ return func(ctx *sdk.Context, fullurl string, fetchdef map[string]any) (any, error) {
52
+ n++
53
+ var spec map[string]any
54
+ if len(res) > 0 {
55
+ i := n
56
+ if i >= len(res) {
57
+ i = len(res) - 1
58
+ }
59
+ spec, _ = res[i].(map[string]any)
60
+ }
61
+ if spec == nil {
62
+ spec = map[string]any{}
63
+ }
64
+
65
+ if thrown, _ := spec["throw"].(bool); thrown {
66
+ return nil, fmt.Errorf("scripted transport failure")
67
+ }
68
+
69
+ status := 200
70
+ if s, ok := fcNum(spec["status"]); ok {
71
+ status = int(s)
72
+ }
73
+ statusText := "OK"
74
+ if status >= 400 {
75
+ statusText = "ERR"
76
+ }
77
+
78
+ headers := map[string]any{}
79
+ if h, ok := spec["headers"].(map[string]any); ok {
80
+ for k, v := range h {
81
+ headers[k] = v
82
+ }
83
+ }
84
+
85
+ body := spec["body"]
86
+ if body == nil {
87
+ body = map[string]any{}
88
+ }
89
+
90
+ // The shape the real fetcher returns: the PARSED body comes back
91
+ // through a `json` thunk, and `body` is the raw string. makeResult
92
+ // reads the thunk, so a scripted response that only set `body` would
93
+ // look like an empty result - which reads as a feature defect rather
94
+ // than a mis-shaped script.
95
+ raw, _ := json.Marshal(body)
96
+
97
+ return map[string]any{
98
+ "status": status,
99
+ "statusText": statusText,
100
+ "headers": headers,
101
+ "json": (func() any)(func() any { return body }),
102
+ "body": string(raw),
103
+ }, nil
104
+ }
105
+ }
106
+
107
+ // fcClient builds a client the way a caller would: the generated constructor,
108
+ // the feature list from the case, and the scripted transport through the
109
+ // documented `utility.fetcher` override.
110
+ //
111
+ // NewProjectNameSDK, not TestSDK: the `test` feature is transport: 'base' and
112
+ // REPLACES the transport, so a client in test mode would shadow the script.
113
+ func fcClient(kase map[string]any) *sdk.ProjectNameSDK {
114
+ res, _ := kase["res"].([]any)
115
+ opts := map[string]any{
116
+ "utility": map[string]any{"fetcher": fcFetcher(res)},
117
+ }
118
+ if f, ok := kase["feature"]; ok {
119
+ opts["feature"] = f
120
+ }
121
+ return sdk.NewProjectNameSDK(opts)
122
+ }
123
+
124
+ // fcCandidates lists the operations this SDK declares, in a stable order.
125
+ //
126
+ // The corpus cannot name an entity - it is shared by SDKs with none in common
127
+ // - so the runner finds them here. An entity accessor is a client method
128
+ // taking one options map and returning something that answers GetName().
129
+ func fcCandidates(client *sdk.ProjectNameSDK) []fcOp {
130
+ out := []fcOp{}
131
+
132
+ cv := reflect.ValueOf(client)
133
+ ct := cv.Type()
134
+ mapType := reflect.TypeOf(map[string]any{})
135
+
136
+ names := []string{}
137
+ byName := map[string]reflect.Value{}
138
+
139
+ for i := 0; i < ct.NumMethod(); i++ {
140
+ m := ct.Method(i)
141
+ mt := m.Type
142
+ // (receiver, entopts) -> entity
143
+ if mt.NumIn() != 2 || mt.NumOut() != 1 || mt.In(1) != mapType {
144
+ continue
145
+ }
146
+ ent := cv.Method(i).Call([]reflect.Value{reflect.ValueOf(map[string]any(nil))})[0]
147
+ if !ent.IsValid() || (ent.Kind() == reflect.Ptr && ent.IsNil()) {
148
+ continue
149
+ }
150
+ gn := ent.MethodByName("GetName")
151
+ if !gn.IsValid() || gn.Type().NumIn() != 0 || gn.Type().NumOut() != 1 ||
152
+ gn.Type().Out(0).Kind() != reflect.String {
153
+ continue
154
+ }
155
+ entname := gn.Call(nil)[0].String()
156
+ if entname == "" {
157
+ continue
158
+ }
159
+ names = append(names, entname)
160
+ byName[entname] = ent
161
+ _ = m
162
+ }
163
+
164
+ // Sorted, so the choice of operation is stable across runs.
165
+ for i := 0; i < len(names); i++ {
166
+ for j := i + 1; j < len(names); j++ {
167
+ if names[j] < names[i] {
168
+ names[i], names[j] = names[j], names[i]
169
+ }
170
+ }
171
+ }
172
+
173
+ for _, entname := range names {
174
+ ent := byName[entname]
175
+ for _, opname := range featureCorpusOps {
176
+ om := ent.MethodByName(opname)
177
+ if !om.IsValid() || om.Type().NumIn() != 2 || om.Type().NumOut() != 2 {
178
+ continue
179
+ }
180
+ out = append(out, fcOp{
181
+ key: entname + "." + strings.ToLower(opname),
182
+ accessor: entname,
183
+ method: opname,
184
+ })
185
+ }
186
+ }
187
+ return out
188
+ }
189
+
190
+ // fcInvoke performs one operation on a client, by entity name and method.
191
+ func fcInvoke(client *sdk.ProjectNameSDK, op fcOp, ctrl map[string]any) error {
192
+ cv := reflect.ValueOf(client)
193
+ ct := cv.Type()
194
+ mapType := reflect.TypeOf(map[string]any{})
195
+
196
+ for i := 0; i < ct.NumMethod(); i++ {
197
+ mt := ct.Method(i).Type
198
+ if mt.NumIn() != 2 || mt.NumOut() != 1 || mt.In(1) != mapType {
199
+ continue
200
+ }
201
+ ent := cv.Method(i).Call([]reflect.Value{reflect.ValueOf(map[string]any(nil))})[0]
202
+ gn := ent.MethodByName("GetName")
203
+ if !gn.IsValid() || gn.Type().NumIn() != 0 || gn.Type().NumOut() != 1 ||
204
+ gn.Type().Out(0).Kind() != reflect.String {
205
+ continue
206
+ }
207
+ if gn.Call(nil)[0].String() != op.accessor {
208
+ continue
209
+ }
210
+ om := ent.MethodByName(op.method)
211
+ if !om.IsValid() {
212
+ return fmt.Errorf("no method %s on entity %s", op.method, op.accessor)
213
+ }
214
+ rets := om.Call([]reflect.Value{
215
+ reflect.ValueOf(map[string]any{}),
216
+ reflect.ValueOf(ctrl),
217
+ })
218
+ if err, ok := rets[1].Interface().(error); ok && err != nil {
219
+ return err
220
+ }
221
+ return nil
222
+ }
223
+ return fmt.Errorf("no entity accessor for %s", op.accessor)
224
+ }
225
+
226
+ // fcUsableOps picks operations by DRIVING them: an op is usable when it
227
+ // completes against a plain 200 with no feature active. Declared operations
228
+ // are not all callable with no arguments (a required path parameter, a body),
229
+ // and a case failing for that reason would read as a feature defect.
230
+ func fcUsableOps(want int) []fcOp {
231
+ picked := []fcOp{}
232
+ probe := fcClient(map[string]any{})
233
+ for _, cand := range fcCandidates(probe) {
234
+ client := fcClient(map[string]any{})
235
+ if err := fcInvoke(client, cand, map[string]any{}); err != nil {
236
+ continue
237
+ }
238
+ picked = append(picked, cand)
239
+ if len(picked) >= want {
240
+ break
241
+ }
242
+ }
243
+ return picked
244
+ }
245
+
246
+ // fcResolve replaces #OPn throughout a case, keys included.
247
+ func fcResolve(node any, tokens map[string]string) any {
248
+ switch v := node.(type) {
249
+ case string:
250
+ out := v
251
+ for tok, val := range tokens {
252
+ out = strings.ReplaceAll(out, tok, val)
253
+ }
254
+ return out
255
+ case []any:
256
+ out := make([]any, len(v))
257
+ for i, item := range v {
258
+ out[i] = fcResolve(item, tokens)
259
+ }
260
+ return out
261
+ case map[string]any:
262
+ out := map[string]any{}
263
+ for k, item := range v {
264
+ key, _ := fcResolve(k, tokens).(string)
265
+ out[key] = fcResolve(item, tokens)
266
+ }
267
+ return out
268
+ }
269
+ return node
270
+ }
271
+
272
+ // fcTokensUsed reports the highest #OPn a case mentions. A case wanting more
273
+ // operations than this SDK offers is skipped rather than failed.
274
+ func fcTokensUsed(kase map[string]any) int {
275
+ raw, err := json.Marshal(kase)
276
+ if err != nil {
277
+ return 0
278
+ }
279
+ max := 0
280
+ s := string(raw)
281
+ for i := 0; i+4 < len(s); i++ {
282
+ if s[i:i+3] != "#OP" {
283
+ continue
284
+ }
285
+ n := 0
286
+ j := i + 3
287
+ for j < len(s) && s[j] >= '0' && s[j] <= '9' {
288
+ n = n*10 + int(s[j]-'0')
289
+ j++
290
+ }
291
+ if n > max {
292
+ max = n
293
+ }
294
+ }
295
+ return max
296
+ }
297
+
298
+ func fcNum(v any) (float64, bool) {
299
+ switch n := v.(type) {
300
+ case float64:
301
+ return n, true
302
+ case float32:
303
+ return float64(n), true
304
+ case int:
305
+ return float64(n), true
306
+ case int64:
307
+ return float64(n), true
308
+ case json.Number:
309
+ f, err := n.Float64()
310
+ return f, err == nil
311
+ }
312
+ return 0, false
313
+ }
314
+
315
+ // fcSubset asserts that `actual` contains `expect`, recursively. Cases assert
316
+ // only the fields they are about, so a full deep-equal would force every case
317
+ // to restate the whole record.
318
+ //
319
+ // `actual` is a Go value, not a map: the aggregates live on the feature as
320
+ // typed structs, so an expected key is matched to an exported field by
321
+ // capitalising it.
322
+ func fcSubset(t *testing.T, actual any, expect any, path string) {
323
+ t.Helper()
324
+
325
+ if em, ok := expect.(map[string]any); ok {
326
+ for k, want := range em {
327
+ got, found := fcMember(actual, k)
328
+ if !found {
329
+ t.Errorf("%s.%s: no such member", path, k)
330
+ continue
331
+ }
332
+ fcSubset(t, got, want, path+"."+k)
333
+ }
334
+ return
335
+ }
336
+
337
+ if wn, ok := fcNum(expect); ok {
338
+ gn, ok := fcNum(actual)
339
+ if !ok {
340
+ t.Errorf("%s: expected number %v, got %v", path, expect, actual)
341
+ return
342
+ }
343
+ // Money is float arithmetic; compare with a tolerance far below any
344
+ // amount a case states.
345
+ if diff := gn - wn; diff > 1e-9 || diff < -1e-9 {
346
+ t.Errorf("%s: got %v, want %v", path, gn, wn)
347
+ }
348
+ return
349
+ }
350
+
351
+ if fmt.Sprintf("%v", actual) != fmt.Sprintf("%v", expect) {
352
+ t.Errorf("%s: got %v, want %v", path, actual, expect)
353
+ }
354
+ }
355
+
356
+ // fcMember reads one member from a struct (by capitalised field name), a
357
+ // pointer to one, or a map (by key).
358
+ func fcMember(actual any, key string) (any, bool) {
359
+ if actual == nil {
360
+ return nil, false
361
+ }
362
+ v := reflect.ValueOf(actual)
363
+ for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
364
+ if v.IsNil() {
365
+ return nil, false
366
+ }
367
+ v = v.Elem()
368
+ }
369
+
370
+ switch v.Kind() {
371
+ case reflect.Map:
372
+ mv := v.MapIndex(reflect.ValueOf(key))
373
+ if !mv.IsValid() {
374
+ return nil, false
375
+ }
376
+ return mv.Interface(), true
377
+ case reflect.Struct:
378
+ f := v.FieldByName(strings.ToUpper(key[:1]) + key[1:])
379
+ if !f.IsValid() {
380
+ return nil, false
381
+ }
382
+ return f.Interface(), true
383
+ }
384
+ return nil, false
385
+ }
386
+
387
+ // fcRecord finds the named feature on the client and hands back the value
388
+ // carrying its aggregates. go keeps them on the feature; ts keeps them on the
389
+ // client. Same data, different home.
390
+ //
391
+ // Returned as the feature value itself, NOT type-asserted to a concrete
392
+ // feature type. Naming one here would be a compile-time reference to a
393
+ // feature the project may not have: `target add` trims unselected features,
394
+ // and this template is not trimmed with them, so an SDK generated without
395
+ // that feature would ship a test that does not build. featuresource.test.ts
396
+ // guards exactly that, and caught it - including, on its first pass, the
397
+ // spelling of the type inside this very comment.
398
+ //
399
+ // fcSubset reads the expected keys off the struct by capitalising them, so
400
+ // `total` finds Total, `ops` finds Ops, and a feature added later needs no
401
+ // new go here.
402
+ func fcRecord(client *sdk.ProjectNameSDK, name string) any {
403
+ for _, f := range client.Features {
404
+ if f.GetName() == name {
405
+ return f
406
+ }
407
+ }
408
+ return nil
409
+ }
410
+
411
+ func TestFeatureCorpus(t *testing.T) {
412
+ spec := loadTestSpec(t)
413
+
414
+ featureSection := getSpec(spec, "feature")
415
+ if featureSection == nil {
416
+ // A corpus with no `feature` section is a SKIP, not a failure. Each
417
+ // project carries its OWN materialised copy of .sdk/test/test.json, so a
418
+ // project scaffolded before the section existed legitimately has no cases
419
+ // to run - and a hard assertion here turned that into a red suite in every
420
+ // SDK on the fleet, for a corpus the project had simply not re-pulled yet.
421
+ // The strict check belongs where the corpus is CONTROLLED: sdkgen's own
422
+ // end-to-end lane supplies one and requires the cases to actually run.
423
+ t.Skip("this project's test.json has no `feature` section - recompile the corpus (create-sdkgen .sdk/test/feature/) to run these cases")
424
+ }
425
+
426
+ ops := fcUsableOps(2)
427
+
428
+ // At least one operation, or every case below would skip and this would
429
+ // report green having run nothing.
430
+ if len(ops) == 0 {
431
+ t.Fatal("no declared operation completed against a plain 200 - the " +
432
+ "corpus cannot exercise a feature without one")
433
+ }
434
+
435
+ for _, name := range featureCorpusNames {
436
+ name := name
437
+ t.Run(name, func(t *testing.T) {
438
+ section, _ := featureSection[name].(map[string]any)
439
+ if section == nil {
440
+ t.Skipf("no corpus section for %s", name)
441
+ }
442
+
443
+ basic, _ := section["basic"].(map[string]any)
444
+ cases, _ := basic["set"].([]any)
445
+ if len(cases) == 0 {
446
+ t.Fatalf("corpus section feature.%s ran ZERO cases - a renamed "+
447
+ "section or an emptied fixture must fail loudly", name)
448
+ }
449
+
450
+ // Only run what this SDK actually has, the same rule the rest of
451
+ // the feature tests use. Probed by ACTIVATING it: the feature
452
+ // defaults to inactive, so an idle client never constructs it and
453
+ // its absence from Features says nothing about the SDK.
454
+ probe := fcClient(map[string]any{
455
+ "feature": []any{map[string]any{"name": name, "active": true}},
456
+ })
457
+ if fcRecord(probe, name) == nil {
458
+ t.Skipf("this SDK was generated without the %s feature", name)
459
+ }
460
+
461
+ ran := 0
462
+ for _, raw := range cases {
463
+ kase, _ := raw.(map[string]any)
464
+ if kase == nil {
465
+ continue
466
+ }
467
+
468
+ need := fcTokensUsed(kase)
469
+ if need > len(ops) {
470
+ t.Logf("skip %q: needs %d operations, this SDK offers %d",
471
+ kase["name"], need, len(ops))
472
+ continue
473
+ }
474
+
475
+ tokens := map[string]string{}
476
+ for i := 0; i < need; i++ {
477
+ tokens[fmt.Sprintf("#OP%d", i+1)] = ops[i].key
478
+ }
479
+ resolved, _ := fcResolve(kase, tokens).(map[string]any)
480
+
481
+ byKey := map[string]fcOp{}
482
+ for _, o := range ops {
483
+ byKey[o.key] = o
484
+ }
485
+
486
+ client := fcClient(resolved)
487
+ label, _ := resolved["name"].(string)
488
+
489
+ steps, _ := resolved["op"].([]any)
490
+ for _, rawstep := range steps {
491
+ step, _ := rawstep.(map[string]any)
492
+ if step == nil {
493
+ continue
494
+ }
495
+ opkey, _ := step["op"].(string)
496
+ op, ok := byKey[opkey]
497
+ if !ok {
498
+ t.Errorf("%s: no operation %s", label, opkey)
499
+ continue
500
+ }
501
+ ctrl := map[string]any{}
502
+ if c, ok := step["ctrl"].(map[string]any); ok {
503
+ ctrl = c
504
+ }
505
+
506
+ err := fcInvoke(client, op, ctrl)
507
+ wanterr, haserr := step["err"]
508
+
509
+ if !haserr {
510
+ if err != nil {
511
+ t.Errorf("%s: %s failed unexpectedly: %v", label, opkey, err)
512
+ }
513
+ continue
514
+ }
515
+ if err == nil {
516
+ t.Errorf("%s: %s was expected to fail, and did not", label, opkey)
517
+ continue
518
+ }
519
+ if code, ok := wanterr.(string); ok {
520
+ // The CODE, not the message. The message is prefixed
521
+ // and humanised by makeError, so matching its text
522
+ // would pass on any error that happened to mention
523
+ // the word.
524
+ got := ""
525
+ var sdkerr *sdk.ProjectNameError
526
+ if errors.As(err, &sdkerr) {
527
+ got = sdkerr.Code
528
+ }
529
+ if got != code {
530
+ t.Errorf("%s: wrong error code: got %q (%v), want %q",
531
+ label, got, err, code)
532
+ }
533
+ }
534
+ }
535
+
536
+ fcSubset(t, fcRecord(client, name), resolved["out"], label+": _"+name)
537
+ ran++
538
+ }
539
+
540
+ if ran == 0 {
541
+ t.Fatalf("every feature.%s case was skipped", name)
542
+ }
543
+ // Say how many ran. A partial run is legitimate (an SDK with one
544
+ // operation skips the cases needing two) but it should be visible
545
+ // rather than inferred from a green tick.
546
+ t.Logf("feature.%s: ran %d of %d case(s) against %d operation(s)",
547
+ name, ran, len(cases), len(ops))
548
+ })
549
+ }
550
+ }
@@ -21,11 +21,17 @@ func makeOptionsUtil(ctx *core.Context) map[string]any {
21
21
 
22
22
  // Merge custom utility overrides onto the utility object.
23
23
  // Read from original options before clone, since vs.Clone strips functions.
24
+ //
25
+ // A key naming a real utility member REPLACES it (overrideUtil); anything
26
+ // else is attached as a custom extra. This mirrors ts, where the utility is
27
+ // an open object and `setprop` does both at once.
24
28
  if customUtils := core.ToMapAny(options["utility"]); customUtils != nil {
25
29
  utility := ctx.Utility
26
30
  if utility != nil {
27
31
  for key, val := range customUtils {
28
- utility.Custom[key] = val
32
+ if !overrideUtil(utility, key, val) {
33
+ utility.Custom[key] = val
34
+ }
29
35
  }
30
36
  }
31
37
  }