@venn-lang/grpc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @venn-lang/grpc
2
+
3
+ > The `grpc` namespace: unary calls, server streams and server reflection as Venn verbs.
4
+
5
+ Three verbs over one port. The method name is the positional argument and the request message is the
6
+ options map, so a call reads the way a `.proto` does. This package never parses a `.proto` itself:
7
+ what a call returns is `dynamic`, and reflection is the only shape it can describe.
8
+
9
+ ## Install
10
+
11
+ The package ships with the stdlib, so the CLI already loads it. Inside a `.vn` file, bring the
12
+ namespace in with `use`:
13
+
14
+ ```ruby
15
+ use "venn/grpc"
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ruby
21
+ module demo.inventory
22
+
23
+ use "venn/grpc"
24
+
25
+ flow "Inventory" {
26
+ step "check the stock" {
27
+ let res = grpc.call "Inventory/Check" { sku: "sku-42" }
28
+ expect res.inStock == true
29
+ }
30
+
31
+ step "watch the prices" {
32
+ let ticks = grpc.stream "Prices/Watch" { sku: "sku-42" }
33
+ expect ticks.len > 0
34
+ }
35
+ }
36
+ ```
37
+
38
+ The request message is a free-form map, so no key is ever "unknown" and `VN3001` cannot fire here.
39
+ The trade is that nothing checks the field names either: they are the server's business.
40
+
41
+ ## Verbs
42
+
43
+ | Verb | Positional argument | Options | Result |
44
+ | --- | --- | --- | --- |
45
+ | `grpc.call` | `method: string`, the full `package.Service/Method` | the request message | `dynamic` |
46
+ | `grpc.stream` | `method: string` | the request message | `list<dynamic>` |
47
+ | `grpc.reflect` | `service: string` | none | `list<grpc.MethodInfo>` |
48
+
49
+ `grpc.stream` hands back the messages already collected into a list. It is not a live stream, so a
50
+ flow reads it like any other list.
51
+
52
+ ## Types
53
+
54
+ The plugin publishes one named type, the only thing it can honestly describe:
55
+
56
+ | Name | Shape |
57
+ | --- | --- |
58
+ | `grpc.MethodInfo` | `{ name: string, requestType: string, responseType: string, clientStreaming: bool, serverStreaming: bool }` |
59
+
60
+ A call's request and response come from a `.proto` nobody here has read, so they stay `dynamic`.
61
+
62
+ ## The GrpcClient port
63
+
64
+ | | |
65
+ | --- | --- |
66
+ | id | `venn.port.grpc-client` |
67
+ | version | `1` |
68
+ | requires | `net` |
69
+ | methods | `call`, `stream`, `reflect` |
70
+
71
+ Two implementations ship with the package. `createFakeClient` replays canned messages keyed by
72
+ method; `createRealClient` is a placeholder that throws `VN8090` on every call, since proto loading,
73
+ channels and real reflection are out of scope for this build. The conformance suite lives in
74
+ `src/clients/grpc-client.suite.ts` and the fake runs it today; the real client joins it the day it
75
+ answers instead of throwing.
76
+
77
+ `@venn-lang/stdlib` binds `createFakeClient()` with no configuration, so out of the box `call` answers
78
+ `{}`, `stream` answers `[]` and `reflect` answers `[]`. To make the example above pass, bind a fake
79
+ of your own:
80
+
81
+ ```ts
82
+ import { createFakeClient, GrpcClientPort } from "@venn-lang/grpc";
83
+
84
+ const binding = {
85
+ port: GrpcClientPort,
86
+ impl: createFakeClient({
87
+ responses: { "Inventory/Check": { inStock: true, quantity: 42 } },
88
+ streams: { "Prices/Watch": [{ price: 1 }, { price: 2 }] },
89
+ reflection: {
90
+ Inventory: [
91
+ {
92
+ name: "Check",
93
+ requestType: "CheckRequest",
94
+ responseType: "CheckResponse",
95
+ clientStreaming: false,
96
+ serverStreaming: false,
97
+ },
98
+ ],
99
+ },
100
+ }),
101
+ };
102
+ ```
103
+
104
+ `responses` and `streams` are keyed by the full method; `reflection` is keyed by service name.
105
+
106
+ ## API
107
+
108
+ | Export | What it is |
109
+ | --- | --- |
110
+ | `grpcPlugin` (also the default export) | The `PluginDefinition`: namespace `grpc`, `requires: ["net"]`, three actions. |
111
+ | `GrpcClientPort` | The port descriptor actions resolve through `ctx.port(...)`. |
112
+ | `createFakeClient({ responses?, streams?, reflection? })` | The test double. Unknown keys fall back to `{}` or `[]`. |
113
+ | `createRealClient()` | The real client's slot. Every method throws `VN8090`. |
114
+ | `GrpcCall`, `GrpcClient`, `GrpcMethodInfo` | Types only. |
115
+
116
+ ## See also
117
+
118
+ - [`@venn-lang/graphql`](../std-graphql), the same port pattern over GraphQL.
119
+ - [`@venn-lang/http`](../std-http), the HTTP verbs and the `Response` type.
120
+ - [`@venn-lang/sdk`](../sdk), `defineAction` / `definePlugin`.
@@ -0,0 +1,81 @@
1
+ import { Port } from "@venn-lang/contracts";
2
+ import { PluginDefinition } from "@venn-lang/sdk";
3
+ //#region src/port/grpc-client.types.d.ts
4
+ /** One invocation: the full `package.Service/Method`, plus the request message. */
5
+ interface GrpcCall {
6
+ method: string;
7
+ request?: Record<string, unknown>;
8
+ }
9
+ /** Metadata for one RPC method, as returned by server reflection. */
10
+ interface GrpcMethodInfo {
11
+ name: string;
12
+ requestType: string;
13
+ responseType: string;
14
+ clientStreaming: boolean;
15
+ serverStreaming: boolean;
16
+ }
17
+ /**
18
+ * Calling a gRPC service.
19
+ *
20
+ * `stream` hands back the whole sequence once it is complete rather than a live
21
+ * stream, so a flow reads it as an ordinary list.
22
+ *
23
+ * Two implementations: `createRealClient` and `createFakeClient`.
24
+ */
25
+ interface GrpcClient {
26
+ call(call: GrpcCall): Promise<unknown>;
27
+ stream(call: GrpcCall): Promise<readonly unknown[]>;
28
+ reflect(service: string): Promise<readonly GrpcMethodInfo[]>;
29
+ }
30
+ //#endregion
31
+ //#region src/port/grpc-client.port.d.ts
32
+ /**
33
+ * The port descriptor every `grpc` verb resolves through `ctx.port(...)`.
34
+ *
35
+ * Declares the `net` capability, so a host that cannot open a channel refuses
36
+ * the binding at load time with a readable diagnostic.
37
+ */
38
+ declare const GrpcClientPort: Port<GrpcClient>;
39
+ //#endregion
40
+ //#region src/clients/fake-client.d.ts
41
+ /**
42
+ * The double: canned answers, and no channel.
43
+ *
44
+ * Anything not named falls back to an empty value (`{}` for a call, `[]` for a
45
+ * stream or a reflection), so a test only lists what it cares about.
46
+ *
47
+ * @param config.responses Unary results, keyed by `package.Service/Method`.
48
+ * @param config.streams Streamed results, keyed by the same.
49
+ * @param config.reflection Method metadata, keyed by service name.
50
+ */
51
+ declare function createFakeClient(config?: {
52
+ responses?: Record<string, unknown>;
53
+ streams?: Record<string, readonly unknown[]>;
54
+ reflection?: Record<string, readonly GrpcMethodInfo[]>;
55
+ }): GrpcClient;
56
+ //#endregion
57
+ //#region src/clients/real-client.d.ts
58
+ /**
59
+ * The real gRPC client. Not implemented in this build: proto loading, channels
60
+ * and reflection are out of scope here.
61
+ *
62
+ * It exists so the port has its second implementation and the failure is a named
63
+ * Venn error rather than a missing method.
64
+ *
65
+ * @throws VN8090 from every method.
66
+ */
67
+ declare function createRealClient(): GrpcClient;
68
+ //#endregion
69
+ //#region src/plugin.d.ts
70
+ /**
71
+ * The `grpc` namespace: `call`, `stream`, `reflect` and the `grpc.MethodInfo`
72
+ * type.
73
+ *
74
+ * Requires the `net` capability, so a host without it refuses the plugin at load
75
+ * time rather than failing mid-flow. No matchers: there is no typed subject to
76
+ * write one against.
77
+ */
78
+ declare const grpcPlugin: PluginDefinition;
79
+ //#endregion
80
+ export { type GrpcCall, type GrpcClient, GrpcClientPort, type GrpcMethodInfo, createFakeClient, createRealClient, grpcPlugin as default, grpcPlugin };
81
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/port/grpc-client.types.ts","../src/port/grpc-client.port.ts","../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/plugin.ts"],"mappings":";;;;UACiB;EACf;EACA,UAAU;;;UAIK;EACf;EACA;EACA;EACA;EACA;;;;;;;;;;UAWe;EACf,KAAK,MAAM,WAAW;EACtB,OAAO,MAAM,WAAW;EACxB,QAAQ,kBAAkB,iBAAiB;;;;;;;;;;cCjBhC,gBAAgB,KAAK;;;;;;;;;;;;;iBCGlB,iBACd;EACE,YAAY;EACZ,UAAU;EACV,aAAa,wBAAwB;IAEtC;;;;;;;;;;;;iBCNa,oBAAoB;;;;;;;;;;;cCAvB,YAAY"}
package/dist/index.js ADDED
@@ -0,0 +1,155 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import { arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
3
+ import { t } from "@venn-lang/types";
4
+ //#region src/clients/fake-client.ts
5
+ /**
6
+ * The double: canned answers, and no channel.
7
+ *
8
+ * Anything not named falls back to an empty value (`{}` for a call, `[]` for a
9
+ * stream or a reflection), so a test only lists what it cares about.
10
+ *
11
+ * @param config.responses Unary results, keyed by `package.Service/Method`.
12
+ * @param config.streams Streamed results, keyed by the same.
13
+ * @param config.reflection Method metadata, keyed by service name.
14
+ */
15
+ function createFakeClient(config = {}) {
16
+ const { responses = {}, streams = {}, reflection = {} } = config;
17
+ return {
18
+ call: async (call) => responses[call.method] ?? {},
19
+ stream: async (call) => streams[call.method] ?? [],
20
+ reflect: async (service) => reflection[service] ?? []
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/clients/real-client.ts
25
+ /**
26
+ * The real gRPC client. Not implemented in this build: proto loading, channels
27
+ * and reflection are out of scope here.
28
+ *
29
+ * It exists so the port has its second implementation and the failure is a named
30
+ * Venn error rather than a missing method.
31
+ *
32
+ * @throws VN8090 from every method.
33
+ */
34
+ function createRealClient() {
35
+ return {
36
+ async call() {
37
+ return unimplemented();
38
+ },
39
+ async stream() {
40
+ return unimplemented();
41
+ },
42
+ async reflect() {
43
+ return unimplemented();
44
+ }
45
+ };
46
+ }
47
+ function unimplemented() {
48
+ throw new VennError({
49
+ code: "VN8090",
50
+ message: "gRPC real client not implemented in this build"
51
+ });
52
+ }
53
+ //#endregion
54
+ //#region src/port/grpc-client.port.ts
55
+ /**
56
+ * The port descriptor every `grpc` verb resolves through `ctx.port(...)`.
57
+ *
58
+ * Declares the `net` capability, so a host that cannot open a channel refuses
59
+ * the binding at load time with a readable diagnostic.
60
+ */
61
+ const GrpcClientPort = {
62
+ id: "venn.port.grpc-client",
63
+ version: 1,
64
+ requires: ["net"],
65
+ methods: [
66
+ "call",
67
+ "stream",
68
+ "reflect"
69
+ ]
70
+ };
71
+ //#endregion
72
+ //#region src/actions/grpc-action.ts
73
+ const requestParams = z.record(z.string(), z.unknown()).optional();
74
+ /**
75
+ * `grpc.call "pkg.Service/Method" { id: 42 }`: one unary call.
76
+ *
77
+ * The method is the only positional argument and the options map is the request
78
+ * message. The result is dynamic because its shape lives in a `.proto` this
79
+ * plugin never reads.
80
+ */
81
+ const callAction = defineAction({
82
+ name: "call",
83
+ doc: "Unary gRPC call.",
84
+ params: requestParams,
85
+ args: [arg("method", t.string, "The full method: `package.Service/Method`.")],
86
+ result: t.dynamic,
87
+ run: (ctx, input) => ctx.port(GrpcClientPort).call(buildCall(input))
88
+ });
89
+ /**
90
+ * `grpc.stream "pkg.Service/Method" { id: 42 }`: a server-streaming call.
91
+ *
92
+ * The messages come back as a list, collected once the stream ends. A flow reads
93
+ * it like any other list; there is nothing live to subscribe to.
94
+ */
95
+ const streamAction = defineAction({
96
+ name: "stream",
97
+ doc: "Server-streaming gRPC call.",
98
+ params: requestParams,
99
+ args: [arg("method", t.string, "The full method: `package.Service/Method`.")],
100
+ result: t.list(t.dynamic),
101
+ run: (ctx, input) => ctx.port(GrpcClientPort).stream(buildCall(input))
102
+ });
103
+ /**
104
+ * `grpc.reflect "pkg.Service"`: ask the server what methods it has.
105
+ *
106
+ * The one verb here with a typed result, because `grpc.MethodInfo` is described
107
+ * by the reflection protocol rather than by anyone's `.proto`.
108
+ */
109
+ const reflectAction = defineAction({
110
+ name: "reflect",
111
+ doc: "Server reflection: list a service's methods.",
112
+ args: [arg("service", t.string, "The service to ask about.")],
113
+ result: t.list(t.ref("grpc.MethodInfo")),
114
+ run: (ctx, input) => ctx.port(GrpcClientPort).reflect(String(input.args[0] ?? ""))
115
+ });
116
+ function buildCall(input) {
117
+ return {
118
+ method: String(input.args[0] ?? ""),
119
+ request: input.params ?? void 0
120
+ };
121
+ }
122
+ //#endregion
123
+ //#region src/plugin.ts
124
+ /**
125
+ * The `grpc` namespace: `call`, `stream`, `reflect` and the `grpc.MethodInfo`
126
+ * type.
127
+ *
128
+ * Requires the `net` capability, so a host without it refuses the plugin at load
129
+ * time rather than failing mid-flow. No matchers: there is no typed subject to
130
+ * write one against.
131
+ */
132
+ const grpcPlugin = definePlugin({
133
+ name: "venn/grpc",
134
+ version: "0.0.0",
135
+ namespace: "grpc",
136
+ requires: ["net"],
137
+ actions: [
138
+ callAction,
139
+ streamAction,
140
+ reflectAction
141
+ ],
142
+ typeDefs: {
143
+ /** One RPC method, as server reflection describes it. */
144
+ MethodInfo: t.record({
145
+ name: t.string,
146
+ requestType: t.string,
147
+ responseType: t.string,
148
+ clientStreaming: t.bool,
149
+ serverStreaming: t.bool
150
+ }) }
151
+ });
152
+ //#endregion
153
+ export { GrpcClientPort, createFakeClient, createRealClient, grpcPlugin as default, grpcPlugin };
154
+
155
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/port/grpc-client.port.ts","../src/actions/grpc-action.ts","../src/actions/index.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import type { GrpcClient, GrpcMethodInfo } from \"../port/index.js\";\n\n/**\n * The double: canned answers, and no channel.\n *\n * Anything not named falls back to an empty value (`{}` for a call, `[]` for a\n * stream or a reflection), so a test only lists what it cares about.\n *\n * @param config.responses Unary results, keyed by `package.Service/Method`.\n * @param config.streams Streamed results, keyed by the same.\n * @param config.reflection Method metadata, keyed by service name.\n */\nexport function createFakeClient(\n config: {\n responses?: Record<string, unknown>;\n streams?: Record<string, readonly unknown[]>;\n reflection?: Record<string, readonly GrpcMethodInfo[]>;\n } = {},\n): GrpcClient {\n const { responses = {}, streams = {}, reflection = {} } = config;\n return {\n call: async (call) => responses[call.method] ?? {},\n stream: async (call) => streams[call.method] ?? [],\n reflect: async (service) => reflection[service] ?? [],\n };\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { GrpcClient } from \"../port/index.js\";\n\n/**\n * The real gRPC client. Not implemented in this build: proto loading, channels\n * and reflection are out of scope here.\n *\n * It exists so the port has its second implementation and the failure is a named\n * Venn error rather than a missing method.\n *\n * @throws VN8090 from every method.\n */\nexport function createRealClient(): GrpcClient {\n return {\n async call() {\n return unimplemented();\n },\n async stream() {\n return unimplemented();\n },\n async reflect() {\n return unimplemented();\n },\n };\n}\n\nfunction unimplemented(): never {\n throw new VennError({\n code: \"VN8090\",\n message: \"gRPC real client not implemented in this build\",\n });\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { GrpcClient } from \"./grpc-client.types.js\";\n\n/**\n * The port descriptor every `grpc` verb resolves through `ctx.port(...)`.\n *\n * Declares the `net` capability, so a host that cannot open a channel refuses\n * the binding at load time with a readable diagnostic.\n */\nexport const GrpcClientPort: Port<GrpcClient> = {\n id: \"venn.port.grpc-client\",\n version: 1,\n requires: [\"net\"],\n methods: [\"call\", \"stream\", \"reflect\"],\n};\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { type GrpcCall, GrpcClientPort } from \"../port/index.js\";\n\nconst requestParams = z.record(z.string(), z.unknown()).optional();\n\n/**\n * `grpc.call \"pkg.Service/Method\" { id: 42 }`: one unary call.\n *\n * The method is the only positional argument and the options map is the request\n * message. The result is dynamic because its shape lives in a `.proto` this\n * plugin never reads.\n */\nexport const callAction: ActionDefinition = defineAction({\n name: \"call\",\n doc: \"Unary gRPC call.\",\n params: requestParams,\n args: [arg(\"method\", t.string, \"The full method: `package.Service/Method`.\")],\n result: t.dynamic,\n run: (ctx, input) => ctx.port(GrpcClientPort).call(buildCall(input)),\n});\n\n/**\n * `grpc.stream \"pkg.Service/Method\" { id: 42 }`: a server-streaming call.\n *\n * The messages come back as a list, collected once the stream ends. A flow reads\n * it like any other list; there is nothing live to subscribe to.\n */\nexport const streamAction: ActionDefinition = defineAction({\n name: \"stream\",\n doc: \"Server-streaming gRPC call.\",\n params: requestParams,\n args: [arg(\"method\", t.string, \"The full method: `package.Service/Method`.\")],\n result: t.list(t.dynamic),\n run: (ctx, input) => ctx.port(GrpcClientPort).stream(buildCall(input)),\n});\n\n/**\n * `grpc.reflect \"pkg.Service\"`: ask the server what methods it has.\n *\n * The one verb here with a typed result, because `grpc.MethodInfo` is described\n * by the reflection protocol rather than by anyone's `.proto`.\n */\nexport const reflectAction: ActionDefinition = defineAction({\n name: \"reflect\",\n doc: \"Server reflection: list a service's methods.\",\n args: [arg(\"service\", t.string, \"The service to ask about.\")],\n result: t.list(t.ref(\"grpc.MethodInfo\")),\n run: (ctx, input) => ctx.port(GrpcClientPort).reflect(String(input.args[0] ?? \"\")),\n});\n\nfunction buildCall(input: ActionInput<unknown>): GrpcCall {\n return {\n method: String(input.args[0] ?? \"\"),\n request: (input.params ?? undefined) as Record<string, unknown> | undefined,\n };\n}\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { callAction, reflectAction, streamAction } from \"./grpc-action.js\";\n\n/** The grpc namespace's verbs. */\nexport const grpcActions: ActionDefinition[] = [callAction, streamAction, reflectAction];\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types the plugin publishes to flows, as `grpc.MethodInfo`.\n *\n * Mirrors `GrpcMethodInfo` in `port/grpc-client.types.ts` by hand. Reflection is\n * the only thing this plugin can describe: a call's request and response come\n * from a `.proto` nobody here has read, so they stay dynamic.\n */\nexport const grpcTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /** One RPC method, as server reflection describes it. */\n MethodInfo: t.record({\n name: t.string,\n requestType: t.string,\n responseType: t.string,\n clientStreaming: t.bool,\n serverStreaming: t.bool,\n }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { grpcActions } from \"./actions/index.js\";\nimport { grpcTypeDefs } from \"./types.js\";\n\n/**\n * The `grpc` namespace: `call`, `stream`, `reflect` and the `grpc.MethodInfo`\n * type.\n *\n * Requires the `net` capability, so a host without it refuses the plugin at load\n * time rather than failing mid-flow. No matchers: there is no typed subject to\n * write one against.\n */\nexport const grpcPlugin: PluginDefinition = definePlugin({\n name: \"venn/grpc\",\n version: \"0.0.0\",\n namespace: \"grpc\",\n requires: [\"net\"],\n actions: grpcActions,\n typeDefs: grpcTypeDefs,\n});\n"],"mappings":";;;;;;;;;;;;;;AAYA,SAAgB,iBACd,SAII,CAAC,GACO;CACZ,MAAM,EAAE,YAAY,CAAC,GAAG,UAAU,CAAC,GAAG,aAAa,CAAC,MAAM;CAC1D,OAAO;EACL,MAAM,OAAO,SAAS,UAAU,KAAK,WAAW,CAAC;EACjD,QAAQ,OAAO,SAAS,QAAQ,KAAK,WAAW,CAAC;EACjD,SAAS,OAAO,YAAY,WAAW,YAAY,CAAC;CACtD;AACF;;;;;;;;;;;;ACbA,SAAgB,mBAA+B;CAC7C,OAAO;EACL,MAAM,OAAO;GACX,OAAO,cAAc;EACvB;EACA,MAAM,SAAS;GACb,OAAO,cAAc;EACvB;EACA,MAAM,UAAU;GACd,OAAO,cAAc;EACvB;CACF;AACF;AAEA,SAAS,gBAAuB;CAC9B,MAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;CACX,CAAC;AACH;;;;;;;;;ACtBA,MAAa,iBAAmC;CAC9C,IAAI;CACJ,SAAS;CACT,UAAU,CAAC,KAAK;CAChB,SAAS;EAAC;EAAQ;EAAU;CAAS;AACvC;;;ACVA,MAAM,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS;;;;;;;;AASjE,MAAa,aAA+B,aAAa;CACvD,MAAM;CACN,KAAK;CACL,QAAQ;CACR,MAAM,CAAC,IAAI,UAAU,EAAE,QAAQ,4CAA4C,CAAC;CAC5E,QAAQ,EAAE;CACV,MAAM,KAAK,UAAU,IAAI,KAAK,cAAc,CAAC,CAAC,KAAK,UAAU,KAAK,CAAC;AACrE,CAAC;;;;;;;AAQD,MAAa,eAAiC,aAAa;CACzD,MAAM;CACN,KAAK;CACL,QAAQ;CACR,MAAM,CAAC,IAAI,UAAU,EAAE,QAAQ,4CAA4C,CAAC;CAC5E,QAAQ,EAAE,KAAK,EAAE,OAAO;CACxB,MAAM,KAAK,UAAU,IAAI,KAAK,cAAc,CAAC,CAAC,OAAO,UAAU,KAAK,CAAC;AACvE,CAAC;;;;;;;AAQD,MAAa,gBAAkC,aAAa;CAC1D,MAAM;CACN,KAAK;CACL,MAAM,CAAC,IAAI,WAAW,EAAE,QAAQ,2BAA2B,CAAC;CAC5D,QAAQ,EAAE,KAAK,EAAE,IAAI,iBAAiB,CAAC;CACvC,MAAM,KAAK,UAAU,IAAI,KAAK,cAAc,CAAC,CAAC,QAAQ,OAAO,MAAM,KAAK,MAAM,EAAE,CAAC;AACnF,CAAC;AAED,SAAS,UAAU,OAAuC;CACxD,OAAO;EACL,QAAQ,OAAO,MAAM,KAAK,MAAM,EAAE;EAClC,SAAU,MAAM,UAAU,KAAA;CAC5B;AACF;;;;;;;;;;;AG5CA,MAAa,aAA+B,aAAa;CACvD,MAAM;CACN,SAAS;CACT,WAAW;CACX,UAAU,CAAC,KAAK;CAChB,SAAS;EFbqC;EAAY;EAAc;CEa/D;CACT,UAAU;;ADPV,YAAY,EAAE,OAAO;EACnB,MAAM,EAAE;EACR,aAAa,EAAE;EACf,cAAc,EAAE;EAChB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;CACrB,CAAC,ECCS;AACZ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/grpc",
3
+ "version": "0.1.0",
4
+ "description": "The grpc namespace: unary calls, server streams and server reflection as Venn verbs.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "grpc"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-grpc#readme",
12
+ "bugs": "https://github.com/venn-lang/venn/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/venn-lang/venn.git",
16
+ "directory": "packages/std-grpc"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Vinicius Borges",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "development": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "!src/**/*.suite.ts"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@venn-lang/contracts": "0.1.0",
41
+ "@venn-lang/sdk": "0.1.0",
42
+ "@venn-lang/types": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.22.14",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,57 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { type GrpcCall, GrpcClientPort } from "../port/index.js";
4
+
5
+ const requestParams = z.record(z.string(), z.unknown()).optional();
6
+
7
+ /**
8
+ * `grpc.call "pkg.Service/Method" { id: 42 }`: one unary call.
9
+ *
10
+ * The method is the only positional argument and the options map is the request
11
+ * message. The result is dynamic because its shape lives in a `.proto` this
12
+ * plugin never reads.
13
+ */
14
+ export const callAction: ActionDefinition = defineAction({
15
+ name: "call",
16
+ doc: "Unary gRPC call.",
17
+ params: requestParams,
18
+ args: [arg("method", t.string, "The full method: `package.Service/Method`.")],
19
+ result: t.dynamic,
20
+ run: (ctx, input) => ctx.port(GrpcClientPort).call(buildCall(input)),
21
+ });
22
+
23
+ /**
24
+ * `grpc.stream "pkg.Service/Method" { id: 42 }`: a server-streaming call.
25
+ *
26
+ * The messages come back as a list, collected once the stream ends. A flow reads
27
+ * it like any other list; there is nothing live to subscribe to.
28
+ */
29
+ export const streamAction: ActionDefinition = defineAction({
30
+ name: "stream",
31
+ doc: "Server-streaming gRPC call.",
32
+ params: requestParams,
33
+ args: [arg("method", t.string, "The full method: `package.Service/Method`.")],
34
+ result: t.list(t.dynamic),
35
+ run: (ctx, input) => ctx.port(GrpcClientPort).stream(buildCall(input)),
36
+ });
37
+
38
+ /**
39
+ * `grpc.reflect "pkg.Service"`: ask the server what methods it has.
40
+ *
41
+ * The one verb here with a typed result, because `grpc.MethodInfo` is described
42
+ * by the reflection protocol rather than by anyone's `.proto`.
43
+ */
44
+ export const reflectAction: ActionDefinition = defineAction({
45
+ name: "reflect",
46
+ doc: "Server reflection: list a service's methods.",
47
+ args: [arg("service", t.string, "The service to ask about.")],
48
+ result: t.list(t.ref("grpc.MethodInfo")),
49
+ run: (ctx, input) => ctx.port(GrpcClientPort).reflect(String(input.args[0] ?? "")),
50
+ });
51
+
52
+ function buildCall(input: ActionInput<unknown>): GrpcCall {
53
+ return {
54
+ method: String(input.args[0] ?? ""),
55
+ request: (input.params ?? undefined) as Record<string, unknown> | undefined,
56
+ };
57
+ }
@@ -0,0 +1,5 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { callAction, reflectAction, streamAction } from "./grpc-action.js";
3
+
4
+ /** The grpc namespace's verbs. */
5
+ export const grpcActions: ActionDefinition[] = [callAction, streamAction, reflectAction];
@@ -0,0 +1,26 @@
1
+ import type { GrpcClient, GrpcMethodInfo } from "../port/index.js";
2
+
3
+ /**
4
+ * The double: canned answers, and no channel.
5
+ *
6
+ * Anything not named falls back to an empty value (`{}` for a call, `[]` for a
7
+ * stream or a reflection), so a test only lists what it cares about.
8
+ *
9
+ * @param config.responses Unary results, keyed by `package.Service/Method`.
10
+ * @param config.streams Streamed results, keyed by the same.
11
+ * @param config.reflection Method metadata, keyed by service name.
12
+ */
13
+ export function createFakeClient(
14
+ config: {
15
+ responses?: Record<string, unknown>;
16
+ streams?: Record<string, readonly unknown[]>;
17
+ reflection?: Record<string, readonly GrpcMethodInfo[]>;
18
+ } = {},
19
+ ): GrpcClient {
20
+ const { responses = {}, streams = {}, reflection = {} } = config;
21
+ return {
22
+ call: async (call) => responses[call.method] ?? {},
23
+ stream: async (call) => streams[call.method] ?? [],
24
+ reflect: async (service) => reflection[service] ?? [],
25
+ };
26
+ }
@@ -0,0 +1,2 @@
1
+ export { createFakeClient } from "./fake-client.js";
2
+ export { createRealClient } from "./real-client.js";
@@ -0,0 +1,32 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import type { GrpcClient } from "../port/index.js";
3
+
4
+ /**
5
+ * The real gRPC client. Not implemented in this build: proto loading, channels
6
+ * and reflection are out of scope here.
7
+ *
8
+ * It exists so the port has its second implementation and the failure is a named
9
+ * Venn error rather than a missing method.
10
+ *
11
+ * @throws VN8090 from every method.
12
+ */
13
+ export function createRealClient(): GrpcClient {
14
+ return {
15
+ async call() {
16
+ return unimplemented();
17
+ },
18
+ async stream() {
19
+ return unimplemented();
20
+ },
21
+ async reflect() {
22
+ return unimplemented();
23
+ },
24
+ };
25
+ }
26
+
27
+ function unimplemented(): never {
28
+ throw new VennError({
29
+ code: "VN8090",
30
+ message: "gRPC real client not implemented in this build",
31
+ });
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The `grpc` plugin: `call`, `stream` and `reflect`.
3
+ *
4
+ * Requests and responses stay dynamic because their shape lives in a `.proto`
5
+ * this package never reads. Reflection is the one thing it can type.
6
+ */
7
+
8
+ export * from "./clients/index.js";
9
+ export { grpcPlugin, grpcPlugin as default } from "./plugin.js";
10
+ export * from "./port/index.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { grpcActions } from "./actions/index.js";
3
+ import { grpcTypeDefs } from "./types.js";
4
+
5
+ /**
6
+ * The `grpc` namespace: `call`, `stream`, `reflect` and the `grpc.MethodInfo`
7
+ * type.
8
+ *
9
+ * Requires the `net` capability, so a host without it refuses the plugin at load
10
+ * time rather than failing mid-flow. No matchers: there is no typed subject to
11
+ * write one against.
12
+ */
13
+ export const grpcPlugin: PluginDefinition = definePlugin({
14
+ name: "venn/grpc",
15
+ version: "0.0.0",
16
+ namespace: "grpc",
17
+ requires: ["net"],
18
+ actions: grpcActions,
19
+ typeDefs: grpcTypeDefs,
20
+ });
@@ -0,0 +1,15 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { GrpcClient } from "./grpc-client.types.js";
3
+
4
+ /**
5
+ * The port descriptor every `grpc` verb resolves through `ctx.port(...)`.
6
+ *
7
+ * Declares the `net` capability, so a host that cannot open a channel refuses
8
+ * the binding at load time with a readable diagnostic.
9
+ */
10
+ export const GrpcClientPort: Port<GrpcClient> = {
11
+ id: "venn.port.grpc-client",
12
+ version: 1,
13
+ requires: ["net"],
14
+ methods: ["call", "stream", "reflect"],
15
+ };
@@ -0,0 +1,28 @@
1
+ /** One invocation: the full `package.Service/Method`, plus the request message. */
2
+ export interface GrpcCall {
3
+ method: string;
4
+ request?: Record<string, unknown>;
5
+ }
6
+
7
+ /** Metadata for one RPC method, as returned by server reflection. */
8
+ export interface GrpcMethodInfo {
9
+ name: string;
10
+ requestType: string;
11
+ responseType: string;
12
+ clientStreaming: boolean;
13
+ serverStreaming: boolean;
14
+ }
15
+
16
+ /**
17
+ * Calling a gRPC service.
18
+ *
19
+ * `stream` hands back the whole sequence once it is complete rather than a live
20
+ * stream, so a flow reads it as an ordinary list.
21
+ *
22
+ * Two implementations: `createRealClient` and `createFakeClient`.
23
+ */
24
+ export interface GrpcClient {
25
+ call(call: GrpcCall): Promise<unknown>;
26
+ stream(call: GrpcCall): Promise<readonly unknown[]>;
27
+ reflect(service: string): Promise<readonly GrpcMethodInfo[]>;
28
+ }
@@ -0,0 +1,2 @@
1
+ export { GrpcClientPort } from "./grpc-client.port.js";
2
+ export type { GrpcCall, GrpcClient, GrpcMethodInfo } from "./grpc-client.types.js";
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types the plugin publishes to flows, as `grpc.MethodInfo`.
5
+ *
6
+ * Mirrors `GrpcMethodInfo` in `port/grpc-client.types.ts` by hand. Reflection is
7
+ * the only thing this plugin can describe: a call's request and response come
8
+ * from a `.proto` nobody here has read, so they stay dynamic.
9
+ */
10
+ export const grpcTypeDefs: Readonly<Record<string, TypeSpec>> = {
11
+ /** One RPC method, as server reflection describes it. */
12
+ MethodInfo: t.record({
13
+ name: t.string,
14
+ requestType: t.string,
15
+ responseType: t.string,
16
+ clientStreaming: t.bool,
17
+ serverStreaming: t.bool,
18
+ }),
19
+ };