@pikku/openapi-parser 0.12.3

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Converts OpenAPI schemas into Zod code strings.
3
+ *
4
+ * Chaining order: base → refinements → .nullable() → .optional() → .default() → .describe()
5
+ */
6
+ export interface OpenAPISchema {
7
+ type?: string;
8
+ format?: string;
9
+ description?: string;
10
+ enum?: unknown[];
11
+ items?: OpenAPISchema;
12
+ properties?: Record<string, OpenAPISchema>;
13
+ required?: string[];
14
+ nullable?: boolean;
15
+ default?: unknown;
16
+ minimum?: number;
17
+ maximum?: number;
18
+ exclusiveMinimum?: number;
19
+ exclusiveMaximum?: number;
20
+ minLength?: number;
21
+ maxLength?: number;
22
+ pattern?: string;
23
+ minItems?: number;
24
+ maxItems?: number;
25
+ oneOf?: OpenAPISchema[];
26
+ anyOf?: OpenAPISchema[];
27
+ allOf?: OpenAPISchema[];
28
+ additionalProperties?: boolean | OpenAPISchema;
29
+ $ref?: string;
30
+ readOnly?: boolean;
31
+ writeOnly?: boolean;
32
+ }
33
+ export interface ZodCodegenContext {
34
+ /** Map from component schema name to its Zod variable name */
35
+ schemaRefs: Map<string, string>;
36
+ /** Track which refs are actually used */
37
+ usedRefs: Set<string>;
38
+ /** Indent level for readability */
39
+ indent: number;
40
+ }
41
+ export declare function createContext(schemaRefs?: Map<string, string>): ZodCodegenContext;
42
+ /**
43
+ * Convert a single OpenAPI schema to a Zod expression string.
44
+ * Does NOT include .optional() — that's handled at the property level based on `required`.
45
+ */
46
+ export declare function schemaToZod(schema: OpenAPISchema, ctx: ZodCodegenContext, opts?: {
47
+ optional?: boolean;
48
+ }): string;
49
+ /**
50
+ * Generate the Zod variable name for a component schema.
51
+ * e.g. "PaginatedResponse" → "PaginatedResponseSchema"
52
+ */
53
+ export declare function sanitizeTypeName(name: string): string;
54
+ export declare function schemaVarName(name: string): string;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Converts OpenAPI schemas into Zod code strings.
3
+ *
4
+ * Chaining order: base → refinements → .nullable() → .optional() → .default() → .describe()
5
+ */
6
+ export function createContext(schemaRefs) {
7
+ return {
8
+ schemaRefs: schemaRefs ?? new Map(),
9
+ usedRefs: new Set(),
10
+ indent: 0,
11
+ };
12
+ }
13
+ /**
14
+ * Convert a single OpenAPI schema to a Zod expression string.
15
+ * Does NOT include .optional() — that's handled at the property level based on `required`.
16
+ */
17
+ export function schemaToZod(schema, ctx, opts = {}) {
18
+ let code = schemaToZodBase(schema, ctx);
19
+ // Refinements
20
+ code = applyRefinements(code, schema);
21
+ // Nullable
22
+ if (schema.nullable) {
23
+ code += '.nullable()';
24
+ }
25
+ // Optional
26
+ if (opts.optional) {
27
+ code += '.optional()';
28
+ }
29
+ // Default
30
+ if (schema.default !== undefined) {
31
+ code += `.default(${JSON.stringify(schema.default)})`;
32
+ }
33
+ // Description
34
+ if (schema.description) {
35
+ code += `.describe(${JSON.stringify(schema.description)})`;
36
+ }
37
+ return code;
38
+ }
39
+ function schemaToZodBase(schema, ctx) {
40
+ // Handle $ref
41
+ if (schema.$ref) {
42
+ const refName = schema.$ref.split('/').pop();
43
+ const zodName = ctx.schemaRefs.get(refName);
44
+ if (zodName) {
45
+ ctx.usedRefs.add(refName);
46
+ return zodName;
47
+ }
48
+ // Unknown ref — fallback to z.unknown()
49
+ return 'z.unknown()';
50
+ }
51
+ // Handle allOf — merge into single object
52
+ if (schema.allOf && schema.allOf.length > 0) {
53
+ return handleAllOf(schema.allOf, ctx);
54
+ }
55
+ // Handle oneOf/anyOf — union
56
+ if (schema.oneOf && schema.oneOf.length > 0) {
57
+ return handleUnion(schema.oneOf, ctx);
58
+ }
59
+ if (schema.anyOf && schema.anyOf.length > 0) {
60
+ return handleUnion(schema.anyOf, ctx);
61
+ }
62
+ // Handle enum
63
+ if (schema.enum && schema.enum.length > 0) {
64
+ return handleEnum(schema.enum);
65
+ }
66
+ // Handle by type
67
+ switch (schema.type) {
68
+ case 'string':
69
+ return handleString(schema);
70
+ case 'integer':
71
+ return 'z.number().int()';
72
+ case 'number':
73
+ return 'z.number()';
74
+ case 'boolean':
75
+ return 'z.boolean()';
76
+ case 'array':
77
+ return handleArray(schema, ctx);
78
+ case 'object':
79
+ return handleObject(schema, ctx);
80
+ default:
81
+ // No type specified but has properties — treat as object
82
+ if (schema.properties) {
83
+ return handleObject(schema, ctx);
84
+ }
85
+ return 'z.unknown()';
86
+ }
87
+ }
88
+ function handleString(schema) {
89
+ switch (schema.format) {
90
+ case 'uuid':
91
+ return 'z.string().uuid()';
92
+ case 'date-time':
93
+ return 'z.string().datetime()';
94
+ case 'date':
95
+ return 'z.string().date()';
96
+ case 'email':
97
+ return 'z.string().email()';
98
+ case 'uri':
99
+ case 'url':
100
+ return 'z.string().url()';
101
+ default:
102
+ return 'z.string()';
103
+ }
104
+ }
105
+ function handleEnum(values) {
106
+ // Single value — use z.literal
107
+ if (values.length === 1) {
108
+ return `z.literal(${JSON.stringify(values[0])})`;
109
+ }
110
+ // If all values are strings, use z.enum
111
+ if (values.every((v) => typeof v === 'string')) {
112
+ const enumValues = values.map((v) => JSON.stringify(v)).join(', ');
113
+ return `z.enum([${enumValues}])`;
114
+ }
115
+ // Mixed types — use z.union of z.literal
116
+ const literals = values
117
+ .map((v) => `z.literal(${JSON.stringify(v)})`)
118
+ .join(', ');
119
+ return `z.union([${literals}])`;
120
+ }
121
+ function handleArray(schema, ctx) {
122
+ const itemsZod = schema.items
123
+ ? schemaToZodBase(schema.items, ctx)
124
+ : 'z.unknown()';
125
+ return `z.array(${itemsZod})`;
126
+ }
127
+ function indent(ctx) {
128
+ return ' '.repeat(ctx.indent);
129
+ }
130
+ function handleObject(schema, ctx) {
131
+ if (!schema.properties || Object.keys(schema.properties).length === 0) {
132
+ // Object with no defined properties
133
+ if (schema.additionalProperties) {
134
+ const valueSchema = typeof schema.additionalProperties === 'object'
135
+ ? schemaToZod(schema.additionalProperties, ctx)
136
+ : 'z.unknown()';
137
+ return `z.record(z.string(), ${valueSchema})`;
138
+ }
139
+ return 'z.record(z.string(), z.unknown())';
140
+ }
141
+ const inner = { ...ctx, indent: ctx.indent + 1 };
142
+ const pad = indent(inner);
143
+ const closePad = indent(ctx);
144
+ const requiredSet = new Set(schema.required ?? []);
145
+ const entries = [];
146
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
147
+ const isOptional = !requiredSet.has(key);
148
+ const propZod = schemaToZod(propSchema, inner, { optional: isOptional });
149
+ entries.push(`${pad}${safeKey(key)}: ${propZod},`);
150
+ }
151
+ return `z.object({\n${entries.join('\n')}\n${closePad}})`;
152
+ }
153
+ function handleAllOf(schemas, ctx) {
154
+ // Collect all properties from all schemas
155
+ const mergedProps = {};
156
+ const mergedRequired = [];
157
+ for (const sub of schemas) {
158
+ if (sub.$ref) {
159
+ const refName = sub.$ref.split('/').pop();
160
+ const zodName = ctx.schemaRefs.get(refName);
161
+ if (zodName) {
162
+ ctx.usedRefs.add(refName);
163
+ // If it's a pure ref in an allOf, we'd need to merge/extend
164
+ // For simplicity, if we have refs mixed with objects, use .merge()
165
+ }
166
+ }
167
+ if (sub.properties) {
168
+ Object.assign(mergedProps, sub.properties);
169
+ }
170
+ if (sub.required) {
171
+ mergedRequired.push(...sub.required);
172
+ }
173
+ }
174
+ // If we have only refs, use .merge()
175
+ const refSchemas = schemas.filter((s) => s.$ref);
176
+ const objSchemas = schemas.filter((s) => !s.$ref && (s.properties || s.type === 'object'));
177
+ if (refSchemas.length > 0 && objSchemas.length === 0) {
178
+ const parts = refSchemas.map((s) => {
179
+ const refName = s.$ref.split('/').pop();
180
+ ctx.usedRefs.add(refName);
181
+ return ctx.schemaRefs.get(refName) || 'z.object({})';
182
+ });
183
+ if (parts.length === 1)
184
+ return parts[0];
185
+ return parts.reduce((acc, part) => `${acc}.merge(${part})`);
186
+ }
187
+ // Otherwise, merge into a single object
188
+ if (Object.keys(mergedProps).length > 0) {
189
+ return handleObject({
190
+ type: 'object',
191
+ properties: mergedProps,
192
+ required: [...new Set(mergedRequired)],
193
+ }, ctx);
194
+ }
195
+ // Fallback
196
+ return 'z.unknown()';
197
+ }
198
+ function handleUnion(schemas, ctx) {
199
+ const members = schemas.map((s) => schemaToZodBase(s, ctx));
200
+ if (members.length === 1)
201
+ return members[0];
202
+ return `z.union([${members.join(', ')}])`;
203
+ }
204
+ function applyRefinements(code, schema) {
205
+ let result = code;
206
+ // String refinements
207
+ if (schema.type === 'string' || (!schema.type && !schema.format)) {
208
+ if (schema.minLength !== undefined) {
209
+ result += `.min(${schema.minLength})`;
210
+ }
211
+ if (schema.maxLength !== undefined) {
212
+ result += `.max(${schema.maxLength})`;
213
+ }
214
+ if (schema.pattern) {
215
+ result += `.regex(new RegExp(${JSON.stringify(schema.pattern)}))`;
216
+ }
217
+ }
218
+ // Number refinements
219
+ if (schema.type === 'number' || schema.type === 'integer') {
220
+ if (schema.minimum !== undefined) {
221
+ result += `.min(${schema.minimum})`;
222
+ }
223
+ if (schema.maximum !== undefined) {
224
+ result += `.max(${schema.maximum})`;
225
+ }
226
+ if (schema.exclusiveMinimum !== undefined) {
227
+ result += `.gt(${schema.exclusiveMinimum})`;
228
+ }
229
+ if (schema.exclusiveMaximum !== undefined) {
230
+ result += `.lt(${schema.exclusiveMaximum})`;
231
+ }
232
+ }
233
+ // Array refinements
234
+ if (schema.type === 'array') {
235
+ if (schema.minItems !== undefined) {
236
+ result += `.min(${schema.minItems})`;
237
+ }
238
+ if (schema.maxItems !== undefined) {
239
+ result += `.max(${schema.maxItems})`;
240
+ }
241
+ }
242
+ return result;
243
+ }
244
+ /** Ensure property keys are safe identifiers, or quote them */
245
+ function safeKey(key) {
246
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
247
+ return key;
248
+ }
249
+ return JSON.stringify(key);
250
+ }
251
+ /**
252
+ * Generate the Zod variable name for a component schema.
253
+ * e.g. "PaginatedResponse" → "PaginatedResponseSchema"
254
+ */
255
+ export function sanitizeTypeName(name) {
256
+ return name.replace(/[^a-zA-Z0-9_$]/g, '');
257
+ }
258
+ export function schemaVarName(name) {
259
+ const sanitized = sanitizeTypeName(name);
260
+ return sanitized.endsWith('Schema') ? sanitized : `${sanitized}Schema`;
261
+ }
@@ -0,0 +1,67 @@
1
+ import type { OpenAPISchema } from './zod-codegen.js';
2
+ export interface ErrorResponse {
3
+ statusCode: number;
4
+ description: string;
5
+ }
6
+ export interface SecuritySchemeInfo {
7
+ type: 'oauth2' | 'http' | 'apiKey';
8
+ scheme?: string;
9
+ bearerFormat?: string;
10
+ name?: string;
11
+ in?: string;
12
+ flows?: {
13
+ authorizationUrl?: string;
14
+ tokenUrl?: string;
15
+ scopes?: Record<string, string>;
16
+ };
17
+ }
18
+ export interface ParsedSpec {
19
+ info: {
20
+ title: string;
21
+ version: string;
22
+ description?: string;
23
+ };
24
+ baseUrl: string;
25
+ serverUrls: string[];
26
+ authType: 'bearer' | 'oauth2' | 'apiKey' | 'none';
27
+ operations: ParsedOperation[];
28
+ componentSchemas: Record<string, OpenAPISchema>;
29
+ securitySchemes: Record<string, SecuritySchemeInfo>;
30
+ tagDescriptions: Record<string, string>;
31
+ }
32
+ export interface ParsedOperation {
33
+ operationId?: string;
34
+ method: string;
35
+ path: string;
36
+ summary?: string;
37
+ description?: string;
38
+ tags: string[];
39
+ pathParams: ParsedParam[];
40
+ queryParams: ParsedParam[];
41
+ headerParams: ParsedParam[];
42
+ requestBody?: OpenAPISchema;
43
+ requestBodyDescription?: string;
44
+ requestBodyRequired?: boolean;
45
+ responseSchema?: OpenAPISchema;
46
+ responseDescription?: string;
47
+ errorResponses: ErrorResponse[];
48
+ deprecated: boolean;
49
+ }
50
+ export interface ParsedParam {
51
+ name: string;
52
+ required: boolean;
53
+ schema: OpenAPISchema;
54
+ description?: string;
55
+ example?: unknown;
56
+ }
57
+ /**
58
+ * Read and parse an OpenAPI spec from a file path.
59
+ * Supports both YAML (.yaml, .yml) and JSON (.json) files.
60
+ */
61
+ export declare function parseOpenAPISpec(filePath: string): Promise<ParsedSpec>;
62
+ /**
63
+ * Compute a SHA-256 hash of the contract-relevant parts of a parsed spec.
64
+ * Excludes presentation fields (summary, description, tags, operationId)
65
+ * so that only structural API changes produce a different hash.
66
+ */
67
+ export declare function computeContractHash(spec: ParsedSpec): string;