@venn-lang/http 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,162 @@
1
+ # @venn-lang/http
2
+
3
+ > The `http` namespace: seven request verbs, a server, and the two ports they ride.
4
+
5
+ Venn's grammar knows no verbs. `@venn-lang/http` registers the `http` namespace with the runtime, so
6
+ `http.get` resolves to an action, `res` gets a known shape, and the editor can complete and document
7
+ both. The plugin itself never touches the network: requests go through the `HttpClient` port and
8
+ servers through the `HttpServer` port, so the CLI binds `fetch` and a real socket while a test binds
9
+ a fake and stays offline.
10
+
11
+ ## Install
12
+
13
+ Nothing to install yet. The package is unpublished (version `0.0.0`) and ships inside
14
+ `@venn-lang/stdlib`, which the `venn` CLI and the language server both load. A `.vn` file declares it:
15
+
16
+ ```ruby
17
+ use "venn/http"
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ruby
23
+ module demo.api
24
+
25
+ use "venn/http"
26
+ use "venn/assert"
27
+
28
+ config { baseUrl: "https://api.test" }
29
+
30
+ flow "Health" {
31
+ step "the service answers" {
32
+ const res = http.get "/health"
33
+ expect res.status == 200
34
+ expect res.ok
35
+ expect res header "content-type"
36
+ }
37
+ }
38
+ ```
39
+
40
+ A relative path is joined onto `config.baseUrl`; a URL with a scheme passes through untouched.
41
+
42
+ ## API
43
+
44
+ Everything below is exported from the package barrel.
45
+
46
+ | Export | What it is |
47
+ | --- | --- |
48
+ | `httpPlugin` (also the default export) | The `PluginDefinition`: namespace `http`, `requires: ["net"]`. |
49
+ | `HttpClientPort` | `Port<HttpClient>`, id `venn.port.http-client`, version 1, method `request`. |
50
+ | `createFetchClient()` | The real client, backed by the global `fetch`. |
51
+ | `createFakeClient({ responses })` | The double: canned responses keyed by URL, `okResponse()` for anything else. |
52
+ | `okResponse(overrides?)` | A `200` response with body `{"ok":true}`, for seeding the fake. |
53
+ | `HttpServerPort` | `Port<HttpServer>`, id `venn.port.http-server`, version 1, method `listen`. |
54
+ | `createMemoryServer()` | The double: no socket, and `deliver(request)` to knock on its door. |
55
+ | `serveAction()`, `onAction()` | The two `ActionDefinition`s behind `http.serve` and `http.on`. |
56
+ | `portInUse`, `listenFailed`, `asListenError` | `VennError` producers for `VN7020` and `VN7021`. |
57
+
58
+ Types: `HttpClient`, `HttpRequest`, `HttpResponse`, `HttpServer`, `RequestHandler`, `RunningServer`,
59
+ `ServerRequest`, `ServerReply`, `ServeHandle`, `MemoryServer`, `MemoryHttpServer`.
60
+
61
+ The subpath `@venn-lang/http/node` carries the one file that imports `node:*`:
62
+
63
+ ```ts
64
+ import { createNodeServer, type NodeHttpServer } from "@venn-lang/http/node";
65
+ ```
66
+
67
+ `createNodeServer()` binds a real socket and adds `closeAll()`, so whoever owns the process can give
68
+ its sockets back on the way out. Keeping it behind a subpath is what lets the main entry stay neutral
69
+ and run in the editor's worker.
70
+
71
+ ## Verbs
72
+
73
+ | Verb | Shape | Result |
74
+ | --- | --- | --- |
75
+ | `http.get` `http.post` `http.put` `http.patch` `http.delete` `http.head` `http.options` | `http.get url { … }` | `http.Response` |
76
+ | `http.serve` | `http.serve { port, host }` | `http.Server` |
77
+ | `http.on` | `http.on(server, handler)` | nothing |
78
+
79
+ Every request verb takes the URL as its one positional argument. The rest is the trailing options map:
80
+
81
+ | Option | Meaning |
82
+ | --- | --- |
83
+ | `headers` | Extra headers. Anything set here wins over what Venn would infer. |
84
+ | `query` | Appended to the URL as a query string, encoded for you. |
85
+ | `body` | What to send. A map becomes JSON; a string is sent as written. |
86
+ | `encode` | `json`, `form`, `multipart` or `raw`. Defaults to `json` for a map, `raw` for a string. |
87
+ | `bearer` | Shorthand for `Authorization: Bearer …`. |
88
+ | `basic` | `{ user, pass }`, as HTTP basic auth. |
89
+
90
+ `http.serve` takes `port` (`0` asks for any free one) and `host` (defaults to `127.0.0.1`).
91
+
92
+ ## Serving
93
+
94
+ A server is not a request-response verb: it stays, and the requests arrive afterwards. So `http.serve`
95
+ hands back a handle, and `http.on` says what to answer with.
96
+
97
+ ```ruby
98
+ use "venn/http"
99
+
100
+ const api = http.serve { port: 0 }
101
+ defer { api.close() }
102
+
103
+ http.on(api, route)
104
+
105
+ fn route(req) {
106
+ const path = req.url.before("?")
107
+ path == "/health" ? { ok: true, method: req.method } : { status: 404 }
108
+ }
109
+
110
+ print "listening on http://127.0.0.1:${api.port}"
111
+ ```
112
+
113
+ The handler is an ordinary `fn`, so everything the language does works inside it. A map carrying
114
+ `status`, `headers` or `body` is taken as a reply; anything else becomes the body of a `200`;
115
+ returning nothing sends `204`. Until `http.on` runs, the server answers `404`, so a request that
116
+ arrives early gets an answer instead of hanging.
117
+
118
+ ## Matchers and types
119
+
120
+ `header` is the one matcher: `expect res header "content-type"` passes when the response carries that
121
+ header. It declares an optional second argument for the expected value, but the check today is
122
+ presence only.
123
+
124
+ The plugin publishes four types to the checker: `http.Response` (`status`, `ok`, `headers`, `body`
125
+ as raw text, `json` as that text parsed, `time`), `http.Request`, `http.Reply` and `http.Server`.
126
+ `json` is the one field nothing can know the shape of, so give it one by naming it:
127
+ `const price: Price = res.json`.
128
+
129
+ ## Ports and conformance
130
+
131
+ Two ports, each with two implementations and a suite both must pass:
132
+
133
+ - `HttpClient`: `createFetchClient` and `createFakeClient`, checked by
134
+ `src/clients/http-client.suite.ts`. The test stubs the global `fetch` so the real client's mapping
135
+ runs the same suite offline.
136
+ - `HttpServer`: `createNodeServer` and `createMemoryServer`, checked by
137
+ `src/server/http-server.suite.ts`. The double keeps its own book of bound ports, so a flow that
138
+ binds the same port twice fails there exactly as it would against a real socket.
139
+
140
+ Binding one implementation looks like this:
141
+
142
+ ```ts
143
+ import { createFakeClient, HttpClientPort, okResponse } from "@venn-lang/http";
144
+
145
+ const ports = [
146
+ {
147
+ port: HttpClientPort,
148
+ impl: createFakeClient({
149
+ responses: { "https://api.test/health": okResponse({ status: 200 }) },
150
+ }),
151
+ },
152
+ ];
153
+ ```
154
+
155
+ A socket that refuses to bind is translated at the producer: `EADDRINUSE` becomes `VN7020`, anything
156
+ else `VN7021`. No caller ever reads a `node:net` errno.
157
+
158
+ ## See also
159
+
160
+ - [`@venn-lang/sdk`](../sdk) for `definePlugin`, `defineAction` and `defineMatcher`.
161
+ - [`@venn-lang/stdlib`](../stdlib) for the plugin list and the default port bindings.
162
+ - [`@venn-lang/ws`](../std-ws) and [`@venn-lang/mqtt`](../std-mqtt), the other two network plugins.
@@ -0,0 +1,213 @@
1
+ import { ActionDefinition, PluginDefinition } from "@venn-lang/sdk";
2
+ import { Port, VennError } from "@venn-lang/contracts";
3
+ //#region src/port/http-client.types.d.ts
4
+ /** One request, ready to send: the verb, the absolute URL and the payload. */
5
+ interface HttpRequest {
6
+ method: string;
7
+ url: string;
8
+ headers?: Record<string, string>;
9
+ body?: string;
10
+ /** Aborted when a `race` this request runs inside has already been won. */
11
+ signal?: AbortSignal;
12
+ }
13
+ /** What an http verb hands back, and what `res` holds in a flow. */
14
+ interface HttpResponse {
15
+ status: number;
16
+ ok: boolean;
17
+ headers: Record<string, string>;
18
+ body: string;
19
+ json: unknown;
20
+ time: number;
21
+ }
22
+ /**
23
+ * Sending one request and reading the reply.
24
+ *
25
+ * Two implementations: `createFetchClient` over a real socket and
26
+ * `createFakeClient` for tests. The conformance suite is what says they agree.
27
+ */
28
+ interface HttpClient {
29
+ request(req: HttpRequest): Promise<HttpResponse>;
30
+ }
31
+ //#endregion
32
+ //#region src/port/http-client.port.d.ts
33
+ /**
34
+ * The port descriptor an http verb resolves through `ctx.port(...)`.
35
+ *
36
+ * Declares the `net` capability, so a host that cannot open sockets refuses the
37
+ * binding at load time with a readable diagnostic.
38
+ */
39
+ declare const HttpClientPort: Port<HttpClient>;
40
+ //#endregion
41
+ //#region src/clients/fake-client.d.ts
42
+ /**
43
+ * A 200 response with a small JSON body, for tests that only care about a field
44
+ * or two.
45
+ *
46
+ * @param overrides Fields to replace on the default response.
47
+ * @returns A complete {@link HttpResponse}.
48
+ */
49
+ declare function okResponse(overrides?: Partial<HttpResponse>): HttpResponse;
50
+ /**
51
+ * The double: answers from a table keyed by the request's full URL.
52
+ *
53
+ * A URL with no entry gets {@link okResponse}, so a test only has to name the
54
+ * responses it cares about. Never touches the network.
55
+ *
56
+ * @param args.responses Canned responses, keyed by the URL the flow requests.
57
+ */
58
+ declare function createFakeClient(args?: {
59
+ responses?: Record<string, HttpResponse>;
60
+ }): HttpClient;
61
+ //#endregion
62
+ //#region src/clients/fetch-client.d.ts
63
+ /**
64
+ * The real client, over the global `fetch`. Requires the `net` capability.
65
+ *
66
+ * The body is always read as text and parsed afterwards, so a reply that claims
67
+ * JSON but is not still arrives whole in `body` instead of throwing.
68
+ */
69
+ declare function createFetchClient(): HttpClient;
70
+ //#endregion
71
+ //#region src/plugin.d.ts
72
+ /**
73
+ * The `http` namespace: the request verbs, `http.serve`/`http.on`, the `header`
74
+ * matcher and the types they trade in.
75
+ *
76
+ * Requires the `net` capability, so a host without it refuses the plugin at load
77
+ * time rather than failing mid-flow. The compiler treats it exactly as it treats
78
+ * a third-party plugin.
79
+ */
80
+ declare const httpPlugin: PluginDefinition;
81
+ //#endregion
82
+ //#region src/server/http-server.errors.d.ts
83
+ /** VN7020: the address the flow asked for is already taken by something else. */
84
+ declare function portInUse(args: {
85
+ port: number;
86
+ host: string;
87
+ }): VennError;
88
+ /** VN7021: the socket refused to bind for any other reason. */
89
+ declare function listenFailed(args: {
90
+ port: number;
91
+ host: string;
92
+ cause: string;
93
+ }): VennError;
94
+ /**
95
+ * Whatever the socket threw, as a Venn error: VN7020 for `EADDRINUSE`, VN7021
96
+ * for anything else.
97
+ *
98
+ * The translation lives at the producer so no caller has to read a `node:net`
99
+ * errno to know what went wrong.
100
+ */
101
+ declare function asListenError(args: {
102
+ port: number;
103
+ host: string;
104
+ error: unknown;
105
+ }): VennError;
106
+ //#endregion
107
+ //#region src/server/http-server.types.d.ts
108
+ /** One request a server received, as the language sees it. */
109
+ interface ServerRequest {
110
+ method: string;
111
+ /** The path with its query string, exactly as it arrived. */
112
+ url: string;
113
+ headers: Record<string, string>;
114
+ body: string;
115
+ }
116
+ /** What a handler answers with. Every field has a sensible default. */
117
+ interface ServerReply {
118
+ status?: number;
119
+ headers?: Record<string, string>;
120
+ /** A string is sent as-is; anything else is sent as JSON. */
121
+ body?: unknown;
122
+ }
123
+ /** Called once per request. Returning nothing sends `204 No Content`. */
124
+ type RequestHandler = (request: ServerRequest) => ServerReply | undefined | Promise<ServerReply | undefined>;
125
+ /** A server that is listening, and how to stop it. */
126
+ interface RunningServer {
127
+ /** The port it actually bound to. Asking for 0 gets one chosen for you. */
128
+ readonly port: number;
129
+ close(): Promise<void>;
130
+ }
131
+ /**
132
+ * Accepting requests over HTTP.
133
+ *
134
+ * Two implementations: `createNodeServer` binds a real socket, and
135
+ * `createMemoryServer` keeps the handler in memory so a test can deliver a
136
+ * request without a network. The conformance suite is what says they agree.
137
+ */
138
+ interface HttpServer {
139
+ listen(args: {
140
+ port: number;
141
+ host?: string;
142
+ handle: RequestHandler;
143
+ }): Promise<RunningServer>;
144
+ }
145
+ //#endregion
146
+ //#region src/server/http-server.port.d.ts
147
+ /**
148
+ * The port descriptor `http.serve` resolves through `ctx.port(...)`.
149
+ *
150
+ * Declares the `net` capability, so a host that cannot bind a socket refuses the
151
+ * binding at load time rather than mid-flow.
152
+ */
153
+ declare const HttpServerPort: Port<HttpServer>;
154
+ //#endregion
155
+ //#region src/server/memory-server.d.ts
156
+ /** A server that is listening in memory, plus the way to knock on its door. */
157
+ interface MemoryServer extends RunningServer {
158
+ /** Deliver a request as though it had arrived over the network. */
159
+ deliver(request: Partial<ServerRequest>): Promise<ServerReply>;
160
+ readonly closed: boolean;
161
+ }
162
+ /** The double factory, which keeps every server it started so a test can find it. */
163
+ interface MemoryHttpServer extends HttpServer {
164
+ /** Every server started through this, newest last. */
165
+ readonly started: readonly MemoryServer[];
166
+ }
167
+ /**
168
+ * The double: no socket, no network, no waiting.
169
+ *
170
+ * A test starts the flow that serves, hands it a request and reads the reply,
171
+ * running the same handler the real server would call. Ports are book-kept here
172
+ * rather than by the operating system, so tests beside each other never collide.
173
+ */
174
+ declare function createMemoryServer(): MemoryHttpServer;
175
+ //#endregion
176
+ //#region src/server/on-action.d.ts
177
+ /**
178
+ * `http.on server handler`: say what the server answers with.
179
+ *
180
+ * The handler is an ordinary `fn`, so everything the language already does
181
+ * applies inside it: it can call any verb, and it waits for what it reaches for
182
+ * without saying so. Whatever it returns becomes the reply. Calling `http.on`
183
+ * again replaces the handler.
184
+ */
185
+ declare function onAction(): ActionDefinition;
186
+ //#endregion
187
+ //#region src/server/serve-action.d.ts
188
+ /**
189
+ * The value `http.serve` hands back, seen by flows as `http.Server`.
190
+ *
191
+ * A server is not a request-response verb: it stays, and the requests arrive
192
+ * afterwards. So the verb returns something the program holds on to: the port it
193
+ * got, and what it may do with it.
194
+ */
195
+ interface ServeHandle {
196
+ kind: "http-server";
197
+ port: number;
198
+ /** Deliver one request to whatever `handle` was last given. Used by tests. */
199
+ deliver: (request: Partial<ServerRequest>) => Promise<ServerReply>;
200
+ close: () => Promise<void>;
201
+ /** Replace the handler. `http.on` is how a flow reaches this. */
202
+ onRequest: (handle: (request: ServerRequest) => unknown) => void;
203
+ }
204
+ /**
205
+ * `let api = http.serve { port: 8080 }`: start listening.
206
+ *
207
+ * The handler starts as a 404 and `http.on` replaces it, so a request arriving
208
+ * before the flow has said what to do with it gets an answer instead of hanging.
209
+ */
210
+ declare function serveAction(): ActionDefinition;
211
+ //#endregion
212
+ export { type HttpClient, HttpClientPort, type HttpRequest, type HttpResponse, type HttpServer, HttpServerPort, type MemoryHttpServer, type MemoryServer, type RequestHandler, type RunningServer, type ServeHandle, type ServerReply, type ServerRequest, asListenError, createFakeClient, createFetchClient, createMemoryServer, httpPlugin as default, httpPlugin, listenFailed, okResponse, onAction, portInUse, serveAction };
213
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/port/http-client.types.ts","../src/port/http-client.port.ts","../src/clients/fake-client.ts","../src/clients/fetch-client.ts","../src/plugin.ts","../src/server/http-server.errors.ts","../src/server/http-server.types.ts","../src/server/http-server.port.ts","../src/server/memory-server.ts","../src/server/on-action.ts","../src/server/serve-action.ts"],"mappings":";;;;UACiB;EACf;EACA;EACA,UAAU;EACV;;EAEA,SAAS;;;UAIM;EACf;EACA;EACA,SAAS;EACT;EACA;EACA;;;;;;;;UASe;EACf,QAAQ,KAAK,cAAc,QAAQ;;;;;;;;;;cClBxB,gBAAgB,KAAK;;;;;;;;;;iBCAlB,WAAW,YAAW,QAAQ,gBAAqB;;;;;;;;;iBAoBnD,iBACd;EAAQ,YAAY,eAAe;IAClC;;;;;;;;;iBCvBa,qBAAqB;;;;;;;;;;;cCKxB,YAAY;;;;iBCVT,UAAU;EAAQ;EAAc;IAAiB;;iBASjD,aAAa;EAAQ;EAAc;EAAc;IAAkB;;;;;;;;iBAenE,cAAc;EAAQ;EAAc;EAAc;IAAmB;;;;UC1BpE;EACf;;EAEA;EACA,SAAS;EACT;;;UAIe;EACf;EACA,UAAU;;EAEV;;;KAIU,kBACV,SAAS,kBACN,0BAA0B,QAAQ;;UAGtB;;WAEN;EACT,SAAS;;;;;;;;;UAUM;EACf,OAAO;IAAQ;IAAc;IAAe,QAAQ;MAAmB,QAAQ;;;;;;;;;;cC5BpE,gBAAgB,KAAK;;;;UCIjB,qBAAqB;;EAEpC,QAAQ,SAAS,QAAQ,iBAAiB,QAAQ;WACzC;;;UAIM,yBAAyB;;WAE/B,kBAAkB;;;;;;;;;iBAUb,sBAAsB;;;;;;;;;;;iBCpBtB,YAAY;;;;;;;;;;UCKX;EACf;EACA;;EAEA,UAAU,SAAS,QAAQ,mBAAmB,QAAQ;EACtD,aAAa;;EAEb,YAAY,SAAS,SAAS;;;;;;;;iBAShB,eAAe"}