osury 1.0.0 → 1.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/README.md +136 -26
- package/package.json +1 -1
- package/src/OpenAPIParser.res.mjs +3 -1
package/README.md
CHANGED
|
@@ -6,23 +6,33 @@ Generate ReScript types with [Sury](https://github.com/DZakh/sury) schemas from
|
|
|
6
6
|
|
|
7
7
|
Huge thanks to the [ReScript](https://rescript-lang.org/) team for an amazing language, and special thanks to [@DZakh](https://github.com/DZakh) for the incredible [Sury](https://github.com/DZakh/sury) library that made this project possible.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Project Status
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Now at **v1.0.0**. The codegen is in production use against real-world OpenAPI specs
|
|
12
|
+
(both Pydantic/FastAPI and Django/DRF generated). The pipeline is feature-complete
|
|
13
|
+
for the OpenAPI 3.x patterns most code-first generators emit; edge cases beyond that
|
|
14
|
+
are added on demand. Issues and PRs welcome.
|
|
14
15
|
|
|
15
16
|
## Features
|
|
16
17
|
|
|
17
|
-
- OpenAPI 3.x → ReScript types
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
18
|
+
- **OpenAPI 3.x → ReScript types** with full discriminated-union support
|
|
19
|
+
- **`@schema`** annotations for Sury PPX runtime validation
|
|
20
|
+
- **`@genType`** for TypeScript interop with type-safe literal unions
|
|
21
|
+
- **Discriminated unions** via `discriminator.mapping` (OpenAPI-standard, primary)
|
|
22
|
+
with fallback to `_tag.const` (Effect-style convention)
|
|
23
|
+
- **Custom discriminator property names** via `discriminator.propertyName`
|
|
24
|
+
(`@tag("type")`, `@tag("kind")`, etc., not just `_tag`)
|
|
25
|
+
- **Inline enum auto-promotion** to named top-level types with field-based naming
|
|
26
|
+
and structural deduplication
|
|
27
|
+
- **Path operation types** — both `Response` types from `responses[200]` and `Params`
|
|
28
|
+
types from query/path `parameters[]`
|
|
29
|
+
- **JSON `null` support** via `Nullable.t<T>` (maps to `T | null` in TypeScript,
|
|
30
|
+
distinct from `option<T>` for `undefined`)
|
|
31
|
+
- **Untyped/Unknown handling** via `JSON.t` with `@s.matches(S.json)` so untyped
|
|
32
|
+
fields don't poison `@schema` propagation through enclosing types
|
|
33
|
+
- **Automatic deduplication** of identical union/enum structures
|
|
34
|
+
- **TypeScript shims** generated alongside (`Dict.gen.ts`, `JSON.gen.ts`,
|
|
35
|
+
`Nullable.res`, `Nullable.shim.ts`)
|
|
26
36
|
|
|
27
37
|
## Installation
|
|
28
38
|
|
|
@@ -40,10 +50,14 @@ npx osury openapi.json
|
|
|
40
50
|
|
|
41
51
|
# Generate to specific directory
|
|
42
52
|
npx osury openapi.json src/API.res
|
|
43
|
-
# Creates: src/API.res, src/Dict.gen.ts, src/
|
|
53
|
+
# Creates: src/API.res, src/Dict.gen.ts, src/JSON.gen.ts,
|
|
54
|
+
# src/Nullable.res, src/Nullable.shim.ts
|
|
44
55
|
|
|
45
56
|
# With explicit output flag
|
|
46
57
|
npx osury generate openapi.json -o src/Schema.res
|
|
58
|
+
|
|
59
|
+
# Show help
|
|
60
|
+
npx osury --help
|
|
47
61
|
```
|
|
48
62
|
|
|
49
63
|
### Full Example: OpenAPI → ReScript → TypeScript
|
|
@@ -179,14 +193,19 @@ Open [http://localhost:4173/demo/](http://localhost:4173/demo/).
|
|
|
179
193
|
|
|
180
194
|
### Helper Files
|
|
181
195
|
|
|
182
|
-
|
|
196
|
+
Generated alongside the main `Schema.res`:
|
|
183
197
|
|
|
184
198
|
**Dict.gen.ts** — TypeScript shim for dictionaries:
|
|
185
199
|
```typescript
|
|
186
200
|
export type t<T> = { [key: string]: T };
|
|
187
201
|
```
|
|
188
202
|
|
|
189
|
-
**
|
|
203
|
+
**JSON.gen.ts** — TypeScript shim for untyped/Unknown fields:
|
|
204
|
+
```typescript
|
|
205
|
+
export type t = unknown;
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Nullable.res** — ReScript nullable type (`option<T>` with `T | null` TS mapping):
|
|
190
209
|
```rescript
|
|
191
210
|
@genType.import(("./Nullable.shim.ts", "t"))
|
|
192
211
|
type t<'a> = option<'a>
|
|
@@ -203,8 +222,9 @@ export type t<T> = T | null;
|
|
|
203
222
|
|------------|---------|
|
|
204
223
|
| `@genType` | TypeScript type generation |
|
|
205
224
|
| `@schema` | Sury PPX validation schema |
|
|
206
|
-
| `@tag("_tag")` | Discriminated union
|
|
207
|
-
| `@s.null` | Field-level JSON `null` support |
|
|
225
|
+
| `@tag("_tag")` | Discriminated union tag — default Effect TS convention; overridable via `discriminator.propertyName` (e.g. `@tag("type")`) |
|
|
226
|
+
| `@s.null` | Field-level JSON `null` support (for `Nullable.t<T>` fields) |
|
|
227
|
+
| `@s.matches(S.json)` | Per-field synthesizer for `JSON.t` so untyped fields don't poison enclosing `@schema` |
|
|
208
228
|
| `@unboxed` | Primitive-only union optimization |
|
|
209
229
|
| `@as("name")` | Reserved keyword field mapping |
|
|
210
230
|
|
|
@@ -227,20 +247,71 @@ For the generated code to compile, your project needs:
|
|
|
227
247
|
| `boolean` | `bool` |
|
|
228
248
|
| `null` | `unit` |
|
|
229
249
|
| `array` | `array<T>` |
|
|
230
|
-
| `object` | `{ field: T }` |
|
|
250
|
+
| `object` | record `{ field: T }` |
|
|
231
251
|
| `$ref` | type reference |
|
|
232
|
-
| `enum` |
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
252
|
+
| `enum` (inline) | extracted to named `type sortDirection = [#asc \| #desc]` |
|
|
253
|
+
| `enum` (top-level) | poly variant `[#A \| #B]` |
|
|
254
|
+
| `const` (single string) | one-element enum (used for discriminator tags) |
|
|
255
|
+
| schema with no `type` | `JSON.t` (TS: `unknown`) with `@s.matches(S.json)` |
|
|
256
|
+
| `anyOf: [T, null]` | `Nullable.t<T>` (TS: `T \| null`) |
|
|
257
|
+
| `anyOf: [A, B, ...]` (no discriminator) | extracted variant type with structural name |
|
|
258
|
+
| `oneOf` + `discriminator` | poly variant with tags from `discriminator.mapping` |
|
|
237
259
|
| `allOf` | merged object type |
|
|
238
260
|
| `additionalProperties` | `Dict.t<T>` |
|
|
239
261
|
| `default` value | field becomes required |
|
|
262
|
+
| `parameters[]` (query + path) | synthetic `<method><Path>Params` record |
|
|
263
|
+
| `responses[200].schema` | `<method><Path>Response` type |
|
|
264
|
+
|
|
265
|
+
## Discriminated Unions
|
|
266
|
+
|
|
267
|
+
osury resolves variant case tags through a three-level priority chain, so the
|
|
268
|
+
ReScript-side tag always matches the **wire-format truth**, never the class name
|
|
269
|
+
on the backend:
|
|
270
|
+
|
|
271
|
+
1. **`discriminator.mapping`** — OpenAPI 3.x standard, primary source. Works with
|
|
272
|
+
any property name (`_tag`, `tag`, `type`, `kind`, …).
|
|
273
|
+
2. **`_tag.const`** — fallback when no explicit mapping is declared (Effect-style
|
|
274
|
+
implicit convention).
|
|
275
|
+
3. **Ref name** — last-resort default for `$ref` items with no const information.
|
|
276
|
+
|
|
277
|
+
The practical effect: **class names on the backend can diverge from wire-format
|
|
278
|
+
discriminator values without breaking osury**. Pydantic's natural style
|
|
279
|
+
(`class MetricGridBlock` ↔ `_tag: "MetricGrid"`) works out of the box as long as
|
|
280
|
+
the discriminator mapping is declared in the schema.
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
Block:
|
|
284
|
+
oneOf:
|
|
285
|
+
- { $ref: "#/components/schemas/MetricGridBlock" }
|
|
286
|
+
- { $ref: "#/components/schemas/ProseBlock" }
|
|
287
|
+
discriminator:
|
|
288
|
+
propertyName: _tag
|
|
289
|
+
mapping:
|
|
290
|
+
MetricGrid: "#/components/schemas/MetricGridBlock"
|
|
291
|
+
Prose: "#/components/schemas/ProseBlock"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Generates:
|
|
295
|
+
|
|
296
|
+
```rescript
|
|
297
|
+
@genType @tag("_tag") @schema
|
|
298
|
+
type block = MetricGrid({
|
|
299
|
+
metrics: array<string>
|
|
300
|
+
}) | Prose({
|
|
301
|
+
text: string
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Note the case names (`MetricGrid`, `Prose`) come from the **mapping keys**, not
|
|
306
|
+
from the schema class names (`MetricGridBlock`, `ProseBlock`). Case payloads
|
|
307
|
+
are inlined records — fields are copied from the referenced schema and the
|
|
308
|
+
discriminator property is filtered out to avoid duplication with `@tag`.
|
|
240
309
|
|
|
241
|
-
## Path
|
|
310
|
+
## Path Types
|
|
242
311
|
|
|
243
|
-
Types are
|
|
312
|
+
Types are generated from both `responses` and `parameters` of each path operation.
|
|
313
|
+
|
|
314
|
+
### Response types
|
|
244
315
|
|
|
245
316
|
```json
|
|
246
317
|
{
|
|
@@ -264,6 +335,45 @@ Types are also generated from path responses:
|
|
|
264
335
|
|
|
265
336
|
Generates: `type getUsersResponse = userList`
|
|
266
337
|
|
|
338
|
+
### Params types
|
|
339
|
+
|
|
340
|
+
Query and path parameters are folded into a synthetic object schema and pushed
|
|
341
|
+
through the same parsing pipeline (so all rules — `default → required`,
|
|
342
|
+
`anyOf [T, null] → Nullable.t<T>`, inline enum → named type — apply uniformly).
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"paths": {
|
|
347
|
+
"/products": {
|
|
348
|
+
"get": {
|
|
349
|
+
"parameters": [
|
|
350
|
+
{ "in": "query", "name": "sort_field",
|
|
351
|
+
"schema": { "type": "string", "enum": ["sales", "clicks", "impressions"] } },
|
|
352
|
+
{ "in": "query", "name": "limit",
|
|
353
|
+
"schema": { "type": "integer", "default": 50 } }
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Generates:
|
|
362
|
+
|
|
363
|
+
```rescript
|
|
364
|
+
@genType @schema
|
|
365
|
+
type sortField = [#sales | #clicks | #impressions] // promoted from inline enum
|
|
366
|
+
|
|
367
|
+
@genType @schema
|
|
368
|
+
type getProductsParams = {
|
|
369
|
+
sort_field: option<sortField>,
|
|
370
|
+
limit: int, // has default → required
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Headers and serialization details (`style`/`explode`) are intentionally excluded —
|
|
375
|
+
they belong to the HTTP client layer, not the schema contract.
|
|
376
|
+
|
|
267
377
|
## License
|
|
268
378
|
|
|
269
379
|
MIT
|
package/package.json
CHANGED
|
@@ -411,7 +411,9 @@ function buildParamsObjectJson(params) {
|
|
|
411
411
|
if (match$2 === undefined) {
|
|
412
412
|
return;
|
|
413
413
|
}
|
|
414
|
-
|
|
414
|
+
let cleanSchema;
|
|
415
|
+
cleanSchema = typeof match$2 === "object" && match$2 !== null && !Array.isArray(match$2) ? Object.fromEntries(Object.entries(match$2).filter(param => param[0] !== "default")) : match$2;
|
|
416
|
+
properties[match$1] = cleanSchema;
|
|
415
417
|
let match$3 = param["required"];
|
|
416
418
|
let isRequired = match$3 === true;
|
|
417
419
|
let pathRequired = location === "path";
|