@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,285 @@
1
+
2
+ // Feature behaviour, driven by the SHARED corpus.
3
+ //
4
+ // This is the route PrimaryUtility.test.ts already takes for the utilities:
5
+ // language-neutral cases in .sdk/test/test.json, executed against the REAL
6
+ // generated SDK. Features here are ordinary classes in ordinary compiled
7
+ // source, unit-tested the ordinary way — no transpiled templates, and no
8
+ // miniature of the pipeline standing in for the pipeline (which is what
9
+ // harness.ts does, and why its assertions can only be as right as the
10
+ // miniature is). A feature is built through the generated config, wrapped
11
+ // into a client built by the generated constructor, and driven by a real
12
+ // entity operation. What is asserted is what ships.
13
+ //
14
+ // Everything in a case is data: features are activated by name, options are
15
+ // plain JSON, the transport is scripted by `res`, and the assertion is a
16
+ // subset of the client's own record. Turning `res` into a fetcher is the one
17
+ // piece each language writes for itself.
18
+ //
19
+ // The ts twin of this file is the reference; keep the two in step.
20
+
21
+ const { test, describe, before } = require('node:test')
22
+ const { ok, deepStrictEqual } = require('node:assert')
23
+
24
+ const { readFileSync } = require('node:fs')
25
+ const { join } = require('node:path')
26
+
27
+ const { SDK, TEST_JSON_FILE } = require('../utility/index')
28
+
29
+
30
+ // Features with a corpus section. A name here with no section is a skip, not
31
+ // a failure: an SDK generated without the feature has nothing to run.
32
+ const FEATURES = ['cost']
33
+
34
+
35
+ // One operation this SDK can actually perform is described by
36
+ // { key, accessor, entity, op }: `key` is '<entity>.<op>', how features
37
+ // attribute spend, and `accessor` is the client method returning the entity.
38
+
39
+
40
+ // A scripted transport built from a case's `res` list. Responses are consumed
41
+ // in order and the last one repeats, so a case that does not care how many
42
+ // attempts happen need only declare one.
43
+ function scriptedFetcher(res) {
44
+ let n = -1
45
+ return async function (_ctx, _url, _fetchdef) {
46
+ n++
47
+ const spec = res[n < res.length ? n : res.length - 1] || {}
48
+
49
+ if (true === spec.throw) {
50
+ throw new Error('scripted transport failure')
51
+ }
52
+
53
+ const headers = spec.headers || {}
54
+ const status = null == spec.status ? 200 : spec.status
55
+
56
+ return {
57
+ status,
58
+ statusText: status < 400 ? 'OK' : 'ERR',
59
+ body: 'not-used',
60
+ json: async () => (undefined === spec.body ? {} : spec.body),
61
+ headers: {
62
+ get(key) {
63
+ const lower = String(key).toLowerCase()
64
+ for (const k of Object.keys(headers)) {
65
+ if (k.toLowerCase() === lower) { return headers[k] }
66
+ }
67
+ return undefined
68
+ },
69
+ forEach(cb) { Object.keys(headers).forEach((k) => cb(headers[k], k, this)) },
70
+ },
71
+ }
72
+ }
73
+ }
74
+
75
+
76
+ function makeClient(kase) {
77
+ return new SDK({
78
+ feature: kase.feature,
79
+ utility: { fetcher: scriptedFetcher(kase.res || [{ status: 200, body: {} }]) },
80
+ })
81
+ }
82
+
83
+
84
+ // Every operation this SDK declares, in a stable order.
85
+ //
86
+ // The corpus cannot name an entity — it is shared by SDKs that have none in
87
+ // common — so the runner finds them here. The generated client exposes one
88
+ // capitalised, zero-argument accessor per entity, and the entity it returns
89
+ // carries the same `name` the config is keyed by; that pairing is what turns
90
+ // a config entry back into a callable method.
91
+ function candidates(client) {
92
+ const entities = client._rootctx.config.entity || {}
93
+
94
+ const accessor = {}
95
+ for (const m of Object.getOwnPropertyNames(Object.getPrototypeOf(client))) {
96
+ if (!/^[A-Z]/.test(m) || 'function' !== typeof client[m]) { continue }
97
+ let inst
98
+ try { inst = client[m]() }
99
+ catch (e) { continue }
100
+ if (null != inst && 'string' === typeof inst.name && null != entities[inst.name]) {
101
+ accessor[inst.name] = m
102
+ }
103
+ }
104
+
105
+ const out = []
106
+ for (const entity of Object.keys(entities).sort()) {
107
+ if (null == accessor[entity]) { continue }
108
+ for (const op of Object.keys(entities[entity].op || {}).sort()) {
109
+ out.push({ key: entity + '.' + op, accessor: accessor[entity], entity, op })
110
+ }
111
+ }
112
+ return out
113
+ }
114
+
115
+
116
+ // Pick operations the corpus can drive, by DRIVING them: an op is usable when
117
+ // it completes against a plain 200 with no feature active. Declared ops are
118
+ // not all callable with no arguments (a required path parameter, a body), and
119
+ // a case that failed for that reason would look like a feature defect.
120
+ async function usableOps(want) {
121
+ const picked = []
122
+ for (const cand of candidates(makeClient({}))) {
123
+ const client = makeClient({})
124
+ try {
125
+ await client[cand.accessor]()[cand.op]({}, {})
126
+ }
127
+ catch (e) { continue }
128
+ picked.push(cand)
129
+ if (want <= picked.length) { break }
130
+ }
131
+ return picked
132
+ }
133
+
134
+
135
+ // Replace #OP1/#OP2 throughout a case, keys included.
136
+ function resolve(node, tokens) {
137
+ if ('string' === typeof node) {
138
+ let s = node
139
+ for (const t of Object.keys(tokens)) { s = s.split(t).join(tokens[t]) }
140
+ return s
141
+ }
142
+ if (Array.isArray(node)) {
143
+ return node.map((n) => resolve(n, tokens))
144
+ }
145
+ if (null != node && 'object' === typeof node) {
146
+ const out = {}
147
+ for (const k of Object.keys(node)) {
148
+ out[resolve(k, tokens)] = resolve(node[k], tokens)
149
+ }
150
+ return out
151
+ }
152
+ return node
153
+ }
154
+
155
+
156
+ // Which #OPn tokens a case uses. A case wanting more operations than this SDK
157
+ // has is skipped rather than failed.
158
+ function tokensUsed(kase) {
159
+ const m = JSON.stringify(kase).match(/#OP(\d+)/g) || []
160
+ return m.reduce((max, t) => Math.max(max, Number(t.slice(3))), 0)
161
+ }
162
+
163
+
164
+ // Assert that `actual` contains `expect`, recursively. Cases assert only the
165
+ // fields they are about, so a full deepStrictEqual would force every case to
166
+ // restate the whole record.
167
+ function subset(actual, expect, path) {
168
+ if (null != expect && 'object' === typeof expect && !Array.isArray(expect)) {
169
+ for (const k of Object.keys(expect)) {
170
+ ok(null != actual, `${path}.${k}: nothing at ${path}`)
171
+ subset(actual[k], expect[k], `${path}.${k}`)
172
+ }
173
+ return
174
+ }
175
+ deepStrictEqual(actual, expect, path)
176
+ }
177
+
178
+
179
+ describe('FeatureCorpus', () => {
180
+
181
+ let corpus
182
+ let ops = []
183
+ let byKey = {}
184
+
185
+
186
+ before(async () => {
187
+ corpus = JSON.parse(readFileSync(join(__dirname, '..', TEST_JSON_FILE), 'utf8'))
188
+ ops = await usableOps(2)
189
+ byKey = {}
190
+ for (const o of ops) { byKey[o.key] = o }
191
+ })
192
+
193
+
194
+ // A corpus with no `feature` section is a SKIP, not a failure.
195
+ //
196
+ // Each project carries its OWN materialised copy of .sdk/test/test.json, so
197
+ // a project scaffolded before the section existed legitimately has no cases
198
+ // to run - and a hard assertion here turned that into a red suite in every
199
+ // SDK on the fleet, for a corpus the project had simply not re-pulled yet.
200
+ //
201
+ // The strict check belongs where the corpus is CONTROLLED, not where it is
202
+ // consumed: sdkgen's own end-to-end lane generates against a corpus it
203
+ // supplies and requires the cases to actually run, so a section that goes
204
+ // missing there still fails loudly.
205
+ test('the corpus carries a feature section', (t) => {
206
+ if (null == corpus.feature) {
207
+ return t.skip(
208
+ 'this project\'s test.json has no `feature` section - recompile the ' +
209
+ 'corpus (create-sdkgen .sdk/test/feature/) to run these cases')
210
+ }
211
+ })
212
+
213
+
214
+ // At least one operation, or every case below would skip and the whole
215
+ // suite would report green having run nothing.
216
+ test('this SDK has an operation the corpus can drive', () => {
217
+ ok(0 < ops.length,
218
+ 'no declared operation completed against a plain 200 — the corpus ' +
219
+ 'cannot exercise a feature without one')
220
+ })
221
+
222
+
223
+ for (const name of FEATURES) {
224
+
225
+ test(name, async (t) => {
226
+ const section = corpus.feature?.[name]
227
+ if (null == section) {
228
+ return t.skip(`no corpus section for ${name}`)
229
+ }
230
+
231
+ const probe = makeClient({})
232
+ if (!probe._rootctx.config.hasFeature(name)) {
233
+ return t.skip(`this SDK was generated without the ${name} feature`)
234
+ }
235
+
236
+ const cases = section.basic?.set || []
237
+ ok(0 < cases.length,
238
+ `corpus section feature.${name} ran ZERO cases — a renamed section ` +
239
+ `or an emptied fixture must fail loudly, not pass silently`)
240
+
241
+ let ran = 0
242
+ for (const raw of cases) {
243
+ const need = tokensUsed(raw)
244
+ if (ops.length < need) {
245
+ t.diagnostic(`skip "${raw.name}": needs ${need} operations, this SDK offers ${ops.length}`)
246
+ continue
247
+ }
248
+
249
+ const tokens = {}
250
+ for (let i = 0; i < need; i++) { tokens['#OP' + (i + 1)] = ops[i].key }
251
+
252
+ const kase = resolve(raw, tokens)
253
+ const client = makeClient(kase)
254
+
255
+ for (const step of (kase.op || [])) {
256
+ const ref = byKey[step.op]
257
+ ok(null != ref, `${kase.name}: no operation ${step.op}`)
258
+ try {
259
+ await client[ref.accessor]()[ref.op]({}, step.ctrl || {})
260
+ ok(null == step.err,
261
+ `${kase.name}: ${step.op} was expected to fail, and did not`)
262
+ }
263
+ catch (err) {
264
+ if (null == step.err) { throw err }
265
+ if ('string' === typeof step.err) {
266
+ deepStrictEqual(err.code, step.err, `${kase.name}: wrong error code`)
267
+ }
268
+ }
269
+ }
270
+
271
+ subset(client[`_${name}`], kase.out, `${kase.name}: _${name}`)
272
+ ran++
273
+ }
274
+
275
+ ok(0 < ran, `every feature.${name} case was skipped`)
276
+ // Say how many ran. A partial run is legitimate (an SDK with one
277
+ // operation skips the cases needing two) but it should be visible
278
+ // rather than inferred from a green tick - and it is the one line
279
+ // sdkgen's end-to-end lane reads, in the same wording, from every
280
+ // language's runner.
281
+ t.diagnostic(`feature.${name}: ran ${ran} of ${cases.length} ` +
282
+ `case(s) against ${ops.length} operation(s)`)
283
+ })
284
+ }
285
+ })
@@ -2,6 +2,7 @@ package KOTLINPACKAGE.utility
2
2
 
3
3
  import KOTLINPACKAGE.core.Context
4
4
  import KOTLINPACKAGE.core.Helpers
5
+ import KOTLINPACKAGE.core.Utility
5
6
  import KOTLINPACKAGE.utility.struct.Struct
6
7
 
7
8
  @Suppress("UNCHECKED_CAST")
@@ -11,11 +12,20 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
11
12
  options = linkedMapOf()
12
13
  }
