@shipworthy/openapi-transformer 0.0.0-stage → 0.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +409 -2
  3. package/dist/decode.d.ts +10 -0
  4. package/dist/decode.js +14 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +2 -0
  7. package/dist/model/body-media-type.d.ts +31 -0
  8. package/dist/model/body-media-type.js +22 -0
  9. package/dist/model/components.d.ts +19 -0
  10. package/dist/model/components.js +22 -0
  11. package/dist/model/example.d.ts +18 -0
  12. package/dist/model/example.js +14 -0
  13. package/dist/model/extensions.d.ts +4 -0
  14. package/dist/model/extensions.js +3 -0
  15. package/dist/model/external-docs.d.ts +12 -0
  16. package/dist/model/external-docs.js +9 -0
  17. package/dist/model/fields.d.ts +14 -0
  18. package/dist/model/fields.js +17 -0
  19. package/dist/model/header.d.ts +24 -0
  20. package/dist/model/header.js +23 -0
  21. package/dist/model/http-method.d.ts +5 -0
  22. package/dist/model/http-method.js +14 -0
  23. package/dist/model/index.d.ts +37 -0
  24. package/dist/model/index.js +37 -0
  25. package/dist/model/info.d.ts +24 -0
  26. package/dist/model/info.js +17 -0
  27. package/dist/model/link.d.ts +18 -0
  28. package/dist/model/link.js +14 -0
  29. package/dist/model/media-type.d.ts +17 -0
  30. package/dist/model/media-type.js +14 -0
  31. package/dist/model/operation.d.ts +59 -0
  32. package/dist/model/operation.js +40 -0
  33. package/dist/model/optional.d.ts +8 -0
  34. package/dist/model/optional.js +13 -0
  35. package/dist/model/param-style.d.ts +4 -0
  36. package/dist/model/param-style.js +11 -0
  37. package/dist/model/parameter.d.ts +27 -0
  38. package/dist/model/parameter.js +12 -0
  39. package/dist/model/path-item.d.ts +24 -0
  40. package/dist/model/path-item.js +23 -0
  41. package/dist/model/reference.d.ts +13 -0
  42. package/dist/model/reference.js +9 -0
  43. package/dist/model/request.d.ts +43 -0
  44. package/dist/model/request.js +29 -0
  45. package/dist/model/response.d.ts +32 -0
  46. package/dist/model/response.js +21 -0
  47. package/dist/model/schema.d.ts +18 -0
  48. package/dist/model/schema.js +11 -0
  49. package/dist/model/security.d.ts +128 -0
  50. package/dist/model/security.js +60 -0
  51. package/dist/model/server.d.ts +26 -0
  52. package/dist/model/server.js +19 -0
  53. package/dist/model/service.d.ts +78 -0
  54. package/dist/model/service.js +42 -0
  55. package/dist/model/tag.d.ts +15 -0
  56. package/dist/model/tag.js +12 -0
  57. package/dist/transformers/3.1/callbacks.d.ts +1747 -0
  58. package/dist/transformers/3.1/callbacks.js +19 -0
  59. package/dist/transformers/3.1/content.d.ts +4255 -0
  60. package/dist/transformers/3.1/content.js +36 -0
  61. package/dist/transformers/3.1/context.d.ts +10 -0
  62. package/dist/transformers/3.1/context.js +1 -0
  63. package/dist/transformers/3.1/examples.d.ts +3072 -0
  64. package/dist/transformers/3.1/examples.js +36 -0
  65. package/dist/transformers/3.1/extensions.d.ts +2 -0
  66. package/dist/transformers/3.1/extensions.js +3 -0
  67. package/dist/transformers/3.1/headers.d.ts +3231 -0
  68. package/dist/transformers/3.1/headers.js +30 -0
  69. package/dist/transformers/3.1/index.d.ts +8871 -0
  70. package/dist/transformers/3.1/index.js +61 -0
  71. package/dist/transformers/3.1/operation.d.ts +1697 -0
  72. package/dist/transformers/3.1/operation.js +88 -0
  73. package/dist/transformers/3.1/parameters.d.ts +3312 -0
  74. package/dist/transformers/3.1/parameters.js +40 -0
  75. package/dist/transformers/3.1/paths.d.ts +3422 -0
  76. package/dist/transformers/3.1/paths.js +35 -0
  77. package/dist/transformers/3.1/pointer.d.ts +985 -0
  78. package/dist/transformers/3.1/pointer.js +75 -0
  79. package/dist/transformers/3.1/request.d.ts +2254 -0
  80. package/dist/transformers/3.1/request.js +25 -0
  81. package/dist/transformers/3.1/responses.d.ts +5952 -0
  82. package/dist/transformers/3.1/responses.js +49 -0
  83. package/dist/transformers/3.1/schemas.d.ts +4 -0
  84. package/dist/transformers/3.1/schemas.js +4 -0
  85. package/dist/transformers/3.1/securities.d.ts +1282 -0
  86. package/dist/transformers/3.1/securities.js +58 -0
  87. package/dist/transformers/3.1/servers.d.ts +72 -0
  88. package/dist/transformers/3.1/servers.js +37 -0
  89. package/dist/transformers/3.1/service.d.ts +42 -0
  90. package/dist/transformers/3.1/service.js +35 -0
  91. package/package.json +46 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipworthy AI
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 CHANGED
@@ -1,3 +1,410 @@
1
- # Temporary Holding Version
1
+ # @shipworthy/openapi-transformer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Turn a bundled OpenAPI 3.1 document into `ParsedService`, a flat HTTP model of the API that code generators can read without walking the raw document. It lists every operation with its method and path. Each operation gets a `request` that merges path-level and operation-level parameters, grouped by location. Responses are keyed by status `code`, and servers and security are resolved through inheritance. Local `$ref`s are resolved, and each resolved node keeps its Reference Object in `reference`.
4
+
5
+ Use this package if you are writing a generator or another tool that consumes an OpenAPI description. Load and bundle the document with [`@shipworthy/openapi-parser`](../openapi-parser) first, then decode it here. The `ParsedService` type and the other `Parsed*` types are the model that the `@shipworthy/*` generator plugins (`openapi-typescript`, `openapi-zod`, `openapi-tanstack-query`, ...) receive. `@shipworthy/openapi-generator` runs parser, transformer, and plugin in that order. The `Generator` interface there declares `generate(model: ParsedService, options)`.
6
+
7
+ The package is built on [Effect](https://effect.website) 4. Every model type is also an Effect `Schema`, so you can validate a model value at runtime or convert it to a [Standard Schema](https://standardschema.dev).
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ bun add @shipworthy/openapi-transformer @shipworthy/openapi-parser effect @effect/platform-node
13
+ ```
14
+
15
+ Requirements:
16
+
17
+ - **`effect` `^4.0.0`.** It is a peer dependency, so install it yourself. `decode` returns an `Effect` and the model types are Effect `Schema`s, so your app and this package must share one copy of `effect` to run and compose them.
18
+ - **`@shipworthy/openapi-parser`.** It is a dependency. You install it yourself to produce the input document, and its `bundleDocument` / `ParsedOpenApiFromString` are the usual entry points.
19
+ - **`@effect/platform-node` `^4.0.0`** (or `@effect/platform-bun`), only because the quick start imports `NodeServices` to provide services to the parser. This package does not depend on it.
20
+ - **ESM only** (`"type": "module"`). There is no CommonJS build. The package ships compiled JavaScript with `.d.ts` declarations, so no TypeScript loader or bundler is needed.
21
+ - **Node.js `>=22.19.0` or Bun.**
22
+
23
+ ## Quick start
24
+
25
+ This example uses the parser to parse, bundle, and upgrade a YAML document (3.0 documents are upgraded to 3.1), then decodes it into a `ParsedService`:
26
+
27
+ ```ts
28
+ import { NodeServices } from "@effect/platform-node";
29
+ import { bundleDocument } from "@shipworthy/openapi-parser";
30
+ import { decode } from "@shipworthy/openapi-transformer";
31
+ import { Effect, Layer } from "effect";
32
+ import { FetchHttpClient } from "effect/http";
33
+
34
+ const yaml = `
35
+ openapi: 3.0.3
36
+ info: { title: Pets, version: "1.0.0" }
37
+ paths:
38
+ /pets/{id}:
39
+ get:
40
+ operationId: getPet
41
+ parameters:
42
+ - { name: id, in: path, required: true, schema: { type: string } }
43
+ responses:
44
+ "200":
45
+ description: A pet
46
+ content:
47
+ application/json:
48
+ schema: { $ref: "#/components/schemas/Pet" }
49
+ components:
50
+ schemas:
51
+ Pet: { type: object, properties: { name: { type: string } } }
52
+ `;
53
+
54
+ const program = Effect.gen(function* () {
55
+ const document = yield* bundleDocument(yaml); // or a file path URL, http(s) URL, object, Buffer, stream
56
+ const service = yield* decode(document);
57
+
58
+ console.log(service.openapi, service.name, service.version); // "3.1.0" "Pets" "1.0.0"
59
+
60
+ for (const operation of service.operations) {
61
+ console.log(
62
+ operation.method, // "GET"
63
+ operation.path, // "/pets/{id}"
64
+ operation.operationId, // "getPet"
65
+ operation.request.path.map((p) => p.name), // ["id"]
66
+ operation.responses.map((r) => r.code), // ["200"]
67
+ );
68
+ }
69
+
70
+ // Schemas are kept as written; $refs still point into the bundled document.
71
+ console.log(service.operations[0]?.responses[0]?.contents[0]?.schema);
72
+ // { $ref: "#/components/schemas/Pet" }
73
+ });
74
+
75
+ await Effect.runPromise(
76
+ program.pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),
77
+ );
78
+ ```
79
+
80
+ `bundleDocument` needs `FileSystem`, `Path`, and `HttpClient` services, which the layers above supply. `@effect/platform-bun`'s `BunServices.layer` also works. `decode` needs no services.
81
+
82
+ ## API
83
+
84
+ The package has a single entry point, `@shipworthy/openapi-transformer`. It exports:
85
+
86
+ | Export | Kind | Purpose |
87
+ | -------------------------------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------- |
88
+ | `decode` | function | Decode a `ParsedOpenApi` document into a `ParsedService`. |
89
+ | `ParsedServiceFromParsedOpenApi` | Effect `Schema` codec | The same transformation as a composable schema. |
90
+ | `ParsedService` and the other `Parsed*` names | `Schema` + type | The model. See [Model](#model). |
91
+ | `HttpMethodName`, `ParamStyle`, `ParameterLocation`, `SecurityDeclaration` | `Schema` + literal type | Enumerations used in the model. |
92
+ | `httpMethods`, `parameterLocations` | `readonly` tuple | The lowercase method keys and the parameter locations, in model order. |
93
+
94
+ Each `Parsed*` name is exported both as a value (an Effect `Schema`) and as a type (`typeof X.Type`). Use `import type { ParsedOperation }` when you only need the type.
95
+
96
+ ### `decode(document, options?)`
97
+
98
+ ```ts
99
+ const decode: (
100
+ document: ParsedOpenApi, // from "@shipworthy/openapi-parser/schemas/3.1"
101
+ options?: SchemaAST.ParseOptions,
102
+ ) => Effect.Effect<ParsedService, Schema.SchemaError>;
103
+ ```
104
+
105
+ `decode` is `Schema.decodeEffect(ParsedServiceFromParsedOpenApi)`. Its input is an OpenAPI 3.1 document that has already been bundled, so it has no external `$ref`s. Use `bundleDocument` or `ParsedOpenApiFromString` from `@shipworthy/openapi-parser` to produce one. The second argument takes Effect's standard schema parse options.
106
+
107
+ Errors: every failure is a `Schema.SchemaError` (`_tag: "SchemaError"`) whose `message` includes the JSON path of the problem. Those failures are:
108
+
109
+ - The input does not match the OpenAPI 3.1 schema. For example, `info.version` is missing, or an `apiKey` security scheme has `in: "path"`.
110
+ - A component schema is neither an object nor a boolean.
111
+ - A `$ref` that the transformer follows has problems:
112
+ - It points at nothing (`Unresolved reference: #/components/responses/Nope`).
113
+ - It is circular (`Circular reference: ...`).
114
+ - It is not a local `#/...` reference (`Expected a local reference: ...`).
115
+ - It resolves to the wrong kind of object, such as a header `$ref` that targets a schema.
116
+
117
+ ```ts
118
+ import { decode } from "@shipworthy/openapi-transformer";
119
+ import { Effect } from "effect";
120
+
121
+ const result = await Effect.runPromise(Effect.result(decode(document))); // Result<ParsedService, SchemaError>
122
+ // or: decode(document).pipe(Effect.catchTag("SchemaError", (e) => ...))
123
+ ```
124
+
125
+ ### `ParsedServiceFromParsedOpenApi`
126
+
127
+ This is an Effect `Schema` codec from `ParsedOpenApi` to `ParsedService`. It validates its input as OpenAPI 3.1, runs the transformation, and validates the resulting model. It is useful in three ways:
128
+
129
+ **Decoding unknown input in one step:**
130
+
131
+ ```ts
132
+ import { ParsedServiceFromParsedOpenApi } from "@shipworthy/openapi-transformer";
133
+ import { Effect, Schema } from "effect";
134
+
135
+ const service = Effect.runSync(
136
+ Schema.decodeUnknownEffect(ParsedServiceFromParsedOpenApi)({
137
+ openapi: "3.1.0",
138
+ info: { title: "Pets", version: "1.0.0" },
139
+ paths: { "/pets": { get: { operationId: "listPets" } } },
140
+ }),
141
+ );
142
+ ```
143
+
144
+ In this form the input must already be a bundled 3.1 object. It does not go through the parser's 3.0 upgrade or external-reference bundling.
145
+
146
+ **Composing with the parser's text codec.** This parses YAML or JSON text, bundles it, upgrades it, and transforms it:
147
+
148
+ ```ts
149
+ import { ParsedOpenApiFromString } from "@shipworthy/openapi-parser";
150
+ import { ParsedServiceFromParsedOpenApi } from "@shipworthy/openapi-transformer";
151
+ import { Schema } from "effect";
152
+
153
+ const codec = ParsedOpenApiFromString({ sourceUrl: "file:///specs/openapi.yaml" }).pipe(
154
+ Schema.decodeTo(ParsedServiceFromParsedOpenApi),
155
+ );
156
+
157
+ // Effect<ParsedService, SchemaError, FileSystem | HttpClient>
158
+ const program = Schema.decodeEffect(codec)(yamlText);
159
+ ```
160
+
161
+ **As a Standard Schema:**
162
+
163
+ ```ts
164
+ const standard = Schema.toStandardSchemaV1(ParsedServiceFromParsedOpenApi);
165
+ const result = await standard["~standard"].validate(input);
166
+ // { value: ParsedService } or { issues: [{ path: ["info", "version"], message: "Missing key" }] }
167
+ ```
168
+
169
+ The transformation is one-way. Encoding fails with a `SchemaError` whose message is `"The meta model cannot be encoded back to OpenAPI for now."`.
170
+
171
+ ### Validating a model
172
+
173
+ Every model schema checks its own values. For example, a plugin can check the `ParsedService` it receives:
174
+
175
+ ```ts
176
+ import { ParsedService } from "@shipworthy/openapi-transformer";
177
+ import { Schema } from "effect";
178
+
179
+ const check = Schema.toStandardSchemaV1(ParsedService);
180
+ check["~standard"].validate(model); // { value } or { issues }
181
+ ```
182
+
183
+ ## Model
184
+
185
+ These conventions apply across the whole model:
186
+
187
+ - **`id`** is the JSON Pointer of the node's definition in the bundled document, for example `"#"`, `"#/paths/~1pets~1{id}/get"`, or `"#/components/parameters/Id"`. A node reached through a `$ref` takes the id of its target, so shared components share an id.
188
+ - **`reference`** (optional, of type `ParsedRef`) is set when a node was reached through a local `$ref`. It holds the Reference Object as written: `{ $ref, summary?, description? }`. All other fields hold the resolved target. When the Reference Object has its own `summary` or `description`, it overrides the target's, as OpenAPI 3.1 allows.
189
+ - **`extensions`** holds the node's `x-*` keys as written (`Record<string, unknown>`). Nodes without extensions have `{}`.
190
+ - **Optional fields** are left out when absent, never set to `undefined`.
191
+ - **Schemas** (`ParsedSchema`) are JSON Schema Draft 2020-12 values, either an object or `true`/`false`. They are passed through exactly as written, unknown keywords included, and they are not dereferenced. Their `$ref`s point into the bundled document, so look them up in `service.components.schemas` or `service.$defs`.
192
+ - **Lists preserve document order.** Maps (`Record`) keep the document's keys.
193
+
194
+ ### `ParsedService`
195
+
196
+ | Field | Type | Notes |
197
+ | ------------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
198
+ | `id` | `string` | Always `"#"`. |
199
+ | `openapi` | `string` | The document's version, e.g. `"3.1.0"`. 3.0 input that went through the parser reads `"3.1.0"`. |
200
+ | `jsonSchemaDialect` | `string?` | |
201
+ | `name` | `string` | `info.title`. |
202
+ | `version` | `string` | `info.version`. |
203
+ | `summary`, `description`, `termsOfService` | `string?` | From `info`. |
204
+ | `contact` | `ParsedContact?` | `{ name?, url?, email?, extensions }` |
205
+ | `license` | `ParsedLicense?` | `{ name, url?, identifier?, extensions }` |
206
+ | `infoExtensions` | `ParsedExtensions` | `x-*` keys of `info`. |
207
+ | `externalDocs` | `ParsedExternalDocs?` | `{ url, description?, extensions }` |
208
+ | `servers` | `ParsedServer[]` | Document-level servers. `[]` when there are none. |
209
+ | `tags` | `ParsedTag[]` | Tags the document declares. |
210
+ | `security` | `ParsedSecurityRequirement[]` | Document-level security. |
211
+ | `securitySchemes` | `Record<string, ParsedSecurityScheme>` | From `components.securitySchemes`. |
212
+ | `operations` | `ParsedOperation[]` | Every operation under `paths`, in path order and then method order. Webhooks and component path items are not included. |
213
+ | `paths` | `Record<string, ParsedPathItem>` | `x-*` keys under `paths` are skipped. |
214
+ | `webhooks` | `Record<string, ParsedPathItem>` | |
215
+ | `components` | `ParsedComponents` | See below. |
216
+ | `$defs` | `Record<string, ParsedSchema>` | Top-level `$defs`, as written. |
217
+ | `extensions` | `ParsedExtensions` | Root `x-*` keys. |
218
+
219
+ ### `ParsedPathItem`
220
+
221
+ `{ id, reference?, summary?, description?, parameters, operations, servers, extensions }`
222
+
223
+ - `parameters` holds the path-level parameters only, in declaration order.
224
+ - `operations` holds this path item's operations in `get`, `put`, `post`, `delete`, `options`, `head`, `patch`, `trace` order. That order matches the exported `httpMethods`.
225
+ - `servers` holds the path item's own servers, or `[]`.
226
+ - `reference` behaves in two ways:
227
+ - A path item that `$ref`s `#/components/pathItems/X` keeps `reference` and takes the target's `id`.
228
+ - A `$ref` into `#/paths/...`, or a `$ref` with sibling keys, is inlined: the siblings are merged over the target and there is no `reference`.
229
+
230
+ ### `ParsedOperation`
231
+
232
+ | Field | Type | Notes |
233
+ | ------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
234
+ | `id` | `string` | e.g. `"#/paths/~1pets~1{id}/get"`. |
235
+ | `operationId` | `string?` | |
236
+ | `method` | `HttpMethodName` | Uppercase: `"GET"`, `"PUT"`, `"POST"`, `"DELETE"`, `"OPTIONS"`, `"HEAD"`, `"PATCH"`, `"TRACE"`. |
237
+ | `path` | `string` | The key of the declaring path item: a URL template, a webhook or `components.pathItems` name, or a callback expression. |
238
+ | `summary`, `description` | `string?` | |
239
+ | `deprecated` | `boolean` | `false` when absent. |
240
+ | `tags` | `ParsedTag[]` | A name the document declares resolves to that tag. Any other name becomes `{ id, name, extensions: {} }`. |
241
+ | `externalDocs` | `ParsedExternalDocs?` | |
242
+ | `request` | `ParsedRequest` | See below. |
243
+ | `responses` | `ParsedOperationResponse[]` | In key order, including `default`. Integer-like codes come first, as in any JS object. `x-*` keys are skipped. |
244
+ | `servers` | `ParsedServer[]` | The operation's servers if it declares any, else the path item's, else the service's. An explicit `servers: []` gives `[]`. |
245
+ | `security` | `ParsedSecurityRequirement[]` | The operation's own requirements, or the service's if it declares none. |
246
+ | `securityDeclaration` | `SecurityDeclaration` | See [Security](#security). |
247
+ | `callbacks` | `ParsedCallbackOperation[]` | Callbacks flattened into operations. |
248
+ | `extensions` | `ParsedExtensions` | |
249
+
250
+ `ParsedCallbackOperation` has the same fields as `ParsedOperation` plus `key`, the callback's name. It has no `callbacks` field, and its `path` is the callback expression (for example `"{$request.body#/url}"`). A recursive callback, such as one that `$ref`s the path item it belongs to, is flattened one level deep.
251
+
252
+ ### `ParsedRequest` and `ParsedParameters`
253
+
254
+ ```ts
255
+ type ParsedParameters = {
256
+ path: ParsedParameter[];
257
+ query: ParsedParameter[];
258
+ headers: ParsedParameter[]; // note: "headers", for parameters with in: "header"
259
+ cookie: ParsedParameter[];
260
+ unknown: ParsedRef[];
261
+ };
262
+ type ParsedRequest = ParsedParameters & { body?: ParsedRequestBody };
263
+ ```
264
+
265
+ Path-level parameters are merged with the operation's own. An operation parameter with the same location and `name` replaces the path-level one in place, so the group keeps its order. `unknown` is reserved for parameter `$ref`s whose target is missing. With the current transformer, such a reference makes `decode` fail instead, so `unknown` is always `[]`.
266
+
267
+ `ParsedRequestBody`: `{ id, reference?, description?, required, contents: ParsedBodyMediaType[], extensions }`. `required` is `false` when absent.
268
+
269
+ ### `ParsedParameter` and `ParsedHeader`
270
+
271
+ `ParsedHeader`: `{ id, reference?, name, style, explode?, required, deprecated, description?, schema?, contents, examples, extensions }`
272
+
273
+ - `name` is the key the header is declared under.
274
+ - `style` is always `"simple"`.
275
+ - `required` and `deprecated` default to `false`.
276
+ - `contents: ParsedMediaType[]` is filled when the header uses `content` instead of `schema`.
277
+
278
+ `ParsedParameter` has every `ParsedHeader` field plus `in: ParameterLocation` (`"path" | "query" | "header" | "cookie"`), `allowEmptyValue?`, and `allowReserved?`. Two of its fields are resolved for you:
279
+
280
+ - `style` (`ParamStyle`): when the document omits it, it defaults by location. `path` and `header` get `"simple"`, and `query` and `cookie` get `"form"`.
281
+ - `required`: always `true` for path parameters.
282
+
283
+ `ParamStyle` is `"simple" | "matrix" | "label" | "form" | "spaceDelimited" | "pipeDelimited" | "deepObject"`.
284
+
285
+ ### Responses
286
+
287
+ - `ParsedResponse`: `{ id, reference?, description?, headers: ParsedHeader[], contents: ParsedBodyMediaType[], links: ParsedLink[], extensions }`
288
+ - `ParsedOperationResponse`: `ParsedResponse` plus `code: string`, which holds a status code (`"200"`), a range (`"4XX"`), or `"default"`.
289
+
290
+ ### Media types and examples
291
+
292
+ - `ParsedMediaType` is used for parameter and header `content`: `{ id, reference?, mediaType, schema?, examples: ParsedExample[], extensions }`.
293
+ - `ParsedBodyMediaType` is used for request and response bodies. It is a `ParsedMediaType` plus `encodings: ParsedEncoding[]`.
294
+ - `ParsedEncoding`: `{ property, mediaType?, style, explode?, allowReserved?, headers: ParsedHeader[], extensions }`.
295
+ - `property` is the key in `encoding`.
296
+ - `mediaType` comes from `contentType`.
297
+ - `style` defaults to `"form"`.
298
+ - `ParsedExample`: `{ id, reference?, key, summary?, description?, value?, externalValue?, extensions }`.
299
+ - A singular `example` keyword becomes an entry with `key: "default"`.
300
+ - Entries of `examples` keep their names as `key`.
301
+ - `ParsedLink`: `{ id, reference?, key, operationRef?, operationId?, parameters?, requestBody?, description?, extensions }`.
302
+ - `ParsedDiscriminator`: `{ propertyName, mapping? }`. This exported schema describes the OpenAPI `discriminator` keyword. Schemas are passed through unchanged, so read `discriminator` from the schema object yourself.
303
+
304
+ ### Servers and tags
305
+
306
+ - `ParsedServer`: `{ id, url, name?, description?, variables?: Record<string, ParsedServerVariable>, extensions }`
307
+ - `ParsedServerVariable`: `{ default, enum?, description?, extensions }`
308
+ - `ParsedTag`: `{ id, name, description?, externalDocs?, extensions }`. `externalDocs` is set only on tags the document declares.
309
+
310
+ ### Security
311
+
312
+ - `ParsedSecurityScheme` is a union discriminated by `type`. Every variant has `{ id, reference?, key, description?, extensions }`, where `key` is the scheme's name in `components.securitySchemes`. The variants add:
313
+ - `"apiKey"`: `name`, `in: "query" | "header" | "cookie"`
314
+ - `"http"`: `scheme`, `bearerFormat?`
315
+ - `"oauth2"`: `flows: ParsedOAuthFlows`, which is `{ implicit?, password?, clientCredentials?, authorizationCode?, extensions }`. Each flow is a `ParsedOAuthFlow`: `{ authorizationUrl?, tokenUrl?, refreshUrl?, scopes: Record<string, string>, extensions }`.
316
+ - `"openIdConnect"`: `openIdConnectUrl`
317
+ - `"mutualTLS"`: no extra fields
318
+ - `ParsedSecurityUse`: `{ scheme: ParsedSecurityScheme, scopes: string[] }`. The full scheme object is embedded, not just its name.
319
+ - `ParsedSecurityRequirement`: `ParsedSecurityUse[]`. Every scheme in a requirement must be satisfied. A `security` array lists alternatives, so any one requirement is enough. An empty requirement (`[]`, from `{}`) makes authentication optional.
320
+ - Requirement entries that name a scheme missing from `components.securitySchemes` are dropped without an error.
321
+ - `SecurityDeclaration` describes where an operation's `security` came from:
322
+
323
+ | Value | Meaning |
324
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
325
+ | `"declared"` | The operation declares a non-empty `security` array. `[{}]` also counts as declared and gives `security: [[]]`. |
326
+ | `"inheritedFromService"` | The operation declares no `security` and the document declares a non-empty one. |
327
+ | `"none"` | The operation declares `security: []`, or neither the operation nor the document declares any security. |
328
+
329
+ ### `ParsedComponents`
330
+
331
+ ```ts
332
+ type ParsedComponents = {
333
+ schemas: Record<string, ParsedSchema>;
334
+ responses: Record<string, ParsedResponse>;
335
+ parameters: Record<string, ParsedParameter>;
336
+ requestBodies: Record<string, ParsedRequestBody>;
337
+ headers: Record<string, ParsedHeader>;
338
+ examples: Record<string, ParsedExample>;
339
+ links: Record<string, ParsedLink>;
340
+ callbacks: Record<string, ParsedCallbackOperation[]>;
341
+ pathItems: Record<string, ParsedPathItem>;
342
+ };
343
+ ```
344
+
345
+ Every key is always present (`{}` when empty). A component that is itself a `$ref` alias (for example `responses.Alias: { $ref: "#/components/responses/Error" }`) is resolved: it keeps `reference` and takes the target's `id`. Operations in `components.pathItems` use the component name as their `path`.
346
+
347
+ ### Walking the model
348
+
349
+ ```ts
350
+ import type { ParsedOperation, ParsedService } from "@shipworthy/openapi-transformer";
351
+
352
+ function jsonResponseSchemas(service: ParsedService) {
353
+ return service.operations.flatMap((operation: ParsedOperation) =>
354
+ operation.responses.flatMap((response) =>
355
+ response.contents
356
+ .filter((content) => content.mediaType === "application/json")
357
+ .map((content) => ({
358
+ operation: operation.operationId ?? `${operation.method} ${operation.path}`,
359
+ code: response.code,
360
+ schema: content.schema, // ParsedSchema | undefined, $refs unresolved
361
+ })),
362
+ ),
363
+ );
364
+ }
365
+
366
+ function requiresAuth(operation: ParsedOperation) {
367
+ // Any requirement with no schemes means anonymous access is allowed.
368
+ return operation.security.length > 0 && operation.security.every((req) => req.length > 0);
369
+ }
370
+ ```
371
+
372
+ ## Supported OpenAPI versions
373
+
374
+ - **Input to `decode`:** an OpenAPI 3.1 document (`ParsedOpenApi`) with only local references.
375
+ - **OpenAPI 3.0:** supported indirectly. `@shipworthy/openapi-parser` accepts `3.0.x` and `3.1.x` sources and upgrades 3.0 to 3.1, including the schemas, before this package sees them. The resulting `service.openapi` is `"3.1.0"`.
376
+ - **Swagger 2.0:** not supported.
377
+
378
+ ## Limitations and caveats
379
+
380
+ - **One-way.** A `ParsedService` cannot be encoded back to OpenAPI.
381
+ - **Schemas are not dereferenced or validated in depth.** A schema only has to be an object or a boolean. Keywords are trusted as written, so `{ properties: { a: "string" } }` passes unchanged.
382
+ - **Only local references.** `decode` follows `#/...` pointers within the document, including percent-encoded ones such as `#/components/responses/caf%C3%A9`. Bundle external references with the parser first.
383
+ - **Fail-fast on bad references.** An unresolved, circular, or wrong-kind `$ref` in a path item, parameter, request body, response, header, example, link, callback, or security scheme fails the whole decode with a `SchemaError`.
384
+ - **`service.operations` covers `paths` only.** Webhook operations are in `service.webhooks[name].operations`, and component path items are in `service.components.pathItems`.
385
+ - **Header styles** are always reported as `"simple"`.
386
+
387
+ ## Troubleshooting
388
+
389
+ - **`SchemaError: Unresolved reference: #/...`:** the `$ref` target does not exist in the bundled document. Check the pointer. Escape `/` as `~1` and `~` as `~0`.
390
+ - **`SchemaError` with `Expected a local reference`:** the document still contains external `$ref`s. Run it through `bundleDocument` or `ParsedOpenApiFromString` from `@shipworthy/openapi-parser` first.
391
+ - **`SchemaError` mentioning `Missing key` or a field path such as `securitySchemes`:** the input is not valid OpenAPI 3.1. The message gives the path.
392
+ - **`SchemaError: The meta model cannot be encoded back to OpenAPI for now.`:** something called `Schema.encode*` on `ParsedServiceFromParsedOpenApi`. That direction is not supported.
393
+ - **A schema `$ref` such as `#/components/schemas/Pet` shows up unresolved in `content.schema`:** this is expected. Look it up in `service.components.schemas`.
394
+
395
+ ## Key facts (for AI agents)
396
+
397
+ - Import from `@shipworthy/openapi-transformer`. It has one entry point, is ESM-only, ships compiled JavaScript, peers on `effect` `^4.0.0`, and runs on Node.js `>=22.19.0` or Bun.
398
+ - Pipeline: `bundleDocument(source)` (from `@shipworthy/openapi-parser`, needs FileSystem/Path/HttpClient layers) → `decode(document)` → `ParsedService`.
399
+ - `decode` is `Schema.decodeEffect(ParsedServiceFromParsedOpenApi)`. It returns `Effect<ParsedService, Schema.SchemaError>` and needs no services. Run it with `Effect.runSync` or `Effect.runPromise`.
400
+ - Generators and plugins must type their input as `ParsedService` and walk it through the exported `Parsed*` types. Schema nodes are `ParsedSchema` (`JsonSchema | boolean`). Do not use a local `{ [key: string]: any }` stand-in.
401
+ - `operations` is flat, contains `paths` only, and is in document order. `method` is uppercase. `path` is the path-item key. `operationId` is optional.
402
+ - `request.{path,query,headers,cookie}` is already merged from path and operation parameters. `request.body` is optional. Responses are an array with `code`.
403
+ - Every node has `id` (the JSON Pointer of its definition) and `extensions`. Nodes reached through a `$ref` also have `reference`.
404
+ - Schemas are passed through as written. Their `$ref`s are not resolved, so look them up in `components.schemas` or `$defs`.
405
+ - Servers and security are already inherited. `securityDeclaration` says whether security was `"declared"`, `"inheritedFromService"`, or `"none"`.
406
+ - Every model schema can be converted to a Standard Schema with `Schema.toStandardSchemaV1`.
407
+
408
+ ## License
409
+
410
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,10 @@
1
+ import { ParsedOpenApi } from "@shipworthy/openapi-parser/schemas/3.1";
2
+ import { type Effect, Schema, type SchemaAST } from "effect";
3
+ import { ParsedService } from "./model/index.ts";
4
+ import type { ParsedServiceSchema } from "./model/service.ts";
5
+ /**
6
+ * Convert a prepared OpenAPI 3.1 document into the HTTP meta model. Input validation and
7
+ * expected transformation failures share the SchemaError channel.
8
+ */
9
+ export declare const ParsedServiceFromParsedOpenApi: Schema.decodeTo<ParsedServiceSchema, typeof ParsedOpenApi>;
10
+ export declare const decode: (input: typeof ParsedServiceFromParsedOpenApi.Encoded, options?: SchemaAST.ParseOptions) => Effect.Effect<ParsedService, Schema.SchemaError>;
package/dist/decode.js ADDED
@@ -0,0 +1,14 @@
1
+ import { ParsedOpenApi } from "@shipworthy/openapi-parser/schemas/3.1";
2
+ import { Schema, SchemaGetter, SchemaTransformation } from "effect";
3
+ import { ParsedService } from "./model/index.js";
4
+ import { transformService } from "./transformers/3.1/index.js";
5
+ const serviceTransformation = SchemaTransformation.makeTransformation({
6
+ decode: SchemaGetter.transformEffect(transformService),
7
+ encode: SchemaGetter.forbidden(() => "The meta model cannot be encoded back to OpenAPI for now."),
8
+ });
9
+ /**
10
+ * Convert a prepared OpenAPI 3.1 document into the HTTP meta model. Input validation and
11
+ * expected transformation failures share the SchemaError channel.
12
+ */
13
+ export const ParsedServiceFromParsedOpenApi = ParsedOpenApi.pipe(Schema.decodeTo(ParsedService, serviceTransformation));
14
+ export const decode = Schema.decodeEffect(ParsedServiceFromParsedOpenApi);
@@ -0,0 +1,2 @@
1
+ export { decode, ParsedServiceFromParsedOpenApi } from "./decode.ts";
2
+ export * from "./model/index.ts";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { decode, ParsedServiceFromParsedOpenApi } from "./decode.js";
2
+ export * from "./model/index.js";
@@ -0,0 +1,31 @@
1
+ import { Schema } from "effect";
2
+ declare const parsedEncodingFields: {
3
+ property: Schema.String;
4
+ mediaType: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.String>>, Schema.optional<Schema.String>, never, never>;
5
+ style: Schema.Literals<readonly ["simple", "matrix", "label", "form", "spaceDelimited", "pipeDelimited", "deepObject"]>;
6
+ explode: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.Boolean>>, Schema.optional<Schema.Boolean>, never, never>;
7
+ allowReserved: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.Boolean>>, Schema.optional<Schema.Boolean>, never, never>;
8
+ headers: Schema.$Array<import("./header.ts").ParsedHeaderSchema>;
9
+ extensions: Schema.$Record<Schema.String, Schema.Unknown>;
10
+ };
11
+ export interface ParsedEncodingSchema extends Schema.Struct<typeof parsedEncodingFields> {
12
+ }
13
+ export declare const ParsedEncoding: ParsedEncodingSchema;
14
+ export interface ParsedEncoding extends Schema.Schema.Type<ParsedEncodingSchema> {
15
+ }
16
+ declare const parsedBodyMediaTypeFields: {
17
+ id: Schema.String;
18
+ extensions: Schema.$Record<Schema.String, Schema.Unknown>;
19
+ reference: Schema.decodeTo<Schema.optionalKey<Schema.toType<import("./reference.ts").ParsedRefSchema>>, Schema.optional<import("./reference.ts").ParsedRefSchema>, never, never>;
20
+ mediaType: Schema.String;
21
+ schema: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.Codec<import("./schema.ts").ParsedSchema, import("./schema.ts").ParsedSchema, never, never>>>, Schema.optional<Schema.Codec<import("./schema.ts").ParsedSchema, import("./schema.ts").ParsedSchema, never, never>>, never, never>;
22
+ examples: Schema.$Array<import("./example.ts").ParsedExampleSchema>;
23
+ encodings: Schema.$Array<ParsedEncodingSchema>;
24
+ };
25
+ export interface ParsedBodyMediaTypeSchema extends Schema.Struct<typeof parsedBodyMediaTypeFields> {
26
+ }
27
+ /** A media type of a request or response body. Only bodies have encodings. */
28
+ export declare const ParsedBodyMediaType: ParsedBodyMediaTypeSchema;
29
+ export interface ParsedBodyMediaType extends Schema.Schema.Type<ParsedBodyMediaTypeSchema> {
30
+ }
31
+ export {};
@@ -0,0 +1,22 @@
1
+ import { Schema } from "effect";
2
+ import { ParsedExtensions } from "./extensions.js";
3
+ import { ParsedHeader } from "./header.js";
4
+ import { ParsedMediaType } from "./media-type.js";
5
+ import { optionalBoolean, optionalString } from "./optional.js";
6
+ import { ParamStyle } from "./param-style.js";
7
+ const parsedEncodingFields = {
8
+ property: Schema.String,
9
+ mediaType: optionalString,
10
+ style: ParamStyle,
11
+ explode: optionalBoolean,
12
+ allowReserved: optionalBoolean,
13
+ headers: Schema.Array(ParsedHeader),
14
+ extensions: ParsedExtensions,
15
+ };
16
+ export const ParsedEncoding = Schema.Struct(parsedEncodingFields);
17
+ const parsedBodyMediaTypeFields = {
18
+ ...ParsedMediaType.fields,
19
+ encodings: Schema.Array(ParsedEncoding),
20
+ };
21
+ /** A media type of a request or response body. Only bodies have encodings. */
22
+ export const ParsedBodyMediaType = Schema.Struct(parsedBodyMediaTypeFields);
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+ import { ParsedSchema } from "./schema.ts";
3
+ declare const parsedComponentsFields: {
4
+ schemas: Schema.$Record<Schema.String, Schema.Codec<ParsedSchema, ParsedSchema, never, never>>;
5
+ responses: Schema.$Record<Schema.String, import("./response.ts").ParsedResponseSchema>;
6
+ parameters: Schema.$Record<Schema.String, import("./parameter.ts").ParsedParameterSchema>;
7
+ requestBodies: Schema.$Record<Schema.String, import("./request.ts").ParsedRequestBodySchema>;
8
+ headers: Schema.$Record<Schema.String, import("./header.ts").ParsedHeaderSchema>;
9
+ examples: Schema.$Record<Schema.String, import("./example.ts").ParsedExampleSchema>;
10
+ links: Schema.$Record<Schema.String, import("./link.ts").ParsedLinkSchema>;
11
+ callbacks: Schema.$Record<Schema.String, Schema.$Array<import("./operation.ts").ParsedCallbackOperationSchema>>;
12
+ pathItems: Schema.$Record<Schema.String, import("./path-item.ts").ParsedPathItemSchema>;
13
+ };
14
+ export interface ParsedComponentsSchema extends Schema.Struct<typeof parsedComponentsFields> {
15
+ }
16
+ export declare const ParsedComponents: ParsedComponentsSchema;
17
+ export interface ParsedComponents extends Schema.Schema.Type<ParsedComponentsSchema> {
18
+ }
19
+ export {};
@@ -0,0 +1,22 @@
1
+ import { Schema } from "effect";
2
+ import { ParsedExample } from "./example.js";
3
+ import { ParsedHeader } from "./header.js";
4
+ import { ParsedLink } from "./link.js";
5
+ import { ParsedCallbackOperation } from "./operation.js";
6
+ import { ParsedParameter } from "./parameter.js";
7
+ import { ParsedPathItem } from "./path-item.js";
8
+ import { ParsedRequestBody } from "./request.js";
9
+ import { ParsedResponse } from "./response.js";
10
+ import { ParsedSchema } from "./schema.js";
11
+ const parsedComponentsFields = {
12
+ schemas: Schema.Record(Schema.String, ParsedSchema),
13
+ responses: Schema.Record(Schema.String, ParsedResponse),
14
+ parameters: Schema.Record(Schema.String, ParsedParameter),
15
+ requestBodies: Schema.Record(Schema.String, ParsedRequestBody),
16
+ headers: Schema.Record(Schema.String, ParsedHeader),
17
+ examples: Schema.Record(Schema.String, ParsedExample),
18
+ links: Schema.Record(Schema.String, ParsedLink),
19
+ callbacks: Schema.Record(Schema.String, Schema.Array(ParsedCallbackOperation)),
20
+ pathItems: Schema.Record(Schema.String, ParsedPathItem),
21
+ };
22
+ export const ParsedComponents = Schema.Struct(parsedComponentsFields);
@@ -0,0 +1,18 @@
1
+ import { Schema } from "effect";
2
+ declare const parsedExampleFields: {
3
+ id: Schema.String;
4
+ extensions: Schema.$Record<Schema.String, Schema.Unknown>;
5
+ reference: Schema.decodeTo<Schema.optionalKey<Schema.toType<import("./reference.ts").ParsedRefSchema>>, Schema.optional<import("./reference.ts").ParsedRefSchema>, never, never>;
6
+ /** The example's name. An `example` keyword becomes the example keyed `default`. */
7
+ key: Schema.String;
8
+ summary: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.String>>, Schema.optional<Schema.String>, never, never>;
9
+ description: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.String>>, Schema.optional<Schema.String>, never, never>;
10
+ value: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.Unknown>>, Schema.optional<Schema.Unknown>, never, never>;
11
+ externalValue: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.String>>, Schema.optional<Schema.String>, never, never>;
12
+ };
13
+ export interface ParsedExampleSchema extends Schema.Struct<typeof parsedExampleFields> {
14
+ }
15
+ export declare const ParsedExample: ParsedExampleSchema;
16
+ export interface ParsedExample extends Schema.Schema.Type<ParsedExampleSchema> {
17
+ }
18
+ export {};
@@ -0,0 +1,14 @@
1
+ import { Schema } from "effect";
2
+ import { node, shareable } from "./fields.js";
3
+ import { optionalField, optionalString } from "./optional.js";
4
+ const parsedExampleFields = {
5
+ ...node,
6
+ ...shareable,
7
+ /** The example's name. An `example` keyword becomes the example keyed `default`. */
8
+ key: Schema.String,
9
+ summary: optionalString,
10
+ description: optionalString,
11
+ value: optionalField(Schema.Unknown),
12
+ externalValue: optionalString,
13
+ };
14
+ export const ParsedExample = Schema.Struct(parsedExampleFields);
@@ -0,0 +1,4 @@
1
+ import { Schema } from "effect";
2
+ /** `x-` keys of a node, as written. */
3
+ export declare const ParsedExtensions: Schema.$Record<Schema.String, Schema.Unknown>;
4
+ export type ParsedExtensions = typeof ParsedExtensions.Type;
@@ -0,0 +1,3 @@
1
+ import { Schema } from "effect";
2
+ /** `x-` keys of a node, as written. */
3
+ export const ParsedExtensions = Schema.Record(Schema.String, Schema.Unknown);
@@ -0,0 +1,12 @@
1
+ import { Schema } from "effect";
2
+ declare const parsedExternalDocsFields: {
3
+ url: Schema.String;
4
+ description: Schema.decodeTo<Schema.optionalKey<Schema.toType<Schema.String>>, Schema.optional<Schema.String>, never, never>;
5
+ extensions: Schema.$Record<Schema.String, Schema.Unknown>;
6
+ };
7
+ export interface ParsedExternalDocsSchema extends Schema.Struct<typeof parsedExternalDocsFields> {
8
+ }
9
+ export declare const ParsedExternalDocs: ParsedExternalDocsSchema;
10
+ export interface ParsedExternalDocs extends Schema.Schema.Type<ParsedExternalDocsSchema> {
11
+ }
12
+ export {};