@shipworthy/openapi-parser 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.
- package/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/bundle.d.ts +973 -0
- package/dist/bundle.js +11 -0
- package/dist/codec.d.ts +2188 -0
- package/dist/codec.js +42 -0
- package/dist/document.d.ts +9 -0
- package/dist/document.js +28 -0
- package/dist/errors.d.ts +105 -0
- package/dist/errors.js +162 -0
- package/dist/hoist.d.ts +10 -0
- package/dist/hoist.js +444 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +163 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/load.d.ts +17 -0
- package/dist/load.js +128 -0
- package/dist/loader.d.ts +11 -0
- package/dist/loader.js +89 -0
- package/dist/parsers.d.ts +5 -0
- package/dist/parsers.js +90 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +446 -0
- package/dist/plugins.d.ts +95 -0
- package/dist/plugins.js +61 -0
- package/dist/pointer.d.ts +7 -0
- package/dist/pointer.js +80 -0
- package/dist/run.d.ts +17 -0
- package/dist/run.js +313 -0
- package/dist/schemas/3.1/callback.d.ts +205 -0
- package/dist/schemas/3.1/callback.js +4 -0
- package/dist/schemas/3.1/components.d.ts +801 -0
- package/dist/schemas/3.1/components.js +25 -0
- package/dist/schemas/3.1/contact.d.ts +7 -0
- package/dist/schemas/3.1/contact.js +7 -0
- package/dist/schemas/3.1/document.d.ts +1093 -0
- package/dist/schemas/3.1/document.js +25 -0
- package/dist/schemas/3.1/encoding.d.ts +14 -0
- package/dist/schemas/3.1/encoding.js +11 -0
- package/dist/schemas/3.1/example.d.ts +8 -0
- package/dist/schemas/3.1/example.js +10 -0
- package/dist/schemas/3.1/external-docs.d.ts +6 -0
- package/dist/schemas/3.1/external-docs.js +6 -0
- package/dist/schemas/3.1/header.d.ts +194 -0
- package/dist/schemas/3.1/header.js +29 -0
- package/dist/schemas/3.1/http-method.d.ts +4 -0
- package/dist/schemas/3.1/http-method.js +12 -0
- package/dist/schemas/3.1/index.d.ts +29 -0
- package/dist/schemas/3.1/index.js +29 -0
- package/dist/schemas/3.1/info.d.ts +19 -0
- package/dist/schemas/3.1/info.js +13 -0
- package/dist/schemas/3.1/license.d.ts +7 -0
- package/dist/schemas/3.1/license.js +9 -0
- package/dist/schemas/3.1/link.d.ts +18 -0
- package/dist/schemas/3.1/link.js +13 -0
- package/dist/schemas/3.1/media-type.d.ts +27 -0
- package/dist/schemas/3.1/media-type.js +14 -0
- package/dist/schemas/3.1/oauth-flow.d.ts +8 -0
- package/dist/schemas/3.1/oauth-flow.js +8 -0
- package/dist/schemas/3.1/oauth-flows.d.ts +28 -0
- package/dist/schemas/3.1/oauth-flows.js +13 -0
- package/dist/schemas/3.1/object.d.ts +5 -0
- package/dist/schemas/3.1/object.js +17 -0
- package/dist/schemas/3.1/operation.d.ts +1040 -0
- package/dist/schemas/3.1/operation.js +25 -0
- package/dist/schemas/3.1/parameter.d.ts +101 -0
- package/dist/schemas/3.1/parameter.js +33 -0
- package/dist/schemas/3.1/path-item.d.ts +127 -0
- package/dist/schemas/3.1/path-item.js +22 -0
- package/dist/schemas/3.1/paths.d.ts +126 -0
- package/dist/schemas/3.1/paths.js +4 -0
- package/dist/schemas/3.1/reference.d.ts +13 -0
- package/dist/schemas/3.1/reference.js +11 -0
- package/dist/schemas/3.1/request-body.d.ts +31 -0
- package/dist/schemas/3.1/request-body.js +8 -0
- package/dist/schemas/3.1/response.d.ts +149 -0
- package/dist/schemas/3.1/response.js +12 -0
- package/dist/schemas/3.1/responses.d.ts +152 -0
- package/dist/schemas/3.1/responses.js +7 -0
- package/dist/schemas/3.1/schema.d.ts +4 -0
- package/dist/schemas/3.1/schema.js +6 -0
- package/dist/schemas/3.1/security-requirement.d.ts +3 -0
- package/dist/schemas/3.1/security-requirement.js +2 -0
- package/dist/schemas/3.1/security-scheme.d.ts +49 -0
- package/dist/schemas/3.1/security-scheme.js +25 -0
- package/dist/schemas/3.1/server-variable.d.ts +7 -0
- package/dist/schemas/3.1/server-variable.js +7 -0
- package/dist/schemas/3.1/server.d.ts +11 -0
- package/dist/schemas/3.1/server.js +8 -0
- package/dist/schemas/3.1/tag.d.ts +10 -0
- package/dist/schemas/3.1/tag.js +8 -0
- package/dist/upgraders/3.0-to-3.1.d.ts +9 -0
- package/dist/upgraders/3.0-to-3.1.js +310 -0
- package/dist/url.d.ts +29 -0
- package/dist/url.js +255 -0
- package/dist/yaml.d.ts +6 -0
- package/dist/yaml.js +235 -0
- package/package.json +50 -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,362 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shipworthy/openapi-parser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Load an OpenAPI 3.0 or 3.1 document from a file path, a URL, a JSON or YAML string, a `Buffer`, a Node `Readable`, or a plain object. The parser bundles every external `$ref` into the document's `components`, upgrades OpenAPI 3.0 to 3.1, and validates the result against an [Effect](https://effect.website) schema of the OpenAPI 3.1 object model. You get back one self-contained, typed `ParsedOpenApi` document. Use it to turn a multi-file or 3.0 spec into a single 3.1 document before you inspect it or generate code from it. The other `@shipworthy/*` packages use it as their first step. `@shipworthy/openapi-generator` calls `bundleDocument`, and `@shipworthy/openapi-transformer` decodes the resulting `ParsedOpenApi` into its `ParsedService` model.
|
|
4
|
+
|
|
5
|
+
The API is Effect-native. Every entry point is an `Effect` or an Effect `Schema` codec, so you need [`effect`](https://www.npmjs.com/package/effect) v4 to run it.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @shipworthy/openapi-parser effect @effect/platform-node
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Requirements
|
|
14
|
+
|
|
15
|
+
| Requirement | Details |
|
|
16
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `effect` | `^4.0.0`, a peer dependency. Install it yourself so your application and the parser share one copy to run the returned effects and to use the schemas. |
|
|
18
|
+
| Platform | `@effect/platform-node` `^4.0.0` is a regular dependency. Install it yourself only if your code imports it, as the examples here do for `NodeServices`. The parser imports `node:stream`, `node:url`, `node:path`, `node:dns/promises`, `node:net`, and the `Buffer` global, so it needs Node.js `>=22.19.0` or Bun. It does not run in browsers. |
|
|
19
|
+
| Module format | ESM only (`"type": "module"`). Ships compiled JavaScript with `.d.ts` declarations; no TypeScript loader or bundler is needed. |
|
|
20
|
+
| Peer deps | `effect` `^4.0.0`. |
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
`openapi.yaml`:
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
openapi: 3.0.3
|
|
28
|
+
info: { title: Pets, version: "1.0.0" }
|
|
29
|
+
paths:
|
|
30
|
+
/pets:
|
|
31
|
+
get:
|
|
32
|
+
responses:
|
|
33
|
+
"200":
|
|
34
|
+
description: ok
|
|
35
|
+
content:
|
|
36
|
+
application/json:
|
|
37
|
+
schema: { $ref: "./pet.yaml" }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`pet.yaml`:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
type: object
|
|
44
|
+
required: [name]
|
|
45
|
+
properties:
|
|
46
|
+
name: { type: string }
|
|
47
|
+
tag: { type: string, nullable: true }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`main.ts`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { NodeServices } from "@effect/platform-node";
|
|
54
|
+
import { bundleDocument } from "@shipworthy/openapi-parser";
|
|
55
|
+
import { Effect, Layer } from "effect";
|
|
56
|
+
import { FetchHttpClient } from "effect/http";
|
|
57
|
+
|
|
58
|
+
const program = Effect.gen(function* () {
|
|
59
|
+
const document = yield* bundleDocument("./openapi.yaml");
|
|
60
|
+
console.log(JSON.stringify(document, null, 2));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await Effect.runPromise(
|
|
64
|
+
program.pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Output (abridged):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"openapi": "3.1.0",
|
|
73
|
+
"info": { "title": "Pets", "version": "1.0.0" },
|
|
74
|
+
"paths": {
|
|
75
|
+
"/pets": {
|
|
76
|
+
"get": {
|
|
77
|
+
"responses": {
|
|
78
|
+
"200": {
|
|
79
|
+
"description": "ok",
|
|
80
|
+
"content": {
|
|
81
|
+
"application/json": { "schema": { "$ref": "#/components/schemas/pet" } }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"components": {
|
|
89
|
+
"schemas": {
|
|
90
|
+
"pet": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["name"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"name": { "type": "string" },
|
|
95
|
+
"tag": { "type": ["string", "null"] }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The external file became `#/components/schemas/pet`, the version is now `3.1.0`, and `nullable: true` became `type: ["string", "null"]`.
|
|
104
|
+
|
|
105
|
+
## API
|
|
106
|
+
|
|
107
|
+
The package has two entry points:
|
|
108
|
+
|
|
109
|
+
| Import path | Exports |
|
|
110
|
+
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| `@shipworthy/openapi-parser` | `bundleDocument`, `ParsedOpenApiFromString`, the types `BundleOptions` and `ParseOptions`, and every OpenAPI 3.1 **type** listed below |
|
|
112
|
+
| `@shipworthy/openapi-parser/schemas/3.1` | The OpenAPI 3.1 Effect **schema values**, along with their types |
|
|
113
|
+
|
|
114
|
+
At runtime, the root entry point exports only `bundleDocument` and `ParsedOpenApiFromString`. Import the schema values from `/schemas/3.1`.
|
|
115
|
+
|
|
116
|
+
### `bundleDocument(source, options?)`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
declare function bundleDocument(
|
|
120
|
+
source: unknown,
|
|
121
|
+
options?: BundleOptions,
|
|
122
|
+
): Effect.Effect<
|
|
123
|
+
ParsedOpenApi,
|
|
124
|
+
Schema.SchemaError,
|
|
125
|
+
FileSystem.FileSystem | Path.Path | HttpClient.HttpClient
|
|
126
|
+
>;
|
|
127
|
+
|
|
128
|
+
interface BundleOptions {
|
|
129
|
+
/** Base for relative path strings and for relative `$ref`s in inline (text, Buffer, stream, or object) sources. Default: process.cwd(). */
|
|
130
|
+
readonly cwd?: URL | string;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This function loads a source, bundles it, upgrades it, and validates it as OpenAPI 3.1. How it reads `source` depends on the value:
|
|
135
|
+
|
|
136
|
+
| `source` | Behavior |
|
|
137
|
+
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `URL` with `file:` protocol | Reads the file through `FileSystem`. Relative refs resolve against the file. |
|
|
139
|
+
| `URL` with `http:` or `https:` protocol | Fetches the file with the provided `HttpClient`. A non-2xx response fails. Relative refs resolve against the URL. |
|
|
140
|
+
| `string` starting with `http://`, `https://`, or `file://` | Treated as a URL, as in the rows above. |
|
|
141
|
+
| File path `string` (see below) | Resolved against `cwd` and read through `FileSystem`, as a `file:` URL is. Relative refs resolve against the file. |
|
|
142
|
+
| Any other `string` | Parsed as document text. Text that starts with `{` or `[` is parsed as JSON first and falls back to YAML. Anything else is parsed as YAML. A leading BOM is stripped. |
|
|
143
|
+
| `Buffer` | Decoded as UTF-8 text. |
|
|
144
|
+
| Node `Readable` | Read to the end and decoded as text. |
|
|
145
|
+
| Plain object | Used directly, without a serialization round trip. |
|
|
146
|
+
|
|
147
|
+
A string is a file path when, after trimming whitespace, it is a single line (no newline) and either:
|
|
148
|
+
|
|
149
|
+
- is absolute according to Effect's `Path` service on the current platform (for example `/srv/openapi.yaml` on Unix or `C:\specs\openapi.yaml` on Windows), or starts with `./`, `../`, `.\`, or `..\`, or
|
|
150
|
+
- ends in `.json`, `.yaml`, or `.yml` (any case), contains no `: `, and doesn't start with `{` or `[`.
|
|
151
|
+
|
|
152
|
+
No inline OpenAPI document matches this rule. A one-line YAML document needs `key: value` or a `{ ... }` flow mapping, and JSON starts with `{`. A path that doesn't exist fails with the `FileSystem` error, such as `NotFound: FileSystem.readFile (/abs/openapi.yaml)`.
|
|
153
|
+
|
|
154
|
+
`cwd` accepts a directory path, which is resolved to an absolute path, or a `URL`. It is the base for relative path strings and for refs in inline sources. A file or URL source resolves its refs against itself.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { bundleDocument } from "@shipworthy/openapi-parser";
|
|
158
|
+
|
|
159
|
+
// File paths, relative to process.cwd() or absolute. Refs resolve against the file.
|
|
160
|
+
bundleDocument("./openapi.yaml");
|
|
161
|
+
bundleDocument("/srv/specs/openapi.yaml");
|
|
162
|
+
bundleDocument("openapi.yaml", { cwd: "/srv/specs" });
|
|
163
|
+
bundleDocument(new URL("file:///srv/specs/openapi.yaml"));
|
|
164
|
+
bundleDocument("https://api.example.com/openapi.json");
|
|
165
|
+
|
|
166
|
+
// Inline text whose `./pet.json` ref resolves against /specs/pet.json
|
|
167
|
+
bundleDocument(
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
openapi: "3.1.0",
|
|
170
|
+
info: { title: "Pets", version: "1" },
|
|
171
|
+
components: { schemas: { Pet: { $ref: "./pet.json" } } },
|
|
172
|
+
}),
|
|
173
|
+
{ cwd: "/specs" },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// An object you already have in memory
|
|
177
|
+
bundleDocument({ openapi: "3.0.3", info: { title: "Pets", version: "1" }, paths: {} });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Required services.** You provide `FileSystem`, `Path`, and `HttpClient` yourself. On Node or Bun, use `Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)`, as the quick start does. `FetchHttpClient` comes from `effect/http`. In tests, `FileSystem.layerNoop({ ... })` and a stub `HttpClient.make(...)` work, as the package's own tests show.
|
|
181
|
+
|
|
182
|
+
**Errors.** Every failure is a single `Schema.SchemaError` (`_tag: "SchemaError"`) with a readable `message`. The parser's own tests assert these messages:
|
|
183
|
+
|
|
184
|
+
| Situation | `message` |
|
|
185
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
186
|
+
| `""`, `null`, or `undefined` | `Empty schema. Please specify a URL or file path` |
|
|
187
|
+
| Swagger 2 (`swagger: "2.0"`) | `Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.` |
|
|
188
|
+
| No `openapi` field | ``Unsupported schema format, expected `openapi: 3.x` `` |
|
|
189
|
+
| `openapi` outside 3.x, such as `"2.0"` | `Unsupported OpenAPI version: 2.0` |
|
|
190
|
+
| `openapi` is 3.x but not 3.0 or 3.1, such as `3.2.0` | `Expected a string matching the RegExp ^3\.[01]\.\d+(?:[-+].*)?$` (at `["openapi"]`) |
|
|
191
|
+
| Text that parses to a non-object, such as `"[]"` | `Expected an OpenAPI document object. Got Array` |
|
|
192
|
+
| Unsupported input type, such as a number | `Expected string, object, or Buffer. Got number` |
|
|
193
|
+
| Malformed JSON or YAML, or an HTTP error on the root URL | The underlying parse or HTTP error message (an HTTP 404 includes `404`) |
|
|
194
|
+
| A `$ref` that can't be loaded | `<url>: <reason>`, for example `/specs/missing.json: Error opening file ...` |
|
|
195
|
+
| Bad JSON pointer, circular `$ref` alias | `Missing $ref pointer ...`, `Invalid $ref pointer ...`, `Definition "A" is a circular alias.` |
|
|
196
|
+
| Document breaks the OpenAPI 3.1 schema | The Effect schema issue for the offending path |
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const result =
|
|
200
|
+
yield *
|
|
201
|
+
bundleDocument(source).pipe(
|
|
202
|
+
Effect.catchTag("SchemaError", (error) => Effect.die(`Invalid spec: ${error.message}`)),
|
|
203
|
+
);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `ParsedOpenApiFromString(options)`
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
declare function ParsedOpenApiFromString(options: ParseOptions): Schema.Codec<ParsedOpenApi, string, ...>;
|
|
210
|
+
|
|
211
|
+
interface ParseOptions {
|
|
212
|
+
/** The root document's URL or absolute file path, used to resolve relative references. */
|
|
213
|
+
readonly sourceUrl: URL | string;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This function returns an Effect `Schema` codec from a `string` to `ParsedOpenApi`.
|
|
218
|
+
|
|
219
|
+
The codec factory validates `sourceUrl` and throws `SchemaError` for invalid options. `bundleDocument` validates its optional `cwd` and reports invalid options through its `SchemaError` channel.
|
|
220
|
+
|
|
221
|
+
- **Decoding** parses JSON or YAML (flow-style YAML, indented YAML, and JSON with a BOM all work), checks that the version is `3.0.x` or `3.1.x`, bundles external refs relative to `sourceUrl`, upgrades 3.0 to 3.1, and validates the result. The decode effect requires `FileSystem` and `HttpClient`.
|
|
222
|
+
- **Encoding** serializes a `ParsedOpenApi` as JSON with 2-space indentation. Values that can't be represented in JSON, such as a `bigint`, fail in the error channel. Encoded output decodes back to an equal document.
|
|
223
|
+
|
|
224
|
+
Use this codec when you already have the text and want to compose it with other Effect schemas. Use `bundleDocument` when you want the package to do the loading too.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { readFile } from "node:fs/promises";
|
|
228
|
+
import { NodeServices } from "@effect/platform-node";
|
|
229
|
+
import { ParsedOpenApiFromString } from "@shipworthy/openapi-parser";
|
|
230
|
+
import { Effect, Layer, Schema } from "effect";
|
|
231
|
+
import { FetchHttpClient } from "effect/http";
|
|
232
|
+
|
|
233
|
+
const sourceUrl = new URL("file:///srv/specs/openapi.yaml");
|
|
234
|
+
const codec = ParsedOpenApiFromString({ sourceUrl });
|
|
235
|
+
|
|
236
|
+
const program = Effect.gen(function* () {
|
|
237
|
+
const text = yield* Effect.promise(() => readFile(sourceUrl, "utf8"));
|
|
238
|
+
const document = yield* Schema.decodeEffect(codec)(text);
|
|
239
|
+
const json = yield* Schema.encodeEffect(codec)(document); // pretty-printed JSON string
|
|
240
|
+
return { document, json };
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await Effect.runPromise(
|
|
244
|
+
program.pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer))),
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Every failure surfaces as `Schema.SchemaError`, including broken syntax, an unsupported version, an invalid structure, and a missing referenced file.
|
|
249
|
+
|
|
250
|
+
### OpenAPI 3.1 schemas and types
|
|
251
|
+
|
|
252
|
+
`@shipworthy/openapi-parser/schemas/3.1` exports one Effect `Schema` per OpenAPI 3.1 object. The root entry point exports a TypeScript type of the same name for each schema (`export type *`).
|
|
253
|
+
|
|
254
|
+
| Name | OpenAPI object |
|
|
255
|
+
| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
256
|
+
| `ParsedOpenApi` | The OpenAPI document (`openapi` must match `3.1.x`) |
|
|
257
|
+
| `OpenApiInfo`, `OpenApiContact`, `OpenApiLicense` | Info, Contact, License |
|
|
258
|
+
| `OpenApiServer`, `OpenApiServerVariable` | Server, Server Variable |
|
|
259
|
+
| `OpenApiPaths`, `OpenApiPathItem`, `OpenApiOperation` | Paths, Path Item, Operation |
|
|
260
|
+
| `OpenApiHttpMethod` | Lowercase operation keys; `.literals` provides their traversal order |
|
|
261
|
+
| `OpenApiParameter`, `OpenApiHeader` | Parameter, Header |
|
|
262
|
+
| `OpenApiRequestBody`, `OpenApiMediaType`, `OpenApiEncoding` | Request Body, Media Type, Encoding |
|
|
263
|
+
| `OpenApiResponses`, `OpenApiResponse` | Responses, Response |
|
|
264
|
+
| `OpenApiCallback`, `OpenApiLink`, `OpenApiExample` | Callback, Link, Example |
|
|
265
|
+
| `OpenApiComponents` | Components |
|
|
266
|
+
| `OpenApiSchema` | Schema Object: any JSON object or a boolean |
|
|
267
|
+
| `OpenApiReference` | Reference Object (`$ref`, optional `summary` and `description`) |
|
|
268
|
+
| `OpenApiSecurityScheme`, `OpenApiSecurityRequirement`, `OpenApiOAuthFlows`, `OpenApiOAuthFlow` | Security objects |
|
|
269
|
+
| `OpenApiTag`, `OpenApiExternalDocs` | Tag, External Documentation |
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import type { ParsedOpenApi, OpenApiOperation } from "@shipworthy/openapi-parser";
|
|
273
|
+
import { ParsedOpenApi as ParsedOpenApiSchema } from "@shipworthy/openapi-parser/schemas/3.1";
|
|
274
|
+
import { Schema } from "effect";
|
|
275
|
+
|
|
276
|
+
// Validate an already-bundled 3.1 object without loading anything
|
|
277
|
+
const document: ParsedOpenApi = Schema.decodeUnknownSync(ParsedOpenApiSchema)(input);
|
|
278
|
+
const getPets: OpenApiOperation | undefined = document.paths?.["/pets"]?.get;
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
How validation works:
|
|
282
|
+
|
|
283
|
+
- Every declared field is validated. A malformed field is rejected, not dropped. Rejected examples include a missing `info.title`, a server without `url`, an `apiKey` scheme with `in: "path"`, a response without `description`, a path key that doesn't start with `/`, a response key that isn't a status code, `2XX`, or `default`, and a schema that isn't an object or boolean.
|
|
284
|
+
- A path parameter must have `required: true`.
|
|
285
|
+
- Unknown fields and `x-` extensions are kept on every object. Patterned maps such as `paths` and `responses` also accept `x-` keys.
|
|
286
|
+
- Reference Objects are allowed only where the specification allows `$ref`.
|
|
287
|
+
- Schema Objects are not checked beyond being an object or a boolean. JSON Schema keywords, custom keywords, and `$schema` dialects pass through unchanged.
|
|
288
|
+
|
|
289
|
+
## What the output looks like
|
|
290
|
+
|
|
291
|
+
The parser applies these transformations, each confirmed by the package's tests.
|
|
292
|
+
|
|
293
|
+
**Bundling**
|
|
294
|
+
|
|
295
|
+
- External `$ref`s in component positions (schemas, parameters, responses, request bodies, headers, examples, links, callbacks, and security schemes) are moved into the matching `components.*` map and replaced with a local `#/components/...` ref. The component name comes from the file name, so `models/pet.yaml` becomes `pet`. When two different files would share a name, the parser adds a suffix such as `pet-2`. The same external file referenced twice is loaded once and gets one component name.
|
|
296
|
+
- A `$ref` to an external file directly at `components.schemas.X` inlines that file's contents as `X`. Later refs to the same file point at `#/components/schemas/X`.
|
|
297
|
+
- `discriminator.mapping` values that point at external files are rewritten to the bundled component refs.
|
|
298
|
+
- `externalValue` on an Example is fetched and inlined as `value`.
|
|
299
|
+
- An absolute `$id` inside a schema is resolved, and its `$defs` are moved into `components.schemas`.
|
|
300
|
+
- Recursive schemas stay as `$ref`s. A circular alias such as `A: {$ref: B}` with `B: {$ref: A}` is an error.
|
|
301
|
+
- Boolean schemas (`true` and `false`) are preserved.
|
|
302
|
+
|
|
303
|
+
**Upgrading from 3.0 to 3.1** (only when `openapi` is `3.0.x`; the output says `openapi: "3.1.0"`)
|
|
304
|
+
|
|
305
|
+
- `type: T, nullable: true` becomes `type: [T, "null"]`, and `null` is added to any `enum`.
|
|
306
|
+
- `nullable` on a `$ref`, a composition (`allOf`, `anyOf`, or `oneOf`), or an untyped schema becomes `anyOf: [<schema>, { type: "null" }]`. Annotations such as `description` stay outside the `anyOf`. No `nullable` keyword remains anywhere in the output.
|
|
307
|
+
- A boolean `exclusiveMinimum` or `exclusiveMaximum` becomes the numeric form. For example, `minimum: 0, exclusiveMinimum: true` becomes `exclusiveMinimum: 0`.
|
|
308
|
+
- `example` on a schema becomes `examples: [...]`. `example` on a parameter is left as it is.
|
|
309
|
+
- Tuple `items: [...]` becomes `prefixItems`, and `additionalItems` becomes `items`.
|
|
310
|
+
- A `$ref` with sibling keywords becomes `allOf: [{ $ref }, { ...siblings }]`. `readOnly` and `writeOnly` stay on the outer schema.
|
|
311
|
+
|
|
312
|
+
## Remote references and network safety
|
|
313
|
+
|
|
314
|
+
When a document refers to an `http(s)` URL, the parser follows these rules:
|
|
315
|
+
|
|
316
|
+
- References to loopback, private, link-local, and similar addresses are blocked. This includes `localhost`, `*.local`, `*.internal`, `*.lan`, and private IPv4 and IPv6 ranges. Hosts that resolve to blocked addresses are also rejected. The parser makes one exception: refs on the same origin as the root document's URL are allowed. This is how a spec served from `http://localhost:3000/...` can load its siblings.
|
|
317
|
+
- A blocked reference fails with `<url>: Could not find resolver for "<url>"`.
|
|
318
|
+
- Each request has a 60-second timeout, retries transient failures up to 3 times, and follows up to 5 redirects. `Authorization`, `Proxy-Authorization`, and `Cookie` headers are dropped on cross-origin redirects.
|
|
319
|
+
- Public-host references are downloaded with the parser's own undici-based client, not the `HttpClient` you provide. Your `HttpClient` fetches the root URL and same-origin private refs.
|
|
320
|
+
|
|
321
|
+
None of these settings can be configured through the public API.
|
|
322
|
+
|
|
323
|
+
## Supported inputs and versions
|
|
324
|
+
|
|
325
|
+
- **OpenAPI:** 3.0.x and 3.1.x, with optional pre-release or build suffixes such as `3.1.0-rc1`. The output is always 3.1.
|
|
326
|
+
- **Rejected:** Swagger 2.0, OpenAPI 3.2 and later, and anything without a 3.x `openapi` field.
|
|
327
|
+
- **Formats:** JSON and YAML. YAML merge keys (`<<`) and compact nested sequences (`- - a`) are supported.
|
|
328
|
+
|
|
329
|
+
## Limitations and caveats
|
|
330
|
+
|
|
331
|
+
- A plain string that isn't an `http://`, `https://`, or `file://` URL is parsed as document **content**. Passing `"./openapi.yaml"` fails with `Expected an OpenAPI document object. Got string`. Use `pathToFileURL(path)` or a `file://` URL instead.
|
|
332
|
+
- `bundleDocument` has one option, `cwd`. You can't configure custom resolvers, HTTP headers, timeouts, or a "keep remote refs" mode.
|
|
333
|
+
- The output is bundled, not fully dereferenced. Local `$ref`s stay as `#/components/...` pointers.
|
|
334
|
+
- Schema Objects are validated only as object-or-boolean. Use a JSON Schema validator if you need deeper checks.
|
|
335
|
+
- The parser can add empty containers, such as `components: { schemas: {} }`, to a document that had none.
|
|
336
|
+
|
|
337
|
+
## Troubleshooting
|
|
338
|
+
|
|
339
|
+
| Symptom | Fix |
|
|
340
|
+
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
341
|
+
| `Expected an OpenAPI document object. Got string` | You passed a file path. Pass `pathToFileURL(path)` or a `file://` URL. |
|
|
342
|
+
| Type error: missing `FileSystem`, `Path`, or `HttpClient` service | Provide `Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)`. |
|
|
343
|
+
| Relative `$ref`s in an inline string resolve to the wrong place | Pass `{ cwd: "/dir/of/spec" }` to `bundleDocument`, or `sourceUrl` to `ParsedOpenApiFromString`. |
|
|
344
|
+
| `Could not find resolver for "http://..."` | The ref points at a private or loopback host on a different origin than the root document, so it is blocked. |
|
|
345
|
+
| On Bun, `dispatcher.destroy is not a function` after the program finishes | Use `FetchHttpClient.layer` instead of `NodeHttpClient.layerUndici`. |
|
|
346
|
+
| `Expected a string matching the RegExp ^3\.[01]...` | The document is OpenAPI 3.2 or later, which isn't supported. |
|
|
347
|
+
|
|
348
|
+
## Key facts (for AI agents)
|
|
349
|
+
|
|
350
|
+
- Runtime exports from `@shipworthy/openapi-parser` are `bundleDocument` and `ParsedOpenApiFromString`. Every other root export is a type. Schema values are exported from `@shipworthy/openapi-parser/schemas/3.1`.
|
|
351
|
+
- `bundleDocument(source, { cwd? })` returns `Effect<ParsedOpenApi, Schema.SchemaError, FileSystem | Path | HttpClient>`.
|
|
352
|
+
- `ParsedOpenApiFromString({ sourceUrl })` returns a `Schema` codec from `string` to `ParsedOpenApi`. Decoding needs `FileSystem | HttpClient`. Encoding produces pretty JSON.
|
|
353
|
+
- Provide the services with `Layer.mergeAll(NodeServices.layer /* @effect/platform-node */, FetchHttpClient.layer /* effect/http */)`.
|
|
354
|
+
- Load files with `URL` or `file://` strings. Bare strings are parsed as JSON or YAML text.
|
|
355
|
+
- Input can be OpenAPI 3.0 or 3.1. Output is always bundled and upgraded OpenAPI 3.1 (`openapi: "3.1.0"` when upgraded from 3.0), with external refs moved into `components`.
|
|
356
|
+
- Every error is a `Schema.SchemaError`. Read `error.message`.
|
|
357
|
+
- Requires `effect` `^4.0.0` (peer dependency), ESM, and Node.js `>=22.19.0` or Bun. Ships compiled JavaScript. It does not run in browsers.
|
|
358
|
+
- Next step in the toolchain: pass the result to `decode` from `@shipworthy/openapi-transformer` to get a `ParsedService` for code generators.
|
|
359
|
+
|
|
360
|
+
## License
|
|
361
|
+
|
|
362
|
+
MIT. See [LICENSE](./LICENSE).
|