@venn-lang/types 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 +141 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +6 -0
- package/src/spec/build.ts +78 -0
- package/src/spec/index.ts +17 -0
- package/src/spec/show.ts +56 -0
- package/src/spec/type-spec.types.ts +130 -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,141 @@
|
|
|
1
|
+
# @venn-lang/types
|
|
2
|
+
|
|
3
|
+
> The language's type vocabulary as plain data: ten shapes, no dependencies, no compiler.
|
|
4
|
+
|
|
5
|
+
A `TypeSpec` is what a plugin publishes about itself and what the checker reads back. It is data
|
|
6
|
+
only, so it survives `JSON.stringify`: a signature written by hand today and one generated from a
|
|
7
|
+
`.d.ts` tomorrow are the same bytes. The compiler's own `Type`, with the inference variables that
|
|
8
|
+
unification writes into, is a separate and internal thing. Nothing mutable lands here.
|
|
9
|
+
|
|
10
|
+
This package is the shared vocabulary that lets the compiler, the plugin SDK and the `.d.ts` reader
|
|
11
|
+
talk about types without depending on one another. It has no dependencies at all, and its whole
|
|
12
|
+
runtime is one object literal (`t`) and one function (`showSpec`).
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Write the type of a verb, and the types it takes and gives back:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { t, type TypeSpec } from "@venn-lang/types";
|
|
20
|
+
|
|
21
|
+
// http.on server handler
|
|
22
|
+
const signature = t.fn(
|
|
23
|
+
[t.ref("http.Server"), t.callback([t.ref("http.Request")], t.dynamic, 1)],
|
|
24
|
+
t.void,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// http.Request, as the handler receives it
|
|
28
|
+
const request: TypeSpec = t.record({
|
|
29
|
+
method: t.string,
|
|
30
|
+
url: t.string,
|
|
31
|
+
headers: t.map(t.string),
|
|
32
|
+
body: t.string,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That is the whole mechanism behind an editor knowing what `req` is with nothing written down:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
use "venn/http"
|
|
40
|
+
|
|
41
|
+
const api = http.serve { port: 0 }
|
|
42
|
+
http.on(api, route)
|
|
43
|
+
|
|
44
|
+
# `http.on` says it hands its handler a request, so `req` is `http.Request`.
|
|
45
|
+
fn route(req) {
|
|
46
|
+
req.url.before("?")
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
Everything `src/index.ts` exports.
|
|
53
|
+
|
|
54
|
+
| Export | What it is |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `TypeSpec` | The union of the ten shapes. The wire format of a Venn type. |
|
|
57
|
+
| `PrimSpec`, `PrimName` | A scalar. `string`, `number`, `bool`, `null`, `void`, plus the units the language treats as first class: `duration`, `size`, `percent`, `instant`. |
|
|
58
|
+
| `LiteralSpec` | A single value (`"GET"`, `200`, `true`). What makes an enum an enum. |
|
|
59
|
+
| `ListSpec`, `MapSpec` | `list<T>`, and keys not known ahead of time with values all alike. |
|
|
60
|
+
| `RecordSpec` | Known fields, with `optional` naming those that may be absent and `open` saying whether extras are tolerated. |
|
|
61
|
+
| `FnSpec` | Params, result, and an optional `takes`: how many params the caller must actually accept. |
|
|
62
|
+
| `UnionSpec` | Members, any of which will do. |
|
|
63
|
+
| `OpaqueSpec` | A named handle with no visible inside. `members` is what it does publish. |
|
|
64
|
+
| `RefSpec` | A named type resolved through the catalog: `http.Request`. |
|
|
65
|
+
| `DynamicSpec` | Unknown, and deliberately so. Unifies with everything, never errors. |
|
|
66
|
+
| `TypeManifest` | What a plugin publishes as a whole: named `types`, and one `FnSpec` per action under `actions`. |
|
|
67
|
+
| `t` | The authoring surface. See below. |
|
|
68
|
+
| `TypeBuilder`, `RecordOptions` | The type of `t`, and the second argument of `t.record`. |
|
|
69
|
+
| `showSpec(spec)` | A spec as one line of text. |
|
|
70
|
+
|
|
71
|
+
### `t`
|
|
72
|
+
|
|
73
|
+
Constants: `t.string`, `t.number`, `t.bool`, `t.null`, `t.void`, `t.duration`, `t.size`,
|
|
74
|
+
`t.percent`, `t.instant`, `t.dynamic`.
|
|
75
|
+
|
|
76
|
+
| Call | Builds |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `t.literal(value)` | `"GET"`, `200`, `true` |
|
|
79
|
+
| `t.list(element)` | `list<element>` |
|
|
80
|
+
| `t.map(value)` | keys unknown, values all `value` |
|
|
81
|
+
| `t.record(fields, { optional, open })` | a shape with known fields |
|
|
82
|
+
| `t.fn(params, result)` | a function, arity as written |
|
|
83
|
+
| `t.callback(params, result, takes)` | the same, but usable by a caller that accepts only `takes` of them |
|
|
84
|
+
| `t.union(...members)` | any of the members |
|
|
85
|
+
| `t.opaque(name, members?)` | a handle, with an optional published surface |
|
|
86
|
+
| `t.ref(name)` | a named type, resolved later |
|
|
87
|
+
|
|
88
|
+
`t.callback` is what makes `req => ...` a valid handler for a verb that also offers the server: a
|
|
89
|
+
callback handed more than it needs is still a good callback. `t.fn` leaves `takes` off, so plain
|
|
90
|
+
arity means what it says.
|
|
91
|
+
|
|
92
|
+
### `showSpec`
|
|
93
|
+
|
|
94
|
+
Reads the wire format directly rather than going through the checker, so nothing has to be resolved
|
|
95
|
+
and a package with no compiler in it can still say what it takes. A `ref` or an `opaque` shows as
|
|
96
|
+
the name the plugin published, not the shape behind it.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
showSpec(t.fn([t.string], t.number)); // "fn(string) -> number"
|
|
100
|
+
showSpec(t.union(t.literal("GET"), t.literal("POST"))); // '"GET" | "POST"'
|
|
101
|
+
showSpec(t.list(t.number)); // "list<number>"
|
|
102
|
+
showSpec(t.opaque("http.Server")); // "http.Server"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Records stop after four fields and finish with `…N more`, because past a few fields a shape stops
|
|
106
|
+
informing and starts filling the line.
|
|
107
|
+
|
|
108
|
+
## The ten shapes, and why only ten
|
|
109
|
+
|
|
110
|
+
`prim`, `literal`, `list`, `map`, `record`, `fn`, `union`, `opaque`, `ref`, `dynamic`.
|
|
111
|
+
|
|
112
|
+
Everything TypeScript can say either projects onto one of these or degrades to `dynamic`. It never
|
|
113
|
+
degrades to a failure. Generics, conditional and mapped types are resolved by the TypeScript
|
|
114
|
+
compiler before anything reaches here, so what arrives is the answer rather than the machinery.
|
|
115
|
+
|
|
116
|
+
`opaque` is the border. Projecting a JS class as a record would drag its whole object graph, the
|
|
117
|
+
`EventEmitter`, the symbols, the hundred inherited members, into a language that means none of it.
|
|
118
|
+
An opaque type can be held, handed to the verbs of its own namespace, and asked for the members it
|
|
119
|
+
chose to publish. Nothing else.
|
|
120
|
+
|
|
121
|
+
`dynamic` is a first-class citizen, not a hole to be closed. A plugin that says nothing about its
|
|
122
|
+
types is still a plugin that works, and a Venn program with zero annotations still runs.
|
|
123
|
+
|
|
124
|
+
## Who reads this
|
|
125
|
+
|
|
126
|
+
| Package | What it does with a `TypeSpec` |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| [`@venn-lang/sdk`](../sdk) | `defineAction` derives an action's `FnSpec` from the `args` and `result` an author named; a plugin's named types go in `typeDefs`. |
|
|
129
|
+
| [`@venn-lang/core`](../core) | `specToType` reads a spec into the checker's own type, with `ref` resolved through a callback. The handle kinds in `kind-types.ts` are themselves written as specs. |
|
|
130
|
+
| [`@venn-lang/runtime`](../runtime) | `createTypeCatalog` qualifies what the loaded plugins publish (`Request` becomes `http.Request`) and answers the checker's questions. |
|
|
131
|
+
| [`@venn-lang/dts`](../dts) | Reads a package's TypeScript declarations through the compiler and emits specs. The CLI stores the result under `target/types/`. |
|
|
132
|
+
| [`@venn-lang/lsp`](../lsp) | `showSpec` for hover, completion detail and signature help. |
|
|
133
|
+
|
|
134
|
+
See [`docs/type-system.md`](../../docs/type-system.md) for how the pieces fit together, what runs
|
|
135
|
+
today and what does not.
|
|
136
|
+
|
|
137
|
+
## See also
|
|
138
|
+
|
|
139
|
+
- [`@venn-lang/sdk`](../sdk), where plugin authors reach for `t`
|
|
140
|
+
- [`@venn-lang/core`](../core), the checker that reads what was published
|
|
141
|
+
- [`@venn-lang/dts`](../dts), TypeScript declarations turned into specs
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//#region src/spec/type-spec.types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The wire format of a Venn type.
|
|
4
|
+
*
|
|
5
|
+
* Plain data: no functions, no `Map`s, no inference variables, so a `TypeSpec`
|
|
6
|
+
* survives `JSON.stringify`. That is what lets a plugin hand-write one and a
|
|
7
|
+
* generator emit the very same bytes from a `.d.ts`. The compiler's own `Type`,
|
|
8
|
+
* with its unification variables, is a separate internal thing; nothing mutable
|
|
9
|
+
* ever lands here.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately smaller than TypeScript's type system. Everything TS can say
|
|
12
|
+
* projects onto these ten shapes or degrades to {@link DynamicSpec}, never to a
|
|
13
|
+
* failure. Generics, conditional and mapped types are resolved by the TypeScript
|
|
14
|
+
* compiler at generation time, so what arrives here is the answer, not the
|
|
15
|
+
* machinery.
|
|
16
|
+
*/
|
|
17
|
+
type TypeSpec = PrimSpec | LiteralSpec | ListSpec | MapSpec | RecordSpec | FnSpec | UnionSpec | OpaqueSpec | RefSpec | DynamicSpec;
|
|
18
|
+
/** The scalars, including the units the language treats as first class. */
|
|
19
|
+
type PrimName = "string" | "number" | "bool" | "null" | "void" | "duration" | "size" | "percent" | "instant";
|
|
20
|
+
/** One of the scalars named by {@link PrimName}. */
|
|
21
|
+
interface PrimSpec {
|
|
22
|
+
readonly kind: "prim";
|
|
23
|
+
readonly name: PrimName;
|
|
24
|
+
}
|
|
25
|
+
/** A single value: `"GET"`, `200`, `true`. What makes an enum an enum. */
|
|
26
|
+
interface LiteralSpec {
|
|
27
|
+
readonly kind: "literal";
|
|
28
|
+
readonly value: string | number | boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Any number of values of one type. The language has no fixed-length list. */
|
|
31
|
+
interface ListSpec {
|
|
32
|
+
readonly kind: "list";
|
|
33
|
+
readonly element: TypeSpec;
|
|
34
|
+
}
|
|
35
|
+
/** Keys not known ahead of time, values all alike: TS's `Record<string, T>`. */
|
|
36
|
+
interface MapSpec {
|
|
37
|
+
readonly kind: "map";
|
|
38
|
+
readonly value: TypeSpec;
|
|
39
|
+
}
|
|
40
|
+
/** Known fields. `open` tolerates extra ones; `optional` lists what may be absent. */
|
|
41
|
+
interface RecordSpec {
|
|
42
|
+
readonly kind: "record";
|
|
43
|
+
readonly fields: Readonly<Record<string, TypeSpec>>;
|
|
44
|
+
readonly optional?: readonly string[];
|
|
45
|
+
readonly open?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** A callable: what it is given, in order, and what it gives back. */
|
|
48
|
+
interface FnSpec {
|
|
49
|
+
readonly kind: "fn";
|
|
50
|
+
readonly params: readonly TypeSpec[];
|
|
51
|
+
readonly result: TypeSpec;
|
|
52
|
+
/**
|
|
53
|
+
* How many parameters the caller must actually take. A callback handed more
|
|
54
|
+
* than it needs is still a good callback, as with `req => …` against a handler
|
|
55
|
+
* that is also offered the server.
|
|
56
|
+
*/
|
|
57
|
+
readonly takes?: number;
|
|
58
|
+
}
|
|
59
|
+
/** A value that is any one of the members. */
|
|
60
|
+
interface UnionSpec {
|
|
61
|
+
readonly kind: "union";
|
|
62
|
+
readonly members: readonly TypeSpec[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A handle with a name and no visible inside: a socket, a browser, an
|
|
66
|
+
* `express.Application`.
|
|
67
|
+
*
|
|
68
|
+
* This is the border. Projecting a JS class as a record would drag its whole
|
|
69
|
+
* object graph (`EventEmitter`, symbols, a hundred inherited members) into a
|
|
70
|
+
* language that means none of it. An opaque type can be held and handed to the
|
|
71
|
+
* verbs of its namespace, and nothing else.
|
|
72
|
+
*/
|
|
73
|
+
interface OpaqueSpec {
|
|
74
|
+
readonly kind: "opaque";
|
|
75
|
+
/**
|
|
76
|
+
* What a program may do with it, such as `{ port, close }` on a server.
|
|
77
|
+
*
|
|
78
|
+
* Opaque is about the *inside* being none of the reader's business, not about
|
|
79
|
+
* having nothing to offer. With none of these the handle is a name and no
|
|
80
|
+
* more, which is right for something only its own verbs ever touch.
|
|
81
|
+
*/
|
|
82
|
+
readonly members?: Readonly<Record<string, TypeSpec>>;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
}
|
|
85
|
+
/** A named type resolved through the catalog: `http.Request`, `User`. */
|
|
86
|
+
interface RefSpec {
|
|
87
|
+
readonly kind: "ref";
|
|
88
|
+
readonly name: string;
|
|
89
|
+
}
|
|
90
|
+
/** Unknown, and deliberately so. Unifies with everything, never errors. */
|
|
91
|
+
interface DynamicSpec {
|
|
92
|
+
readonly kind: "dynamic";
|
|
93
|
+
}
|
|
94
|
+
/** What a plugin publishes: its named types, and one signature per action. */
|
|
95
|
+
interface TypeManifest {
|
|
96
|
+
/** Fully qualified: `http.Request`. */
|
|
97
|
+
readonly types?: Readonly<Record<string, TypeSpec>>;
|
|
98
|
+
/** Keyed by action name within the namespace: `serve`, `on`. */
|
|
99
|
+
readonly actions?: Readonly<Record<string, FnSpec>>;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/spec/build.d.ts
|
|
103
|
+
/** What a record needs beyond its fields: which are absent, and whether extras pass. */
|
|
104
|
+
interface RecordOptions {
|
|
105
|
+
optional?: readonly string[];
|
|
106
|
+
open?: boolean;
|
|
107
|
+
}
|
|
108
|
+
/** The authoring surface for a {@link TypeSpec}. */
|
|
109
|
+
interface TypeBuilder {
|
|
110
|
+
readonly string: PrimSpec;
|
|
111
|
+
readonly number: PrimSpec;
|
|
112
|
+
readonly bool: PrimSpec;
|
|
113
|
+
readonly null: PrimSpec;
|
|
114
|
+
readonly void: PrimSpec;
|
|
115
|
+
readonly duration: PrimSpec;
|
|
116
|
+
readonly size: PrimSpec;
|
|
117
|
+
readonly percent: PrimSpec;
|
|
118
|
+
readonly instant: PrimSpec;
|
|
119
|
+
readonly dynamic: DynamicSpec;
|
|
120
|
+
literal(value: string | number | boolean): LiteralSpec;
|
|
121
|
+
list(element: TypeSpec): ListSpec;
|
|
122
|
+
map(value: TypeSpec): MapSpec;
|
|
123
|
+
record(fields: Readonly<Record<string, TypeSpec>>, options?: RecordOptions): RecordSpec;
|
|
124
|
+
fn(params: readonly TypeSpec[], result: TypeSpec): FnSpec;
|
|
125
|
+
/** A function passed as an argument, which may take fewer params than it is handed. */
|
|
126
|
+
callback(params: readonly TypeSpec[], result: TypeSpec, takes: number): FnSpec;
|
|
127
|
+
union(...members: readonly TypeSpec[]): UnionSpec;
|
|
128
|
+
/** A named handle. `members` is what it publishes; without them, a bare name. */
|
|
129
|
+
opaque(name: string, members?: Readonly<Record<string, TypeSpec>>): OpaqueSpec;
|
|
130
|
+
ref(name: string): RefSpec;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Write a type by hand, for what has no `.d.ts` worth reading.
|
|
134
|
+
*
|
|
135
|
+
* Everything here is data, the same data a generator emits, so an action typed
|
|
136
|
+
* by hand today and typed from its TypeScript tomorrow are the same to every
|
|
137
|
+
* reader downstream.
|
|
138
|
+
*/
|
|
139
|
+
declare const t: TypeBuilder;
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/spec/show.d.ts
|
|
142
|
+
/**
|
|
143
|
+
* A published type as one line of text.
|
|
144
|
+
*
|
|
145
|
+
* Reads the wire format directly rather than going through the checker. Nothing
|
|
146
|
+
* has to be resolved, so a `ref` shows as the name a plugin published
|
|
147
|
+
* (`http.Server`, not the record behind it), and a package with no compiler in
|
|
148
|
+
* it can still say what it takes.
|
|
149
|
+
*
|
|
150
|
+
* @param spec the type to render
|
|
151
|
+
* @returns one line; a record past four fields is elided rather than wrapped
|
|
152
|
+
*/
|
|
153
|
+
declare function showSpec(spec: TypeSpec): string;
|
|
154
|
+
//#endregion
|
|
155
|
+
export { type DynamicSpec, type FnSpec, type ListSpec, type LiteralSpec, type MapSpec, type OpaqueSpec, type PrimName, type PrimSpec, type RecordOptions, type RecordSpec, type RefSpec, type TypeBuilder, type TypeManifest, type TypeSpec, type UnionSpec, showSpec, t };
|
|
156
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/spec/type-spec.types.ts","../src/spec/build.ts","../src/spec/show.ts"],"mappings":";;;;;;;;;;;;;;;;KAeY,WACR,WACA,cACA,WACA,UACA,aACA,SACA,YACA,aACA,UACA;;KAGQ;;UAYK;WACN;WACA,MAAM;;;UAIA;WACN;WACA;;;UAIM;WACN;WACA,SAAS;;;UAIH;WACN;WACA,OAAO;;;UAID;WACN;WACA,QAAQ,SAAS,eAAe;WAChC;WACA;;;UAIM;WACN;WACA,iBAAiB;WACjB,QAAQ;;;;;;WAMR;;;UAIM;WACN;WACA,kBAAkB;;;;;;;;;;;UAYZ;WACN;;;;;;;;WAQA,UAAU,SAAS,eAAe;WAClC;;;UAIM;WACN;WACA;;;UAIM;WACN;;;UAIM;;WAEN,QAAQ,SAAS,eAAe;;WAEhC,UAAU,SAAS,eAAe;;;;;UCjH5B;EACf;EACA;;;UAIe;WACN,QAAQ;WACR,QAAQ;WACR,MAAM;WACN,MAAM;WACN,MAAM;WACN,UAAU;WACV,MAAM;WACN,SAAS;WACT,SAAS;WACT,SAAS;EAClB,QAAQ,mCAAmC;EAC3C,KAAK,SAAS,WAAW;EACzB,IAAI,OAAO,WAAW;EACtB,OAAO,QAAQ,SAAS,eAAe,YAAY,UAAU,gBAAgB;EAC7E,GAAG,iBAAiB,YAAY,QAAQ,WAAW;;EAEnD,SAAS,iBAAiB,YAAY,QAAQ,UAAU,gBAAgB;EACxE,SAAS,kBAAkB,aAAa;;EAExC,OAAO,cAAc,UAAU,SAAS,eAAe,aAAa;EACpE,IAAI,eAAe;;;;;;;;;cAcR,GAAG;;;;;;;;;;;;;;iBC3CA,SAAS,MAAM"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//#region src/spec/build.ts
|
|
2
|
+
function prim(name) {
|
|
3
|
+
return {
|
|
4
|
+
kind: "prim",
|
|
5
|
+
name
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Write a type by hand, for what has no `.d.ts` worth reading.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is data, the same data a generator emits, so an action typed
|
|
12
|
+
* by hand today and typed from its TypeScript tomorrow are the same to every
|
|
13
|
+
* reader downstream.
|
|
14
|
+
*/
|
|
15
|
+
const t = {
|
|
16
|
+
string: prim("string"),
|
|
17
|
+
number: prim("number"),
|
|
18
|
+
bool: prim("bool"),
|
|
19
|
+
null: prim("null"),
|
|
20
|
+
void: prim("void"),
|
|
21
|
+
duration: prim("duration"),
|
|
22
|
+
size: prim("size"),
|
|
23
|
+
percent: prim("percent"),
|
|
24
|
+
instant: prim("instant"),
|
|
25
|
+
dynamic: { kind: "dynamic" },
|
|
26
|
+
literal: (value) => ({
|
|
27
|
+
kind: "literal",
|
|
28
|
+
value
|
|
29
|
+
}),
|
|
30
|
+
list: (element) => ({
|
|
31
|
+
kind: "list",
|
|
32
|
+
element
|
|
33
|
+
}),
|
|
34
|
+
map: (value) => ({
|
|
35
|
+
kind: "map",
|
|
36
|
+
value
|
|
37
|
+
}),
|
|
38
|
+
record: (fields, options) => ({
|
|
39
|
+
kind: "record",
|
|
40
|
+
fields,
|
|
41
|
+
...options
|
|
42
|
+
}),
|
|
43
|
+
fn: (params, result) => ({
|
|
44
|
+
kind: "fn",
|
|
45
|
+
params,
|
|
46
|
+
result
|
|
47
|
+
}),
|
|
48
|
+
callback: (params, result, takes) => ({
|
|
49
|
+
kind: "fn",
|
|
50
|
+
params,
|
|
51
|
+
result,
|
|
52
|
+
takes
|
|
53
|
+
}),
|
|
54
|
+
union: (...members) => ({
|
|
55
|
+
kind: "union",
|
|
56
|
+
members
|
|
57
|
+
}),
|
|
58
|
+
opaque: (name, members) => members ? {
|
|
59
|
+
kind: "opaque",
|
|
60
|
+
name,
|
|
61
|
+
members
|
|
62
|
+
} : {
|
|
63
|
+
kind: "opaque",
|
|
64
|
+
name
|
|
65
|
+
},
|
|
66
|
+
ref: (name) => ({
|
|
67
|
+
kind: "ref",
|
|
68
|
+
name
|
|
69
|
+
})
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/spec/show.ts
|
|
73
|
+
/**
|
|
74
|
+
* A published type as one line of text.
|
|
75
|
+
*
|
|
76
|
+
* Reads the wire format directly rather than going through the checker. Nothing
|
|
77
|
+
* has to be resolved, so a `ref` shows as the name a plugin published
|
|
78
|
+
* (`http.Server`, not the record behind it), and a package with no compiler in
|
|
79
|
+
* it can still say what it takes.
|
|
80
|
+
*
|
|
81
|
+
* @param spec the type to render
|
|
82
|
+
* @returns one line; a record past four fields is elided rather than wrapped
|
|
83
|
+
*/
|
|
84
|
+
function showSpec(spec) {
|
|
85
|
+
switch (spec.kind) {
|
|
86
|
+
case "prim": return spec.name;
|
|
87
|
+
case "literal": return typeof spec.value === "string" ? `"${spec.value}"` : String(spec.value);
|
|
88
|
+
case "list": return `list<${showSpec(spec.element)}>`;
|
|
89
|
+
case "map": return `map<${showSpec(spec.value)}>`;
|
|
90
|
+
case "record": return showRecord(spec);
|
|
91
|
+
case "fn": return showFn(spec);
|
|
92
|
+
case "union": return spec.members.map(showSpec).join(" | ");
|
|
93
|
+
case "opaque":
|
|
94
|
+
case "ref": return spec.name;
|
|
95
|
+
default: return "dynamic";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function showFn(spec) {
|
|
99
|
+
return `fn(${spec.params.map(showSpec).join(", ")}) -> ${showSpec(spec.result)}`;
|
|
100
|
+
}
|
|
101
|
+
/** Past a few fields a shape stops informing and starts filling the line. */
|
|
102
|
+
const SHOWN_FIELDS = 4;
|
|
103
|
+
function showRecord(spec) {
|
|
104
|
+
const names = Object.keys(spec.fields);
|
|
105
|
+
if (names.length === 0) return "{}";
|
|
106
|
+
const shown = names.slice(0, SHOWN_FIELDS).map((name) => field(spec, name));
|
|
107
|
+
const rest = names.length > shown.length ? `, …${names.length - shown.length} more` : "";
|
|
108
|
+
return `{ ${shown.join(", ")}${rest} }`;
|
|
109
|
+
}
|
|
110
|
+
function field(spec, name) {
|
|
111
|
+
const type = spec.fields[name];
|
|
112
|
+
return `${name}: ${type ? showSpec(type) : "dynamic"}`;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { showSpec, t };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/spec/build.ts","../src/spec/show.ts"],"sourcesContent":["import type {\n DynamicSpec,\n FnSpec,\n ListSpec,\n LiteralSpec,\n MapSpec,\n OpaqueSpec,\n PrimSpec,\n RecordSpec,\n RefSpec,\n TypeSpec,\n UnionSpec,\n} from \"./type-spec.types.js\";\n\n/** What a record needs beyond its fields: which are absent, and whether extras pass. */\nexport interface RecordOptions {\n optional?: readonly string[];\n open?: boolean;\n}\n\n/** The authoring surface for a {@link TypeSpec}. */\nexport interface TypeBuilder {\n readonly string: PrimSpec;\n readonly number: PrimSpec;\n readonly bool: PrimSpec;\n readonly null: PrimSpec;\n readonly void: PrimSpec;\n readonly duration: PrimSpec;\n readonly size: PrimSpec;\n readonly percent: PrimSpec;\n readonly instant: PrimSpec;\n readonly dynamic: DynamicSpec;\n literal(value: string | number | boolean): LiteralSpec;\n list(element: TypeSpec): ListSpec;\n map(value: TypeSpec): MapSpec;\n record(fields: Readonly<Record<string, TypeSpec>>, options?: RecordOptions): RecordSpec;\n fn(params: readonly TypeSpec[], result: TypeSpec): FnSpec;\n /** A function passed as an argument, which may take fewer params than it is handed. */\n callback(params: readonly TypeSpec[], result: TypeSpec, takes: number): FnSpec;\n union(...members: readonly TypeSpec[]): UnionSpec;\n /** A named handle. `members` is what it publishes; without them, a bare name. */\n opaque(name: string, members?: Readonly<Record<string, TypeSpec>>): OpaqueSpec;\n ref(name: string): RefSpec;\n}\n\nfunction prim(name: PrimSpec[\"name\"]): PrimSpec {\n return { kind: \"prim\", name };\n}\n\n/**\n * Write a type by hand, for what has no `.d.ts` worth reading.\n *\n * Everything here is data, the same data a generator emits, so an action typed\n * by hand today and typed from its TypeScript tomorrow are the same to every\n * reader downstream.\n */\nexport const t: TypeBuilder = {\n string: prim(\"string\"),\n number: prim(\"number\"),\n bool: prim(\"bool\"),\n null: prim(\"null\"),\n void: prim(\"void\"),\n duration: prim(\"duration\"),\n size: prim(\"size\"),\n percent: prim(\"percent\"),\n instant: prim(\"instant\"),\n dynamic: { kind: \"dynamic\" },\n literal: (value) => ({ kind: \"literal\", value }),\n list: (element) => ({ kind: \"list\", element }),\n map: (value) => ({ kind: \"map\", value }),\n record: (fields, options) => ({ kind: \"record\", fields, ...options }),\n fn: (params, result) => ({ kind: \"fn\", params, result }),\n callback: (params, result, takes) => ({ kind: \"fn\", params, result, takes }),\n union: (...members) => ({ kind: \"union\", members }),\n opaque: (name, members) =>\n members ? { kind: \"opaque\", name, members } : { kind: \"opaque\", name },\n ref: (name) => ({ kind: \"ref\", name }),\n};\n","import type { FnSpec, RecordSpec, TypeSpec } from \"./type-spec.types.js\";\n\n/**\n * A published type as one line of text.\n *\n * Reads the wire format directly rather than going through the checker. Nothing\n * has to be resolved, so a `ref` shows as the name a plugin published\n * (`http.Server`, not the record behind it), and a package with no compiler in\n * it can still say what it takes.\n *\n * @param spec the type to render\n * @returns one line; a record past four fields is elided rather than wrapped\n */\nexport function showSpec(spec: TypeSpec): string {\n switch (spec.kind) {\n case \"prim\":\n return spec.name;\n case \"literal\":\n return typeof spec.value === \"string\" ? `\"${spec.value}\"` : String(spec.value);\n case \"list\":\n return `list<${showSpec(spec.element)}>`;\n case \"map\":\n return `map<${showSpec(spec.value)}>`;\n case \"record\":\n return showRecord(spec);\n case \"fn\":\n return showFn(spec);\n case \"union\":\n return spec.members.map(showSpec).join(\" | \");\n case \"opaque\":\n case \"ref\":\n return spec.name;\n default:\n return \"dynamic\";\n }\n}\n\nfunction showFn(spec: FnSpec): string {\n return `fn(${spec.params.map(showSpec).join(\", \")}) -> ${showSpec(spec.result)}`;\n}\n\n/** Past a few fields a shape stops informing and starts filling the line. */\nconst SHOWN_FIELDS = 4;\n\nfunction showRecord(spec: RecordSpec): string {\n const names = Object.keys(spec.fields);\n if (names.length === 0) return \"{}\";\n const shown = names.slice(0, SHOWN_FIELDS).map((name) => field(spec, name));\n const rest = names.length > shown.length ? `, …${names.length - shown.length} more` : \"\";\n return `{ ${shown.join(\", \")}${rest} }`;\n}\n\nfunction field(spec: RecordSpec, name: string): string {\n const type = spec.fields[name];\n return `${name}: ${type ? showSpec(type) : \"dynamic\"}`;\n}\n"],"mappings":";AA6CA,SAAS,KAAK,MAAkC;CAC9C,OAAO;EAAE,MAAM;EAAQ;CAAK;AAC9B;;;;;;;;AASA,MAAa,IAAiB;CAC5B,QAAQ,KAAK,QAAQ;CACrB,QAAQ,KAAK,QAAQ;CACrB,MAAM,KAAK,MAAM;CACjB,MAAM,KAAK,MAAM;CACjB,MAAM,KAAK,MAAM;CACjB,UAAU,KAAK,UAAU;CACzB,MAAM,KAAK,MAAM;CACjB,SAAS,KAAK,SAAS;CACvB,SAAS,KAAK,SAAS;CACvB,SAAS,EAAE,MAAM,UAAU;CAC3B,UAAU,WAAW;EAAE,MAAM;EAAW;CAAM;CAC9C,OAAO,aAAa;EAAE,MAAM;EAAQ;CAAQ;CAC5C,MAAM,WAAW;EAAE,MAAM;EAAO;CAAM;CACtC,SAAS,QAAQ,aAAa;EAAE,MAAM;EAAU;EAAQ,GAAG;CAAQ;CACnE,KAAK,QAAQ,YAAY;EAAE,MAAM;EAAM;EAAQ;CAAO;CACtD,WAAW,QAAQ,QAAQ,WAAW;EAAE,MAAM;EAAM;EAAQ;EAAQ;CAAM;CAC1E,QAAQ,GAAG,aAAa;EAAE,MAAM;EAAS;CAAQ;CACjD,SAAS,MAAM,YACb,UAAU;EAAE,MAAM;EAAU;EAAM;CAAQ,IAAI;EAAE,MAAM;EAAU;CAAK;CACvE,MAAM,UAAU;EAAE,MAAM;EAAO;CAAK;AACtC;;;;;;;;;;;;;;AChEA,SAAgB,SAAS,MAAwB;CAC/C,QAAQ,KAAK,MAAb;EACE,KAAK,QACH,OAAO,KAAK;EACd,KAAK,WACH,OAAO,OAAO,KAAK,UAAU,WAAW,IAAI,KAAK,MAAM,KAAK,OAAO,KAAK,KAAK;EAC/E,KAAK,QACH,OAAO,QAAQ,SAAS,KAAK,OAAO,EAAE;EACxC,KAAK,OACH,OAAO,OAAO,SAAS,KAAK,KAAK,EAAE;EACrC,KAAK,UACH,OAAO,WAAW,IAAI;EACxB,KAAK,MACH,OAAO,OAAO,IAAI;EACpB,KAAK,SACH,OAAO,KAAK,QAAQ,IAAI,QAAQ,CAAC,CAAC,KAAK,KAAK;EAC9C,KAAK;EACL,KAAK,OACH,OAAO,KAAK;EACd,SACE,OAAO;CACX;AACF;AAEA,SAAS,OAAO,MAAsB;CACpC,OAAO,MAAM,KAAK,OAAO,IAAI,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,OAAO,SAAS,KAAK,MAAM;AAC/E;;AAGA,MAAM,eAAe;AAErB,SAAS,WAAW,MAA0B;CAC5C,MAAM,QAAQ,OAAO,KAAK,KAAK,MAAM;CACrC,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,QAAQ,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,KAAK,SAAS,MAAM,MAAM,IAAI,CAAC;CAC1E,MAAM,OAAO,MAAM,SAAS,MAAM,SAAS,MAAM,MAAM,SAAS,MAAM,OAAO,SAAS;CACtF,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,KAAK;AACtC;AAEA,SAAS,MAAM,MAAkB,MAAsB;CACrD,MAAM,OAAO,KAAK,OAAO;CACzB,OAAO,GAAG,KAAK,IAAI,OAAO,SAAS,IAAI,IAAI;AAC7C"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@venn-lang/types",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The language's type vocabulary as plain data: ten shapes, no dependencies, no compiler.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"venn",
|
|
7
|
+
"testing",
|
|
8
|
+
"e2e",
|
|
9
|
+
"types",
|
|
10
|
+
"type-system"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/venn-lang/venn/tree/main/packages/types#readme",
|
|
13
|
+
"bugs": "https://github.com/venn-lang/venn/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/venn-lang/venn.git",
|
|
17
|
+
"directory": "packages/types"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Vinicius Borges",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"development": "./src/index.ts",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"!src/**/*.test.ts",
|
|
35
|
+
"!src/**/*.suite.ts"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"tsdown": "^0.22.14",
|
|
42
|
+
"typescript": "^7.0.2",
|
|
43
|
+
"vitest": "^4.1.10"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @venn-lang/types: the language's type vocabulary as plain data.
|
|
2
|
+
//
|
|
3
|
+
// Zero dependencies on purpose. The compiler, the plugin SDK and the generator
|
|
4
|
+
// that reads TypeScript declarations all speak this, and none of them should
|
|
5
|
+
// have to depend on the others to do it.
|
|
6
|
+
export * from "./spec/index.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DynamicSpec,
|
|
3
|
+
FnSpec,
|
|
4
|
+
ListSpec,
|
|
5
|
+
LiteralSpec,
|
|
6
|
+
MapSpec,
|
|
7
|
+
OpaqueSpec,
|
|
8
|
+
PrimSpec,
|
|
9
|
+
RecordSpec,
|
|
10
|
+
RefSpec,
|
|
11
|
+
TypeSpec,
|
|
12
|
+
UnionSpec,
|
|
13
|
+
} from "./type-spec.types.js";
|
|
14
|
+
|
|
15
|
+
/** What a record needs beyond its fields: which are absent, and whether extras pass. */
|
|
16
|
+
export interface RecordOptions {
|
|
17
|
+
optional?: readonly string[];
|
|
18
|
+
open?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The authoring surface for a {@link TypeSpec}. */
|
|
22
|
+
export interface TypeBuilder {
|
|
23
|
+
readonly string: PrimSpec;
|
|
24
|
+
readonly number: PrimSpec;
|
|
25
|
+
readonly bool: PrimSpec;
|
|
26
|
+
readonly null: PrimSpec;
|
|
27
|
+
readonly void: PrimSpec;
|
|
28
|
+
readonly duration: PrimSpec;
|
|
29
|
+
readonly size: PrimSpec;
|
|
30
|
+
readonly percent: PrimSpec;
|
|
31
|
+
readonly instant: PrimSpec;
|
|
32
|
+
readonly dynamic: DynamicSpec;
|
|
33
|
+
literal(value: string | number | boolean): LiteralSpec;
|
|
34
|
+
list(element: TypeSpec): ListSpec;
|
|
35
|
+
map(value: TypeSpec): MapSpec;
|
|
36
|
+
record(fields: Readonly<Record<string, TypeSpec>>, options?: RecordOptions): RecordSpec;
|
|
37
|
+
fn(params: readonly TypeSpec[], result: TypeSpec): FnSpec;
|
|
38
|
+
/** A function passed as an argument, which may take fewer params than it is handed. */
|
|
39
|
+
callback(params: readonly TypeSpec[], result: TypeSpec, takes: number): FnSpec;
|
|
40
|
+
union(...members: readonly TypeSpec[]): UnionSpec;
|
|
41
|
+
/** A named handle. `members` is what it publishes; without them, a bare name. */
|
|
42
|
+
opaque(name: string, members?: Readonly<Record<string, TypeSpec>>): OpaqueSpec;
|
|
43
|
+
ref(name: string): RefSpec;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function prim(name: PrimSpec["name"]): PrimSpec {
|
|
47
|
+
return { kind: "prim", name };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write a type by hand, for what has no `.d.ts` worth reading.
|
|
52
|
+
*
|
|
53
|
+
* Everything here is data, the same data a generator emits, so an action typed
|
|
54
|
+
* by hand today and typed from its TypeScript tomorrow are the same to every
|
|
55
|
+
* reader downstream.
|
|
56
|
+
*/
|
|
57
|
+
export const t: TypeBuilder = {
|
|
58
|
+
string: prim("string"),
|
|
59
|
+
number: prim("number"),
|
|
60
|
+
bool: prim("bool"),
|
|
61
|
+
null: prim("null"),
|
|
62
|
+
void: prim("void"),
|
|
63
|
+
duration: prim("duration"),
|
|
64
|
+
size: prim("size"),
|
|
65
|
+
percent: prim("percent"),
|
|
66
|
+
instant: prim("instant"),
|
|
67
|
+
dynamic: { kind: "dynamic" },
|
|
68
|
+
literal: (value) => ({ kind: "literal", value }),
|
|
69
|
+
list: (element) => ({ kind: "list", element }),
|
|
70
|
+
map: (value) => ({ kind: "map", value }),
|
|
71
|
+
record: (fields, options) => ({ kind: "record", fields, ...options }),
|
|
72
|
+
fn: (params, result) => ({ kind: "fn", params, result }),
|
|
73
|
+
callback: (params, result, takes) => ({ kind: "fn", params, result, takes }),
|
|
74
|
+
union: (...members) => ({ kind: "union", members }),
|
|
75
|
+
opaque: (name, members) =>
|
|
76
|
+
members ? { kind: "opaque", name, members } : { kind: "opaque", name },
|
|
77
|
+
ref: (name) => ({ kind: "ref", name }),
|
|
78
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { type RecordOptions, type TypeBuilder, t } from "./build.js";
|
|
2
|
+
export { showSpec } from "./show.js";
|
|
3
|
+
export type {
|
|
4
|
+
DynamicSpec,
|
|
5
|
+
FnSpec,
|
|
6
|
+
ListSpec,
|
|
7
|
+
LiteralSpec,
|
|
8
|
+
MapSpec,
|
|
9
|
+
OpaqueSpec,
|
|
10
|
+
PrimName,
|
|
11
|
+
PrimSpec,
|
|
12
|
+
RecordSpec,
|
|
13
|
+
RefSpec,
|
|
14
|
+
TypeManifest,
|
|
15
|
+
TypeSpec,
|
|
16
|
+
UnionSpec,
|
|
17
|
+
} from "./type-spec.types.js";
|
package/src/spec/show.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { FnSpec, RecordSpec, TypeSpec } from "./type-spec.types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A published type as one line of text.
|
|
5
|
+
*
|
|
6
|
+
* Reads the wire format directly rather than going through the checker. Nothing
|
|
7
|
+
* has to be resolved, so a `ref` shows as the name a plugin published
|
|
8
|
+
* (`http.Server`, not the record behind it), and a package with no compiler in
|
|
9
|
+
* it can still say what it takes.
|
|
10
|
+
*
|
|
11
|
+
* @param spec the type to render
|
|
12
|
+
* @returns one line; a record past four fields is elided rather than wrapped
|
|
13
|
+
*/
|
|
14
|
+
export function showSpec(spec: TypeSpec): string {
|
|
15
|
+
switch (spec.kind) {
|
|
16
|
+
case "prim":
|
|
17
|
+
return spec.name;
|
|
18
|
+
case "literal":
|
|
19
|
+
return typeof spec.value === "string" ? `"${spec.value}"` : String(spec.value);
|
|
20
|
+
case "list":
|
|
21
|
+
return `list<${showSpec(spec.element)}>`;
|
|
22
|
+
case "map":
|
|
23
|
+
return `map<${showSpec(spec.value)}>`;
|
|
24
|
+
case "record":
|
|
25
|
+
return showRecord(spec);
|
|
26
|
+
case "fn":
|
|
27
|
+
return showFn(spec);
|
|
28
|
+
case "union":
|
|
29
|
+
return spec.members.map(showSpec).join(" | ");
|
|
30
|
+
case "opaque":
|
|
31
|
+
case "ref":
|
|
32
|
+
return spec.name;
|
|
33
|
+
default:
|
|
34
|
+
return "dynamic";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function showFn(spec: FnSpec): string {
|
|
39
|
+
return `fn(${spec.params.map(showSpec).join(", ")}) -> ${showSpec(spec.result)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Past a few fields a shape stops informing and starts filling the line. */
|
|
43
|
+
const SHOWN_FIELDS = 4;
|
|
44
|
+
|
|
45
|
+
function showRecord(spec: RecordSpec): string {
|
|
46
|
+
const names = Object.keys(spec.fields);
|
|
47
|
+
if (names.length === 0) return "{}";
|
|
48
|
+
const shown = names.slice(0, SHOWN_FIELDS).map((name) => field(spec, name));
|
|
49
|
+
const rest = names.length > shown.length ? `, …${names.length - shown.length} more` : "";
|
|
50
|
+
return `{ ${shown.join(", ")}${rest} }`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function field(spec: RecordSpec, name: string): string {
|
|
54
|
+
const type = spec.fields[name];
|
|
55
|
+
return `${name}: ${type ? showSpec(type) : "dynamic"}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire format of a Venn type.
|
|
3
|
+
*
|
|
4
|
+
* Plain data: no functions, no `Map`s, no inference variables, so a `TypeSpec`
|
|
5
|
+
* survives `JSON.stringify`. That is what lets a plugin hand-write one and a
|
|
6
|
+
* generator emit the very same bytes from a `.d.ts`. The compiler's own `Type`,
|
|
7
|
+
* with its unification variables, is a separate internal thing; nothing mutable
|
|
8
|
+
* ever lands here.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately smaller than TypeScript's type system. Everything TS can say
|
|
11
|
+
* projects onto these ten shapes or degrades to {@link DynamicSpec}, never to a
|
|
12
|
+
* failure. Generics, conditional and mapped types are resolved by the TypeScript
|
|
13
|
+
* compiler at generation time, so what arrives here is the answer, not the
|
|
14
|
+
* machinery.
|
|
15
|
+
*/
|
|
16
|
+
export type TypeSpec =
|
|
17
|
+
| PrimSpec
|
|
18
|
+
| LiteralSpec
|
|
19
|
+
| ListSpec
|
|
20
|
+
| MapSpec
|
|
21
|
+
| RecordSpec
|
|
22
|
+
| FnSpec
|
|
23
|
+
| UnionSpec
|
|
24
|
+
| OpaqueSpec
|
|
25
|
+
| RefSpec
|
|
26
|
+
| DynamicSpec;
|
|
27
|
+
|
|
28
|
+
/** The scalars, including the units the language treats as first class. */
|
|
29
|
+
export type PrimName =
|
|
30
|
+
| "string"
|
|
31
|
+
| "number"
|
|
32
|
+
| "bool"
|
|
33
|
+
| "null"
|
|
34
|
+
| "void"
|
|
35
|
+
| "duration"
|
|
36
|
+
| "size"
|
|
37
|
+
| "percent"
|
|
38
|
+
| "instant";
|
|
39
|
+
|
|
40
|
+
/** One of the scalars named by {@link PrimName}. */
|
|
41
|
+
export interface PrimSpec {
|
|
42
|
+
readonly kind: "prim";
|
|
43
|
+
readonly name: PrimName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A single value: `"GET"`, `200`, `true`. What makes an enum an enum. */
|
|
47
|
+
export interface LiteralSpec {
|
|
48
|
+
readonly kind: "literal";
|
|
49
|
+
readonly value: string | number | boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Any number of values of one type. The language has no fixed-length list. */
|
|
53
|
+
export interface ListSpec {
|
|
54
|
+
readonly kind: "list";
|
|
55
|
+
readonly element: TypeSpec;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Keys not known ahead of time, values all alike: TS's `Record<string, T>`. */
|
|
59
|
+
export interface MapSpec {
|
|
60
|
+
readonly kind: "map";
|
|
61
|
+
readonly value: TypeSpec;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Known fields. `open` tolerates extra ones; `optional` lists what may be absent. */
|
|
65
|
+
export interface RecordSpec {
|
|
66
|
+
readonly kind: "record";
|
|
67
|
+
readonly fields: Readonly<Record<string, TypeSpec>>;
|
|
68
|
+
readonly optional?: readonly string[];
|
|
69
|
+
readonly open?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A callable: what it is given, in order, and what it gives back. */
|
|
73
|
+
export interface FnSpec {
|
|
74
|
+
readonly kind: "fn";
|
|
75
|
+
readonly params: readonly TypeSpec[];
|
|
76
|
+
readonly result: TypeSpec;
|
|
77
|
+
/**
|
|
78
|
+
* How many parameters the caller must actually take. A callback handed more
|
|
79
|
+
* than it needs is still a good callback, as with `req => …` against a handler
|
|
80
|
+
* that is also offered the server.
|
|
81
|
+
*/
|
|
82
|
+
readonly takes?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A value that is any one of the members. */
|
|
86
|
+
export interface UnionSpec {
|
|
87
|
+
readonly kind: "union";
|
|
88
|
+
readonly members: readonly TypeSpec[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A handle with a name and no visible inside: a socket, a browser, an
|
|
93
|
+
* `express.Application`.
|
|
94
|
+
*
|
|
95
|
+
* This is the border. Projecting a JS class as a record would drag its whole
|
|
96
|
+
* object graph (`EventEmitter`, symbols, a hundred inherited members) into a
|
|
97
|
+
* language that means none of it. An opaque type can be held and handed to the
|
|
98
|
+
* verbs of its namespace, and nothing else.
|
|
99
|
+
*/
|
|
100
|
+
export interface OpaqueSpec {
|
|
101
|
+
readonly kind: "opaque";
|
|
102
|
+
/**
|
|
103
|
+
* What a program may do with it, such as `{ port, close }` on a server.
|
|
104
|
+
*
|
|
105
|
+
* Opaque is about the *inside* being none of the reader's business, not about
|
|
106
|
+
* having nothing to offer. With none of these the handle is a name and no
|
|
107
|
+
* more, which is right for something only its own verbs ever touch.
|
|
108
|
+
*/
|
|
109
|
+
readonly members?: Readonly<Record<string, TypeSpec>>;
|
|
110
|
+
readonly name: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A named type resolved through the catalog: `http.Request`, `User`. */
|
|
114
|
+
export interface RefSpec {
|
|
115
|
+
readonly kind: "ref";
|
|
116
|
+
readonly name: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Unknown, and deliberately so. Unifies with everything, never errors. */
|
|
120
|
+
export interface DynamicSpec {
|
|
121
|
+
readonly kind: "dynamic";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** What a plugin publishes: its named types, and one signature per action. */
|
|
125
|
+
export interface TypeManifest {
|
|
126
|
+
/** Fully qualified: `http.Request`. */
|
|
127
|
+
readonly types?: Readonly<Record<string, TypeSpec>>;
|
|
128
|
+
/** Keyed by action name within the namespace: `serve`, `on`. */
|
|
129
|
+
readonly actions?: Readonly<Record<string, FnSpec>>;
|
|
130
|
+
}
|