13
14
 
14
- // Merge custom utility overrides onto the utility object.
15
+ // Merge utility overrides from options onto the utility object.
15
16
  // Read from original options before clone for parity with the donors.
17
+ //
18
+ // A key naming a real utility member REPLACES it; anything else is attached
19
+ // as a custom extra. Shelving everything in `custom` - a map nothing reads -
20
+ // made `utility = mapOf("fetcher" to ...)`, the documented transport seam, a
21
+ // silent no-op here while ts honoured it.
16
22
  val customUtils = Helpers.toMapAny(options["utility"])
17
23
  if (customUtils != null && ctx.utility != null) {
18
- ctx.utility!!.custom.putAll(customUtils)
24
+ for ((key, value) in customUtils) {
25
+ if (!overrideUtil(ctx.utility!!, key, value)) {
26
+ ctx.utility!!.custom[key] = value
27
+ }
28
+ }
19
29
  }
20
30
 
21
31
  var opts = Struct.clone(options) as MutableMap<String, Any?>
@@ -165,3 +175,47 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
165
175
 
166
176
  return opts
167
177
  }
178
+
179
+
180
+ /**
181
+ * Replaces one utility member from `options.utility`, matching the ts
182
+ * reference: a key naming a real member REPLACES it, and any other key is
183
+ * attached as a custom extra. Returns false when the key names no member or
184
+ * the value is not that member's type, so the caller keeps it in `custom`.
185
+ *
186
+ * REFLECTION, NOT A KEYED SWITCH. The go and java ports list every member by
187
+ * hand and carry a "keep this in step with registerAll" warning, because a
188
+ * utility added to one list and not the other is overridable there and not
189
+ * here. The field set is readable off the class, so the list cannot drift.
190
+ *
191
+ * Kotlin function types erase to FunctionN, so `isInstance` checks arity and
192
+ * not the full signature - the same limit java's port documents. A wrongly
193
+ * shaped value of the right arity is accepted, exactly as the dynamic donors
194
+ * accept whatever they are given.
195
+ *
196
+ * Only a PUBLIC name may replace a member: public utility names are camelCase
197
+ * and carry no underscore, so an underscore means the caller named something
198
+ * of their own rather than a member.
199
+ */
200
+ internal fun overrideUtil(utility: Utility, key: String, value: Any?): Boolean {
201
+ if (key.isEmpty() || key.contains('_') || "custom" == key) {
202
+ return false
203
+ }
204
+ if (null == value) {
205
+ return false
206
+ }
207
+
208
+ val field = try {
209
+ Utility::class.java.getDeclaredField(key)
210
+ } catch (e: NoSuchFieldException) {
211
+ return false
212
+ }
213
+
214
+ if (!field.type.isInstance(value)) {
215
+ return false
216
+ }
217
+
218
+ field.isAccessible = true
219
+ field.set(utility, value)
220
+ return true
221
+ }
@@ -5,13 +5,33 @@ local vs = require("utility.struct.struct")
5
5
  local function make_options_util(ctx)
