@geekmidas/schema 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 (47) hide show
  1. package/README.md +351 -0
  2. package/dist/conversion-CH_EmflL.d.cts +30 -0
  3. package/dist/conversion-Civd6bIU.mjs +89 -0
  4. package/dist/conversion-Civd6bIU.mjs.map +1 -0
  5. package/dist/conversion-CrSNoSRa.cjs +125 -0
  6. package/dist/conversion-CrSNoSRa.cjs.map +1 -0
  7. package/dist/conversion-DUyZYTWO.d.mts +30 -0
  8. package/dist/conversion.cjs +8 -0
  9. package/dist/conversion.d.cts +2 -0
  10. package/dist/conversion.d.mts +2 -0
  11. package/dist/conversion.mjs +3 -0
  12. package/dist/index.cjs +7 -0
  13. package/dist/index.d.cts +4 -0
  14. package/dist/index.d.mts +4 -0
  15. package/dist/index.mjs +4 -0
  16. package/dist/openapi-DH5yCqKh.mjs +46 -0
  17. package/dist/openapi-DH5yCqKh.mjs.map +1 -0
  18. package/dist/openapi-DR4_PG-e.d.cts +26 -0
  19. package/dist/openapi-DVvLYx-8.cjs +58 -0
  20. package/dist/openapi-DVvLYx-8.cjs.map +1 -0
  21. package/dist/openapi-j01kFjpI.d.mts +26 -0
  22. package/dist/openapi.cjs +4 -0
  23. package/dist/openapi.d.cts +2 -0
  24. package/dist/openapi.d.mts +2 -0
  25. package/dist/openapi.mjs +3 -0
  26. package/dist/parser.cjs +23 -0
  27. package/dist/parser.cjs.map +1 -0
  28. package/dist/parser.d.cts +17 -0
  29. package/dist/parser.d.mts +17 -0
  30. package/dist/parser.mjs +21 -0
  31. package/dist/parser.mjs.map +1 -0
  32. package/dist/types-ByLHeRGs.d.mts +13 -0
  33. package/dist/types-DfcgE7cO.d.cts +13 -0
  34. package/dist/types.cjs +0 -0
  35. package/dist/types.d.cts +2 -0
  36. package/dist/types.d.mts +2 -0
  37. package/dist/types.mjs +0 -0
  38. package/package.json +44 -0
  39. package/src/__tests__/conversion.spec.ts +319 -0
  40. package/src/__tests__/openapi.spec.ts +396 -0
  41. package/src/__tests__/parser.spec.ts +236 -0
  42. package/src/conversion.ts +199 -0
  43. package/src/index.ts +15 -0
  44. package/src/openapi.ts +75 -0
  45. package/src/parser.ts +30 -0
  46. package/src/types.ts +23 -0
  47. package/tsdown.config.ts +5 -0
