osury 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +136 -26
  2. package/package.json +1 -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
- ## Early Stage Warning
9
+ ## Project Status
10
10
 
11
- This project is in a very early stage of development and is tailored to my specific needs. It comes with **no guarantees** of stability, correctness, or completeness.
12
-
13
- **Suggestions and contributions are very welcome!** Feel free to open issues or submit PRs.
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
- - `@schema` annotations for Sury PPX validation
19
- - `@genType` for TypeScript interop
20
- - Union types extracted as proper variants with `@tag("_tag")`
21
- - Automatic deduplication of identical union structures
22
- - Generates `module S = Sury` alias (required by sury-ppx)
23
- - Generates helper shims for TypeScript interop (`Dict.gen.ts`, `Nullable.shim.ts`)
24
- - Proper JSON `null` support via `Nullable.t<T>` (maps to `T | null` in TypeScript)
25
- - Path response types generation from `paths.*.responses`
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/Nullable.res, src/Nullable.shim.ts
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
- Also generates helper files:
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
- **Nullable.res** — ReScript nullable type:
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 support (Effect TS compatible) |
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` | poly variant `[#A \| #B]` |
233
- | `const` | single-value enum (for `_tag`) |
234
- | `anyOf` (nullable) | `Nullable.t<T>` `T \| null` in TS |
235
- | `anyOf` (union) | variant type with `@tag("_tag")` |
236
- | `oneOf` (discriminated) | poly variant with `_tag.const` extraction |
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 Responses
310
+ ## Path Types
242
311
 
243
- Types are also generated from path responses:
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
@@ -2,7 +2,7 @@
2
2
  "name": "osury",
3
3
  "type": "module",
4
4
  "description": "Generate ReScript types with Sury schemas from OpenAPI specifications",
5
- "version": "1.0.0",
5
+ "version": "1.0.1",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "osury": "bin/osury.mjs"