6
6
  local options = ctx.options or {}
7
7
 
8
- -- Merge custom utility overrides.
8
+ -- Merge utility overrides from options onto the utility object.
9
+ --
10
+ -- A key naming a real utility member REPLACES it; anything else is attached
11
+ -- as a custom extra. Shelving everything in `custom` - a table nothing reads
12
+ -- - made `utility = { fetcher = ... }`, the documented transport seam, a
13
+ -- silent no-op here while ts honoured it.
14
+ --
15
+ -- Option keys are camelCase, as ts spells them; members here are
16
+ -- snake_case. Only a PUBLIC name may replace: public utility names carry no
17
+ -- underscore, so an underscore means the caller named something of their
18
+ -- own - possibly the internal spelling of a real member. `make_error` must
19
+ -- stay an extension, or a non-callable would break the error path on the
20
+ -- next request.
9
21
  local custom_utils = vs.getprop(options, "utility")
10
22
  if type(custom_utils) == "table" then
11
23
  local utility = ctx.utility
12
24
  if utility ~= nil then
13
25
  for key, val in pairs(custom_utils) do
14
- utility.custom[key] = val
26
+ local member = nil
27
+ if type(key) == "string" and not key:find("_") then
28
+ member = key:gsub("(%u)", function(c) return "_" .. c:lower() end)
29
+ end
30
+ if member ~= nil and member ~= "custom" and utility[member] ~= nil then
31
+ utility[member] = val
32
+ else
33
+ utility.custom[key] = val
34
+ end
15
35
  end
