tangled
alpha
login
or
join now
danabra.mov
/
typelex
An experimental TypeSpec syntax for Lexicon
53
fork
atom
overview
issues
1
pulls
pipelines
init
danabra.mov
4 months ago
c8a05c17
+501
4 changed files
expand all
collapse all
unified
split
TYPELEX-PLAN.md
lexicon-output-example.json
lexicon-query-example.json
typelex-example.tsp
+281
TYPELEX-PLAN.md
···
1
1
+
# TypeLex: TypeSpec-based IDL for ATProto Lexicons
2
2
+
3
3
+
## Executive Summary
4
4
+
5
5
+
TypeSpec can effectively serve as an IDL for ATProto lexicons. By creating a custom emitter, we can leverage TypeSpec's robust parsing, validation, and extensibility infrastructure while maintaining a lean project structure. This approach avoids maintaining complex parsers and tooling while providing a superior developer experience.
6
6
+
7
7
+
## Why TypeSpec Works for This
8
8
+
9
9
+
### 1. **Architecture Alignment**
10
10
+
- TypeSpec's type system maps well to Lexicon's type system
11
11
+
- Built-in support for validation constraints (minLength, maxLength, format, etc.)
12
12
+
- Native handling of unions, references, and complex types
13
13
+
- Extensible decorator system for ATProto-specific features
14
14
+
15
15
+
### 2. **Minimal Maintenance Burden**
16
16
+
- Leverages Microsoft's actively maintained parser and compiler
17
17
+
- Benefits from TypeSpec's tooling ecosystem (VS Code extension, language server)
18
18
+
- Automatic syntax highlighting, auto-completion, and error checking
19
19
+
- No need to maintain custom parsers or AST transformations
20
20
+
21
21
+
### 3. **Developer Experience Benefits**
22
22
+
- Familiar syntax for developers coming from TypeScript/OpenAPI
23
23
+
- Better IDE support than raw JSON
24
24
+
- Type-safe with compile-time validation
25
25
+
- Supports comments and documentation
26
26
+
27
27
+
## Implementation Plan
28
28
+
29
29
+
### Phase 1: Core Emitter Development
30
30
+
31
31
+
1. **Create TypeLex Emitter Package**
32
32
+
```
33
33
+
@typespec/typelex-emitter/
34
34
+
├── src/
35
35
+
│ ├── index.ts # Main emitter entry point
36
36
+
│ ├── emitter.ts # Core emitter logic
37
37
+
│ ├── lexicon-types.ts # Lexicon type definitions
38
38
+
│ └── validators.ts # ATProto-specific validations
39
39
+
├── package.json
40
40
+
└── tsconfig.json
41
41
+
```
42
42
+
43
43
+
2. **Implement Core Type Mappings**
44
44
+
- Map TypeSpec primitives to Lexicon types
45
45
+
- Handle ATProto-specific formats (at-uri, did, handle, etc.)
46
46
+
- Support record, query, procedure, and subscription types
47
47
+
48
48
+
3. **Add ATProto Decorators**
49
49
+
```typescript
50
50
+
@lexicon("com.example.actor.profile")
51
51
+
@record
52
52
+
model Profile {
53
53
+
@format("did") did: string;
54
54
+
@format("handle") handle: string;
55
55
+
@maxGraphemes(150) bio?: string;
56
56
+
}
57
57
+
```
58
58
+
59
59
+
### Phase 2: Feature Completeness
60
60
+
61
61
+
1. **Advanced Type Support**
62
62
+
- Union types with proper `$type` handling
63
63
+
- Blob references
64
64
+
- CID links
65
65
+
- Unknown types
66
66
+
67
67
+
2. **XRPC Integration**
68
68
+
- Map HTTP operations to query/procedure types
69
69
+
- Handle input/output schemas
70
70
+
- Support error definitions
71
71
+
72
72
+
3. **Validation Rules**
73
73
+
- Implement all Lexicon validation constraints
74
74
+
- Add custom validation for NSIDs
75
75
+
- Ensure closed unions are properly enforced
76
76
+
77
77
+
### Phase 3: Developer Experience
78
78
+
79
79
+
1. **TypeLex Standard Library**
80
80
+
```typescript
81
81
+
// typelex-lib.tsp
82
82
+
import "@typespec/typelex";
83
83
+
84
84
+
// Common ATProto types
85
85
+
model AtUri extends string {
86
86
+
@format("at-uri")
87
87
+
}
88
88
+
89
89
+
model Did extends string {
90
90
+
@format("did")
91
91
+
}
92
92
+
93
93
+
model Cid extends string {
94
94
+
@format("cid")
95
95
+
}
96
96
+
```
97
97
+
98
98
+
2. **CLI Tool**
99
99
+
```bash
100
100
+
npx typelex compile app.bsky.feed.tsp --out-dir ./lexicons
101
101
+
```
102
102
+
103
103
+
3. **VS Code Extension Enhancements**
104
104
+
- Custom snippets for ATProto patterns
105
105
+
- NSID validation and auto-completion
106
106
+
- Lexicon preview panel
107
107
+
108
108
+
## Technical Architecture
109
109
+
110
110
+
### Emitter Structure
111
111
+
112
112
+
```typescript
113
113
+
export async function $onEmit(context: EmitContext) {
114
114
+
const { program, options } = context;
115
115
+
const lexicons = new Map<string, LexiconDocument>();
116
116
+
117
117
+
// Walk the TypeSpec program
118
118
+
for (const [type, metadata] of program.stateMap(LexiconKey)) {
119
119
+
const lexicon = buildLexicon(type, metadata);
120
120
+
lexicons.set(lexicon.id, lexicon);
121
121
+
}
122
122
+
123
123
+
// Write lexicon files
124
124
+
for (const [id, lexicon] of lexicons) {
125
125
+
const path = nsidToPath(id);
126
126
+
await writeFile(path, JSON.stringify(lexicon, null, 2));
127
127
+
}
128
128
+
}
129
129
+
```
130
130
+
131
131
+
### Key Mappings
132
132
+
133
133
+
| TypeSpec | Lexicon |
134
134
+
|----------|---------|
135
135
+
| `string` | `"string"` |
136
136
+
| `int32` | `"integer"` |
137
137
+
| `boolean` | `"boolean"` |
138
138
+
| `bytes` | `"bytes"` |
139
139
+
| `utcDateTime` | `"string"` with `format: "datetime"` |
140
140
+
| `model` | `"object"` or `"record"` |
141
141
+
| `op` | `"query"` or `"procedure"` |
142
142
+
| `union` | `"union"` |
143
143
+
144
144
+
### Decorator Extensions
145
145
+
146
146
+
```typescript
147
147
+
// Define ATProto-specific decorators
148
148
+
export const $record = defineDecorator({
149
149
+
name: "record",
150
150
+
target: "Model",
151
151
+
args: { key: "tid" | "literal" }
152
152
+
});
153
153
+
154
154
+
export const $maxGraphemes = defineDecorator({
155
155
+
name: "maxGraphemes",
156
156
+
target: "ModelProperty",
157
157
+
args: { count: "number" }
158
158
+
});
159
159
+
```
160
160
+
161
161
+
## Example Usage
162
162
+
163
163
+
### Input (TypeLex)
164
164
+
165
165
+
```typescript
166
166
+
import "@typespec/typelex";
167
167
+
using TypeLex;
168
168
+
169
169
+
@lexicon("app.bsky.feed.post")
170
170
+
@record
171
171
+
model Post {
172
172
+
@maxGraphemes(300)
173
173
+
text: string;
174
174
+
175
175
+
@format("datetime")
176
176
+
createdAt: utcDateTime;
177
177
+
178
178
+
reply?: {
179
179
+
root: PostRef;
180
180
+
parent: PostRef;
181
181
+
};
182
182
+
183
183
+
@maxLength(3)
184
184
+
langs?: string[];
185
185
+
}
186
186
+
187
187
+
model PostRef {
188
188
+
@format("at-uri")
189
189
+
uri: string;
190
190
+
191
191
+
@format("cid")
192
192
+
cid: string;
193
193
+
}
194
194
+
```
195
195
+
196
196
+
### Output (Lexicon JSON)
197
197
+
198
198
+
```json
199
199
+
{
200
200
+
"lexicon": 1,
201
201
+
"id": "app.bsky.feed.post",
202
202
+
"defs": {
203
203
+
"main": {
204
204
+
"type": "record",
205
205
+
"key": "tid",
206
206
+
"record": {
207
207
+
"type": "object",
208
208
+
"required": ["text", "createdAt"],
209
209
+
"properties": {
210
210
+
"text": {
211
211
+
"type": "string",
212
212
+
"maxGraphemes": 300
213
213
+
},
214
214
+
"createdAt": {
215
215
+
"type": "string",
216
216
+
"format": "datetime"
217
217
+
},
218
218
+
"reply": {
219
219
+
"type": "object",
220
220
+
"required": ["root", "parent"],
221
221
+
"properties": {
222
222
+
"root": { "type": "ref", "ref": "#postRef" },
223
223
+
"parent": { "type": "ref", "ref": "#postRef" }
224
224
+
}
225
225
+
},
226
226
+
"langs": {
227
227
+
"type": "array",
228
228
+
"maxLength": 3,
229
229
+
"items": { "type": "string" }
230
230
+
}
231
231
+
}
232
232
+
}
233
233
+
},
234
234
+
"postRef": {
235
235
+
"type": "object",
236
236
+
"required": ["uri", "cid"],
237
237
+
"properties": {
238
238
+
"uri": { "type": "string", "format": "at-uri" },
239
239
+
"cid": { "type": "string", "format": "cid" }
240
240
+
}
241
241
+
}
242
242
+
}
243
243
+
}
244
244
+
```
245
245
+
246
246
+
## Advantages Over Alternatives
247
247
+
248
248
+
1. **vs. Custom DSL**
249
249
+
- No parser maintenance
250
250
+
- Existing tooling support
251
251
+
- Familiar syntax
252
252
+
253
253
+
2. **vs. TypeScript Code Generation**
254
254
+
- Declarative, not imperative
255
255
+
- Better validation at compile time
256
256
+
- Cleaner separation of schema and implementation
257
257
+
258
258
+
3. **vs. Raw JSON**
259
259
+
- Type safety
260
260
+
- IDE support
261
261
+
- Comments and documentation
262
262
+
- Less verbose
263
263
+
264
264
+
## Implementation Timeline
265
265
+
266
266
+
- **Week 1-2**: Basic emitter with core types
267
267
+
- **Week 3-4**: XRPC support and validations
268
268
+
- **Week 5-6**: Developer experience improvements
269
269
+
- **Week 7-8**: Testing, documentation, and examples
270
270
+
271
271
+
## Open Questions
272
272
+
273
273
+
1. **Subscription Handling**: TypeSpec doesn't have native WebSocket support. We'll need custom decorators for subscription types.
274
274
+
275
275
+
2. **Blob Management**: Need to determine best representation for blob references in TypeSpec.
276
276
+
277
277
+
3. **Package Distribution**: Should this be part of official ATProto tooling or a community package?
278
278
+
279
279
+
## Conclusion
280
280
+
281
281
+
TypeSpec provides an excellent foundation for building a TypeLex IDL. The approach minimizes maintenance burden while providing superior developer experience. The emitter architecture allows for incremental development and easy extension as ATProto lexicon evolves.
+28
lexicon-output-example.json
···
1
1
+
{
2
2
+
"lexicon": 1,
3
3
+
"id": "xyz.statusphere.status",
4
4
+
"defs": {
5
5
+
"main": {
6
6
+
"type": "record",
7
7
+
"description": "A simple status record",
8
8
+
"key": "tid",
9
9
+
"record": {
10
10
+
"type": "object",
11
11
+
"required": ["status", "createdAt"],
12
12
+
"properties": {
13
13
+
"status": {
14
14
+
"type": "string",
15
15
+
"description": "The status emoji",
16
16
+
"minLength": 1,
17
17
+
"maxLength": 32
18
18
+
},
19
19
+
"createdAt": {
20
20
+
"type": "string",
21
21
+
"description": "When the status was created",
22
22
+
"format": "datetime"
23
23
+
}
24
24
+
}
25
25
+
}
26
26
+
}
27
27
+
}
28
28
+
}
+102
lexicon-query-example.json
···
1
1
+
{
2
2
+
"lexicon": 1,
3
3
+
"id": "xyz.statusphere.feed.getAuthorFeed",
4
4
+
"defs": {
5
5
+
"main": {
6
6
+
"type": "query",
7
7
+
"description": "Get posts from a user's feed",
8
8
+
"parameters": {
9
9
+
"type": "params",
10
10
+
"required": ["actor"],
11
11
+
"properties": {
12
12
+
"actor": {
13
13
+
"type": "string",
14
14
+
"description": "AT-identifier of the account"
15
15
+
},
16
16
+
"limit": {
17
17
+
"type": "integer",
18
18
+
"minimum": 1,
19
19
+
"maximum": 100,
20
20
+
"default": 50
21
21
+
},
22
22
+
"cursor": {
23
23
+
"type": "string"
24
24
+
}
25
25
+
}
26
26
+
},
27
27
+
"output": {
28
28
+
"encoding": "application/json",
29
29
+
"schema": {
30
30
+
"type": "object",
31
31
+
"required": ["feed"],
32
32
+
"properties": {
33
33
+
"cursor": {
34
34
+
"type": "string"
35
35
+
},
36
36
+
"feed": {
37
37
+
"type": "array",
38
38
+
"items": {
39
39
+
"type": "ref",
40
40
+
"ref": "#post"
41
41
+
}
42
42
+
}
43
43
+
}
44
44
+
}
45
45
+
}
46
46
+
},
47
47
+
"post": {
48
48
+
"type": "object",
49
49
+
"required": ["uri", "cid", "author", "record", "indexedAt"],
50
50
+
"properties": {
51
51
+
"uri": {
52
52
+
"type": "string",
53
53
+
"format": "at-uri"
54
54
+
},
55
55
+
"cid": {
56
56
+
"type": "string",
57
57
+
"format": "cid"
58
58
+
},
59
59
+
"author": {
60
60
+
"type": "ref",
61
61
+
"ref": "#profileViewBasic"
62
62
+
},
63
63
+
"record": {
64
64
+
"type": "unknown"
65
65
+
},
66
66
+
"replyCount": {
67
67
+
"type": "integer"
68
68
+
},
69
69
+
"repostCount": {
70
70
+
"type": "integer"
71
71
+
},
72
72
+
"likeCount": {
73
73
+
"type": "integer"
74
74
+
},
75
75
+
"indexedAt": {
76
76
+
"type": "string",
77
77
+
"format": "datetime"
78
78
+
}
79
79
+
}
80
80
+
},
81
81
+
"profileViewBasic": {
82
82
+
"type": "object",
83
83
+
"required": ["did", "handle"],
84
84
+
"properties": {
85
85
+
"did": {
86
86
+
"type": "string",
87
87
+
"format": "did"
88
88
+
},
89
89
+
"handle": {
90
90
+
"type": "string",
91
91
+
"format": "handle"
92
92
+
},
93
93
+
"displayName": {
94
94
+
"type": "string"
95
95
+
},
96
96
+
"avatar": {
97
97
+
"type": "string"
98
98
+
}
99
99
+
}
100
100
+
}
101
101
+
}
102
102
+
}
+90
typelex-example.tsp
···
1
1
+
import "@typespec/http";
2
2
+
3
3
+
using TypeSpec.Http;
4
4
+
5
5
+
// Example TypeLex definition for ATProto lexicons
6
6
+
// This demonstrates how TypeSpec could be used to define ATProto schemas
7
7
+
8
8
+
@service({
9
9
+
title: "Status Sphere",
10
10
+
})
11
11
+
@server("https://statusphere.xyz", "StatusSphere API")
12
12
+
namespace xyz.statusphere;
13
13
+
14
14
+
// ATProto record type example
15
15
+
@route("/status")
16
16
+
@doc("A simple status record")
17
17
+
model Status {
18
18
+
@doc("The status emoji")
19
19
+
@minLength(1)
20
20
+
@maxLength(32)
21
21
+
status: string;
22
22
+
23
23
+
@doc("When the status was created")
24
24
+
@format("date-time")
25
25
+
createdAt: utcDateTime;
26
26
+
}
27
27
+
28
28
+
// ATProto query (XRPC) example
29
29
+
@route("/feed.getAuthorFeed")
30
30
+
@get
31
31
+
op getAuthorFeed(
32
32
+
@query actor: string,
33
33
+
@query limit?: int32 = 50,
34
34
+
@query cursor?: string,
35
35
+
): {
36
36
+
@body feed: {
37
37
+
cursor?: string;
38
38
+
posts: Post[];
39
39
+
};
40
40
+
};
41
41
+
42
42
+
model Post {
43
43
+
uri: string;
44
44
+
cid: string;
45
45
+
author: ProfileViewBasic;
46
46
+
record: unknown; // Would be the actual post record
47
47
+
replyCount?: int32;
48
48
+
repostCount?: int32;
49
49
+
likeCount?: int32;
50
50
+
indexedAt: utcDateTime;
51
51
+
}
52
52
+
53
53
+
model ProfileViewBasic {
54
54
+
did: string;
55
55
+
handle: string;
56
56
+
displayName?: string;
57
57
+
avatar?: string;
58
58
+
}
59
59
+
60
60
+
// ATProto procedure (XRPC) example
61
61
+
@route("/feed.like")
62
62
+
@post
63
63
+
op createLike(
64
64
+
@body body: {
65
65
+
repo: string;
66
66
+
collection: "app.bsky.feed.like";
67
67
+
record: {
68
68
+
subject: {
69
69
+
uri: string;
70
70
+
cid: string;
71
71
+
};
72
72
+
createdAt: utcDateTime;
73
73
+
};
74
74
+
}
75
75
+
): {
76
76
+
@statusCode statusCode: 200;
77
77
+
@body result: {
78
78
+
uri: string;
79
79
+
cid: string;
80
80
+
};
81
81
+
};
82
82
+
83
83
+
// ATProto subscription example
84
84
+
@route("/com.atproto.sync.subscribeRepos")
85
85
+
model RepoSubscription {
86
86
+
// This would need special handling for websocket subscriptions
87
87
+
@doc("Subscribe to repository updates")
88
88
+
seq?: int64;
89
89
+
since?: string;
90
90
+
}