@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 +21 -0
- package/README.md +120 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/actions/grpc-action.ts +57 -0
- package/src/actions/index.ts +5 -0
- package/src/clients/fake-client.ts +26 -0
- package/src/clients/index.ts +2 -0
- package/src/clients/real-client.ts +32 -0
- package/src/index.ts +10 -0
- package/src/plugin.ts +20 -0
- package/src/port/grpc-client.port.ts +15 -0
- package/src/port/grpc-client.types.ts +28 -0
- package/src/port/index.ts +2 -0
- package/src/types.ts +19 -0
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`.
|
package/dist/index.d.ts
ADDED
|
@@ -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,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,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
|
+
}
|
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
|
+
};
|