@voxgig/sdkgen 4.8.3 → 4.9.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 (201) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/package.json +1 -1
  3. package/project/.sdk/model/target/zig.aon +9 -0
  4. package/project/.sdk/src/cmp/zig/Package_zig.ts +15 -11
  5. package/project/.sdk/src/cmp/zig/fragment/Entity.fragment.zig +3 -3
  6. package/project/.sdk/src/cmp/zig/fragment/EntityListOp.fragment.zig +3 -3
  7. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +3 -3
  8. package/project/.sdk/src/cmp/zig/utility_zig.ts +50 -1
  9. package/project/.sdk/tm/c/tests/vendor/omni/json.c +2 -2
  10. package/project/.sdk/tm/c/tests/vendor/omni/omni.h +2 -2
  11. package/project/.sdk/tm/c/tests/vendor/omni/runner.c +2 -2
  12. package/project/.sdk/tm/c/tests/vendor/omni/util.c +2 -2
  13. package/project/.sdk/tm/clojure/test/vendor/omni/voxgig/omni/json.clj +2 -2
  14. package/project/.sdk/tm/clojure/test/vendor/omni/voxgig/omni/runner.clj +2 -2
  15. package/project/.sdk/tm/clojure/test/vendor/omni/voxgig/omni/util.clj +2 -2
  16. package/project/.sdk/tm/cpp/test/vendor/omni/json.hpp +2 -2
  17. package/project/.sdk/tm/cpp/test/vendor/omni/omni.hpp +2 -2
  18. package/project/.sdk/tm/cpp/test/vendor/omni/util.hpp +2 -2
  19. package/project/.sdk/tm/csharp/test/vendor/omni/Runner.cs +2 -2
  20. package/project/.sdk/tm/csharp/test/vendor/omni/Util.cs +2 -2
  21. package/project/.sdk/tm/csharp/utility/struct/Struct.cs +1 -1
  22. package/project/.sdk/tm/dart/test/vendor/omni/omni.dart +2 -2
  23. package/project/.sdk/tm/dart/test/vendor/omni/runner.dart +2 -2
  24. package/project/.sdk/tm/dart/test/vendor/omni/util.dart +2 -2
  25. package/project/.sdk/tm/elixir/test/vendor/omni/json.ex +2 -2
  26. package/project/.sdk/tm/elixir/test/vendor/omni/runner.ex +2 -2
  27. package/project/.sdk/tm/elixir/test/vendor/omni/util.ex +2 -2
  28. package/project/.sdk/tm/go/feature/secrets/plugin/capability.go +1 -1
  29. package/project/.sdk/tm/go/feature/secrets/plugin/catalog.go +1 -1
  30. package/project/.sdk/tm/go/feature/secrets/plugin/config.go +1 -1
  31. package/project/.sdk/tm/go/feature/secrets/plugin/depend.go +1 -1
  32. package/project/.sdk/tm/go/feature/secrets/plugin/env.go +1 -1
  33. package/project/.sdk/tm/go/feature/secrets/plugin/export.go +1 -1
  34. package/project/.sdk/tm/go/feature/secrets/plugin/graph.go +1 -1
  35. package/project/.sdk/tm/go/feature/secrets/plugin/host.go +1 -1
  36. package/project/.sdk/tm/go/feature/secrets/plugin/order.go +1 -1
  37. package/project/.sdk/tm/go/feature/secrets/plugin/point.go +1 -1
  38. package/project/.sdk/tm/go/feature/secrets/plugin/ref.go +1 -1
  39. package/project/.sdk/tm/go/feature/secrets/plugin/resolve.go +1 -1
  40. package/project/.sdk/tm/go/feature/secrets/plugin/types.go +1 -1
  41. package/project/.sdk/tm/go/feature/secrets/plugin/util.go +1 -1
  42. package/project/.sdk/tm/go/feature/secrets/plugin/version.go +1 -1
  43. package/project/.sdk/tm/go/feature/secrets/plugins/aws/aws.go +4 -4
  44. package/project/.sdk/tm/go/feature/secrets/plugins/aws/sigv4.go +2 -2
  45. package/project/.sdk/tm/go/feature/secrets/plugins/azuresecrets/azuresecrets.go +2 -2
  46. package/project/.sdk/tm/go/feature/secrets/plugins/boru/boru.go +2 -2
  47. package/project/.sdk/tm/go/feature/secrets/plugins/doppler/doppler.go +2 -2
  48. package/project/.sdk/tm/go/feature/secrets/plugins/gcpsecrets/gcpsecrets.go +2 -2
  49. package/project/.sdk/tm/go/feature/secrets/plugins/hashicorp/hashicorp.go +4 -5
  50. package/project/.sdk/tm/go/feature/secrets/plugins/httpjson/httpjson.go +3 -3
  51. package/project/.sdk/tm/go/feature/secrets/plugins/infisical/infisical.go +3 -4
  52. package/project/.sdk/tm/go/feature/secrets/plugins/onepassword/onepassword.go +2 -2
  53. package/project/.sdk/tm/go/feature/secrets/plugins/secretspec/secretspec.go +2 -2
  54. package/project/.sdk/tm/go/feature/secrets/sekreto/addr.go +2 -2
  55. package/project/.sdk/tm/go/feature/secrets/sekreto/providers.go +5 -2
  56. package/project/.sdk/tm/go/feature/secrets/sekreto/sekreto.go +31 -2
  57. package/project/.sdk/tm/go/test/omni/omni.go +8 -3
  58. package/project/.sdk/tm/go/test/omni/util.go +25 -4
  59. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +1 -1
  60. package/project/.sdk/tm/java/test/vendor/omni/Json.java +2 -2
  61. package/project/.sdk/tm/java/test/vendor/omni/Runner.java +2 -2
  62. package/project/.sdk/tm/java/test/vendor/omni/Util.java +2 -2
  63. package/project/.sdk/tm/java/utility/struct/Struct.java +1 -1
  64. package/project/.sdk/tm/js/src/utility/StructUtility.js +2 -2
  65. package/project/.sdk/tm/js/test/vendor/omni/index.js +1 -1
  66. package/project/.sdk/tm/js/test/vendor/omni/runner.js +18 -3
  67. package/project/.sdk/tm/js/test/vendor/omni/util.js +25 -8
  68. package/project/.sdk/tm/kotlin/test/vendor/omni/Json.kt +2 -2
  69. package/project/.sdk/tm/kotlin/test/vendor/omni/Runner.kt +2 -2
  70. package/project/.sdk/tm/kotlin/test/vendor/omni/Util.kt +2 -2
  71. package/project/.sdk/tm/kotlin/utility/struct/Struct.kt +1 -1
  72. package/project/.sdk/tm/lean/test/vendor/omni/Omni.lean +2 -2
  73. package/project/.sdk/tm/lua/test/vendor/omni/json.lua +2 -2
  74. package/project/.sdk/tm/lua/test/vendor/omni/regex.lua +2 -2
  75. package/project/.sdk/tm/lua/test/vendor/omni/runner.lua +2 -2
  76. package/project/.sdk/tm/lua/test/vendor/omni/util.lua +2 -2
  77. package/project/.sdk/tm/lua/utility/struct/regex.lua +1 -1
  78. package/project/.sdk/tm/lua/utility/struct/struct.lua +1 -1
  79. package/project/.sdk/tm/ocaml/test/vendor/omni/omni.ml +2 -2
  80. package/project/.sdk/tm/perl/lib/Voxgig/Struct.pm +1 -1
  81. package/project/.sdk/tm/perl/t/vendor/omni/Voxgig/Omni/Runner.pm +1 -1
  82. package/project/.sdk/tm/perl/t/vendor/omni/Voxgig/Omni/Util.pm +1 -1
  83. package/project/.sdk/tm/perl/t/vendor/omni/Voxgig/Omni.pm +1 -1
  84. package/project/.sdk/tm/php/test/vendor/omni/Runner.php +2 -2
  85. package/project/.sdk/tm/php/test/vendor/omni/Util.php +2 -2
  86. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/__init__.py +1 -1
  87. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/capability.py +1 -1
  88. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/catalog.py +1 -1
  89. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/config.py +1 -1
  90. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/depend.py +1 -1
  91. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/env.py +1 -1
  92. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/export.py +1 -1
  93. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/graph.py +1 -1
  94. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/host.py +1 -1
  95. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/order.py +1 -1
  96. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/point.py +1 -1
  97. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/ref.py +1 -1
  98. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/resolve.py +1 -1
  99. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/types.py +1 -1
  100. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/version.py +1 -1
  101. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/__init__.py +3 -1
  102. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/addr.py +1 -1
  103. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/aws.py +3 -3
  104. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/azuresecrets.py +1 -1
  105. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/boru.py +1 -1
  106. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/doppler.py +1 -1
  107. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/gcpsecrets.py +1 -1
  108. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/hashicorp.py +3 -4
  109. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/httpjson.py +1 -1
  110. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/infisical.py +4 -6
  111. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/onepassword.py +1 -1
  112. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/secretspec.py +1 -1
  113. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/sigv4.py +1 -1
  114. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/providers.py +1 -1
  115. package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/sekreto.py +21 -1
  116. package/project/.sdk/tm/py/pkg/utility/voxgig_struct/voxgig_struct.py +1 -1
  117. package/project/.sdk/tm/py/test/voxgig_omni/__init__.py +1 -1
  118. package/project/.sdk/tm/py/test/voxgig_omni/runner.py +16 -2
  119. package/project/.sdk/tm/py/test/voxgig_omni/util.py +27 -12
  120. package/project/.sdk/tm/rb/test/vendor/omni/runner.rb +2 -2
  121. package/project/.sdk/tm/rb/test/vendor/omni/util.rb +2 -2
  122. package/project/.sdk/tm/rb/test/vendor/omni/voxgig_omni.rb +2 -2
  123. package/project/.sdk/tm/rust/tests/vendor/omni/json.rs +2 -2
  124. package/project/.sdk/tm/rust/tests/vendor/omni/mod.rs +2 -2
  125. package/project/.sdk/tm/rust/tests/vendor/omni/regex.rs +2 -2
  126. package/project/.sdk/tm/rust/tests/vendor/omni/runner.rs +2 -2
  127. package/project/.sdk/tm/rust/tests/vendor/omni/util.rs +2 -2
  128. package/project/.sdk/tm/scala/sdktest/vendor/omni/Json.scala +2 -2
  129. package/project/.sdk/tm/scala/sdktest/vendor/omni/Runner.scala +2 -2
  130. package/project/.sdk/tm/scala/sdktest/vendor/omni/Util.scala +2 -2
  131. package/project/.sdk/tm/swift/Tests/vendor/omni/Json.swift +2 -2
  132. package/project/.sdk/tm/swift/Tests/vendor/omni/Runner.swift +2 -2
  133. package/project/.sdk/tm/swift/Tests/vendor/omni/Util.swift +2 -2
  134. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Capability.ts +1 -1
  135. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Catalog.ts +1 -1
  136. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Config.ts +1 -1
  137. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Depend.ts +1 -1
  138. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Env.ts +1 -1
  139. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Export.ts +1 -1
  140. package/project/.sdk/tm/ts/src/feature/secrets/plugin/FeatureHost.ts +1 -1
  141. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Graph.ts +1 -1
  142. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Host.ts +1 -1
  143. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Order.ts +1 -1
  144. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Point.ts +1 -1
  145. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Ref.ts +1 -1
  146. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Resolve.ts +1 -1
  147. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Types.ts +1 -1
  148. package/project/.sdk/tm/ts/src/feature/secrets/plugin/Version.ts +1 -1
  149. package/project/.sdk/tm/ts/src/feature/secrets/plugin/index.ts +1 -1
  150. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +1 -1
  151. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +1 -1
  152. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/aws.ts +1 -1
  153. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/azuresecrets.ts +1 -1
  154. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/boru.ts +1 -1
  155. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/doppler.ts +1 -1
  156. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/gcpsecrets.ts +1 -1
  157. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/hashicorp.ts +1 -1
  158. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/httpjson.ts +1 -1
  159. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/infisical.ts +1 -1
  160. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/onepassword.ts +1 -1
  161. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/secretspec.ts +1 -1
  162. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/sigv4.ts +1 -1
  163. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +1 -1
  164. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/builtin.ts +1 -1
  165. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +1 -1
  166. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +1 -1
  167. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +1 -1
  168. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +1 -1
  169. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +1 -1
  170. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +2 -2
  171. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +1 -1
  172. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +1 -1
  173. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
  174. package/project/.sdk/tm/zig/Makefile +15 -1
  175. package/project/.sdk/tm/zig/build.zig +23 -5
  176. package/project/.sdk/tm/zig/core/helpers.zig +52 -11
  177. package/project/.sdk/tm/zig/core/mem.zig +17 -1
  178. package/project/.sdk/tm/zig/core/utility.zig +23 -19
  179. package/project/.sdk/tm/zig/feature/audit.zig +2 -2
  180. package/project/.sdk/tm/zig/feature/cache.zig +2 -2
  181. package/project/.sdk/tm/zig/feature/debug.zig +5 -5
  182. package/project/.sdk/tm/zig/feature/netsim.zig +2 -2
  183. package/project/.sdk/tm/zig/feature/paging.zig +2 -2
  184. package/project/.sdk/tm/zig/feature/proxy.zig +15 -8
  185. package/project/.sdk/tm/zig/feature/streaming.zig +5 -5
  186. package/project/.sdk/tm/zig/feature/support.zig +3 -3
  187. package/project/.sdk/tm/zig/feature/telemetry.zig +5 -5
  188. package/project/.sdk/tm/zig/feature/test.zig +10 -5
  189. package/project/.sdk/tm/zig/test/fh.zig +10 -10
  190. package/project/.sdk/tm/zig/test/gotcha_test.zig +5 -5
  191. package/project/.sdk/tm/zig/test/omniresolver.zig +515 -0
  192. package/project/.sdk/tm/zig/test/omnismoke_test.zig +284 -0
  193. package/project/.sdk/tm/zig/test/pipeline_test.zig +8 -8
  194. package/project/.sdk/tm/zig/test/primary_utility_test.zig +144 -154
  195. package/project/.sdk/tm/zig/test/struct_corpus.zig +264 -187
  196. package/project/.sdk/tm/zig/test/vendor/omni/omni.zig +1314 -0
  197. package/project/.sdk/tm/zig/test/vendor/omni/regex.zig +446 -0
  198. package/project/.sdk/tm/zig/utility/voxgigstruct/regex.zig +7 -4
  199. package/project/.sdk/tm/zig/utility/voxgigstruct/struct.zig +356 -157
  200. package/project/sdkgen-package.json +1 -1
  201. package/project/.sdk/tm/zig/test/struct_runner.zig +0 -465