@@ -0,0 +1,236 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod/v4';
3
+ import { parseSchema, validate } from '../parser';
4
+
5
+ describe('Schema Parser', () => {
6
+ describe('validate', () => {
7
+ it('should validate data against a Zod schema', async () => {
8
+ const schema = z.object({
9
+ name: z.string(),
10
+ age: z.number(),
11
+ });
12
+
13
+ const result = await validate(schema, { name: 'John', age: 30 });
14
+
15
+ expect(result.issues).toBeUndefined();
16
+ if ('value' in result) {
17
+ expect(result.value).toEqual({ name: 'John', age: 30 });
18
+ }
19
+ });
20
+
21
+ it('should return issues for invalid data', async () => {
22
+ const schema = z.object({
23
+ name: z.string(),
24
+ age: z.number(),
25
+ });
26
+
27
+ const result = await validate(schema, { name: 'John', age: 'invalid' });
28
+
29
+ expect(result.issues).toBeDefined();
30
+ });
31
+
32
+ it('should validate string schema', async () => {
33
+ const schema = z.string();
34
+
35
+ const result = await validate(schema, 'test string');
36
+
37
+ expect(result.issues).toBeUndefined();
38
+ if ('value' in result) {
39
+ expect(result.value).toBe('test string');
40
+ }
41
+ });
42
+
43
+ it('should validate array schema', async () => {
44
+ const schema = z.array(z.number());
45
+
46
+ const result = await validate(schema, [1, 2, 3]);
47
+
48
+ expect(result.issues).toBeUndefined();
49
+ if ('value' in result) {
50
+ expect(result.value).toEqual([1, 2, 3]);
51
+ }
52
+ });
53
+
54
+ it('should return issues for invalid array items', async () => {
55
+ const schema = z.array(z.number());
56
+
57
+ const result = await validate(schema, [1, 'invalid', 3]);
58
+
59
+ expect(result.issues).toBeDefined();
60
+ });
61
+
62
+ it('should validate nested objects', async () => {
63
+ const schema = z.object({
64
+ user: z.object({
65
+ name: z.string(),
66
+ profile: z.object({
67
+ bio: z.string(),
68
+ }),
69
+ }),
70
+ });
71
+
72
+ const result = await validate(schema, {
73
+ user: {
74
+ name: 'John',
75
+ profile: { bio: 'Developer' },
76
+ },
77
+ });
78
+
79
+ expect(result.issues).toBeUndefined();
80
+ if ('value' in result) {
81
+ expect(result.value).toEqual({
82
+ user: {
83
+ name: 'John',
84
+ profile: { bio: 'Developer' },
85
+ },
86
+ });
87
+ }
88
+ });
89
+ });
90
+
91
+ describe('parseSchema', () => {
92
+ it('should parse valid data', async () => {
93
+ const schema = z.object({
94
+ name: z.string(),
95
+ age: z.number(),
96
+ });
97
+
98
+ const result = await parseSchema(schema, { name: 'John', age: 30 });
99
+
100
+ expect(result).toEqual({ name: 'John', age: 30 });
101
+ });
102
+
103
+ it('should throw for invalid data', async () => {
104
+ const schema = z.object({
105
+ name: z.string(),
106
+ age: z.number(),
107
+ });
108
+
109
+ await expect(
110
+ parseSchema(schema, { name: 'John', age: 'invalid' }),
111
+ ).rejects.toBeDefined();
112
+ });
113
+
114
+ it('should return undefined for undefined schema', async () => {
115
+ const result = await parseSchema(undefined as any, { name: 'John' });
116
+
117
+ expect(result).toBeUndefined();
118
+ });
119
+
120
+ it('should parse string schema', async () => {
121
+ const schema = z.string().min(3);
122
+
123
+ const result = await parseSchema(schema, 'test');
124
+
125
+ expect(result).toBe('test');
126
+ });
127
+
128
+ it('should throw for string that is too short', async () => {
129
+ const schema = z.string().min(5);
130
+
131
+ await expect(parseSchema(schema, 'abc')).rejects.toBeDefined();
132
+ });
133
+
134
+ it('should parse number schema', async () => {
135
+ const schema = z.number().positive();
136
+
137
+ const result = await parseSchema(schema, 42);
138
+
139
+ expect(result).toBe(42);
140
+ });
141
+
142
+ it('should throw for negative number when positive required', async () => {
143
+ const schema = z.number().positive();
144
+
145
+ await expect(parseSchema(schema, -5)).rejects.toBeDefined();
146
+ });
147
+
148
+ it('should parse optional fields', async () => {
149
+ const schema = z.object({
150
+ required: z.string(),
151
+ optional: z.string().optional(),
152
+ });
153
+
154
+ const result = await parseSchema(schema, { required: 'value' });
155
+
156
+ expect(result).toEqual({ required: 'value' });
157
+ });
158
+
159
+ it('should parse with defaults', async () => {
160
+ const schema = z.object({
161
+ name: z.string(),
162
+ role: z.string().default('user'),
163
+ });
164
+
165
+ const result = await parseSchema(schema, { name: 'John' });
166
+
167
+ expect(result).toEqual({ name: 'John', role: 'user' });
168
+ });
169
+
170
+ it('should parse enum values', async () => {
171
+ const schema = z.object({
172
+ status: z.enum(['active', 'inactive', 'pending']),
173
+ });
174
+
175
+ const result = await parseSchema(schema, { status: 'active' });
176
+
177
+ expect(result).toEqual({ status: 'active' });
178
+ });
179
+
180
+ it('should throw for invalid enum value', async () => {
181
+ const schema = z.object({
182
+ status: z.enum(['active', 'inactive']),
183
+ });
184
+
185
+ await expect(
186
+ parseSchema(schema, { status: 'invalid' }),
187
+ ).rejects.toBeDefined();
188
+ });
189
+
190
+ it('should parse union types', async () => {
191
+ const schema = z.union([z.string(), z.number()]);
192
+
193
+ const result1 = await parseSchema(schema, 'text');
194
+ const result2 = await parseSchema(schema, 42);
195
+
196
+ expect(result1).toBe('text');
197
+ expect(result2).toBe(42);
198
+ });
199
+
200
+ it('should throw for invalid union value', async () => {
201
+ const schema = z.union([z.string(), z.number()]);
202
+
203
+ await expect(parseSchema(schema, true)).rejects.toBeDefined();
204
+ });
205
+
206
+ it('should parse array of objects', async () => {
207
+ const schema = z.array(
208
+ z.object({
209
+ id: z.number(),
210
+ name: z.string(),
211
+ }),
212
+ );
213
+
214
+ const result = await parseSchema(schema, [
215
+ { id: 1, name: 'Alice' },
216
+ { id: 2, name: 'Bob' },
217
+ ]);
218
+
219
+ expect(result).toEqual([
220
+ { id: 1, name: 'Alice' },
221
+ { id: 2, name: 'Bob' },
222
+ ]);
223
+ });
224
+
225
+ it('should validate required fields', async () => {
226
+ const schema = z.object({
227
+ required: z.string(),
228
+ optional: z.string().optional(),
229
+ });
230
+
231
+ await expect(
232
+ parseSchema(schema, { optional: 'value' }),
233
+ ).rejects.toBeDefined();
234
+ });
235
+ });
236
+ });
@@ -0,0 +1,199 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+
3
+ export enum SchemaVendor {
4
+ zod = 'zod',
5
+ valibot = 'valibot',
6
+ }
7
+
8
+ export type VendorConvertor = (schema: any) => Promise<any>;
9
+ export type VendorConvertors = Record<SchemaVendor, VendorConvertor>;
10
+
11
+ function isSchemaVendor(vendor?: string): vendor is SchemaVendor {
12
+ if (!vendor) {
13
+ return false;
14
+ }
15
+
16
+ return Object.values(SchemaVendor).includes(vendor as SchemaVendor);
17
+ }
18
+
19
+ export const StandardSchemaJsonSchema: VendorConvertors = {
20
+ zod: async (schema): Promise<any> => {
21
+ try {
22
+ const { z } = await import('zod/v4').catch(() => ({ z: {} }));
23
+
24
+ if ('toJSONSchema' in z && typeof z.toJSONSchema === 'function') {
25
+ return z.toJSONSchema(schema);
26
+ }
27
+ const { zodToJsonSchema } = await import('zod-to-json-schema');
28
+
29
+ const result = zodToJsonSchema(schema, {
30
+ removeAdditionalStrategy: 'strict',
31
+ });
32
+
33
+ return result;
34
+ } catch (error) {
35
+ // Fallback to basic conversion if zod-to-json-schema is not available
36
+ console.warn(
37
+ 'zod-to-json-schema not available, using basic conversion',
38
+ error,
39
+ );
40
+ return { type: 'object' };
41
+ }
42
+ },
43
+ valibot: async (schema): Promise<any> => {
44
+ const { toJsonSchema } = await import('@valibot/to-json-schema');
45
+ return toJsonSchema(schema as any);
46
+ },
47
+ };
48
+
49
+ function extractAndConvertDefs(
50
+ jsonSchema: any,
51
+ componentCollector?: {
52
+ addSchema(id: string, schema: any): void;
53
+ getReference(id: string): { $ref: string };
54
+ },
55
+ ): any {
56
+ if (!jsonSchema || typeof jsonSchema !== 'object') {
57
+ return jsonSchema;
58
+ }
59
+
60
+ // Process the schema recursively to update references
61
+ const processSchema = (schema: any): any => {
62
+ if (!schema || typeof schema !== 'object') {
63
+ return schema;
64
+ }
65
+
66
+ // Handle $ref
67
+ if (schema.$ref && typeof schema.$ref === 'string') {
68
+ // Convert #/$defs/X to #/components/schemas/X
69
+ if (schema.$ref.startsWith('#/$defs/')) {
70
+ const refName = schema.$ref.replace('#/$defs/', '');
71
+ return componentCollector
72
+ ? componentCollector.getReference(refName)
73
+ : schema;
74
+ }
75
+ return schema;
76
+ }
77
+
78
+ // Handle arrays
79
+ if (Array.isArray(schema)) {
80
+ return schema.map(processSchema);
81
+ }
82
+
83
+ // Process all properties recursively
84
+ const processed: any = {};
85
+ for (const [key, value] of Object.entries(schema)) {
86
+ if (key === '$defs') {
87
+ // Skip $defs as they've been extracted
88
+ continue;
89
+ }
90
+ processed[key] = processSchema(value);
91
+ }
92
+ return processed;
93
+ };
94
+
95
+ // Extract $defs if present
96
+ if (jsonSchema.$defs && componentCollector) {
97
+ for (const [defName, defSchema] of Object.entries(jsonSchema.$defs)) {
98
+ // Process the definition recursively to handle nested $refs
99
+ const processedDefSchema = processSchema(defSchema);
100
+ // Add each definition to the component collector
101
+ componentCollector.addSchema(defName, processedDefSchema);
102
+ }
103
+ }
104
+
105
+ // Process the schema and remove $defs
106
+ const { $defs, ...schemaWithoutDefs } = jsonSchema;
107
+ return processSchema(schemaWithoutDefs);
108
+ }
109
+
110
+ export async function convertStandardSchemaToJsonSchema(
111
+ schema?: StandardSchemaV1,
112
+ componentCollector?: {
113
+ addSchema(id: string, schema: any): void;
114
+ getReference(id: string): { $ref: string };
115
+ },
116
+ ): Promise<any> {
117
+ if (!schema) {
118
+ return undefined;
119
+ }
120
+
121
+ const vendor = schema['~standard']?.vendor;
122
+ if (!isSchemaVendor(vendor)) {
123
+ throw new Error(
124
+ `Unsupported or missing vendor "${vendor}" for Standard Schema. Supported vendors are: ${Object.keys(StandardSchemaJsonSchema).join(', ')}`,
125
+ );
126
+ }
127
+
128
+ const toJSONSchema = StandardSchemaJsonSchema[vendor];
129
+ const jsonSchema = await toJSONSchema(schema);
130
+
131
+ // Extract and convert $defs to components
132
+ return extractAndConvertDefs(jsonSchema, componentCollector);
133
+ }
134
+
135
+ export async function getZodMetadata(
136
+ schema: StandardSchemaV1,
137
+ ): Promise<SchemaMeta | undefined> {
138
+ const { ZodObject } = await import('zod/v4');
139
+
140
+ if (schema instanceof ZodObject) {
141
+ return schema.meta();
142
+ }
143
+
144
+ return undefined;
145
+ }
146
+
147
+ export async function getSchemaMetadata(
148
+ schema: StandardSchemaV1,
149
+ ): Promise<SchemaMeta | undefined> {
150
+ const vendor = schema['~standard']?.vendor;
151
+
152
+ if (vendor === 'zod') {
153
+ return getZodMetadata(schema);
154
+ }
155
+
156
+ return undefined;
157
+ }
158
+
159
+ interface SchemaMeta {
160
+ id?: string;
161
+ }
162
+
163
+ export async function convertSchemaWithComponents(
164
+ schema: StandardSchemaV1 | undefined,
165
+ componentCollector?: {
166
+ addSchema(id: string, schema: any): void;
167
+ getReference(id: string): { $ref: string };
168
+ },
169
+ ): Promise<any> {
170
+ if (!schema) {
171
+ return undefined;
172
+ }
173
+
174
+ // Convert to JSON Schema with component collector to handle $defs
175
+ const jsonSchema = await convertStandardSchemaToJsonSchema(
176
+ schema,
177
+ componentCollector,
178
+ );
179
+
180
+ if (!componentCollector) {
181
+ return jsonSchema;
182
+ }
183
+
184
+ // Check if this schema has metadata with an ID
185
+ const metadata = await getSchemaMetadata(schema);
186
+
187
+ // Also check if the JSON Schema itself has an id field (from Zod's meta)
188
+ const schemaId = metadata?.id || jsonSchema?.id;
189
+
190
+ if (schemaId) {
191
+ // Remove the id from the schema before adding to components
192
+ const { id, ...schemaWithoutId } = jsonSchema;
193
+ // Add this schema to components and return a reference
194
+ componentCollector.addSchema(schemaId, schemaWithoutId);
195
+ return componentCollector.getReference(schemaId);
196
+ }
197
+
198
+ return jsonSchema;
199
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type {
2
+ InferStandardSchema,
3
+ ComposableStandardSchema,
4
+ InferComposableStandardSchema,
5
+ } from './types';
6
+
7
+ // Re-export conversion utilities for convenience
8
+ export {
9
+ convertStandardSchemaToJsonSchema,
10
+ convertSchemaWithComponents,
11
+ } from './conversion';
12
+
13
+ // Re-export OpenAPI utilities for convenience
14
+ export { buildOpenApiSchema, createComponentCollector } from './openapi';
15
+ export type { OpenApiSchemaOptions, ComponentCollector } from './openapi';
package/src/openapi.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { OpenAPIV3_1 } from 'openapi-types';
2
+
3
+ export interface OpenApiSchemaOptions {
4
+ title?: string;
5
+ version?: string;
6
+ description?: string;
7
+ }
8
+
9
+ export interface ComponentCollector {
10
+ schemas: Record<string, OpenAPIV3_1.SchemaObject>;
11
+ addSchema(id: string, schema: OpenAPIV3_1.SchemaObject): void;
12
+ getReference(id: string): OpenAPIV3_1.ReferenceObject;
13
+ }
14
+
15
+ export function createComponentCollector(): ComponentCollector {
16
+ const schemas: Record<string, OpenAPIV3_1.SchemaObject> = {};
17
+
18
+ return {
19
+ schemas,
20
+ addSchema(id: string, schema: OpenAPIV3_1.SchemaObject) {
21
+ schemas[id] = schema;
22
+ },
23
+ getReference(id: string): OpenAPIV3_1.ReferenceObject {
24
+ return { $ref: `#/components/schemas/${id}` };
25
+ },
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Builds OpenAPI 3.1 schema from an array of endpoints.
31
+ *
32
+ * Note: This function requires endpoints with toOpenApi3Route method.
33
+ * The actual implementation is in @geekmidas/constructs to avoid circular dependencies.
34
+ */
35
+ export async function buildOpenApiSchema(
36
+ endpoints: Array<{
37
+ toOpenApi3Route(collector?: ComponentCollector): Promise<any>;
38
+ }>,
39
+ options: OpenApiSchemaOptions = {},
40
+ ): Promise<OpenAPIV3_1.Document> {
41
+ const { title = 'API', version = '1.0.0', description } = options;
42
+ const paths: OpenAPIV3_1.PathsObject = {};
43
+ const componentCollector = createComponentCollector();
44
+
45
+ for (const endpoint of endpoints) {
46
+ const route = await endpoint.toOpenApi3Route(componentCollector);
47
+
48
+ // Merge the route into the paths object
49
+ for (const [path, methods] of Object.entries(route)) {
50
+ if (!paths[path]) {
51
+ paths[path] = {};
52
+ }
53
+ Object.assign(paths[path], methods);
54
+ }
55
+ }
56
+
57
+ const doc: OpenAPIV3_1.Document = {
58
+ openapi: '3.0.0',
59
+ info: {
60
+ title,
61
+ version,
62
+ ...(description && { description }),
63
+ },
64
+ paths,
65
+ };
66
+
67
+ // Add components if any schemas were collected
68
+ if (Object.keys(componentCollector.schemas).length > 0) {
69
+ doc.components = {
70
+ schemas: componentCollector.schemas,
71
+ };
72
+ }
73
+
74
+ return doc;
75
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { InferStandardSchema } from './types';
3
+
4
+ /**
5
+ * Validates data against a StandardSchema.
6
+ *
7
+ * @param schema - The StandardSchema to validate against
8
+ * @param data - The data to validate
9
+ * @returns Validation result with value or issues
10
+ */
11
+ export function validate<T extends StandardSchemaV1>(schema: T, data: unknown) {
12
+ return schema['~standard'].validate(data);
13
+ }
14
+
15
+ export async function parseSchema<T extends StandardSchemaV1>(
16
+ schema: T,
17
+ data: unknown,
18
+ ): Promise<InferStandardSchema<T>> {
19
+ if (!schema) {
20
+ return undefined as InferStandardSchema<T>;
21
+ }
22
+
23
+ const parsed = await validate(schema as unknown as StandardSchemaV1, data);
24
+
25
+ if (parsed.issues) {
26
+ throw parsed.issues;
27
+ }
28
+
29
+ return parsed.value as InferStandardSchema<T>;
30
+ }
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+
3
+ export type InferStandardSchema<T> = T extends StandardSchemaV1
4
+ ? StandardSchemaV1.InferOutput<T>
5
+ : never;
6
+
7
+ export type ComposableStandardSchema =
8
+ | StandardSchemaV1
9
+ | {
10
+ [key: string]: StandardSchemaV1 | undefined;
11
+ };
12
+
13
+ export type InferComposableStandardSchema<T> = T extends StandardSchemaV1
14
+ ? StandardSchemaV1.InferOutput<T>
15
+ : T extends { [key: string]: StandardSchemaV1 | undefined }
16
+ ? {
17
+ [K in keyof T as T[K] extends StandardSchemaV1
18
+ ? K
19
+ : never]: T[K] extends StandardSchemaV1
20
+ ? StandardSchemaV1.InferOutput<T[K]>
21
+ : never;
22
+ }
23
+ : {};
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from 'tsdown';
2
+
3
+ export default defineConfig({
4
+ external: ['@valibot/to-json-schema', 'zod', 'zod-to-json-schema'],
5
+ });