@glubean/grpc 0.1.4 → 0.2.2

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/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # @glubean/grpc
2
2
 
3
- gRPC plugin for [Glubean](https://glubean.dev) workflow-level gRPC testing with built-in tracing.
3
+ gRPC for [Glubean](https://glubean.dev). This package owns two layers:
4
+
5
+ - **Contract** — author gRPC API intent as a single artifact (`contract.grpc.with(...)`). Executable spec, agent-readable, fits `contract.flow()` composition. **Recommended for new work.**
6
+ - **Transport plugin** — low-level gRPC client with auto-tracing, used via `configure({ plugins: { ... } })`. Still supported for test-after / exploratory work.
7
+
8
+ > **v0.2.0 single-package release note:** in earlier drafts, gRPC contract was planned as a separate `@glubean/contract-grpc` package. Decision 2026-04-20: one package per protocol. Importing `@glubean/grpc` now registers the contract adapter as a side effect. No new install, no second package.
4
9
 
5
10
  ## Install
6
11
 
@@ -8,9 +13,102 @@ gRPC plugin for [Glubean](https://glubean.dev) — workflow-level gRPC testing w
8
13
  npm install @glubean/grpc @grpc/grpc-js @grpc/proto-loader
9
14
  ```
10
15
 
11
- `@grpc/grpc-js` and `@grpc/proto-loader` are peer dependencies — you install them alongside the plugin.
16
+ `@grpc/grpc-js` and `@grpc/proto-loader` are peer dependencies.
17
+
18
+ ---
19
+
20
+ ## Quick Start — Contract
21
+
22
+ ```ts
23
+ import { contract, configure } from "@glubean/sdk";
24
+ import { grpc } from "@glubean/grpc";
25
+ // ^ importing @glubean/grpc registers contract.grpc side-effect
26
+
27
+ const { payment } = configure({
28
+ plugins: {
29
+ payment: grpc({
30
+ proto: "./protos/payment.proto",
31
+ address: "{{PAYMENT_SERVICE_ADDR}}",
32
+ package: "acme.payment.v1",
33
+ service: "PaymentService",
34
+ }),
35
+ },
36
+ });
37
+
38
+ const paymentContracts = contract.grpc.with("payment-api", {
39
+ client: payment,
40
+ });
41
+
42
+ export const completePayment = paymentContracts("complete-payment", {
43
+ target: "PaymentService/Complete",
44
+ description: "Complete a pending payment by order id + amount",
45
+ cases: {
46
+ happy: {
47
+ description: "order with valid payment method completes successfully",
48
+ request: { currency: "USD" },
49
+ expect: {
50
+ statusCode: 0, // OK
51
+ message: { status: "completed" },
52
+ },
53
+ },
54
+ notFound: {
55
+ description: "unknown order id returns NOT_FOUND",
56
+ request: { orderId: "does-not-exist" },
57
+ expect: {
58
+ statusCode: 5, // NOT_FOUND
59
+ },
60
+ },
61
+ },
62
+ });
63
+ ```
64
+
65
+ Run with `glubean run`. Each case becomes a first-class test; failure surfaces structured gRPC status on the trace.
66
+
67
+ ### Cases in a flow
68
+
69
+ Contract cases compose into `contract.flow()` steps — the same artifact serves both single-case and multi-step verification:
70
+
71
+ ```ts
72
+ import { contract } from "@glubean/sdk";
73
+ import { completePayment } from "./payment.contract.ts";
74
+ import { createOrder, getOrder } from "./orders.contract.ts"; // HTTP
75
+
76
+ export const checkoutFlow = contract
77
+ .flow("checkout")
78
+ .meta({
79
+ description: "Create order → complete payment → confirm",
80
+ tags: ["e2e"],
81
+ })
82
+ // Step 1: HTTP — create order
83
+ .step(createOrder.case("happy"), {
84
+ out: (_s, res: any) => ({ orderId: res.body.id, amount: res.body.total }),
85
+ })
86
+ // Step 2: gRPC — complete payment
87
+ .step(completePayment.case("happy"), {
88
+ in: (s: any) => ({ request: { orderId: s.orderId, amount: s.amount } }),
89
+ out: (s, res: any) => ({ ...s, paymentId: res.message.paymentId }),
90
+ })
91
+ // Step 3: HTTP — confirm
92
+ .step(getOrder.case("byId"), {
93
+ in: (s: any) => ({ params: { id: s.orderId } }),
94
+ });
95
+ ```
96
+
97
+ Flow state threads through via typed `in` / `out` lenses, **across protocols**.
98
+
99
+ ### What you get
100
+
101
+ - **Case-level lifecycle** — mark cases `deferred` (with reason) or `deprecated` (with replacement hint)
102
+ - **Structured failure classification** — gRPC status codes map to `transient` / `client` / `semantic` / `auth` / `server` kinds; transient codes (1 CANCELLED, 4 DEADLINE_EXCEEDED, 8 RESOURCE_EXHAUSTED, 14 UNAVAILABLE) marked `retryable`
103
+ - **Projection to Markdown** — case inventory with lifecycle markers, via `glubean contracts`
104
+ - **Flow composition** — mix with HTTP / GraphQL cases, same artifact
105
+ - **Scanner + MCP integration** — `glubean scan`, `glubean_extract_contracts` MCP tool, all work unchanged for `contract.grpc(...)`
12
106
 
13
- ## Quick Start
107
+ ---
108
+
109
+ ## Quick Start — Transport plugin (low-level)
110
+
111
+ For quick tests or exploratory work that doesn't need a declared contract:
14
112
 
15
113
  ```ts
16
114
  import { test, configure } from "@glubean/sdk";
@@ -35,7 +133,7 @@ export const getUser = test("get-user", async (ctx) => {
35
133
  });
36
134
  ```
37
135
 
38
- ## Standalone (without `configure()`)
136
+ ### Standalone (without `configure()`)
39
137
 
40
138
  ```ts
41
139
  import { createGrpcClient } from "@glubean/grpc";
@@ -59,9 +157,79 @@ console.log(res.duration); // ms
59
157
  client.close();
60
158
  ```
61
159
 
62
- ## API
160
+ ---
161
+
162
+ ## API Reference
163
+
164
+ ### Contract
165
+
166
+ #### `contract.grpc.with(instanceName, defaults?)`
167
+
168
+ Returns a scoped factory. Direct `contract.grpc("id", spec)` is not supported — use `.with(...)` first.
169
+
170
+ Instance defaults (`GrpcContractDefaults`):
171
+
172
+ | Option | Type | Description |
173
+ |--------|------|-------------|
174
+ | `client` | `GrpcClient` | Default client (from `configure({ plugins })`) |
175
+ | `tags` | `string[]` | Tags inherited by all contracts in this instance |
176
+ | `feature` | `string` | Grouping key for projection |
177
+ | `metadata` | `Record<string, string>` | Default metadata for all contracts |
178
+ | `deadlineMs` | `number` | Default deadline |
179
+ | `extensions` | `Extensions` | Projection-level extensions (x-* keys) |
180
+
181
+ #### `contract.grpc.with(...)("contractId", spec)`
182
+
183
+ Creates one contract. Spec shape (`GrpcContractSpec`):
184
+
185
+ | Field | Type | Description |
186
+ |-------|------|-------------|
187
+ | `target` | `string` | Wire target `"Service/Method"` — renders as `"Service.Method"` in UI |
188
+ | `description` | `string` | Contract-level description |
189
+ | `requestSchema` | `SchemaLike<Req>` | Contract-level request schema (for projection) |
190
+ | `defaultRequest` | `Partial<Req>` | Merged under each case's `request` |
191
+ | `defaultMetadata` | `Record<string, string>` | Merged under each case's `metadata` |
192
+ | `deadlineMs` | `number` | Contract-level deadline |
193
+ | `client` | `GrpcClient` | Override instance client |
194
+ | `cases` | `Record<string, GrpcContractCase>` | Named cases — required |
195
+
196
+ Case shape (`GrpcContractCase<Req, Res, S>`):
197
+
198
+ | Field | Type | Description |
199
+ |-------|------|-------------|
200
+ | `description` | `string` | Required — why this case exists |
201
+ | `request` | `Req \| (state) => Req` | Request message; deep-merged over contract defaults |
202
+ | `metadata` | `Record<string, string> \| fn` | Per-call metadata |
203
+ | `deadlineMs` | `number` | Per-call deadline override |
204
+ | `expect` | `GrpcContractExpect<Res>` | `statusCode` / `schema` / `message` / `metadata` / `metadataMatch` |
205
+ | `setup` / `teardown` | `(ctx, state?) => Promise<void>` | Lifecycle |
206
+ | `verify` | `(ctx, GrpcCaseResult) => Promise<void>` | Business-logic check after other assertions |
207
+ | `deferred` | `string` | Skip with reason |
208
+ | `deprecated` | `string` | Deprecate with reason |
209
+ | `tags` / `severity` / `requires` / `defaultRun` | — | Standard case metadata |
210
+
211
+ `expect` fields:
212
+
213
+ | Field | Type | Description |
214
+ |-------|------|-------------|
215
+ | `statusCode` | `number` | Expected gRPC status code (default: `0` / OK) |
216
+ | `schema` | `SchemaLike<Res>` | Response schema; validated via `ctx.validate` |
217
+ | `message` | `Partial<Res>` | Partial match on response message |
218
+ | `metadata` | `SchemaLike<Record<string, string>>` | Schema for trailing metadata |
219
+ | `metadataMatch` | `Record<string, string>` | Partial match on trailing metadata |
220
+
221
+ `GrpcCaseResult<Res>` — shape passed to `verify` and flow `out` lens:
63
222
 
64
- ### `grpc(options)` Plugin Factory
223
+ | Field | Type |
224
+ |-------|------|
225
+ | `message` | `Res` |
226
+ | `status.code` / `status.details` | `number` / `string` |
227
+ | `responseMetadata` | `Record<string, string>` |
228
+ | `duration` | `number` (ms) |
229
+
230
+ ### Transport
231
+
232
+ #### `grpc(options)` — Plugin Factory
65
233
 
66
234
  For use with `configure({ plugins })`. Supports `{{template}}` placeholders in `address` and `metadata` values, resolved from Glubean vars and secrets.
67
235
 
@@ -75,9 +243,9 @@ For use with `configure({ plugins })`. Supports `{{template}}` placeholders in `
75
243
  | `tls` | `boolean` | Use TLS (default: `false`) |
76
244
  | `deadlineMs` | `number` | Default deadline in ms (default: `30000`) |
77
245
 
78
- ### `createGrpcClient(options, hooks?)` — Standalone
246
+ #### `createGrpcClient(options, hooks?)` — Standalone
79
247
 
80
- Same options as above (without template support). Optional `hooks` parameter for instrumentation:
248
+ Same options as above (without template support). Optional `hooks` parameter:
81
249
 
82
250
  ```ts
83
251
  createGrpcClient(options, {
@@ -85,7 +253,7 @@ createGrpcClient(options, {
85
253
  });
86
254
  ```
87
255
 
88
- ### `client.call(method, request, options?)`
256
+ #### `client.call(method, request, options?)`
89
257
 
90
258
  Make a unary RPC call.
91
259
 
@@ -108,10 +276,36 @@ Returns `GrpcCallResult`:
108
276
 
109
277
  Errors don't throw — they return with a non-zero `status.code` for assertion-friendly testing.
110
278
 
111
- ### `client.close()`
279
+ #### `client.close()`
112
280
 
113
281
  Close the underlying gRPC channel.
114
282
 
283
+ ---
284
+
285
+ ## Custom matchers
286
+
287
+ `import "@glubean/grpc"` side-effect registers gRPC matchers onto the shared
288
+ `ctx.expect()` surface. No extra import or configure field needed.
289
+
290
+ ```ts
291
+ import "@glubean/grpc";
292
+
293
+ // Works on GrpcCallResult (transport) and GrpcCaseResult (contract verify / flow out lens)
294
+ ctx.expect(res).toHaveGrpcStatus(0); // exact code
295
+ ctx.expect(res).toHaveGrpcOk(); // convenience for code 0
296
+ ctx.expect(res).toHaveGrpcStatus(5, "user lookup"); // with context label
297
+ ctx.expect(res).toHaveGrpcMetadata("x-request-id"); // presence
298
+ ctx.expect(res).toHaveGrpcMetadata("x-tenant", "acme"); // value
299
+ ctx.expect(res).not.toHaveGrpcStatus(0); // negation
300
+ ```
301
+
302
+ All matchers inherit `.not` negation, `.orFail()` chaining, and soft-by-default
303
+ semantics from `@glubean/sdk`'s `Expectation`. Types come through
304
+ `CustomMatchers<T>` declaration merging automatically — no user-side
305
+ `declare module` required.
306
+
307
+ ---
308
+
115
309
  ## Tracing
116
310
 
117
311
  Every RPC call emits a single `trace` event with the full request/response cycle:
@@ -130,7 +324,7 @@ Every RPC call emits a single `trace` event with the full request/response cycle
130
324
  | `response` | `object` | Response payload (success only) |
131
325
  | `metadata` | `object` | Merged request metadata (static + per-call) |
132
326
 
133
- These traces share the same event channel as HTTP traces, enabling unified timeline rendering in the Glubean dashboard.
327
+ Traces share the same event channel as HTTP enables unified timeline rendering in the Glubean dashboard and cross-protocol flow inspection.
134
328
 
135
329
  ## Auth
136
330
 
@@ -151,15 +345,54 @@ await client.call("GetUser", { id: "u_123" }, {
151
345
  });
152
346
  ```
153
347
 
348
+ At the contract layer, metadata merges in this order (right wins):
349
+ instance `defaults.metadata` < contract `defaultMetadata` < case `metadata` < flow-step `in` lens `metadata`.
350
+
351
+ ---
352
+
353
+ ## Migration: 0.1.x → 0.2.0
354
+
355
+ **What's new:**
356
+ - Contract adapter shipped inside this package. `import "@glubean/grpc"` now also registers `contract.grpc.with(...)`.
357
+ - Single-package model: no separate `@glubean/contract-grpc` package.
358
+
359
+ **What's not broken:**
360
+ - Existing `configure({ plugins: { x: grpc({ ... }) } })` usage is unchanged.
361
+ - Existing `createGrpcClient(...)` usage is unchanged.
362
+ - All 0.1.x transport tests still pass without modification.
363
+
364
+ **Only additive API changes:**
365
+ - `contract.grpc.with(...)` now available from `@glubean/sdk` after importing this package.
366
+ - Export surface gained contract types (`GrpcContractSpec`, `GrpcContractCase`, etc.).
367
+
368
+ If you currently use `@glubean/grpc` only as a transport plugin and do not import `contract.grpc` anywhere, no migration is required.
369
+
154
370
  ## Scope
155
371
 
156
- This is a v1 focused on unary RPC. Not included:
372
+ ### Phase 1 (shipped)
373
+
374
+ - Unary RPC calls (contract + transport layers)
375
+ - Status code assertions + schema validation
376
+ - Response metadata match
377
+ - Deadline + metadata merge through contract → instance → case → flow-step
378
+ - gRPC status → FailureKind classification for repair loop
379
+ - Markdown projection (case list + lifecycle markers)
380
+ - Cross-protocol flow composition (HTTP + gRPC verified end-to-end)
381
+
382
+ ### Phase 2 (planned)
157
383
 
158
384
  - Server / client / bidirectional streaming
385
+ - GraphQL subscription sharing the same streaming case design
386
+ - See `internal/40-discovery/proposals/contract-async-protocol-plugins.md`
387
+
388
+ ### Out of scope (Phase 3+)
389
+
159
390
  - Reflection-based service discovery
160
- - Static codegen / type generation
161
- - grpc-web / Connect RPC
162
- - Retry policies
391
+ - grpc-gateway / HTTP transcoding projection
392
+ - Buf schema registry integration
393
+ - Generating `.proto` from contract (solvable via annotation passthrough, but deferred — see proposal §7b)
394
+
395
+ ---
163
396
 
164
397
  ## License
165
398
 
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Built-in gRPC contract adapter for @glubean/grpc 0.2.0.
3
+ *
4
+ * Shipped alongside the transport plugin in @glubean/grpc (single-package
5
+ * model — "contract is a first-class citizen"). Registered via
6
+ * `contract.register("grpc", grpcAdapter)` on import — see ./index.ts.
7
+ *
8
+ * Responsibilities (same interface as HTTP adapter):
9
+ * - execute: run a case's setup → request → expect → verify → teardown
10
+ * - executeCaseInFlow: deep-merge resolvedInputs, run case in flow mode
11
+ * - validateCaseForFlow: reject function-valued request/metadata fields
12
+ * - project: runtime ContractProjection<GrpcPayloadSchemas>
13
+ * - normalize: runtime → JSON-safe ExtractedContractProjection
14
+ * - classifyFailure: gRPC status 0-16 → FailureKind
15
+ * - renderTarget: "Service/Method" → "Service.Method" (display-only)
16
+ * - toMarkdown: case list
17
+ * - describePayload: high-level summary for index views
18
+ *
19
+ * Phase 1 scope: unary RPCs only. Streaming deferred to Phase 2.
20
+ */
21
+ import type { ContractProtocolAdapter } from "@glubean/sdk";
22
+ import type { GrpcContractMeta, GrpcContractSafeMeta, GrpcContractSpec, GrpcPayloadSchemas, GrpcSafeSchemas } from "./types.js";
23
+ /**
24
+ * Split "Service/Method" wire target into parsed parts.
25
+ * Returns `undefined` for malformed targets rather than throwing, so
26
+ * normalize + projection can tolerate hand-authored data in scanner output.
27
+ */
28
+ export declare function parseTarget(target: string): {
29
+ service: string;
30
+ method: string;
31
+ } | undefined;
32
+ /** Convert a SchemaLike to a JSON Schema fragment if possible (best-effort). */
33
+ export declare function schemaToJsonSchema(schema: unknown): Record<string, unknown> | null;
34
+ /**
35
+ * Reject cases that reference case-local setup state via function-valued
36
+ * `request` or `metadata` fields when used in a flow. Flow mode can't
37
+ * supply per-case setup state (state belongs to flow orchestration).
38
+ *
39
+ * Parallels HTTP's `validateHttpCaseForFlow` — same discipline as HTTP's
40
+ * function-valued body/params/query/headers rejection.
41
+ */
42
+ export declare function validateGrpcCaseForFlow(spec: GrpcContractSpec, caseKey: string, contractId: string): void;
43
+ export declare const grpcAdapter: ContractProtocolAdapter<GrpcContractSpec, GrpcPayloadSchemas, GrpcContractMeta, GrpcSafeSchemas, GrpcContractSafeMeta>;
44
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/contract/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAEV,uBAAuB,EAMxB,MAAM,cAAc,CAAC;AAOtB,OAAO,KAAK,EAEV,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EAEhB,MAAM,YAAY,CAAC;AAMpB;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,GACb;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAOjD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAalF;AAuZD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,IAAI,CAoBN;AAqGD,eAAO,MAAM,WAAW,EAAE,uBAAuB,CAC/C,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,CAuBrB,CAAC"}