@@ -0,0 +1,1314 @@
1
+ // VENDORED: @voxgig/omni sdk-20260907-0029-0 (zig/src/omni.zig)
2
+ // Source: https://github.com/voxgig/omni @ 274708cc2d12b21707d975543953f845f8444be0 [tag: sdk-20260907-0029-0]
3
+ // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
+ //! Omni: the shared multi-language test runner (Zig port).
5
+ //!
6
+ //! A test spec is plain JSON. The same spec file drives the same tests in
7
+ //! every language that ships an omni port.
8
+ //!
9
+ //! Port of the canonical TypeScript implementation
10
+ //! (typescript/src/Runner.ts). Behaviour must match, case for case.
11
+ //!
12
+ //! Zig has no exceptions, so a failing check is returned as a message:
13
+ //! `if (pack.runset(group, subject)) |failure| { ... }`. A subject reports
14
+ //! failure the same way, with `SubjectResult.err`.
15
+ //!
16
+ //! Memory: every value the runner allocates comes from the allocator given
17
+ //! to `makeRunner`. Use an arena and free it once at the end.
18
+
19
+ const std = @import("std");
20
+
21
+ pub const regex = @import("regex.zig");
22
+
23
+ /// A JSON value. `null` (the Zig optional) is the absence marker, as
24
+ /// distinct from `Json{ .null = {} }`, which is a JSON null.
25
+ pub const Json = std.json.Value;
26
+
27
+ /// Absence: no value at all.
28
+ pub const Maybe = ?Json;
29
+
30
+ pub const NULLMARK = "__NULL__"; // Value is JSON null.
31
+ pub const UNDEFMARK = "__UNDEF__"; // Value is not present.
32
+ pub const EXISTSMARK = "__EXISTS__"; // Value exists (not undefined).
33
+
34
+ // ---- value helpers ---------------------------------------------------
35
+
36
+ pub fn ismap(val: Maybe) bool {
37
+ return if (val) |entry| .object == entry else false;
38
+ }
39
+
40
+ pub fn islist(val: Maybe) bool {
41
+ return if (val) |entry| .array == entry else false;
42
+ }
43
+
44
+ pub fn isnode(val: Maybe) bool {
45
+ return ismap(val) or islist(val);
46
+ }
47
+
48
+ pub fn isabsent(val: Maybe) bool {
49
+ return null == val;
50
+ }
51
+
52
+ pub fn isnull(val: Maybe) bool {
53
+ return if (val) |entry| .null == entry else false;
54
+ }
55
+
56
+ pub fn isnone(val: Maybe) bool {
57
+ return isabsent(val) or isnull(val);
58
+ }
59
+
60
+ pub fn isnum(val: Maybe) bool {
61
+ const entry = val orelse return false;
62
+ return .integer == entry or .float == entry;
63
+ }
64
+
65
+ pub fn isstr(val: Maybe) bool {
66
+ return if (val) |entry| .string == entry else false;
67
+ }
68
+
69
+ pub fn asnum(val: Maybe) ?f64 {
70
+ const entry = val orelse return null;
71
+ return switch (entry) {
72
+ .integer => |num| @floatFromInt(num),
73
+ .float => |num| num,
74
+ else => null,
75
+ };
76
+ }
77
+
78
+ pub fn asstr(val: Maybe) ?[]const u8 {
79
+ const entry = val orelse return null;
80
+ return switch (entry) {
81
+ .string => |text| text,
82
+ else => null,
83
+ };
84
+ }
85
+
86
+ /// Read a map entry. Returns null (absent) when missing.
87
+ pub fn jget(val: Maybe, key: []const u8) Maybe {
88
+ const entry = val orelse return null;
89
+ return switch (entry) {
90
+ .object => |map| map.get(key),
91
+ else => null,
92
+ };
93
+ }
94
+
95
+ pub fn jhas(val: Maybe, key: []const u8) bool {
96
+ const entry = val orelse return false;
97
+ return switch (entry) {
98
+ .object => |map| map.contains(key),
99
+ else => false,
100
+ };
101
+ }
102
+
103
+ pub fn jnum(val: f64) Json {
104
+ return .{ .float = val };
105
+ }
106
+
107
+ pub fn jstr(val: []const u8) Json {
108
+ return .{ .string = val };
109
+ }
110
+
111
+ pub fn jbool(val: bool) Json {
112
+ return .{ .bool = val };
113
+ }
114
+
115
+ pub fn jlist(alloc: std.mem.Allocator, entries: []const Json) !Json {
116
+ var out = std.json.Array.init(alloc);
117
+ for (entries) |entry| {
118
+ try out.append(entry);
119
+ }
120
+ return .{ .array = out };
121
+ }
122
+
123
+ pub const Pair = struct { []const u8, Json };
124
+
125
+ pub fn jmap(alloc: std.mem.Allocator, entries: []const Pair) !Json {
126
+ var out: std.json.ObjectMap = .{};
127
+ for (entries) |entry| {
128
+ try out.put(alloc, entry[0], entry[1]);
129
+ }
130
+ return .{ .object = out };
131
+ }
132
+
133
+ /// Set a map entry, returning the updated value (no-op for non-maps).
134
+ pub fn jset(alloc: std.mem.Allocator, val: Json, key: []const u8, entry: Json) !Json {
135
+ switch (val) {
136
+ .object => |map| {
137
+ var out = map;
138
+ try out.put(alloc, key, entry);
139
+ return .{ .object = out };
140
+ },
141
+ else => return val,
142
+ }
143
+ }
144
+
145
+ // ---- util ------------------------------------------------------------
146
+
147
+ /// Deep copy a JSON value. Containers are rebuilt in the given allocator;
148
+ /// scalars are values already.
149
+ pub fn clone(alloc: std.mem.Allocator, val: Maybe) error{OutOfMemory}!Json {
150
+ const entry = val orelse return Json{ .null = {} };
151
+
152
+ return switch (entry) {
153
+ .array => |list| blk: {
154
+ var out = std.json.Array.init(alloc);
155
+ for (list.items) |item| {
156
+ try out.append(try clone(alloc, item));
157
+ }
158
+ break :blk Json{ .array = out };
159
+ },
160
+ .object => |map| blk: {
161
+ var out: std.json.ObjectMap = .{};
162
+ var it = map.iterator();
163
+ while (it.next()) |field| {
164
+ try out.put(alloc, field.key_ptr.*, try clone(alloc, field.value_ptr.*));
165
+ }
166
+ break :blk Json{ .object = out };
167
+ },
168
+ else => entry,
169
+ };
170
+ }
171
+
172
+ /// The callback of a walk.
173
+ pub const Apply = *const fn (alloc: std.mem.Allocator, val: Json, path: []const []const u8) Json;
174
+
175
+ /// Depth-first walk: children first, then the containing node.
176
+ pub fn walk(
177
+ alloc: std.mem.Allocator,
178
+ val: Json,
179
+ apply: Apply,
180
+ path: []const []const u8,
181
+ ) error{OutOfMemory}!Json {
182
+ const next = switch (val) {
183
+ .array => |list| blk: {
184
+ var out = std.json.Array.init(alloc);
185
+ for (list.items, 0..) |item, index| {
186
+ const childpath = try std.mem.concat(alloc, []const u8, &.{
187
+ path,
188
+ &.{try std.fmt.allocPrint(alloc, "{d}", .{index})},
189
+ });
190
+ try out.append(try walk(alloc, item, apply, childpath));
191
+ }
192
+ break :blk Json{ .array = out };
193
+ },
194
+ .object => |map| blk: {
195
+ var out: std.json.ObjectMap = .{};
196
+ var it = map.iterator();
197
+ while (it.next()) |field| {
198
+ const childpath = try std.mem.concat(alloc, []const u8, &.{
199
+ path,
200
+ &.{field.key_ptr.*},
201
+ });
202
+ try out.put(alloc, field.key_ptr.*, try walk(alloc, field.value_ptr.*, apply, childpath));
203
+ }
204
+ break :blk Json{ .object = out };
205
+ },
206
+ else => val,
207
+ };
208
+
209
+ return apply(alloc, next, path);
210
+ }
211
+
212
+ /// Read a value by path. Returns null (absent) when any step is missing.
213
+ pub fn getpath(val: Maybe, path: []const []const u8) Maybe {
214
+ var current = val;
215
+
216
+ for (path) |part| {
217
+ const entry = current orelse return null;
218
+
219
+ switch (entry) {
220
+ .array => |list| {
221
+ const index = std.fmt.parseInt(usize, part, 10) catch return null;
222
+ if (index >= list.items.len) {
223
+ return null;
224
+ }
225
+ current = list.items[index];
226
+ },
227
+ .object => |map| {
228
+ current = map.get(part) orelse return null;
229
+ },
230
+ else => return null,
231
+ }
232
+ }
233
+
234
+ return current;
235
+ }
236
+
237
+ /// Deep structural equality: numbers by value, bools never numbers.
238
+ pub fn deepequal(a: Maybe, b: Maybe) bool {
239
+ if (isabsent(a) or isabsent(b)) {
240
+ return isabsent(a) and isabsent(b);
241
+ }
242
+
243
+ const x = a.?;
244
+ const y = b.?;
245
+
246
+ if (isnum(a) or isnum(b)) {
247
+ if (!isnum(a) or !isnum(b)) {
248
+ return false;
249
+ }
250
+ const anum = asnum(a).?;
251
+ const bnum = asnum(b).?;
252
+
253
+ // NaN equals NaN: deepequal is structural, not IEEE (as canonical).
254
+ return anum == bnum or (std.math.isNan(anum) and std.math.isNan(bnum));
255
+ }
256
+
257
+ return switch (x) {
258
+ .null => .null == y,
259
+ .bool => |flag| .bool == y and flag == y.bool,
260
+ .string => |text| .string == y and std.mem.eql(u8, text, y.string),
261
+ .array => |list| blk: {
262
+ if (.array != y) break :blk false;
263
+ if (list.items.len != y.array.items.len) break :blk false;
264
+ for (list.items, 0..) |entry, index| {
265
+ if (!deepequal(entry, y.array.items[index])) break :blk false;
266
+ }
267
+ break :blk true;
268
+ },
269
+ .object => |map| blk: {
270
+ if (.object != y) break :blk false;
271
+ if (map.count() != y.object.count()) break :blk false;
272
+ var it = map.iterator();
273
+ while (it.next()) |field| {
274
+ const other = y.object.get(field.key_ptr.*) orelse break :blk false;
275
+ if (!deepequal(field.value_ptr.*, other)) break :blk false;
276
+ }
277
+ break :blk true;
278
+ },
279
+ else => false,
280
+ };
281
+ }
282
+
283
+ /// Render a number the same way in every port: 5.0 prints as 5.
284
+ pub fn numstr(alloc: std.mem.Allocator, val: f64) ![]const u8 {
285
+ if (std.math.isNan(val) or std.math.isInf(val)) {
286
+ return "null";
287
+ }
288
+ if (val == @trunc(val) and @abs(val) < 9007199254740992.0) {
289
+ return std.fmt.allocPrint(alloc, "{d}", .{@as(i64, @intFromFloat(val))});
290
+ }
291
+ return std.fmt.allocPrint(alloc, "{d}", .{val});
292
+ }
293
+
294
+ /// Render a string as a JSON string literal.
295
+ pub fn quote(alloc: std.mem.Allocator, val: []const u8) ![]const u8 {
296
+ var out: std.ArrayList(u8) = .empty;
297
+ try out.append(alloc, '"');
298
+
299
+ for (val) |ch| {
300
+ switch (ch) {
301
+ '"' => try out.appendSlice(alloc, "\\\""),
302
+ '\\' => try out.appendSlice(alloc, "\\\\"),
303
+ '\n' => try out.appendSlice(alloc, "\\n"),
304
+ '\r' => try out.appendSlice(alloc, "\\r"),
305
+ '\t' => try out.appendSlice(alloc, "\\t"),
306
+ else => {
307
+ if (0x20 > ch) {
308
+ const escape = try std.fmt.allocPrint(alloc, "\\u{x:0>4}", .{ch});
309
+ try out.appendSlice(alloc, escape);
310
+ } else {
311
+ try out.append(alloc, ch);
312
+ }
313
+ },
314
+ }
315
+ }
316
+
317
+ try out.append(alloc, '"');
318
+ return out.items;
319
+ }
320
+
321
+ /// Compact JSON text with map keys sorted, identical in every port.
322
+ pub fn jsonstr(alloc: std.mem.Allocator, val: Maybe) error{OutOfMemory}![]const u8 {
323
+ const entry = val orelse return "undefined";
324
+
325
+ return switch (entry) {
326
+ .null => "null",
327
+ .bool => |flag| if (flag) "true" else "false",
328
+ .integer, .float, .number_string => try numstr(alloc, asnum(val).?),
329
+ .string => |text| try quote(alloc, text),
330
+ .array => |list| blk: {
331
+ var out: std.ArrayList(u8) = .empty;
332
+ try out.append(alloc, '[');
333
+ for (list.items, 0..) |item, index| {
334
+ if (0 < index) try out.append(alloc, ',');
335
+ try out.appendSlice(alloc, try jsonstr(alloc, item));
336
+ }
337
+ try out.append(alloc, ']');
338
+ break :blk out.items;
339
+ },
340
+ .object => |map| blk: {
341
+ var keys: std.ArrayList([]const u8) = .empty;
342
+ var it = map.iterator();
343
+ while (it.next()) |field| {
344
+ try keys.append(alloc, field.key_ptr.*);
345
+ }
346
+ std.mem.sort([]const u8, keys.items, {}, lessThanStr);
347
+
348
+ var out: std.ArrayList(u8) = .empty;
349
+ try out.append(alloc, '{');
350
+ for (keys.items, 0..) |key, index| {
351
+ if (0 < index) try out.append(alloc, ',');
352
+ try out.appendSlice(alloc, try quote(alloc, key));
353
+ try out.append(alloc, ':');
354
+ try out.appendSlice(alloc, try jsonstr(alloc, map.get(key).?));
355
+ }
356
+ try out.append(alloc, '}');
357
+ break :blk out.items;
358
+ },
359
+ };
360
+ }
361
+
362
+ fn lessThanStr(_: void, a: []const u8, b: []const u8) bool {
363
+ return .lt == std.mem.order(u8, a, b);
364
+ }
365
+
366
+ /// Strings verbatim, everything else as compact JSON.
367
+ pub fn stringify(alloc: std.mem.Allocator, val: Maybe) ![]const u8 {
368
+ if (asstr(val)) |text| {
369
+ return text;
370
+ }
371
+ return jsonstr(alloc, val);
372
+ }
373
+
374
+ /// Render a path as a dotted string.
375
+ pub fn pathify(alloc: std.mem.Allocator, path: []const []const u8) ![]const u8 {
376
+ return std.mem.join(alloc, ".", path);
377
+ }
378
+
379
+ // ---- runner ----------------------------------------------------------
380
+
381
+ /// What a subject returns: a value, or the message of a failure.
382
+ pub const SubjectResult = union(enum) {
383
+ ok: Json,
384
+ err: []const u8,
385
+ };
386
+
387
+ /// The function under test. Zig has no closures, so a subject is a
388
+ /// function pointer plus its own data.
389
+ pub const Subject = struct {
390
+ call: *const fn (self: *const Subject, args: []const Json) SubjectResult,
391
+ data: ?*const anyopaque = null,
392
+ };
393
+
394
+ /// What an args-subject returns: the arguments it ended up with, alongside
395
+ /// the value - or the message of a failure.
396
+ pub const SubjectArgsResult = union(enum) {
397
+ ok: struct { args: []const Json, res: Json },
398
+ err: []const u8,
399
+ };
400
+
401
+ /// A subject that may REPLACE its arguments, which `match.args` can then
402
+ /// assert on. `minor/setpath` is the case in point: eight of its nine entries
403
+ /// assert that the store was rewritten in place, and `merge/integrity` all
404
+ /// six.
405
+ ///
406
+ /// A consumer whose value model is not `std.json.Value` has to convert, and a
407
+ /// conversion is a copy: whatever it does to its own nodes is invisible here.
408
+ /// Returning the arguments alongside the result is the only channel there is.
409
+ ///
410
+ /// Separate from `Subject` rather than replacing it: a signature change would
411
+ /// break every consumer for a capability most subjects do not need. omni-rust,
412
+ /// -cpp, -ocaml, -elixir, -haskell, -clojure, -scala, -swift and -lean carry
413
+ /// the same pair, for the same reason.
414
+ pub const SubjectArgs = struct {
415
+ call: *const fn (self: *const SubjectArgs, args: []const Json) SubjectArgsResult,
416
+ data: ?*const anyopaque = null,
417
+ };
418
+
419
+ /// The host of the system under test. Every hook is optional.
420
+ pub const Provider = struct {
421
+ subject: ?*const fn (self: *const Provider, name: []const u8) ?*const Subject = null,
422
+ client: ?*const fn (self: *const Provider, options: Maybe) *const Provider = null,
423
+ contextify: ?*const fn (self: *const Provider, val: Json) Json = null,
424
+ /// Build the `match.err` base from the failure, REPLACING `errify`.
425
+ ///
426
+ /// Zig reaches this point with a message and nothing else - an error
427
+ /// value carries no payload - so a library whose errors carry a code
428
+ /// has no other way to put one in the base. The hook receives that
429
+ /// message and returns the base, letting a spec assert
430
+ /// `match: {err: {code: "x"}}` instead of pattern-matching prose.
431
+ errify: ?*const fn (self: *const Provider, alloc: std.mem.Allocator, message: []const u8) anyerror!Json = null,
432
+ data: ?*const anyopaque = null,
433
+ };
434
+
435
+ /// Run-time options for a set of test entries.
436
+ pub const Flags = struct {
437
+ null_: bool = true,
438
+ name: ?[]const u8 = null,
439
+
440
+ pub fn nonull() Flags {
441
+ return .{ .null_ = false };
442
+ }
443
+ };
444
+
445
+ /// The newest spec format version this runner understands. A spec with no
446
+ /// OMNI block is version 0: the original, lenient format, frozen forever.
447
+ /// Version 1 turns on strict entry validation (see checkentry).
448
+ pub const SPECVERSION = 1;
449
+
450
+ /// Capability strings this runner supports beyond the version baseline. A
451
+ /// spec's OMNI.requires list is checked against this: an unknown
452
+ /// capability refuses the spec loudly at load time, instead of a lagging
453
+ /// port silently mis-running it. (Empty today; future format features
454
+ /// mint a string here.)
455
+ pub const CAPABILITIES: []const []const u8 = &.{};
456
+
457
+ /// The complete set of fields an entry may carry. Under version 1 anything
458
+ /// else is an error: an unrecognised key is almost always a typo'd
459
+ /// assertion, and a typo'd assertion is a test that silently stopped
460
+ /// testing.
461
+ const ENTRYFIELDS = [_][]const u8{ "in", "args", "ctx", "out", "err", "match", "client", "id", "doc" };
462
+
463
+ fn isentryfield(key: []const u8) bool {
464
+ for (ENTRYFIELDS) |field| {
465
+ if (std.mem.eql(u8, field, key)) return true;
466
+ }
467
+ return false;
468
+ }
469
+
470
+ /// Load a spec: a path to a JSON file.
471
+ pub fn loadspec(alloc: std.mem.Allocator, io: std.Io, path: []const u8) !Json {
472
+ const text = std.Io.Dir.cwd().readFileAlloc(io, path, alloc, .unlimited) catch {
473
+ return error.CannotReadSpec;
474
+ };
475
+
476
+ const parsed = try std.json.parseFromSlice(Json, alloc, text, .{});
477
+
478
+ return parsed.value;
479
+ }
480
+
481
+ /// Find `primary.<name>`, then `<name>`, then the whole spec.
482
+ pub fn resolvespec(name: []const u8, alltests: Json) Json {
483
+ if (0 == name.len) {
484
+ return alltests;
485
+ }
486
+
487
+ if (jget(jget(alltests, "primary"), name)) |section| {
488
+ return section;
489
+ }
490
+
491
+ if (jget(alltests, name)) |section| {
492
+ return section;
493
+ }
494
+
495
+ return alltests;
496
+ }
497
+
498
+ /// Read the spec's format version from its optional top-level OMNI block,
499
+ /// and refuse a spec this runner cannot faithfully run: a version newer
500
+ /// than SPECVERSION, or a required capability outside CAPABILITIES. Only a
501
+ /// genuinely absent OMNI key is legacy (version 0) - a present-but-null
502
+ /// block, or a present-but-null requires list, is malformed; that
503
+ /// distinction is why this checks presence (jhas) rather than nullness.
504
+ /// This is a load-time failure, so it surfaces through makeRunner's own
505
+ /// error-union return path rather than the ?[]const u8 channel runset
506
+ /// failures use - Zig error values carry no message payload, so (unlike
507
+ /// the checkset/checkentry failures below) this one is exempt from
508
+ /// carrying byte-identical text; the distinct error name is what a caller
509
+ /// matches on instead.
510
+ fn resolveversion(alltests: Json) !i64 {
511
+ if (!jhas(alltests, "OMNI")) {
512
+ return 0;
513
+ }
514
+
515
+ const meta = jget(alltests, "OMNI");
516
+ const version = jget(meta, "version");
517
+
518
+ if (!ismap(meta) or !isnum(version)) {
519
+ return error.MalformedOmniVersion;
520
+ }
521
+
522
+ const vnum = asnum(version).?;
523
+ if (vnum != @trunc(vnum)) {
524
+ return error.MalformedOmniVersion;
525
+ }
526
+
527
+ const maxversion: f64 = SPECVERSION;
528
+ if (vnum < 0 or maxversion < vnum) {
529
+ return error.UnsupportedSpecVersion;
530
+ }
531
+
532
+ if (jhas(meta, "requires")) {
533
+ const requires = jget(meta, "requires").?;
534
+ if (!islist(requires)) {
535
+ return error.MalformedOmniRequires;
536
+ }
537
+
538
+ for (requires.array.items) |cap| {
539
+ const capstr = asstr(cap) orelse return error.UnsupportedCapability;
540
+
541
+ var known = false;
542
+ for (CAPABILITIES) |allowed| {
543
+ if (std.mem.eql(u8, allowed, capstr)) {
544
+ known = true;
545
+ break;
546
+ }
547
+ }
548
+ if (!known) {
549
+ return error.UnsupportedCapability;
550
+ }
551
+ }
552
+ }
553
+
554
+ return @intFromFloat(vnum);
555
+ }
556
+
557
+ /// Nulls (and absent values) become NULLMARK. Always a fresh copy.
558
+ pub fn fixjson(alloc: std.mem.Allocator, val: Maybe, donull: bool) error{OutOfMemory}!Json {
559
+ if (isnone(val)) {
560
+ return if (donull) jstr(NULLMARK) else Json{ .null = {} };
561
+ }
562
+
563
+ const entry = val.?;
564
+
565
+ return switch (entry) {
566
+ .array => |list| blk: {
567
+ var out = std.json.Array.init(alloc);
568
+ for (list.items) |item| {
569
+ try out.append(try fixjson(alloc, item, donull));
570
+ }
571
+ break :blk Json{ .array = out };
572
+ },
573
+ .object => |map| blk: {
574
+ var out: std.json.ObjectMap = .{};
575
+ var it = map.iterator();
576
+ while (it.next()) |field| {
577
+ try out.put(alloc, field.key_ptr.*, try fixjson(alloc, field.value_ptr.*, donull));
578
+ }
579
+ break :blk Json{ .object = out };
580
+ },
581
+ else => entry,
582
+ };
583
+ }
584
+
585
+ /// The JSON form of an error: always at least {name,message}.
586
+ pub fn errify(alloc: std.mem.Allocator, message: []const u8) !Json {
587
+ return jmap(alloc, &.{ .{ "name", jstr("Error") }, .{ "message", jstr(message) } });
588
+ }
589
+
590
+ /// Match one leaf: /regex/ or case-insensitive substring for strings.
591
+ pub fn matchval(alloc: std.mem.Allocator, check: Maybe, base: Maybe) !bool {
592
+ if (deepequal(check, base)) {
593
+ return true;
594
+ }
595
+
596
+ var want = check;
597
+ if (asstr(check)) |text| {
598
+ if (std.mem.eql(u8, UNDEFMARK, text) or std.mem.eql(u8, NULLMARK, text)) {
599
+ want = Json{ .null = {} };
600
+ }
601
+ }
602
+
603
+ if (isnone(want)) {
604
+ if (isnone(base)) {
605
+ return true;
606
+ }
607
+ if (asstr(base)) |text| {
608
+ return std.mem.eql(u8, NULLMARK, text);
609
+ }
610
+ return false;
611
+ }
612
+
613
+ if (asstr(want)) |text| {
614
+ // An empty want is not a wildcard: it matches only an empty string.
615
+ if (0 == text.len) {
616
+ return if (asstr(base)) |basetext| 0 == basetext.len else false;
617
+ }
618
+
619
+ const basestr = try stringify(alloc, base);
620
+
621
+ if (2 < text.len and '/' == text[0] and '/' == text[text.len - 1]) {
622
+ return regex.find(alloc, text[1 .. text.len - 1], basestr);
623
+ }
624
+
625
+ const lowbase = try std.ascii.allocLowerString(alloc, basestr);
626
+ const lowwant = try std.ascii.allocLowerString(alloc, text);
627
+
628
+ return null != std.mem.indexOf(u8, lowbase, lowwant);
629
+ }
630
+
631
+ return deepequal(want, base);
632
+ }
633
+
634
+ /// Convert NULLMARK sentinels back into real nulls.
635
+ pub fn nullmodifier(alloc: std.mem.Allocator, val: Json) !Json {
636
+ const text = asstr(val) orelse return val;
637
+
638
+ if (std.mem.eql(u8, NULLMARK, text)) {
639
+ return Json{ .null = {} };
640
+ }
641
+
642
+ const replaced = try std.mem.replaceOwned(u8, alloc, text, NULLMARK, "null");
643
+ return jstr(replaced);
644
+ }
645
+
646
+ /// What a runner returns for one named spec section.
647
+ pub const RunPack = struct {
648
+ alloc: std.mem.Allocator,
649
+ spec: Json,
650
+ subject: ?*const Subject,
651
+ client: *const Provider,
652
+ name: []const u8,
653
+ clientnames: []const []const u8,
654
+ clients: []const *const Provider,
655
+ specversion: i64,
656
+
657
+ /// A named group of the resolved spec.
658
+ pub fn set(self: *const RunPack, setname: []const u8) Maybe {
659
+ return jget(self.spec, setname);
660
+ }
661
+
662
+ /// Run one set of test entries. Returns a failure message, or null.
663
+ pub fn runset(self: *const RunPack, testspec: Maybe, subject: ?*const Subject) !?[]const u8 {
664
+ return self.runsetflags(testspec, .{}, subject);
665
+ }
666
+
667
+ /// Run one set of test entries with flags.
668
+ pub fn runsetflags(
669
+ self: *const RunPack,
670
+ testspec: Maybe,
671
+ flags: Flags,
672
+ subject: ?*const Subject,
673
+ ) !?[]const u8 {
674
+ return self.drive(testspec, flags, subject, null);
675
+ }
676
+
677
+ /// Everything `runsetflags` does, for a subject that may replace its
678
+ /// arguments. See `SubjectArgs`.
679
+ pub fn runsetflagsargs(
680
+ self: *const RunPack,
681
+ testspec: Maybe,
682
+ flags: Flags,
683
+ argsubject: *const SubjectArgs,
684
+ ) !?[]const u8 {
685
+ return self.drive(testspec, flags, null, argsubject);
686
+ }
687
+
688
+ fn drive(
689
+ self: *const RunPack,
690
+ testspec: Maybe,
691
+ flags: Flags,
692
+ subject: ?*const Subject,
693
+ argsubject: ?*const SubjectArgs,
694
+ ) !?[]const u8 {
695
+ const alloc = self.alloc;
696
+ const label = flags.name orelse (if (0 == self.name.len) "set" else self.name);
697
+
698
+ const usesubject: ?*const Subject = if (null != argsubject)
699
+ null
700
+ else
701
+ subject orelse self.subject orelse
702
+ return try std.fmt.allocPrint(alloc, "omni: no test subject for: {s}", .{label});
703
+
704
+ const testspecmap = try fixjson(alloc, testspec, flags.null_);
705
+ const testset = jget(testspecmap, "set") orelse
706
+ return try std.fmt.allocPrint(alloc, "omni: test spec has no set: {s}", .{label});
707
+
708
+ if (.array != testset) {
709
+ return try std.fmt.allocPrint(alloc, "omni: test spec has no set: {s}", .{label});
710
+ }
711
+
712
+ // Validate the AUTHORED group (pre-fixjson), not the normalised
713
+ // one: null-normalisation would otherwise rewrite an authored
714
+ // `id: null` into a sentinel string and hide it from checkentry.
715
+ if (1 <= self.specversion) {
716
+ if (try self.checkset(label, testspec, testset)) |failure| {
717
+ return failure;
718
+ }
719
+ }
720
+
721
+ for (testset.array.items, 0..) |rawentry, index| {
722
+ if (.object != rawentry) {
723
+ return try std.fmt.allocPrint(
724
+ alloc,
725
+ "omni: {s}[{d}]: entry is not a map",
726
+ .{ label, index },
727
+ );
728
+ }
729
+
730
+ var entry = rawentry;
731
+
732
+ // An entry with no `out` expects a null (or absent) result.
733
+ if (flags.null_ and isnone(jget(entry, "out"))) {
734
+ entry = try jset(alloc, entry, "out", jstr(NULLMARK));
735
+ }
736
+
737
+ var entrysubject: ?*const Subject = usesubject;
738
+
739
+ if (null != entrysubject) if (asstr(jget(entry, "client"))) |clientname| {
740
+ var found: ?*const Provider = null;
741
+ for (self.clientnames, 0..) |candidate, at| {
742
+ if (std.mem.eql(u8, candidate, clientname)) {
743
+ found = self.clients[at];
744
+ }
745
+ }
746
+
747
+ const client = found orelse
748
+ return try std.fmt.allocPrint(alloc, "omni: unknown client: {s}", .{clientname});
749
+
750
+ if (client.subject) |resolve| {
751
+ if (resolve(client, self.name)) |clientsubject| {
752
+ entrysubject = clientsubject;
753
+ }
754
+ }
755
+ };
756
+
757
+ // Build the argument list: `ctx`, `args`, or `in`.
758
+ var args: std.ArrayList(Json) = .empty;
759
+ const hasctx = jhas(entry, "ctx");
760
+ const hasargs = jhas(entry, "args");
761
+
762
+ if (hasctx) {
763
+ try args.append(alloc, jget(entry, "ctx").?);
764
+ } else if (hasargs) {
765
+ const rawargs = jget(entry, "args").?;
766
+ if (.array == rawargs) {
767
+ for (rawargs.array.items) |item| {
768
+ try args.append(alloc, item);
769
+ }
770
+ } else {
771
+ try args.append(alloc, rawargs);
772
+ }
773
+ } else {
774
+ try args.append(alloc, try clone(alloc, jget(entry, "in")));
775
+ }
776
+
777
+ if ((hasctx or hasargs) and 0 < args.items.len and .object == args.items[0]) {
778
+ var first = args.items[0];
779
+ if (self.client.contextify) |contextify| {
780
+ first = contextify(self.client, first);
781
+ }
782
+ // Canonical attaches the resolved client onto ctx/args[0]
783
+ // (testpack.client) so a context subject can tell it was
784
+ // invoked through one. std.json.Value has no case for a
785
+ // live Provider reference, and the resolved client is
786
+ // never absent (it defaults to the root provider), so a
787
+ // boolean marker carries the same observable meaning the
788
+ // corpus checks for - presence, never identity.
789
+ first = try jset(alloc, first, "client", jbool(true));
790
+ args.items[0] = first;
791
+ entry = try jset(alloc, entry, "ctx", first);
792
+ }
793
+
794
+ // An args-subject returns its arguments alongside the result, so
795
+ // `match.args` sees what the subject actually did with them.
796
+ const outcome: SubjectArgsResult = if (argsubject) |call|
797
+ call.call(call, args.items)
798
+ else if (entrysubject) |call| switch (call.call(call, args.items)) {
799
+ .ok => |value| SubjectArgsResult{ .ok = .{ .args = args.items, .res = value } },
800
+ .err => |message| SubjectArgsResult{ .err = message },
801
+ } else return try std.fmt.allocPrint(
802
+ alloc,
803
+ "omni: no test subject for: {s}",
804
+ .{label},
805
+ );
806
+
807
+ switch (outcome) {
808
+ .err => |message| {
809
+ if (try self.handleerror(label, index, entry, message)) |failure| {
810
+ return failure;
811
+ }
812
+ },
813
+ .ok => |call| {
814
+ const res = try fixjson(alloc, call.res, flags.null_);
815
+ entry = try jset(alloc, entry, "res", res);
816
+
817
+ if (try self.checkresult(label, index, entry, call.args, res)) |failure| {
818
+ return failure;
819
+ }
820
+ },
821
+ }
822
+ }
823
+
824
+ return null;
825
+ }
826
+
827
+ // Validate a version-1 group up front, against the AUTHORED entries -
828
+ // see the note on runsetflags's call site for why. Absorbs the
829
+ // empty-set check too: an empty `set` is refused unless the group
830
+ // carries `empty: true`, making the emptiness a stated fact rather
831
+ // than an accident.
832
+ fn checkset(self: *const RunPack, label: []const u8, testspec: Maybe, normalset: Json) !?[]const u8 {
833
+ const origset = if (ismap(testspec) and islist(jget(testspec, "set")))
834
+ jget(testspec, "set").?
835
+ else
836
+ normalset;
837
+
838
+ var isempty = false;
839
+ if (ismap(testspec)) {
840
+ if (jget(testspec, "empty")) |flag| {
841
+ isempty = .bool == flag and flag.bool;
842
+ }
843
+ }
844
+
845
+ if (0 == origset.array.items.len and !isempty) {
846
+ return try std.fmt.allocPrint(self.alloc, "omni: empty test set: {s}", .{label});
847
+ }
848
+
849
+ for (origset.array.items, 0..) |entry, index| {
850
+ if (try self.checkentry(label, index, entry)) |failure| {
851
+ return failure;
852
+ }
853
+ }
854
+
855
+ return null;
856
+ }
857
+
858
+ // Strict entry validation, applied when the spec declares version 1
859
+ // or later. The lenient format converts each of these mistakes into a
860
+ // silent pass or a dead field; here they fail with the entry named.
861
+ fn checkentry(self: *const RunPack, label: []const u8, index: usize, entry: Json) !?[]const u8 {
862
+ if (.object != entry) {
863
+ return try self.fail(label, index, entry, "entry is not a map", null, null);
864
+ }
865
+
866
+ var fields = entry.object.iterator();
867
+ while (fields.next()) |field| {
868
+ if (!isentryfield(field.key_ptr.*)) {
869
+ const reason = try std.fmt.allocPrint(
870
+ self.alloc,
871
+ "unknown entry field: {s}",
872
+ .{field.key_ptr.*},
873
+ );
874
+ return try self.fail(label, index, entry, reason, null, null);
875
+ }
876
+ }
877
+
878
+ var argsources: usize = 0;
879
+ for ([_][]const u8{ "in", "args", "ctx" }) |key| {
880
+ if (jhas(entry, key)) argsources += 1;
881
+ }
882
+ if (1 < argsources) {
883
+ return try self.fail(label, index, entry, "entry has more than one of in, args, ctx", null, null);
884
+ }
885
+
886
+ if (!isnone(jget(entry, "err")) and jhas(entry, "out")) {
887
+ return try self.fail(label, index, entry, "entry has both err and out", null, null);
888
+ }
889
+
890
+ // Presence, not nullness: an authored `id: null` must fail here,
891
+ // before resolveentry's null-normalisation ever gets a chance to
892
+ // turn it into the sentinel string "__NULL__".
893
+ if (jhas(entry, "id") and null == asstr(jget(entry, "id"))) {
894
+ return try self.fail(label, index, entry, "entry id is not a string", null, null);
895
+ }
896
+
897
+ return null;
898
+ }
899
+
900
+ fn checkresult(
901
+ self: *const RunPack,
902
+ label: []const u8,
903
+ index: usize,
904
+ entry: Json,
905
+ args: []const Json,
906
+ res: Json,
907
+ ) !?[]const u8 {
908
+ const alloc = self.alloc;
909
+ var matched = false;
910
+
911
+ const entryerr = jget(entry, "err");
912
+ if (!isnone(entryerr)) {
913
+ return try self.fail(
914
+ label,
915
+ index,
916
+ entry,
917
+ "expected error did not occur",
918
+ try stringify(alloc, entryerr),
919
+ try stringify(alloc, res),
920
+ );
921
+ }
922
+
923
+ const check = jget(entry, "match");
924
+ if (!isnone(check)) {
925
+ const base = try jmap(alloc, &.{
926
+ .{ "in", jget(entry, "in") orelse Json{ .null = {} } },
927
+ .{ "args", try jlist(alloc, args) },
928
+ .{ "out", jget(entry, "res") orelse Json{ .null = {} } },
929
+ .{ "ctx", jget(entry, "ctx") orelse Json{ .null = {} } },
930
+ });
931
+
932
+ if (try self.matchcheck(label, index, entry, check.?, base, &.{})) |failure| {
933
+ return failure;
934
+ }
935
+ matched = true;
936
+ }
937
+
938
+ const out = jget(entry, "out");
939
+
940
+ if (deepequal(res, out)) {
941
+ return null;
942
+ }
943
+
944
+ // NOTE: a match with no explicit out is a complete check on its own.
945
+ if (matched) {
946
+ if (isnone(out)) {
947
+ return null;
948
+ }
949
+ if (asstr(out)) |text| {
950
+ if (std.mem.eql(u8, NULLMARK, text)) {
951
+ return null;
952
+ }
953
+ }
954
+ }
955
+
956
+ return try self.fail(
957
+ label,
958
+ index,
959
+ entry,
960
+ "result mismatch",
961
+ try stringify(alloc, out),
962
+ try stringify(alloc, res),
963
+ );
964
+ }
965
+
966
+ /// The error base a `match.err` sees: the provider's own, when it has one.
967
+ fn errbase(self: *const RunPack, message: []const u8) !Json {
968
+ if (self.client.errify) |hook| {
969
+ return hook(self.client, self.alloc, message);
970
+ }
971
+ return errify(self.alloc, message);
972
+ }
973
+
974
+ fn handleerror(
975
+ self: *const RunPack,
976
+ label: []const u8,
977
+ index: usize,
978
+ entry: Json,
979
+ message: []const u8,
980
+ ) !?[]const u8 {
981
+ const alloc = self.alloc;
982
+ const entryerr = jget(entry, "err");
983
+
984
+ if (isnone(entryerr)) {
985
+ return try self.fail(label, index, entry, "unexpected error", null, message);
986
+ }
987
+
988
+ var istrue = false;
989
+ if (.bool == entryerr.?) {
990
+ istrue = entryerr.?.bool;
991
+ }
992
+
993
+ if (istrue or try matchval(alloc, entryerr, jstr(message))) {
994
+ const check = jget(entry, "match");
995
+ if (!isnone(check)) {
996
+ const base = try jmap(alloc, &.{
997
+ .{ "in", jget(entry, "in") orelse Json{ .null = {} } },
998
+ .{ "out", jget(entry, "res") orelse Json{ .null = {} } },
999
+ .{ "ctx", jget(entry, "ctx") orelse Json{ .null = {} } },
1000
+ .{ "err", try self.errbase(message) },
1001
+ });
1002
+ return self.matchcheck(label, index, entry, check.?, base, &.{});
1003
+ }
1004
+ return null;
1005
+ }
1006
+
1007
+ return try self.fail(
1008
+ label,
1009
+ index,
1010
+ entry,
1011
+ "error mismatch",
1012
+ try stringify(alloc, entryerr),
1013
+ message,
1014
+ );
1015
+ }
1016
+
1017
+ // Check that every leaf of `check` is present, and matches, in `base`.
1018
+ fn matchcheck(
1019
+ self: *const RunPack,
1020
+ label: []const u8,
1021
+ index: usize,
1022
+ entry: Json,
1023
+ check: Json,
1024
+ base: Json,
1025
+ path: []const []const u8,
1026
+ ) error{OutOfMemory}!?[]const u8 {
1027
+ const alloc = self.alloc;
1028
+
1029
+ switch (check) {
1030
+ .array => |list| {
1031
+ for (list.items, 0..) |subcheck, at| {
1032
+ const childpath = try std.mem.concat(alloc, []const u8, &.{
1033
+ path,
1034
+ &.{try std.fmt.allocPrint(alloc, "{d}", .{at})},
1035
+ });
1036
+ if (try self.matchcheck(label, index, entry, subcheck, base, childpath)) |failure| {
1037
+ return failure;
1038
+ }
1039
+ }
1040
+ return null;
1041
+ },
1042
+ .object => |map| {
1043
+ var it = map.iterator();
1044
+ while (it.next()) |field| {
1045
+ const childpath = try std.mem.concat(alloc, []const u8, &.{
1046
+ path,
1047
+ &.{field.key_ptr.*},
1048
+ });
1049
+ if (try self.matchcheck(label, index, entry, field.value_ptr.*, base, childpath)) |failure| {
1050
+ return failure;
1051
+ }
1052
+ }
1053
+ return null;
1054
+ },
1055
+ else => {},
1056
+ }
1057
+
1058
+ const baseval = getpath(base, path);
1059
+
1060
+ // The sentinels are tested BEFORE the identity check below.
1061
+ // Otherwise a subject returning the literal string "__UNDEF__"
1062
+ // satisfies an assertion that the key is absent - two mutually
1063
+ // exclusive states passing one check. A sentinel that accepts its
1064
+ // own literal is not a sentinel. (NULLMARK still accepts NULLMARK:
1065
+ // under the default null flag a real null has already been
1066
+ // normalised to it, so the two are genuinely indistinguishable
1067
+ // here - that one needs a raw-value escape, not an ordering
1068
+ // change.)
1069
+ if (asstr(check)) |text| {
1070
+ // Explicitly absent: satisfied only by a genuinely missing
1071
+ // key, never by a present null (the distinction the sentinels
1072
+ // exist to keep).
1073
+ if (std.mem.eql(u8, UNDEFMARK, text)) {
1074
+ if (isabsent(baseval)) {
1075
+ return null;
1076
+ }
1077
+ return try self.fail(
1078
+ label,
1079
+ index,
1080
+ entry,
1081
+ try std.fmt.allocPrint(alloc, "expected absent at {s}", .{try self.matchat(path)}),
1082
+ "absent",
1083
+ try stringify(alloc, baseval),
1084
+ );
1085
+ }
1086
+ // Explicitly null: satisfied only by a present null.
1087
+ if (std.mem.eql(u8, NULLMARK, text)) {
1088
+ if (isnull(baseval)) {
1089
+ return null;
1090
+ }
1091
+ if (asstr(baseval)) |basetext| {
1092
+ if (std.mem.eql(u8, NULLMARK, basetext)) {
1093
+ return null;
1094
+ }
1095
+ }
1096
+ return try self.fail(
1097
+ label,
1098
+ index,
1099
+ entry,
1100
+ try std.fmt.allocPrint(alloc, "expected null at {s}", .{try self.matchat(path)}),
1101
+ "null",
1102
+ try stringify(alloc, baseval),
1103
+ );
1104
+ }
1105
+ // Explicitly present: any present value, including null.
1106
+ if (std.mem.eql(u8, EXISTSMARK, text)) {
1107
+ if (!isabsent(baseval)) {
1108
+ return null;
1109
+ }
1110
+ return try self.fail(
1111
+ label,
1112
+ index,
1113
+ entry,
1114
+ try std.fmt.allocPrint(alloc, "expected present at {s}", .{try self.matchat(path)}),
1115
+ "present",
1116
+ "absent",
1117
+ );
1118
+ }
1119
+ }
1120
+
1121
+ // Identical values match. This sits below the sentinel branches
1122
+ // on purpose - see the note above.
1123
+ if (deepequal(check, baseval)) {
1124
+ return null;
1125
+ }
1126
+
1127
+ // A concrete expectation never matches a missing key - a match
1128
+ // leaf against an absent value must fail, not substring-match
1129
+ // "undefined".
1130
+ if (isabsent(baseval)) {
1131
+ return try self.fail(
1132
+ label,
1133
+ index,
1134
+ entry,
1135
+ try std.fmt.allocPrint(alloc, "match failed at {s}", .{try self.matchat(path)}),
1136
+ try stringify(alloc, check),
1137
+ "absent",
1138
+ );
1139
+ }
1140
+
1141
+ if (try matchval(alloc, check, baseval)) {
1142
+ return null;
1143
+ }
1144
+
1145
+ return try self.matchfail(label, index, entry, check, baseval, path);
1146
+ }
1147
+
1148
+ // A "match failed" report at one path.
1149
+ fn matchfail(
1150
+ self: *const RunPack,
1151
+ label: []const u8,
1152
+ index: usize,
1153
+ entry: Json,
1154
+ check: Maybe,
1155
+ baseval: Maybe,
1156
+ path: []const []const u8,
1157
+ ) error{OutOfMemory}![]const u8 {
1158
+ const alloc = self.alloc;
1159
+ return try self.fail(
1160
+ label,
1161
+ index,
1162
+ entry,
1163
+ try std.fmt.allocPrint(alloc, "match failed at {s}", .{try self.matchat(path)}),
1164
+ try stringify(alloc, check),
1165
+ try stringify(alloc, baseval),
1166
+ );
1167
+ }
1168
+
1169
+ // The location of one match leaf, for failure messages.
1170
+ fn matchat(self: *const RunPack, path: []const []const u8) error{OutOfMemory}![]const u8 {
1171
+ return if (0 == path.len) "<root>" else try pathify(self.alloc, path);
1172
+ }
1173
+
1174
+ // The spec-defined part of an entry (drop runner bookkeeping).
1175
+ fn entrysummary(self: *const RunPack, entry: Json) !Json {
1176
+ if (.object != entry) {
1177
+ return entry;
1178
+ }
1179
+
1180
+ var out: std.json.ObjectMap = .{};
1181
+ var it = entry.object.iterator();
1182
+ while (it.next()) |field| {
1183
+ const key = field.key_ptr.*;
1184
+ if (!std.mem.eql(u8, "res", key) and !std.mem.eql(u8, "thrown", key) and
1185
+ !std.mem.eql(u8, "ctx", key))
1186
+ {
1187
+ try out.put(self.alloc, key, field.value_ptr.*);
1188
+ }
1189
+ }
1190
+
1191
+ return .{ .object = out };
1192
+ }
1193
+
1194
+ fn fail(
1195
+ self: *const RunPack,
1196
+ label: []const u8,
1197
+ index: usize,
1198
+ entry: Json,
1199
+ reason: []const u8,
1200
+ expected: ?[]const u8,
1201
+ actual: ?[]const u8,
1202
+ ) error{OutOfMemory}![]const u8 {
1203
+ const alloc = self.alloc;
1204
+
1205
+ const id = jget(entry, "id");
1206
+ const idpart = if (isabsent(id))
1207
+ ""
1208
+ else
1209
+ try std.fmt.allocPrint(alloc, " ({s})", .{try stringify(alloc, id)});
1210
+
1211
+ var out: std.ArrayList(u8) = .empty;
1212
+ try out.appendSlice(alloc, try std.fmt.allocPrint(
1213
+ alloc,
1214
+ "omni: {s}[{d}]{s}: {s}",
1215
+ .{ label, index, idpart, reason },
1216
+ ));
1217
+
1218
+ if (expected) |text| {
1219
+ try out.appendSlice(alloc, try std.fmt.allocPrint(alloc, "\n expected: {s}", .{text}));
1220
+ }
1221
+ if (actual) |text| {
1222
+ try out.appendSlice(alloc, try std.fmt.allocPrint(alloc, "\n actual: {s}", .{text}));
1223
+ }
1224
+
1225
+ try out.appendSlice(alloc, try std.fmt.allocPrint(
1226
+ alloc,
1227
+ "\n entry: {s}",
1228
+ .{try stringify(alloc, try self.entrysummary(entry))},
1229
+ ));
1230
+
1231
+ return out.items;
1232
+ }
1233
+ };
1234
+
1235
+ /// A loaded spec plus its provider.
1236
+ pub const Runner = struct {
1237
+ alloc: std.mem.Allocator,
1238
+ alltests: Json,
1239
+ provider: *const Provider,
1240
+ specversion: i64,
1241
+
1242
+ /// Resolve one named section of the spec.
1243
+ pub fn runner(self: *const Runner, name: []const u8, store: Maybe) !RunPack {
1244
+ _ = store;
1245
+
1246
+ const spec = resolvespec(name, self.alltests);
1247
+
1248
+ var clientnames: std.ArrayList([]const u8) = .empty;
1249
+ var clients: std.ArrayList(*const Provider) = .empty;
1250
+
1251
+ // A spec may define clients that a given test run never references.
1252
+ const defclient = jget(jget(spec, "DEF"), "client");
1253
+ if (ismap(defclient)) {
1254
+ if (self.provider.client) |clientmaker| {
1255
+ var it = defclient.?.object.iterator();
1256
+ while (it.next()) |field| {
1257
+ var copts = jget(jget(field.value_ptr.*, "test"), "options");
1258
+ if (isabsent(copts)) {
1259
+ copts = try jmap(self.alloc, &.{});
1260
+ }
1261
+ try clientnames.append(self.alloc, field.key_ptr.*);
1262
+ try clients.append(self.alloc, clientmaker(self.provider, copts));
1263
+ }
1264
+ }
1265
+ }
1266
+
1267
+ var subject: ?*const Subject = null;
1268
+ if (0 < name.len) {
1269
+ if (self.provider.subject) |resolve| {
1270
+ subject = resolve(self.provider, name);
1271
+ }
1272
+ }
1273
+
1274
+ return .{
1275
+ .alloc = self.alloc,
1276
+ .spec = spec,
1277
+ .subject = subject,
1278
+ .client = self.provider,
1279
+ .name = name,
1280
+ .clientnames = clientnames.items,
1281
+ .clients = clients.items,
1282
+ .specversion = self.specversion,
1283
+ };
1284
+ }
1285
+ };
1286
+
1287
+ /// Make a runner for a spec file path and a provider. Resolves the spec's
1288
+ /// format version immediately, so an unsupported or malformed OMNI block
1289
+ /// fails fast, before any group runs.
1290
+ pub fn makeRunner(
1291
+ alloc: std.mem.Allocator,
1292
+ io: std.Io,
1293
+ path: []const u8,
1294
+ provider: *const Provider,
1295
+ ) !Runner {
1296
+ const alltests = try loadspec(alloc, io, path);
1297
+ return .{
1298
+ .alloc = alloc,
1299
+ .alltests = alltests,
1300
+ .provider = provider,
1301
+ .specversion = try resolveversion(alltests),
1302
+ };
1303
+ }
1304
+
1305
+ /// Make a runner for an in-memory spec and a provider. Same fail-fast
1306
+ /// version resolution as makeRunner.
1307
+ pub fn makeRunnerSpec(alloc: std.mem.Allocator, spec: Json, provider: *const Provider) !Runner {
1308
+ return .{
1309
+ .alloc = alloc,
1310
+ .alltests = spec,
1311
+ .provider = provider,
1312
+ .specversion = try resolveversion(spec),
1313
+ };
1314
+ }