16
36
  end
17
37
  end
@@ -34,11 +34,16 @@ SDK_TEST = $(RUNTIME) sdk_config.ml $(ENTITIES) sdk_client.ml sdk_error.ml \
34
34
  $(ENTITY_TESTS) $(DOC_TESTS) test/t_main.ml
35
35
 
36
36
  CORPUS_SRC = utility/vregex.ml utility/voxgig_struct.ml sdk_json.ml \
37
- test/struct_corpus.ml
37
+ test/corpus_runner.ml test/struct_corpus.ml
38
38
 
39
- .PHONY: test test-sdk test-corpus build lint clean
39
+ # The primary-utility corpus needs the whole SDK, because it drives the SDK's
40
+ # own request-shaping utilities — not just the struct library.
41
+ PRIMARY_SRC = $(RUNTIME) sdk_config.ml $(ENTITIES) sdk_client.ml sdk_error.ml \
42
+ test/corpus_runner.ml test/primary_utility_test.ml
40
43
 
41
- test: test-sdk test-corpus
44
+ .PHONY: test test-sdk test-corpus test-primary build lint clean
45
+
46
+ test: test-sdk test-corpus test-primary
42
47
 
43
48
  test-sdk: run_sdk_test
44
49
  ./run_sdk_test
@@ -50,7 +55,13 @@ run_sdk_test: $(SDK_TEST)
50
55
  $(OCAMLC) $(INC) $(SDK_TEST) -o run_sdk_test
