ty-fetch 0.0.1 → 0.0.2-beta.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 ADDED
@@ -0,0 +1,172 @@
1
+ # ty-fetch
2
+
3
+ TypeScript tooling that validates API calls against OpenAPI specs. Get autocomplete, diagnostics, and fully typed responses with zero manual types.
4
+
5
+ ```ts
6
+ import tf from "ty-fetch";
7
+
8
+ const customers = await tf.get("https://api.stripe.com/v1/customers").json();
9
+ // customers is fully typed — data, has_more, object, url all autocomplete
10
+
11
+ tf.get("https://api.stripe.com/v1/cutsomers");
12
+ // ~~~~~~~~~~
13
+ // Error: Path '/v1/cutsomers' does not exist in Stripe API.
14
+ // Did you mean '/v1/customers'?
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ - **Path validation** — red squiggles for typos in API URLs, with "did you mean?" suggestions
20
+ - **Typed responses** — response types generated from OpenAPI schemas, no manual `as` casts
21
+ - **Typed request bodies** — body params validated against the spec
22
+ - **Path & query params** — typed `params.path` and `params.query` based on the endpoint
23
+ - **Autocomplete** — URL path completions inside string literals, filtered by HTTP method
24
+ - **Hover info** — hover over a URL to see available methods and descriptions
25
+
26
+ Works as both a **TS language service plugin** (editor DX) and a **CLI** (CI validation).
27
+
28
+ ## Setup
29
+
30
+ ```bash
31
+ npm install github:alnorris/ty-fetch
32
+ ```
33
+
34
+ Add the plugin to your `tsconfig.json`:
35
+
36
+ ```jsonc
37
+ {
38
+ "compilerOptions": {
39
+ "plugins": [{ "name": "ty-fetch/plugin" }]
40
+ }
41
+ }
42
+ ```
43
+
44
+ In VS Code, make sure you're using the workspace TypeScript version (not the built-in one). Open the command palette and run **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
45
+
46
+ ## Usage
47
+
48
+ ### The `ty-fetch` client
49
+
50
+ A lightweight HTTP client (similar to [ky](https://github.com/sindresorhus/ky)) with typed methods:
51
+
52
+ ```ts
53
+ import tf from "ty-fetch";
54
+
55
+ // GET with typed response
56
+ const customers = await tf.get("https://api.stripe.com/v1/customers").json();
57
+
58
+ // POST with typed body
59
+ const customer = await tf.post("https://api.stripe.com/v1/customers", {
60
+ body: { name: "Jane Doe", email: "jane@example.com" },
61
+ }).json();
62
+
63
+ // Path params
64
+ const repo = await tf.get("https://api.github.com/repos/{owner}/{repo}", {
65
+ params: { path: { owner: "anthropics", repo: "claude-code" } },
66
+ }).json();
67
+
68
+ // Query params
69
+ const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
70
+ params: { query: { status: "available" } },
71
+ }).json();
72
+ ```
73
+
74
+ Response methods:
75
+
76
+ | Method | Returns |
77
+ |---|---|
78
+ | `.json()` | `Promise<T>` (typed from spec) |
79
+ | `.text()` | `Promise<string>` |
80
+ | `.blob()` | `Promise<Blob>` |
81
+ | `.arrayBuffer()` | `Promise<ArrayBuffer>` |
82
+ | `await` directly | `T` (same as `.json()`) |
83
+
84
+ ### CLI
85
+
86
+ Run validation in CI or from the terminal:
87
+
88
+ ```bash
89
+ npx ty-fetch # uses ./tsconfig.json
90
+ npx ty-fetch tsconfig.json # explicit path
91
+ npx ty-fetch --verbose # show spec fetching details
92
+ ```
93
+
94
+ ```
95
+ example.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API. Did you mean '/v1/customers'?
96
+ example.ts:57:11 - error TF99001: Path '/pets' does not exist in Swagger Petstore. Did you mean '/pet'?
97
+
98
+ 2 error(s) found.
99
+ ```
100
+
101
+ ## Custom specs
102
+
103
+ Map domains to local files or URLs in your tsconfig plugin config:
104
+
105
+ ```jsonc
106
+ {
107
+ "compilerOptions": {
108
+ "plugins": [
109
+ {
110
+ "name": "ty-fetch/plugin",
111
+ "specs": {
112
+ "api.internal.company.com": "./specs/internal-api.json",
113
+ "api.partner.com": "https://partner.com/openapi.json"
114
+ }
115
+ }
116
+ ]
117
+ }
118
+ }
119
+ ```
120
+
121
+ - **File paths** are resolved relative to the tsconfig directory
122
+ - **URLs** are fetched over HTTPS
123
+ - Custom specs override built-in defaults for the same domain
124
+
125
+ This works in both the editor plugin and the CLI.
126
+
127
+ ### Built-in specs
128
+
129
+ These APIs are supported out of the box (no config needed):
130
+
131
+ | Domain | API | Paths |
132
+ |---|---|---|
133
+ | `api.stripe.com` | Stripe API | 414 |
134
+ | `petstore3.swagger.io` | Swagger Petstore | 13 |
135
+ | `api.github.com` | GitHub REST API | 551 |
136
+
137
+ ## How it works
138
+
139
+ 1. Plugin intercepts the TS language service (`getSemanticDiagnostics`, `getCompletionsAtPosition`, `getQuickInfoAtPosition`)
140
+ 2. Finds `fetch()` / `tf.get()` / `tf.post()` etc. calls with string literal URLs
141
+ 3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
142
+ 4. Validates paths against the spec, suggests corrections via Levenshtein distance
143
+ 5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts` using interface declaration merging — only for URLs actually used in your code
144
+
145
+ Spec fetching is async. On first encounter of a domain, the plugin fires a background fetch and returns no extra diagnostics. When the spec arrives, `refreshDiagnostics()` triggers the editor to re-check. This follows the same pattern as [graphqlsp](https://github.com/0no-co/graphqlsp).
146
+
147
+ ## Architecture
148
+
149
+ ```
150
+ src/
151
+ plugin/index.ts TS language service plugin (diagnostics, completions, hover)
152
+ cli/index.ts CLI entry point for CI validation
153
+ core/ Shared logic (URL parsing, spec cache, path matching, body validation)
154
+ generate-types.ts OpenAPI schema -> TypeScript type declarations
155
+ test-project/ Example project using the plugin
156
+ test/ Unit tests
157
+ ```
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ npm run build # compile TypeScript
163
+ npm run watch # compile in watch mode
164
+ npm test # run unit tests
165
+ ```
166
+
167
+ To test the editor experience:
168
+
169
+ 1. Open `test-project/` in VS Code
170
+ 2. Select the workspace TypeScript version
171
+ 3. Restart the TS server (`TypeScript: Restart TS Server`)
172
+ 4. Edit `test-project/example.ts` and observe diagnostics/completions
package/base.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ export class HTTPError extends Error {
2
+ response: Response;
3
+ }
4
+
5
+ export interface Options<
6
+ TBody = never,
7
+ TPathParams = never,
8
+ TQueryParams = never,
9
+ > extends Omit<RequestInit, 'body'> {
10
+ body?: TBody;
11
+ params?: {
12
+ path?: TPathParams;
13
+ query?: TQueryParams;
14
+ };
15
+ prefixUrl?: string;
16
+ }
17
+
18
+ export interface ResponsePromise<T = unknown> extends PromiseLike<T> {
19
+ json(): Promise<T>;
20
+ text(): Promise<string>;
21
+ blob(): Promise<Blob>;
22
+ arrayBuffer(): Promise<ArrayBuffer>;
23
+ formData(): Promise<FormData>;
24
+ }
25
+
26
+ export interface TyFetch {
27
+ (url: string, options?: Options): ResponsePromise;
28
+ get(url: string, options?: Options): ResponsePromise;
29
+ post(url: string, options?: Options): ResponsePromise;
30
+ put(url: string, options?: Options): ResponsePromise;
31
+ patch(url: string, options?: Options): ResponsePromise;
32
+ delete(url: string, options?: Options): ResponsePromise;
33
+ head(url: string, options?: Options): ResponsePromise;
34
+ create(defaults?: Options<unknown>): TyFetch;
35
+ extend(defaults?: Options<unknown>): TyFetch;
36
+ HTTPError: typeof HTTPError;
37
+ }
38
+
39
+ declare const tf: TyFetch;
40
+ export default tf;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const ts = __importStar(require("typescript"));
38
+ const path = __importStar(require("path"));
39
+ const core_1 = require("../core");
40
+ async function main() {
41
+ const args = process.argv.slice(2);
42
+ const tsconfigPath = args[0] ?? "tsconfig.json";
43
+ const configFile = ts.readConfigFile(path.resolve(tsconfigPath), ts.sys.readFile);
44
+ if (configFile.error) {
45
+ console.error("Error reading tsconfig:", ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
46
+ process.exit(1);
47
+ }
48
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(path.resolve(tsconfigPath)));
49
+ // Load custom spec overrides from tsconfig plugin config
50
+ const plugins = configFile.config?.compilerOptions?.plugins ?? [];
51
+ const pluginConfig = plugins.find((p) => p.name === "ty-fetch" || p.name === "ty-fetch/plugin");
52
+ if (pluginConfig?.specs) {
53
+ (0, core_1.registerSpecs)(pluginConfig.specs, path.dirname(path.resolve(tsconfigPath)));
54
+ }
55
+ const program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
56
+ // Step 1: Collect all fetch URLs and their domains
57
+ const allCalls = [];
58
+ const domains = new Set();
59
+ for (const sourceFile of program.getSourceFiles()) {
60
+ if (sourceFile.isDeclarationFile)
61
+ continue;
62
+ const calls = (0, core_1.findFetchCalls)(ts, sourceFile);
63
+ for (const call of calls) {
64
+ allCalls.push({ file: sourceFile, call });
65
+ const parsed = (0, core_1.parseFetchUrl)(call.url);
66
+ if (parsed)
67
+ domains.add(parsed.domain);
68
+ }
69
+ }
70
+ if (allCalls.length === 0) {
71
+ console.log("No fetch calls found.");
72
+ process.exit(0);
73
+ }
74
+ const log = (msg) => {
75
+ if (args.includes("--verbose"))
76
+ console.error(`[ty-fetch] ${msg}`);
77
+ };
78
+ // Step 2: Fetch all specs (async, in parallel)
79
+ log(`Found ${allCalls.length} fetch call(s) across ${domains.size} domain(s)`);
80
+ await Promise.all([...domains].map((d) => (0, core_1.fetchSpecForDomain)(d, log)));
81
+ // Step 3: Validate
82
+ const diagnostics = [];
83
+ for (const { file: sourceFile, call } of allCalls) {
84
+ const parsed = (0, core_1.parseFetchUrl)(call.url);
85
+ if (!parsed)
86
+ continue;
87
+ const entry = await (0, core_1.fetchSpecForDomain)(parsed.domain, log);
88
+ if (entry.status !== "loaded" || !entry.spec)
89
+ continue;
90
+ const apiPath = (0, core_1.stripBasePath)(parsed.path, entry.spec);
91
+ // Path validation
92
+ if (!(0, core_1.pathExistsInSpec)(apiPath, entry.spec)) {
93
+ const allPaths = Object.keys(entry.spec.paths);
94
+ const suggestion = (0, core_1.findClosestPath)(apiPath, allPaths);
95
+ const msg = `Path '${apiPath}' does not exist in ${entry.spec.info?.title ?? parsed.domain}.`
96
+ + (suggestion ? ` Did you mean '${suggestion}'?` : "");
97
+ const pos = sourceFile.getLineAndCharacterOfPosition(call.urlStart);
98
+ diagnostics.push({
99
+ file: path.relative(process.cwd(), sourceFile.fileName),
100
+ line: pos.line + 1, col: pos.character + 1,
101
+ message: msg, code: 99001,
102
+ });
103
+ }
104
+ // Body validation
105
+ if (call.httpMethod && call.jsonBody) {
106
+ const specPath = (0, core_1.findSpecPath)(apiPath, entry.spec);
107
+ if (specPath) {
108
+ const operation = entry.spec.paths[specPath]?.[call.httpMethod];
109
+ const reqSchema = operation?.requestBody?.content?.["application/json"]?.schema ??
110
+ operation?.requestBody?.content?.["application/x-www-form-urlencoded"]?.schema;
111
+ if (reqSchema) {
112
+ const resolved = (0, core_1.resolveSchemaRef)(reqSchema, entry.spec);
113
+ if (resolved?.properties) {
114
+ const jsonObjStart = call.jsonBody.length > 0 ? call.jsonBody[0].nameStart - 2 : call.callStart;
115
+ const bodyDiags = (0, core_1.validateJsonBody)(call.jsonBody, resolved, entry.spec, jsonObjStart);
116
+ for (const d of bodyDiags) {
117
+ const pos = sourceFile.getLineAndCharacterOfPosition(d.start);
118
+ diagnostics.push({
119
+ file: path.relative(process.cwd(), sourceFile.fileName),
120
+ line: pos.line + 1, col: pos.character + 1,
121
+ message: d.message, code: d.code,
122
+ });
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ // Step 4: Output
130
+ if (diagnostics.length === 0) {
131
+ console.log("No errors found.");
132
+ process.exit(0);
133
+ }
134
+ for (const d of diagnostics) {
135
+ console.log(`${d.file}:${d.line}:${d.col} - error TF${d.code}: ${d.message}`);
136
+ }
137
+ console.log(`\n${diagnostics.length} error(s) found.`);
138
+ process.exit(1);
139
+ }
140
+ main().catch((err) => {
141
+ console.error("Fatal:", err);
142
+ process.exit(2);
143
+ });
@@ -0,0 +1,8 @@
1
+ import type { FetchCallInfo } from "./types";
2
+ type TS = typeof import("typescript");
3
+ type SourceFile = import("typescript").SourceFile;
4
+ /**
5
+ * Find all fetch()/tf.get()/api.post() calls in a source file.
6
+ */
7
+ export declare function findFetchCalls(ts: TS, sourceFile: SourceFile): FetchCallInfo[];
8
+ export {};
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findFetchCalls = findFetchCalls;
4
+ /**
5
+ * Find all fetch()/tf.get()/api.post() calls in a source file.
6
+ */
7
+ function findFetchCalls(ts, sourceFile) {
8
+ const results = [];
9
+ function nodeStart(node) { return node.getStart(sourceFile); }
10
+ function nodeLen(node) { return node.getEnd() - nodeStart(node); }
11
+ function visit(node) {
12
+ if (ts.isCallExpression(node) && node.arguments.length > 0) {
13
+ const expr = node.expression;
14
+ let httpMethod = null;
15
+ if (ts.isIdentifier(expr) && (expr.text === "fetch" || expr.text === "typedFetch")) {
16
+ httpMethod = null;
17
+ }
18
+ else if (ts.isPropertyAccessExpression(expr) &&
19
+ ts.isIdentifier(expr.expression) &&
20
+ ["get", "post", "put", "patch", "delete", "head", "request"].includes(expr.name.text)) {
21
+ httpMethod = expr.name.text === "request" || expr.name.text === "head" ? null : expr.name.text;
22
+ }
23
+ else if (ts.isIdentifier(expr)) {
24
+ httpMethod = null;
25
+ }
26
+ else {
27
+ ts.forEachChild(node, visit);
28
+ return;
29
+ }
30
+ const arg = node.arguments[0];
31
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
32
+ const urlStart = nodeStart(arg) + 1; // skip opening quote
33
+ const urlLength = nodeLen(arg) - 2; // exclude quotes
34
+ let jsonBody = null;
35
+ if (node.arguments.length >= 2) {
36
+ const optionsArg = node.arguments[1];
37
+ if (ts.isObjectLiteralExpression(optionsArg)) {
38
+ const jsonProp = optionsArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "body");
39
+ if (jsonProp && ts.isObjectLiteralExpression(jsonProp.initializer)) {
40
+ jsonBody = extractJsonProperties(ts, sourceFile, jsonProp.initializer);
41
+ }
42
+ }
43
+ }
44
+ results.push({
45
+ url: arg.text,
46
+ httpMethod,
47
+ urlStart,
48
+ urlLength,
49
+ callStart: nodeStart(node),
50
+ callLength: nodeLen(node),
51
+ jsonBody,
52
+ });
53
+ }
54
+ }
55
+ ts.forEachChild(node, visit);
56
+ }
57
+ visit(sourceFile);
58
+ return results;
59
+ }
60
+ function extractJsonProperties(ts, sf, obj) {
61
+ function nodeStart(n) { return n.getStart(sf); }
62
+ function nodeLen(n) { return n.getEnd() - nodeStart(n); }
63
+ const props = [];
64
+ for (const prop of obj.properties) {
65
+ if (!ts.isPropertyAssignment(prop))
66
+ continue;
67
+ const name = ts.isIdentifier(prop.name) ? prop.name.text
68
+ : ts.isStringLiteral(prop.name) ? prop.name.text
69
+ : null;
70
+ if (!name)
71
+ continue;
72
+ const valueNode = prop.initializer;
73
+ let valueKind = "other";
74
+ let valueText = "";
75
+ if (ts.isNumericLiteral(valueNode)) {
76
+ valueKind = "number";
77
+ valueText = valueNode.text;
78
+ }
79
+ else if (ts.isStringLiteral(valueNode)) {
80
+ valueKind = "string";
81
+ valueText = valueNode.text;
82
+ }
83
+ else if (valueNode.kind === ts.SyntaxKind.TrueKeyword || valueNode.kind === ts.SyntaxKind.FalseKeyword) {
84
+ valueKind = "boolean";
85
+ valueText = valueNode.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false";
86
+ }
87
+ else if (valueNode.kind === ts.SyntaxKind.NullKeyword) {
88
+ valueKind = "null";
89
+ }
90
+ else if (ts.isArrayLiteralExpression(valueNode)) {
91
+ valueKind = "array";
92
+ }
93
+ else if (ts.isObjectLiteralExpression(valueNode)) {
94
+ valueKind = "object";
95
+ }
96
+ props.push({
97
+ name,
98
+ nameStart: nodeStart(prop.name),
99
+ nameLength: nodeLen(prop.name),
100
+ valueStart: nodeStart(valueNode),
101
+ valueLength: nodeLen(valueNode),
102
+ valueText,
103
+ valueKind,
104
+ });
105
+ }
106
+ return props;
107
+ }
@@ -0,0 +1,6 @@
1
+ import type { OpenAPISpec, JsonBodyProperty, ValidationDiagnostic } from "./types";
2
+ /**
3
+ * Validate a JSON body object against an OpenAPI schema.
4
+ * Returns diagnostics with positions pointing at the specific offending properties.
5
+ */
6
+ export declare function validateJsonBody(properties: JsonBodyProperty[], schema: any, spec: OpenAPISpec, jsonObjectStart: number): ValidationDiagnostic[];
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateJsonBody = validateJsonBody;
4
+ const schema_utils_1 = require("./schema-utils");
5
+ /**
6
+ * Validate a JSON body object against an OpenAPI schema.
7
+ * Returns diagnostics with positions pointing at the specific offending properties.
8
+ */
9
+ function validateJsonBody(properties, schema, spec, jsonObjectStart) {
10
+ const diagnostics = [];
11
+ const schemaProps = schema.properties;
12
+ if (!schemaProps)
13
+ return diagnostics;
14
+ const requiredSet = new Set(schema.required ?? []);
15
+ // Check each property
16
+ for (const prop of properties) {
17
+ // Unknown property
18
+ if (!schemaProps[prop.name]) {
19
+ diagnostics.push({
20
+ start: prop.nameStart,
21
+ length: prop.nameLength,
22
+ message: `Property '${prop.name}' does not exist in the request body schema.`,
23
+ code: 99002,
24
+ });
25
+ continue;
26
+ }
27
+ // Type mismatch
28
+ const expectedSchema = (0, schema_utils_1.resolveSchemaRef)(schemaProps[prop.name], spec);
29
+ if (!expectedSchema?.type)
30
+ continue;
31
+ const mismatch = checkTypeMismatch(prop, expectedSchema);
32
+ if (mismatch) {
33
+ diagnostics.push({
34
+ start: prop.valueStart,
35
+ length: prop.valueLength,
36
+ message: mismatch,
37
+ code: 99003,
38
+ });
39
+ }
40
+ }
41
+ // Missing required properties
42
+ const providedNames = new Set(properties.map((p) => p.name));
43
+ for (const reqProp of requiredSet) {
44
+ if (!providedNames.has(reqProp)) {
45
+ diagnostics.push({
46
+ start: jsonObjectStart,
47
+ length: 1, // opening brace
48
+ message: `Missing required property '${reqProp}' in request body.`,
49
+ code: 99004,
50
+ });
51
+ }
52
+ }
53
+ return diagnostics;
54
+ }
55
+ function checkTypeMismatch(prop, schema) {
56
+ const expectedType = schema.type;
57
+ const enumValues = schema.enum;
58
+ switch (prop.valueKind) {
59
+ case "number":
60
+ if (expectedType === "string")
61
+ return `Type 'number' is not assignable to type 'string'.`;
62
+ break;
63
+ case "string":
64
+ if (expectedType === "number" || expectedType === "integer")
65
+ return `Type 'string' is not assignable to type 'number'.`;
66
+ if (enumValues && !enumValues.includes(prop.valueText))
67
+ return `Value '${prop.valueText}' is not assignable to type '${enumValues.map((v) => `"${v}"`).join(" | ")}'.`;
68
+ break;
69
+ case "boolean":
70
+ if (expectedType === "string")
71
+ return `Type 'boolean' is not assignable to type 'string'.`;
72
+ if (expectedType === "number" || expectedType === "integer")
73
+ return `Type 'boolean' is not assignable to type 'number'.`;
74
+ break;
75
+ case "array":
76
+ if (expectedType !== "array")
77
+ return `Type 'array' is not assignable to type '${expectedType}'.`;
78
+ break;
79
+ case "object":
80
+ if (expectedType !== "object" && !schema.properties)
81
+ return `Type 'object' is not assignable to type '${expectedType}'.`;
82
+ break;
83
+ }
84
+ return null;
85
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types";
2
+ export * from "./url-parser";
3
+ export * from "./path-validator";
4
+ export * from "./schema-utils";
5
+ export * from "./body-validator";
6
+ export * from "./spec-cache";
7
+ export * from "./ast-helpers";
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./url-parser"), exports);
19
+ __exportStar(require("./path-validator"), exports);
20
+ __exportStar(require("./schema-utils"), exports);
21
+ __exportStar(require("./body-validator"), exports);
22
+ __exportStar(require("./spec-cache"), exports);
23
+ __exportStar(require("./ast-helpers"), exports);
@@ -0,0 +1,5 @@
1
+ import type { OpenAPISpec } from "./types";
2
+ export declare function matchesPathTemplate(actualPath: string, templatePath: string): boolean;
3
+ export declare function pathExistsInSpec(path: string, spec: OpenAPISpec): boolean;
4
+ export declare function findSpecPath(apiPath: string, spec: OpenAPISpec): string | null;
5
+ export declare function findClosestPath(target: string, paths: string[]): string | null;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesPathTemplate = matchesPathTemplate;
4
+ exports.pathExistsInSpec = pathExistsInSpec;
5
+ exports.findSpecPath = findSpecPath;
6
+ exports.findClosestPath = findClosestPath;
7
+ function matchesPathTemplate(actualPath, templatePath) {
8
+ const actualParts = actualPath.split("/");
9
+ const templateParts = templatePath.split("/");
10
+ if (actualParts.length !== templateParts.length)
11
+ return false;
12
+ return templateParts.every((tp, i) => tp.startsWith("{") || tp === actualParts[i]);
13
+ }
14
+ function pathExistsInSpec(path, spec) {
15
+ if (spec.paths[path])
16
+ return true;
17
+ return Object.keys(spec.paths).some((tp) => matchesPathTemplate(path, tp));
18
+ }
19
+ function findSpecPath(apiPath, spec) {
20
+ if (spec.paths[apiPath])
21
+ return apiPath;
22
+ return Object.keys(spec.paths).find((tp) => matchesPathTemplate(apiPath, tp)) ?? null;
23
+ }
24
+ function findClosestPath(target, paths) {
25
+ let best = null;
26
+ let bestDist = Infinity;
27
+ for (const p of paths) {
28
+ const d = levenshtein(target, p);
29
+ if (d < bestDist && d <= Math.max(target.length, p.length) * 0.4) {
30
+ bestDist = d;
31
+ best = p;
32
+ }
33
+ }
34
+ return best;
35
+ }
36
+ function levenshtein(a, b) {
37
+ const m = a.length, n = b.length;
38
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
39
+ for (let i = 0; i <= m; i++)
40
+ dp[i][0] = i;
41
+ for (let j = 0; j <= n; j++)
42
+ dp[0][j] = j;
43
+ for (let i = 1; i <= m; i++)
44
+ for (let j = 1; j <= n; j++)
45
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
46
+ return dp[m][n];
47
+ }
@@ -0,0 +1,5 @@
1
+ import type { OpenAPISpec } from "./types";
2
+ export declare function resolveSchemaRef(schema: any, spec: OpenAPISpec): any;
3
+ export declare function getRequestBodySchema(operation: any): any | null;
4
+ export declare function getResponseSchema(operation: any): any | null;
5
+ export declare function isRequestBodyRequired(operation: any): boolean;