51
56
 
52
57
  run_struct_corpus: $(CORPUS_SRC)
53
- $(OCAMLC) -I utility $(CORPUS_SRC) -o run_struct_corpus
58
+ $(OCAMLC) -I utility -I test $(CORPUS_SRC) -o run_struct_corpus
59
+
60
+ test-primary: run_primary_corpus
61
+ ./run_primary_corpus $(CORPUS)
62
+
63
+ run_primary_corpus: $(PRIMARY_SRC)
64
+ $(OCAMLC) $(INC) $(PRIMARY_SRC) -o run_primary_corpus
54
65
 
55
66
  # Type-check the SDK library (a clean compile means it is sound).
56
67
  build: lint
@@ -345,7 +345,14 @@ let prepare_method_util (ctx : ctx) : string =
345
345
  | _ ->
346
346
  match ctx.c_op.op_name with
347
347
  | "create" -> "POST" | "update" -> "PUT" | "load" -> "GET"
348
- | "list" -> "GET" | "remove" -> "DELETE" | "patch" -> "PATCH" | _ -> "GET"
348
+ | "list" -> "GET" | "remove" -> "DELETE" | "patch" -> "PATCH"
349
+ (* NO CATCH-ALL GET. The ts reference returns methodMap[key], which is
350
+ * undefined for an op the map does not name — the request is then rejected
351
+ * rather than silently issued. A `| _ -> "GET"` here turned every
352
+ * unrecognised op into a GET, which is both a divergence from the corpus
353
+ * (which expects no method for opname "bad") and the more dangerous of the
354
+ * two behaviours: a mistyped or unsupported op quietly fetched. *)
355
+ | _ -> ""
349
356
 
350
357
  let prepare_headers_util (ctx : ctx) : value =
351
358
  let options = client_options_map (cc ctx) in
@@ -0,0 +1,185 @@
1
+ (* Shared corpus harness for the ProjectName SDK.
2
+ *
3
+ * Extracted from struct_corpus.ml so the primary-utility suite can drive the
4
+ * SAME runner over test.json's "primary" section. OCaml runs a module's
5
+ * top-level effects on link, so a harness sitting beside a `let () = ...` main
6
+ * cannot be reused — linking it would run the struct corpus as a side effect.
7
+ * This module deliberately has no main.
8
+ *)
9
+
10
+ (* Test runner for the shared JSON corpus (build/test/test.json).
11
+ * Self-contained: an in-tree JSON reader builds the library's `value` type
12
+ * directly, so the OCaml port is exercised exactly as in production. *)
13
+
14
+ open Voxgig_struct
15
+
16
+ let nullmark = "__NULL__"
17
+ let undefmark = "__UNDEF__"
18
+ let existsmark = "__EXISTS__"
19
+
20
+ (* The JSON reader now lives in the runtime (sdk_json.ml) so the SDK's
21
+ * generated config can use it; the corpus runs against that same function
22
+ * rather than a second copy that could drift from it. *)
23
+ let json_read = Sdk_json.json_read
24
+
25
+ (* ---------------- fixJSON / equality ---------------- *)
26
+
27
+ let rec fix_json v flag_null =
28
+ match v with
29
+ | Noval | Null -> if flag_null then Str nullmark else v
30
+ | Map m -> let o = empty_map () in
31
+ List.iter (fun (k, x) -> ignore (setprop o (Str k) (fix_json x flag_null))) m.entries; o
32
+ | List r -> lst (List.map (fun x -> fix_json x flag_null) !r)
33
+ | _ -> v
34
+
35
+ (* Order-independent deep equality for maps; sequence equality for lists. *)
36
+ let rec eqv a b =
37
+ match a, b with
38
+ | (Noval | Null), (Noval | Null) -> true
39
+ | Bool x, Bool y -> x = y
40
+ | Num x, Num y -> x = y
41
+ | Str x, Str y -> x = y
42
+ | List x, List y -> List.length !x = List.length !y && List.for_all2 eqv !x !y
43
+ | Map x, Map y ->
44
+ omap_len x = omap_len y &&
45
+ List.for_all (fun (k, v) -> match omap_get y k with Some w -> eqv v w | None -> false) x.entries
46
+ | _ -> a == b
47
+
48
+ (* ---------------- match support ---------------- *)
49
+
50
+ let matchval check base =
51
+ let check = if check = Str undefmark || check = Str nullmark then Noval else check in
52
+ if eqv check base then true
53
+ else match check with
54
+ | Str cs ->
55
+ let basestr = stringify base in
56
+ if String.length cs >= 2 && cs.[0] = '/' && cs.[String.length cs - 1] = '/' then
57
+ Vregex.test_str (String.sub cs 1 (String.length cs - 2)) basestr
58
+ else
59
+ let low s = String.lowercase_ascii s in
60
+ let contains hay needle =
61
+ let hl = String.length hay and nl = String.length needle in
62
+ let rec go i = if i + nl > hl then false
63
+ else if String.sub hay i nl = needle then true else go (i + 1) in
64
+ nl = 0 || go 0 in
65
+ contains (low basestr) (low (stringify check))
66
+ | Func _ -> true
67
+ | _ -> false
68
+
69
+ let do_match check base =
70
+ let base = clone base in
71
+ ignore (walk ~before:(fun _k v _p path ->
72
+ (if not (isnode v) then begin
73
+ let baseval = getpath base path in
74
+ if eqv baseval v then ()
75
+ else if v = Str undefmark && is_nullish baseval then ()
76
+ else if v = Str existsmark && not (is_nullish baseval) then ()
77
+ else if not (matchval v baseval) then
78
+ raise (Struct_error (Printf.sprintf "MATCH: %s: [%s] <=> [%s]"
79
+ (String.concat "." (List.map js_string (match path with List r -> !r | _ -> [])))
80
+ (stringify v) (stringify baseval)))
81
+ end);
82
+ v) check)
83
+
84
+ (* ---------------- result tracking ---------------- *)
85
+
86
+ let npass = ref 0
87
+ let nfail = ref 0
88
+ let failures = ref []
89
+
90
+ let record group name ok msg =
91
+ if ok then incr npass
92
+ else (incr nfail; failures := Printf.sprintf "FAIL %s %s - %s" group name msg :: !failures)
93
+
94
+ (* ---------------- per-entry runner ---------------- *)
95
+
96
+ let omap_v kvs =
97
+ let m = empty_map () in
98
+ List.iter (fun (k, v) -> ignore (setprop m (Str k) v)) kvs; m
99
+
100
+ let getprop_raw_pub e k = (match e with Map m -> (match omap_get m k with Some x -> x | None -> Noval) | _ -> Noval)
101
+ let entry_get e k = getprop_raw_pub e k
102
+ let entry_has e k = match e with Map m -> omap_has m k | _ -> false
103
+ let default_injdef_pub () =
104
+ { d_meta = Noval; d_extra = Noval; d_errs = Noval; d_modify = None; d_handler = None;
105
+ d_base = Noval; d_dparent = Noval; d_dpath = Noval; d_key = Noval }
106
+
107
+ let resolve_args entry =
108
+ if entry_has entry "ctx" then [entry_get entry "ctx"]
109
+ else if entry_has entry "args" then (
110
+ match entry_get entry "args" with
111
+ | List r ->
112
+ (* ts's resolveArgs writes the live first arg back as entry.ctx, so a
113
+ `match: {ctx: ...}` resolves for args-style entries too. Without it
114
+ every such assertion reads null and asserts nothing. *)
115
+ (match !r with
116
+ | (Map _ as first) :: _ -> ignore (setprop entry (Str "ctx") first)
117
+ | _ -> ());
118
+ !r
119
+ | _ -> [])
120
+ else if entry_has entry "in" then [clone (entry_get entry "in")]
121
+ else [Noval]
122
+
123
+ let check_result entry args res =
124
+ let matched = ref false in
125
+ (if entry_has entry "match" then begin
126
+ do_match (entry_get entry "match")
127
+ (omap_v ["in", entry_get entry "in"; "args", lst args;
128
+ "out", entry_get entry "res"; "ctx", entry_get entry "ctx"]);
129
+ matched := true
130
+ end);
131
+ let out = entry_get entry "out" in
132
+ if eqv out res then ()
133
+ else if !matched && (out = Str nullmark || is_nullish out) then ()
134
+ else raise (Struct_error (Printf.sprintf "Expected: %s, got: %s" (stringify out) (stringify res)))
135
+
136
+ let handle_error entry err =
137
+ let msg = (match err with Struct_error m -> m | e -> Printexc.to_string e) in
138
+ if entry_has entry "err" then begin
139
+ let entry_err = entry_get entry "err" in
140
+ if entry_err = Bool true || matchval entry_err (Str msg) then begin
141
+ if entry_has entry "match" then
142
+ do_match (entry_get entry "match")
143
+ (omap_v ["in", entry_get entry "in"; "out", entry_get entry "res";
144
+ (* ts hands do_match the ERROR OBJECT, so a corpus
145
+ `match: {err: {message: ...}}` resolves; a bare string
146
+ leaves err.message reading null. *)
147
+ "ctx", entry_get entry "ctx";
148
+ "err", omap_v ["message", Str msg]])
149
+ end else
150
+ raise (Struct_error (Printf.sprintf "ERROR MATCH: [%s] <=> [%s]" (stringify entry_err) msg))
151
+ end else raise err
152
+
153
+ let run_set ?(flags = []) group node subject =
154
+ let flag_null = (match List.assoc_opt "null" flags with Some b -> b | None -> true) in
155
+ let fixed = fix_json node flag_null in
156
+ let testset = (match getprop fixed (Str "set") with List r -> !r | _ -> []) in
157
+ List.iter (fun entry ->
158
+ let name = js_string (entry_get entry "name") in
159
+ try
160
+ (if not (entry_has entry "out") && flag_null then ignore (setprop entry (Str "out") (Str nullmark)));
161
+ let args = resolve_args entry in
162
+ let res = fix_json (subject args) flag_null in
163
+ ignore (setprop entry (Str "res") res);
164
+ check_result entry args res;
165
+ record group name true ""
166
+ with
167
+ | e ->
168
+ (try handle_error entry e; record group name true ""
169
+ with e2 -> record group name false
170
+ (match e2 with Struct_error m -> m | _ -> Printexc.to_string e2)))
171
+ testset
172
+
173
+ let run_single group node actual_fn =
174
+ try
175
+ let expected = getprop_raw_pub node "out" in
176
+ let actual = actual_fn (getprop_raw_pub node "in") in
177
+ if eqv expected actual then record group "single" true ""
178
+ else record group "single" false (Printf.sprintf "Expected: %s, got: %s" (stringify expected) (stringify actual))
179
+ with e -> record group "single" false (match e with Struct_error m -> m | _ -> Printexc.to_string e)
180
+
181
+ (* ---------------- arg helpers ---------------- *)
182
+
183
+ let arg1 f = fun args -> f (match args with x :: _ -> x | [] -> Noval)
184
+ let vget vin k = match vin with Map m -> (match omap_get m k with Some x -> x | None -> Noval) | _ -> Noval
185
+ let vhas vin k = match vin with Map m -> omap_has m k | _ -> false