schemantic 0.1.0 → 0.2.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 (49) hide show
  1. package/CHANGELOG.md +70 -3
  2. package/README.md +17 -14
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +24 -14
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/core/schemantic.d.ts.map +1 -1
  7. package/dist/core/schemantic.js +61 -13
  8. package/dist/core/schemantic.js.map +1 -1
  9. package/dist/generators/api-client-generator.d.ts.map +1 -1
  10. package/dist/generators/api-client-generator.js +24 -4
  11. package/dist/generators/api-client-generator.js.map +1 -1
  12. package/dist/generators/hook-generator.d.ts.map +1 -1
  13. package/dist/generators/hook-generator.js +56 -6
  14. package/dist/generators/hook-generator.js.map +1 -1
  15. package/dist/lib/api/api-client.d.ts +71 -0
  16. package/dist/lib/api/api-client.d.ts.map +1 -0
  17. package/dist/lib/api/api-client.js +238 -0
  18. package/dist/lib/api/api-client.js.map +1 -0
  19. package/dist/lib/api/barrel.d.ts +4 -0
  20. package/dist/lib/api/barrel.d.ts.map +1 -0
  21. package/dist/lib/api/barrel.js +26 -0
  22. package/dist/lib/api/barrel.js.map +1 -0
  23. package/dist/lib/api/hooks.d.ts +36 -0
  24. package/dist/lib/api/hooks.d.ts.map +1 -0
  25. package/dist/lib/api/hooks.js +120 -0
  26. package/dist/lib/api/hooks.js.map +1 -0
  27. package/dist/lib/api/index.d.ts +4 -0
  28. package/dist/lib/api/index.d.ts.map +1 -0
  29. package/dist/lib/api/index.js +8 -0
  30. package/dist/lib/api/index.js.map +1 -0
  31. package/dist/lib/api/types.d.ts +366 -0
  32. package/dist/lib/api/types.d.ts.map +1 -0
  33. package/dist/lib/api/types.js +315 -0
  34. package/dist/lib/api/types.js.map +1 -0
  35. package/dist/plugins/branded-types.js +1 -1
  36. package/dist/plugins/builtin.d.ts.map +1 -1
  37. package/dist/plugins/builtin.js +13 -6
  38. package/dist/plugins/builtin.js.map +1 -1
  39. package/dist/plugins/performance-monitoring.js +1 -1
  40. package/dist/plugins/request-deduplication.js +1 -1
  41. package/dist/plugins/zod-validation.d.ts +2 -1
  42. package/dist/plugins/zod-validation.d.ts.map +1 -1
  43. package/dist/plugins/zod-validation.js +66 -30
  44. package/dist/plugins/zod-validation.js.map +1 -1
  45. package/docs/CONFIGURATION.md +4 -4
  46. package/docs/FASTAPI_TUTORIAL.md +1497 -1497
  47. package/docs/PLUGINS.md +4 -0
  48. package/docs/PLUGIN_DEVELOPMENT.md +720 -720
  49. package/package.json +11 -6
@@ -1,720 +1,720 @@
1
- # Plugin Development Guide
2
-
3
- This guide explains how to create custom plugins for Schemantic to extend its functionality.
4
-
5
- ## Overview
6
-
7
- Schemantic's plugin system allows you to:
8
-
9
- - Transform OpenAPI schemas before processing
10
- - Customize type generation logic
11
- - Add additional generators (e.g., validation schemas, documentation)
12
- - Modify generated code after creation
13
- - Add new CLI commands and options
14
-
15
- ## Plugin Interface
16
-
17
- Every plugin must implement the `SchemanticPlugin` interface:
18
-
19
- ```typescript
20
- interface SchemanticPlugin {
21
- name: string;
22
- version: string;
23
- description: string;
24
-
25
- // Optional lifecycle hooks
26
- beforeSchemaLoad?: (context: GenerationContext) => Promise<void>;
27
- afterSchemaLoad?: (
28
- schema: OpenAPIV3.Document,
29
- context: GenerationContext
30
- ) => Promise<void>;
31
- beforeTypeGeneration?: (
32
- typeName: string,
33
- schema: any,
34
- context: GenerationContext
35
- ) => Promise<void>;
36
- afterTypeGeneration?: (
37
- typeName: string,
38
- generatedType: GeneratedType,
39
- context: GenerationContext
40
- ) => Promise<void>;
41
- beforeClientGeneration?: (context: GenerationContext) => Promise<void>;
42
- afterClientGeneration?: (
43
- generatedClient: GeneratedClient,
44
- context: GenerationContext
45
- ) => Promise<void>;
46
- beforeOutput?: (
47
- files: GeneratedFile[],
48
- context: GenerationContext
49
- ) => Promise<void>;
50
- afterOutput?: (
51
- files: GeneratedFile[],
52
- context: GenerationContext
53
- ) => Promise<void>;
54
-
55
- // Optional transformers
56
- transformSchema?: (
57
- schema: OpenAPIV3.Document,
58
- context: GenerationContext
59
- ) => OpenAPIV3.Document;
60
- transformType?: (
61
- generatedType: GeneratedType,
62
- context: GenerationContext
63
- ) => GeneratedType;
64
- transformClient?: (
65
- generatedClient: GeneratedClient,
66
- context: GenerationContext
67
- ) => GeneratedClient;
68
-
69
- // Optional custom generators
70
- customTypeGenerators?: { [key: string]: TypeGenerator };
71
- customOutputGenerators?: OutputGenerator[];
72
-
73
- // Optional CLI extensions
74
- cliCommands?: CliCommand[];
75
- cliOptions?: CliOption[];
76
- }
77
- ```
78
-
79
- ## Core Types
80
-
81
- ### GenerationContext
82
-
83
- ```typescript
84
- interface GenerationContext {
85
- config: SchemanticConfig;
86
- schema: OpenAPIV3.Document;
87
- outputDir: string;
88
- logger: Logger;
89
- utilities: PluginUtilities;
90
- }
91
- ```
92
-
93
- ### GeneratedType
94
-
95
- ```typescript
96
- interface GeneratedType {
97
- name: string;
98
- content: string;
99
- dependencies: string[];
100
- exports: string[];
101
- isInterface: boolean;
102
- isEnum: boolean;
103
- isUnion: boolean;
104
- sourceSchema: any;
105
- }
106
- ```
107
-
108
- ### GeneratedClient
109
-
110
- ```typescript
111
- interface GeneratedClient {
112
- className: string;
113
- content: string;
114
- methods: GeneratedMethod[];
115
- dependencies: string[];
116
- exports: string[];
117
- }
118
- ```
119
-
120
- ### GeneratedFile
121
-
122
- ```typescript
123
- interface GeneratedFile {
124
- path: string;
125
- content: string;
126
- type: "types" | "client" | "hooks" | "other";
127
- }
128
- ```
129
-
130
- ## Creating a Basic Plugin
131
-
132
- ### 1. Simple Transform Plugin
133
-
134
- ```typescript
135
- import { SchemanticPlugin, GenerationContext, GeneratedType } from "schemantic";
136
-
137
- const addCommentsPlugin: SchemanticPlugin = {
138
- name: "add-comments",
139
- version: "1.0.0",
140
- description: "Adds helpful comments to generated types",
141
-
142
- afterTypeGeneration: async (
143
- typeName: string,
144
- generatedType: GeneratedType,
145
- context: GenerationContext
146
- ) => {
147
- // Add a comment at the top of each generated type
148
- const comment = `/**\n * Generated type: ${typeName}\n * Source: ${context.schema.info.title}\n */\n`;
149
- generatedType.content = comment + generatedType.content;
150
- },
151
- };
152
-
153
- export default addCommentsPlugin;
154
- ```
155
-
156
- ### 2. Schema Transform Plugin
157
-
158
- ```typescript
159
- import { SchemanticPlugin, GenerationContext } from "schemantic";
160
- import { OpenAPIV3 } from "openapi-types";
161
-
162
- const schemaTransformPlugin: SchemanticPlugin = {
163
- name: "schema-transformer",
164
- version: "1.0.0",
165
- description: "Transforms schema before processing",
166
-
167
- transformSchema: (
168
- schema: OpenAPIV3.Document,
169
- context: GenerationContext
170
- ): OpenAPIV3.Document => {
171
- // Example: Add custom properties to all schemas
172
- if (schema.components?.schemas) {
173
- Object.values(schema.components.schemas).forEach((schemaObj: any) => {
174
- if (schemaObj.type === "object" && schemaObj.properties) {
175
- // Add a metadata field to all objects
176
- schemaObj.properties._metadata = {
177
- type: "object",
178
- description: "Metadata added by plugin",
179
- properties: {
180
- generatedAt: { type: "string", format: "date-time" },
181
- version: { type: "string" },
182
- },
183
- };
184
- }
185
- });
186
- }
187
-
188
- return schema;
189
- },
190
- };
191
-
192
- export default schemaTransformPlugin;
193
- ```
194
-
195
- ### 3. Custom Type Generator Plugin
196
-
197
- ```typescript
198
- import { SchemanticPlugin, GenerationContext, TypeGenerator } from "schemantic";
199
-
200
- const customGeneratorPlugin: SchemanticPlugin = {
201
- name: "validation-generator",
202
- version: "1.0.0",
203
- description: "Generates validation schemas alongside types",
204
-
205
- customTypeGenerators: {
206
- "validation-schema": (schema: any, context: GenerationContext) => {
207
- const typeName = schema.title || "UnknownType";
208
- const validationName = `${typeName}ValidationSchema`;
209
-
210
- // Generate Zod validation schema
211
- let validationContent = `export const ${validationName} = z.object({\n`;
212
-
213
- if (schema.properties) {
214
- Object.entries(schema.properties).forEach(
215
- ([propName, propSchema]: [string, any]) => {
216
- const zodType = mapOpenAPIToZod(propSchema);
217
- const isRequired = schema.required?.includes(propName);
218
- validationContent += ` ${propName}: ${zodType}${
219
- isRequired ? "" : ".optional()"
220
- },\n`;
221
- }
222
- );
223
- }
224
-
225
- validationContent += "});\n\n";
226
- validationContent += `export type ${typeName}ValidationInput = z.infer<typeof ${validationName}>;\n`;
227
-
228
- return {
229
- name: validationName,
230
- content: validationContent,
231
- dependencies: ["zod"],
232
- exports: [validationName, `${typeName}ValidationInput`],
233
- isInterface: false,
234
- isEnum: false,
235
- isUnion: false,
236
- sourceSchema: schema,
237
- };
238
- },
239
- },
240
- };
241
-
242
- function mapOpenAPIToZod(schema: any): string {
243
- switch (schema.type) {
244
- case "string":
245
- if (schema.format === "email") return "z.string().email()";
246
- if (schema.format === "uuid") return "z.string().uuid()";
247
- return "z.string()";
248
- case "number":
249
- case "integer":
250
- return "z.number()";
251
- case "boolean":
252
- return "z.boolean()";
253
- case "array":
254
- return `z.array(${mapOpenAPIToZod(schema.items)})`;
255
- default:
256
- return "z.unknown()";
257
- }
258
- }
259
-
260
- export default customGeneratorPlugin;
261
- ```
262
-
263
- ## Advanced Plugin Features
264
-
265
- ### 1. CLI Command Extension
266
-
267
- ```typescript
268
- import { SchemanticPlugin, CliCommand } from "schemantic";
269
-
270
- const cliExtensionPlugin: SchemanticPlugin = {
271
- name: "cli-extension",
272
- version: "1.0.0",
273
- description: "Adds custom CLI commands",
274
-
275
- cliCommands: [
276
- {
277
- name: "analyze",
278
- description: "Analyze OpenAPI schema complexity",
279
- options: [
280
- { name: "url", alias: "u", description: "Schema URL", type: "string" },
281
- {
282
- name: "output",
283
- alias: "o",
284
- description: "Output file",
285
- type: "string",
286
- },
287
- ],
288
- action: async (options: any) => {
289
- console.log("Analyzing schema complexity...");
290
- // Implementation here
291
- },
292
- },
293
- ],
294
- };
295
- ```
296
-
297
- ### 2. File Output Generator
298
-
299
- ```typescript
300
- import {
301
- SchemanticPlugin,
302
- OutputGenerator,
303
- GenerationContext,
304
- } from "schemantic";
305
-
306
- const docGeneratorPlugin: SchemanticPlugin = {
307
- name: "doc-generator",
308
- version: "1.0.0",
309
- description: "Generates API documentation",
310
-
311
- customOutputGenerators: [
312
- {
313
- name: "api-docs",
314
- generate: async (context: GenerationContext) => {
315
- const { schema, outputDir } = context;
316
-
317
- let markdownContent = `# ${schema.info.title} API Documentation\n\n`;
318
- markdownContent += `Version: ${schema.info.version}\n\n`;
319
- markdownContent += `${schema.info.description || ""}\n\n`;
320
-
321
- // Generate endpoint documentation
322
- if (schema.paths) {
323
- markdownContent += "## Endpoints\n\n";
324
- Object.entries(schema.paths).forEach(
325
- ([path, pathItem]: [string, any]) => {
326
- Object.entries(pathItem).forEach(
327
- ([method, operation]: [string, any]) => {
328
- if (operation && typeof operation === "object") {
329
- markdownContent += `### ${method.toUpperCase()} ${path}\n\n`;
330
- markdownContent += `${
331
- operation.summary || operation.description || ""
332
- }\n\n`;
333
- }
334
- }
335
- );
336
- }
337
- );
338
- }
339
-
340
- return [
341
- {
342
- path: `${outputDir}/API_DOCUMENTATION.md`,
343
- content: markdownContent,
344
- type: "other" as const,
345
- },
346
- ];
347
- },
348
- },
349
- ],
350
- };
351
- ```
352
-
353
- ### 3. Configuration-Based Plugin
354
-
355
- ```typescript
356
- import { SchemanticPlugin, GenerationContext } from "schemantic";
357
-
358
- interface PluginConfig {
359
- addTimestamps: boolean;
360
- customPrefix: string;
361
- excludePatterns: string[];
362
- }
363
-
364
- class ConfigurablePlugin implements SchemanticPlugin {
365
- name = "configurable-plugin";
366
- version = "1.0.0";
367
- description = "A configurable plugin example";
368
-
369
- private config: PluginConfig;
370
-
371
- constructor(config: Partial<PluginConfig> = {}) {
372
- this.config = {
373
- addTimestamps: true,
374
- customPrefix: "",
375
- excludePatterns: [],
376
- ...config,
377
- };
378
- }
379
-
380
- afterTypeGeneration = async (
381
- typeName: string,
382
- generatedType: any,
383
- context: GenerationContext
384
- ) => {
385
- if (this.shouldExclude(typeName)) {
386
- return;
387
- }
388
-
389
- if (this.config.addTimestamps) {
390
- const timestamp = `// Generated on ${new Date().toISOString()}\n`;
391
- generatedType.content = timestamp + generatedType.content;
392
- }
393
-
394
- if (this.config.customPrefix) {
395
- generatedType.name = `${this.config.customPrefix}${generatedType.name}`;
396
- }
397
- };
398
-
399
- private shouldExclude(typeName: string): boolean {
400
- return this.config.excludePatterns.some((pattern) =>
401
- new RegExp(pattern).test(typeName)
402
- );
403
- }
404
- }
405
-
406
- // Usage
407
- const plugin = new ConfigurablePlugin({
408
- addTimestamps: true,
409
- customPrefix: "My",
410
- excludePatterns: ["^Internal.*", ".*Error$"],
411
- });
412
-
413
- export default plugin;
414
- ```
415
-
416
- ## Plugin Utilities
417
-
418
- Schemantic provides utilities to help with plugin development:
419
-
420
- ```typescript
421
- interface PluginUtilities {
422
- // String utilities
423
- toCamelCase(str: string): string;
424
- toPascalCase(str: string): string;
425
- toSnakeCase(str: string): string;
426
-
427
- // Type utilities
428
- isOptionalProperty(property: any, required: string[]): boolean;
429
- getTypeFromSchema(schema: any): string;
430
- resolveReference(ref: string, document: OpenAPIV3.Document): any;
431
-
432
- // File utilities
433
- ensureDirectoryExists(path: string): Promise<void>;
434
- writeFile(path: string, content: string): Promise<void>;
435
-
436
- // Validation utilities
437
- validateSchema(schema: any): { isValid: boolean; errors: string[] };
438
- validateGeneratedType(type: GeneratedType): {
439
- isValid: boolean;
440
- errors: string[];
441
- };
442
- }
443
- ```
444
-
445
- ## Plugin Loading
446
-
447
- ### 1. File-based Plugin
448
-
449
- ```typescript
450
- // my-plugin.js
451
- module.exports = {
452
- name: "my-plugin",
453
- version: "1.0.0",
454
- description: "My custom plugin",
455
-
456
- afterTypeGeneration: async (typeName, generatedType, context) => {
457
- // Plugin logic here
458
- },
459
- };
460
- ```
461
-
462
- Load with CLI:
463
-
464
- ```bash
465
- npx schemantic plugin load ./my-plugin.js
466
- npx schemantic generate --plugins my-plugin
467
- ```
468
-
469
- ### 2. NPM Package Plugin
470
-
471
- ```typescript
472
- // @my-org/Schemantic-plugin/index.js
473
- const plugin = {
474
- name: "npm-plugin",
475
- version: "1.0.0",
476
- description: "NPM distributed plugin",
477
-
478
- transformSchema: (schema, context) => {
479
- // Transform logic
480
- return schema;
481
- },
482
- };
483
-
484
- module.exports = plugin;
485
- ```
486
-
487
- Load with CLI:
488
-
489
- ```bash
490
- npm install @my-org/Schemantic-plugin
491
- npx schemantic plugin load @my-org/Schemantic-plugin
492
- npx schemantic generate --plugins npm-plugin
493
- ```
494
-
495
- ### 3. Programmatic Plugin
496
-
497
- ```typescript
498
- import { Schemantic, SchemanticConfig } from "schemantic";
499
- import myPlugin from "./my-plugin";
500
-
501
- const config: SchemanticConfig = {
502
- schemaUrl: "http://localhost:8000/openapi.json",
503
- outputDir: "./generated",
504
- plugins: [{ name: "my-plugin", enabled: true }],
505
- };
506
-
507
- const Schemantic = new Schemantic(config);
508
- const pluginManager = Schemantic.getPluginManager();
509
-
510
- // Register custom plugin
511
- pluginManager.registerPlugin(myPlugin);
512
- pluginManager.enablePlugin("my-plugin");
513
-
514
- // Generate with plugin
515
- const result = await Schemantic.generate();
516
- ```
517
-
518
- ## Testing Plugins
519
-
520
- ### Unit Testing
521
-
522
- ```typescript
523
- import { describe, it, expect } from "jest";
524
- import myPlugin from "./my-plugin";
525
- import { GenerationContext, GeneratedType } from "schemantic";
526
-
527
- describe("MyPlugin", () => {
528
- it("should add comments to generated types", async () => {
529
- const mockContext: GenerationContext = {
530
- config: {},
531
- schema: { info: { title: "Test API" } },
532
- outputDir: "./test",
533
- logger: console,
534
- utilities: {},
535
- } as any;
536
-
537
- const generatedType: GeneratedType = {
538
- name: "TestType",
539
- content: "export interface TestType { id: string; }",
540
- dependencies: [],
541
- exports: ["TestType"],
542
- isInterface: true,
543
- isEnum: false,
544
- isUnion: false,
545
- sourceSchema: {},
546
- };
547
-
548
- await myPlugin.afterTypeGeneration?.(
549
- "TestType",
550
- generatedType,
551
- mockContext
552
- );
553
-
554
- expect(generatedType.content).toContain("/**");
555
- expect(generatedType.content).toContain("Generated type: TestType");
556
- });
557
- });
558
- ```
559
-
560
- ### Integration Testing
561
-
562
- ```typescript
563
- import { Schemantic } from "schemantic";
564
- import myPlugin from "./my-plugin";
565
-
566
- describe("Plugin Integration", () => {
567
- it("should work with full generation process", async () => {
568
- const Schemantic = new Schemantic({
569
- schemaFile: "./test-schema.json",
570
- outputDir: "./test-output",
571
- });
572
-
573
- const pluginManager = Schemantic.getPluginManager();
574
- pluginManager.registerPlugin(myPlugin);
575
- pluginManager.enablePlugin("my-plugin");
576
-
577
- const result = await Schemantic.generate();
578
- expect(result.success).toBe(true);
579
-
580
- // Verify plugin effects in generated files
581
- const typesContent = result.generatedFiles.find(
582
- (f) => f.type === "types"
583
- )?.content;
584
- expect(typesContent).toContain("Generated type:");
585
- });
586
- });
587
- ```
588
-
589
- ## Best Practices
590
-
591
- ### 1. Error Handling
592
-
593
- ```typescript
594
- const robustPlugin: SchemanticPlugin = {
595
- name: "robust-plugin",
596
- version: "1.0.0",
597
- description: "Example of robust error handling",
598
-
599
- afterTypeGeneration: async (typeName, generatedType, context) => {
600
- try {
601
- // Plugin logic that might fail
602
- generatedType.content = processContent(generatedType.content);
603
- } catch (error) {
604
- context.logger.warn(
605
- `Plugin failed for type ${typeName}: ${error.message}`
606
- );
607
- // Don't throw - allow generation to continue
608
- }
609
- },
610
- };
611
- ```
612
-
613
- ### 2. Performance Considerations
614
-
615
- ```typescript
616
- const performantPlugin: SchemanticPlugin = {
617
- name: 'performant-plugin',
618
- version: '1.0.0',
619
- description: 'Example of performance-conscious plugin',
620
-
621
- // Cache expensive computations
622
- private cache = new Map<string, any>();
623
-
624
- transformSchema: (schema, context) => {
625
- const cacheKey = JSON.stringify(schema);
626
-
627
- if (this.cache.has(cacheKey)) {
628
- return this.cache.get(cacheKey);
629
- }
630
-
631
- const transformed = expensiveTransform(schema);
632
- this.cache.set(cacheKey, transformed);
633
-
634
- return transformed;
635
- }
636
- };
637
- ```
638
-
639
- ### 3. Configuration Validation
640
-
641
- ```typescript
642
- import * as Joi from "joi";
643
-
644
- const configSchema = Joi.object({
645
- enabled: Joi.boolean().default(true),
646
- options: Joi.object({
647
- prefix: Joi.string().default(""),
648
- includeComments: Joi.boolean().default(true),
649
- }),
650
- });
651
-
652
- const validatedPlugin: SchemanticPlugin = {
653
- name: "validated-plugin",
654
- version: "1.0.0",
655
- description: "Plugin with configuration validation",
656
-
657
- beforeSchemaLoad: async (context) => {
658
- const pluginConfig = context.config.plugins?.find(
659
- (p) => p.name === "validated-plugin"
660
- );
661
-
662
- if (pluginConfig) {
663
- const { error } = configSchema.validate(pluginConfig);
664
- if (error) {
665
- throw new Error(`Invalid plugin configuration: ${error.message}`);
666
- }
667
- }
668
- },
669
- };
670
- ```
671
-
672
- ## Publishing Plugins
673
-
674
- ### 1. NPM Package Structure
675
-
676
- ```
677
- my-Schemantic-plugin/
678
- ├── package.json
679
- ├── index.js
680
- ├── README.md
681
- ├── LICENSE
682
- └── test/
683
- └── plugin.test.js
684
- ```
685
-
686
- ### 2. Package.json
687
-
688
- ```json
689
- {
690
- "name": "@my-org/Schemantic-plugin-name",
691
- "version": "1.0.0",
692
- "description": "Description of your plugin",
693
- "main": "index.js",
694
- "keywords": ["Schemantic", "plugin", "openapi", "typescript"],
695
- "peerDependencies": {
696
- "schemantic": "^1.0.0"
697
- },
698
- "files": ["index.js", "README.md", "LICENSE"]
699
- }
700
- ```
701
-
702
- ### 3. Plugin Registry
703
-
704
- Consider adding your plugin to the community registry by submitting a PR to the Schemantic plugins repository.
705
-
706
- ## Community Plugins
707
-
708
- Popular community plugins include:
709
-
710
- - `@Schemantic/plugin-zod` - Generates Zod validation schemas
711
- - `@Schemantic/plugin-react-query` - Generates React Query hooks
712
- - `@Schemantic/plugin-swagger-ui` - Generates Swagger UI documentation
713
- - `@Schemantic/plugin-mock-data` - Generates mock data factories
714
-
715
- ## Support
716
-
717
- - 📖 [Plugin API Reference](./API.md#plugin-api)
718
- - 💬 [Plugin Development Discussions](https://github.com/Cstannahill/schemantic/discussions)
719
- - 🐛 [Report Plugin Issues](https://github.com/Cstannahill/schemantic/issues)
720
- - 📧 [Plugin Development Support](mailto:plugins@Schemantic.dev)
1
+ # Plugin Development Guide
2
+
3
+ This guide explains how to create custom plugins for Schemantic to extend its functionality.
4
+
5
+ ## Overview
6
+
7
+ Schemantic's plugin system allows you to:
8
+
9
+ - Transform OpenAPI schemas before processing
10
+ - Customize type generation logic
11
+ - Add additional generators (e.g., validation schemas, documentation)
12
+ - Modify generated code after creation
13
+ - Add new CLI commands and options
14
+
15
+ ## Plugin Interface
16
+
17
+ Every plugin must implement the `SchemanticPlugin` interface:
18
+
19
+ ```typescript
20
+ interface SchemanticPlugin {
21
+ name: string;
22
+ version: string;
23
+ description: string;
24
+
25
+ // Optional lifecycle hooks
26
+ beforeSchemaLoad?: (context: GenerationContext) => Promise<void>;
27
+ afterSchemaLoad?: (
28
+ schema: OpenAPIV3.Document,
29
+ context: GenerationContext
30
+ ) => Promise<void>;
31
+ beforeTypeGeneration?: (
32
+ typeName: string,
33
+ schema: any,
34
+ context: GenerationContext
35
+ ) => Promise<void>;
36
+ afterTypeGeneration?: (
37
+ typeName: string,
38
+ generatedType: GeneratedType,
39
+ context: GenerationContext
40
+ ) => Promise<void>;
41
+ beforeClientGeneration?: (context: GenerationContext) => Promise<void>;
42
+ afterClientGeneration?: (
43
+ generatedClient: GeneratedClient,
44
+ context: GenerationContext
45
+ ) => Promise<void>;
46
+ beforeOutput?: (
47
+ files: GeneratedFile[],
48
+ context: GenerationContext
49
+ ) => Promise<void>;
50
+ afterOutput?: (
51
+ files: GeneratedFile[],
52
+ context: GenerationContext
53
+ ) => Promise<void>;
54
+
55
+ // Optional transformers
56
+ transformSchema?: (
57
+ schema: OpenAPIV3.Document,
58
+ context: GenerationContext
59
+ ) => OpenAPIV3.Document;
60
+ transformType?: (
61
+ generatedType: GeneratedType,
62
+ context: GenerationContext
63
+ ) => GeneratedType;
64
+ transformClient?: (
65
+ generatedClient: GeneratedClient,
66
+ context: GenerationContext
67
+ ) => GeneratedClient;
68
+
69
+ // Optional custom generators
70
+ customTypeGenerators?: { [key: string]: TypeGenerator };
71
+ customOutputGenerators?: OutputGenerator[];
72
+
73
+ // Optional CLI extensions
74
+ cliCommands?: CliCommand[];
75
+ cliOptions?: CliOption[];
76
+ }
77
+ ```
78
+
79
+ ## Core Types
80
+
81
+ ### GenerationContext
82
+
83
+ ```typescript
84
+ interface GenerationContext {
85
+ config: SchemanticConfig;
86
+ schema: OpenAPIV3.Document;
87
+ outputDir: string;
88
+ logger: Logger;
89
+ utilities: PluginUtilities;
90
+ }
91
+ ```
92
+
93
+ ### GeneratedType
94
+
95
+ ```typescript
96
+ interface GeneratedType {
97
+ name: string;
98
+ content: string;
99
+ dependencies: string[];
100
+ exports: string[];
101
+ isInterface: boolean;
102
+ isEnum: boolean;
103
+ isUnion: boolean;
104
+ sourceSchema: any;
105
+ }
106
+ ```
107
+
108
+ ### GeneratedClient
109
+
110
+ ```typescript
111
+ interface GeneratedClient {
112
+ className: string;
113
+ content: string;
114
+ methods: GeneratedMethod[];
115
+ dependencies: string[];
116
+ exports: string[];
117
+ }
118
+ ```
119
+
120
+ ### GeneratedFile
121
+
122
+ ```typescript
123
+ interface GeneratedFile {
124
+ path: string;
125
+ content: string;
126
+ type: "types" | "client" | "hooks" | "other";
127
+ }
128
+ ```
129
+
130
+ ## Creating a Basic Plugin
131
+
132
+ ### 1. Simple Transform Plugin
133
+
134
+ ```typescript
135
+ import { SchemanticPlugin, GenerationContext, GeneratedType } from "schemantic";
136
+
137
+ const addCommentsPlugin: SchemanticPlugin = {
138
+ name: "add-comments",
139
+ version: "1.0.0",
140
+ description: "Adds helpful comments to generated types",
141
+
142
+ afterTypeGeneration: async (
143
+ typeName: string,
144
+ generatedType: GeneratedType,
145
+ context: GenerationContext
146
+ ) => {
147
+ // Add a comment at the top of each generated type
148
+ const comment = `/**\n * Generated type: ${typeName}\n * Source: ${context.schema.info.title}\n */\n`;
149
+ generatedType.content = comment + generatedType.content;
150
+ },
151
+ };
152
+
153
+ export default addCommentsPlugin;
154
+ ```
155
+
156
+ ### 2. Schema Transform Plugin
157
+
158
+ ```typescript
159
+ import { SchemanticPlugin, GenerationContext } from "schemantic";
160
+ import { OpenAPIV3 } from "openapi-types";
161
+
162
+ const schemaTransformPlugin: SchemanticPlugin = {
163
+ name: "schema-transformer",
164
+ version: "1.0.0",
165
+ description: "Transforms schema before processing",
166
+
167
+ transformSchema: (
168
+ schema: OpenAPIV3.Document,
169
+ context: GenerationContext
170
+ ): OpenAPIV3.Document => {
171
+ // Example: Add custom properties to all schemas
172
+ if (schema.components?.schemas) {
173
+ Object.values(schema.components.schemas).forEach((schemaObj: any) => {
174
+ if (schemaObj.type === "object" && schemaObj.properties) {
175
+ // Add a metadata field to all objects
176
+ schemaObj.properties._metadata = {
177
+ type: "object",
178
+ description: "Metadata added by plugin",
179
+ properties: {
180
+ generatedAt: { type: "string", format: "date-time" },
181
+ version: { type: "string" },
182
+ },
183
+ };
184
+ }
185
+ });
186
+ }
187
+
188
+ return schema;
189
+ },
190
+ };
191
+
192
+ export default schemaTransformPlugin;
193
+ ```
194
+
195
+ ### 3. Custom Type Generator Plugin
196
+
197
+ ```typescript
198
+ import { SchemanticPlugin, GenerationContext, TypeGenerator } from "schemantic";
199
+
200
+ const customGeneratorPlugin: SchemanticPlugin = {
201
+ name: "validation-generator",
202
+ version: "1.0.0",
203
+ description: "Generates validation schemas alongside types",
204
+
205
+ customTypeGenerators: {
206
+ "validation-schema": (schema: any, context: GenerationContext) => {
207
+ const typeName = schema.title || "UnknownType";
208
+ const validationName = `${typeName}ValidationSchema`;
209
+
210
+ // Generate Zod validation schema
211
+ let validationContent = `export const ${validationName} = z.object({\n`;
212
+
213
+ if (schema.properties) {
214
+ Object.entries(schema.properties).forEach(
215
+ ([propName, propSchema]: [string, any]) => {
216
+ const zodType = mapOpenAPIToZod(propSchema);
217
+ const isRequired = schema.required?.includes(propName);
218
+ validationContent += ` ${propName}: ${zodType}${
219
+ isRequired ? "" : ".optional()"
220
+ },\n`;
221
+ }
222
+ );
223
+ }
224
+
225
+ validationContent += "});\n\n";
226
+ validationContent += `export type ${typeName}ValidationInput = z.infer<typeof ${validationName}>;\n`;
227
+
228
+ return {
229
+ name: validationName,
230
+ content: validationContent,
231
+ dependencies: ["zod"],
232
+ exports: [validationName, `${typeName}ValidationInput`],
233
+ isInterface: false,
234
+ isEnum: false,
235
+ isUnion: false,
236
+ sourceSchema: schema,
237
+ };
238
+ },
239
+ },
240
+ };
241
+
242
+ function mapOpenAPIToZod(schema: any): string {
243
+ switch (schema.type) {
244
+ case "string":
245
+ if (schema.format === "email") return "z.string().email()";
246
+ if (schema.format === "uuid") return "z.string().uuid()";
247
+ return "z.string()";
248
+ case "number":
249
+ case "integer":
250
+ return "z.number()";
251
+ case "boolean":
252
+ return "z.boolean()";
253
+ case "array":
254
+ return `z.array(${mapOpenAPIToZod(schema.items)})`;
255
+ default:
256
+ return "z.unknown()";
257
+ }
258
+ }
259
+
260
+ export default customGeneratorPlugin;
261
+ ```
262
+
263
+ ## Advanced Plugin Features
264
+
265
+ ### 1. CLI Command Extension
266
+
267
+ ```typescript
268
+ import { SchemanticPlugin, CliCommand } from "schemantic";
269
+
270
+ const cliExtensionPlugin: SchemanticPlugin = {
271
+ name: "cli-extension",
272
+ version: "1.0.0",
273
+ description: "Adds custom CLI commands",
274
+
275
+ cliCommands: [
276
+ {
277
+ name: "analyze",
278
+ description: "Analyze OpenAPI schema complexity",
279
+ options: [
280
+ { name: "url", alias: "u", description: "Schema URL", type: "string" },
281
+ {
282
+ name: "output",
283
+ alias: "o",
284
+ description: "Output file",
285
+ type: "string",
286
+ },
287
+ ],
288
+ action: async (options: any) => {
289
+ console.log("Analyzing schema complexity...");
290
+ // Implementation here
291
+ },
292
+ },
293
+ ],
294
+ };
295
+ ```
296
+
297
+ ### 2. File Output Generator
298
+
299
+ ```typescript
300
+ import {
301
+ SchemanticPlugin,
302
+ OutputGenerator,
303
+ GenerationContext,
304
+ } from "schemantic";
305
+
306
+ const docGeneratorPlugin: SchemanticPlugin = {
307
+ name: "doc-generator",
308
+ version: "1.0.0",
309
+ description: "Generates API documentation",
310
+
311
+ customOutputGenerators: [
312
+ {
313
+ name: "api-docs",
314
+ generate: async (context: GenerationContext) => {
315
+ const { schema, outputDir } = context;
316
+
317
+ let markdownContent = `# ${schema.info.title} API Documentation\n\n`;
318
+ markdownContent += `Version: ${schema.info.version}\n\n`;
319
+ markdownContent += `${schema.info.description || ""}\n\n`;
320
+
321
+ // Generate endpoint documentation
322
+ if (schema.paths) {
323
+ markdownContent += "## Endpoints\n\n";
324
+ Object.entries(schema.paths).forEach(
325
+ ([path, pathItem]: [string, any]) => {
326
+ Object.entries(pathItem).forEach(
327
+ ([method, operation]: [string, any]) => {
328
+ if (operation && typeof operation === "object") {
329
+ markdownContent += `### ${method.toUpperCase()} ${path}\n\n`;
330
+ markdownContent += `${
331
+ operation.summary || operation.description || ""
332
+ }\n\n`;
333
+ }
334
+ }
335
+ );
336
+ }
337
+ );
338
+ }
339
+
340
+ return [
341
+ {
342
+ path: `${outputDir}/API_DOCUMENTATION.md`,
343
+ content: markdownContent,
344
+ type: "other" as const,
345
+ },
346
+ ];
347
+ },
348
+ },
349
+ ],
350
+ };
351
+ ```
352
+
353
+ ### 3. Configuration-Based Plugin
354
+
355
+ ```typescript
356
+ import { SchemanticPlugin, GenerationContext } from "schemantic";
357
+
358
+ interface PluginConfig {
359
+ addTimestamps: boolean;
360
+ customPrefix: string;
361
+ excludePatterns: string[];
362
+ }
363
+
364
+ class ConfigurablePlugin implements SchemanticPlugin {
365
+ name = "configurable-plugin";
366
+ version = "1.0.0";
367
+ description = "A configurable plugin example";
368
+
369
+ private config: PluginConfig;
370
+
371
+ constructor(config: Partial<PluginConfig> = {}) {
372
+ this.config = {
373
+ addTimestamps: true,
374
+ customPrefix: "",
375
+ excludePatterns: [],
376
+ ...config,
377
+ };
378
+ }
379
+
380
+ afterTypeGeneration = async (
381
+ typeName: string,
382
+ generatedType: any,
383
+ context: GenerationContext
384
+ ) => {
385
+ if (this.shouldExclude(typeName)) {
386
+ return;
387
+ }
388
+
389
+ if (this.config.addTimestamps) {
390
+ const timestamp = `// Generated on ${new Date().toISOString()}\n`;
391
+ generatedType.content = timestamp + generatedType.content;
392
+ }
393
+
394
+ if (this.config.customPrefix) {
395
+ generatedType.name = `${this.config.customPrefix}${generatedType.name}`;
396
+ }
397
+ };
398
+
399
+ private shouldExclude(typeName: string): boolean {
400
+ return this.config.excludePatterns.some((pattern) =>
401
+ new RegExp(pattern).test(typeName)
402
+ );
403
+ }
404
+ }
405
+
406
+ // Usage
407
+ const plugin = new ConfigurablePlugin({
408
+ addTimestamps: true,
409
+ customPrefix: "My",
410
+ excludePatterns: ["^Internal.*", ".*Error$"],
411
+ });
412
+
413
+ export default plugin;
414
+ ```
415
+
416
+ ## Plugin Utilities
417
+
418
+ Schemantic provides utilities to help with plugin development:
419
+
420
+ ```typescript
421
+ interface PluginUtilities {
422
+ // String utilities
423
+ toCamelCase(str: string): string;
424
+ toPascalCase(str: string): string;
425
+ toSnakeCase(str: string): string;
426
+
427
+ // Type utilities
428
+ isOptionalProperty(property: any, required: string[]): boolean;
429
+ getTypeFromSchema(schema: any): string;
430
+ resolveReference(ref: string, document: OpenAPIV3.Document): any;
431
+
432
+ // File utilities
433
+ ensureDirectoryExists(path: string): Promise<void>;
434
+ writeFile(path: string, content: string): Promise<void>;
435
+
436
+ // Validation utilities
437
+ validateSchema(schema: any): { isValid: boolean; errors: string[] };
438
+ validateGeneratedType(type: GeneratedType): {
439
+ isValid: boolean;
440
+ errors: string[];
441
+ };
442
+ }
443
+ ```
444
+
445
+ ## Plugin Loading
446
+
447
+ ### 1. File-based Plugin
448
+
449
+ ```typescript
450
+ // my-plugin.js
451
+ module.exports = {
452
+ name: "my-plugin",
453
+ version: "1.0.0",
454
+ description: "My custom plugin",
455
+
456
+ afterTypeGeneration: async (typeName, generatedType, context) => {
457
+ // Plugin logic here
458
+ },
459
+ };
460
+ ```
461
+
462
+ Load with CLI:
463
+
464
+ ```bash
465
+ npx schemantic plugin load ./my-plugin.js
466
+ npx schemantic generate --plugins my-plugin
467
+ ```
468
+
469
+ ### 2. NPM Package Plugin
470
+
471
+ ```typescript
472
+ // @my-org/Schemantic-plugin/index.js
473
+ const plugin = {
474
+ name: "npm-plugin",
475
+ version: "1.0.0",
476
+ description: "NPM distributed plugin",
477
+
478
+ transformSchema: (schema, context) => {
479
+ // Transform logic
480
+ return schema;
481
+ },
482
+ };
483
+
484
+ module.exports = plugin;
485
+ ```
486
+
487
+ Load with CLI:
488
+
489
+ ```bash
490
+ npm install @my-org/Schemantic-plugin
491
+ npx schemantic plugin load @my-org/Schemantic-plugin
492
+ npx schemantic generate --plugins npm-plugin
493
+ ```
494
+
495
+ ### 3. Programmatic Plugin
496
+
497
+ ```typescript
498
+ import { Schemantic, SchemanticConfig } from "schemantic";
499
+ import myPlugin from "./my-plugin";
500
+
501
+ const config: SchemanticConfig = {
502
+ schemaUrl: "http://localhost:8000/openapi.json",
503
+ outputDir: "./generated",
504
+ plugins: [{ name: "my-plugin", enabled: true }],
505
+ };
506
+
507
+ const Schemantic = new Schemantic(config);
508
+ const pluginManager = Schemantic.getPluginManager();
509
+
510
+ // Register custom plugin
511
+ pluginManager.registerPlugin(myPlugin);
512
+ pluginManager.enablePlugin("my-plugin");
513
+
514
+ // Generate with plugin
515
+ const result = await Schemantic.generate();
516
+ ```
517
+
518
+ ## Testing Plugins
519
+
520
+ ### Unit Testing
521
+
522
+ ```typescript
523
+ import { describe, it, expect } from "jest";
524
+ import myPlugin from "./my-plugin";
525
+ import { GenerationContext, GeneratedType } from "schemantic";
526
+
527
+ describe("MyPlugin", () => {
528
+ it("should add comments to generated types", async () => {
529
+ const mockContext: GenerationContext = {
530
+ config: {},
531
+ schema: { info: { title: "Test API" } },
532
+ outputDir: "./test",
533
+ logger: console,
534
+ utilities: {},
535
+ } as any;
536
+
537
+ const generatedType: GeneratedType = {
538
+ name: "TestType",
539
+ content: "export interface TestType { id: string; }",
540
+ dependencies: [],
541
+ exports: ["TestType"],
542
+ isInterface: true,
543
+ isEnum: false,
544
+ isUnion: false,
545
+ sourceSchema: {},
546
+ };
547
+
548
+ await myPlugin.afterTypeGeneration?.(
549
+ "TestType",
550
+ generatedType,
551
+ mockContext
552
+ );
553
+
554
+ expect(generatedType.content).toContain("/**");
555
+ expect(generatedType.content).toContain("Generated type: TestType");
556
+ });
557
+ });
558
+ ```
559
+
560
+ ### Integration Testing
561
+
562
+ ```typescript
563
+ import { Schemantic } from "schemantic";
564
+ import myPlugin from "./my-plugin";
565
+
566
+ describe("Plugin Integration", () => {
567
+ it("should work with full generation process", async () => {
568
+ const Schemantic = new Schemantic({
569
+ schemaFile: "./test-schema.json",
570
+ outputDir: "./test-output",
571
+ });
572
+
573
+ const pluginManager = Schemantic.getPluginManager();
574
+ pluginManager.registerPlugin(myPlugin);
575
+ pluginManager.enablePlugin("my-plugin");
576
+
577
+ const result = await Schemantic.generate();
578
+ expect(result.success).toBe(true);
579
+
580
+ // Verify plugin effects in generated files
581
+ const typesContent = result.generatedFiles.find(
582
+ (f) => f.type === "types"
583
+ )?.content;
584
+ expect(typesContent).toContain("Generated type:");
585
+ });
586
+ });
587
+ ```
588
+
589
+ ## Best Practices
590
+
591
+ ### 1. Error Handling
592
+
593
+ ```typescript
594
+ const robustPlugin: SchemanticPlugin = {
595
+ name: "robust-plugin",
596
+ version: "1.0.0",
597
+ description: "Example of robust error handling",
598
+
599
+ afterTypeGeneration: async (typeName, generatedType, context) => {
600
+ try {
601
+ // Plugin logic that might fail
602
+ generatedType.content = processContent(generatedType.content);
603
+ } catch (error) {
604
+ context.logger.warn(
605
+ `Plugin failed for type ${typeName}: ${error.message}`
606
+ );
607
+ // Don't throw - allow generation to continue
608
+ }
609
+ },
610
+ };
611
+ ```
612
+
613
+ ### 2. Performance Considerations
614
+
615
+ ```typescript
616
+ const performantPlugin: SchemanticPlugin = {
617
+ name: 'performant-plugin',
618
+ version: '1.0.0',
619
+ description: 'Example of performance-conscious plugin',
620
+
621
+ // Cache expensive computations
622
+ private cache = new Map<string, any>();
623
+
624
+ transformSchema: (schema, context) => {
625
+ const cacheKey = JSON.stringify(schema);
626
+
627
+ if (this.cache.has(cacheKey)) {
628
+ return this.cache.get(cacheKey);
629
+ }
630
+
631
+ const transformed = expensiveTransform(schema);
632
+ this.cache.set(cacheKey, transformed);
633
+
634
+ return transformed;
635
+ }
636
+ };
637
+ ```
638
+
639
+ ### 3. Configuration Validation
640
+
641
+ ```typescript
642
+ import * as Joi from "joi";
643
+
644
+ const configSchema = Joi.object({
645
+ enabled: Joi.boolean().default(true),
646
+ options: Joi.object({
647
+ prefix: Joi.string().default(""),
648
+ includeComments: Joi.boolean().default(true),
649
+ }),
650
+ });
651
+
652
+ const validatedPlugin: SchemanticPlugin = {
653
+ name: "validated-plugin",
654
+ version: "1.0.0",
655
+ description: "Plugin with configuration validation",
656
+
657
+ beforeSchemaLoad: async (context) => {
658
+ const pluginConfig = context.config.plugins?.find(
659
+ (p) => p.name === "validated-plugin"
660
+ );
661
+
662
+ if (pluginConfig) {
663
+ const { error } = configSchema.validate(pluginConfig);
664
+ if (error) {
665
+ throw new Error(`Invalid plugin configuration: ${error.message}`);
666
+ }
667
+ }
668
+ },
669
+ };
670
+ ```
671
+
672
+ ## Publishing Plugins
673
+
674
+ ### 1. NPM Package Structure
675
+
676
+ ```
677
+ my-Schemantic-plugin/
678
+ ├── package.json
679
+ ├── index.js
680
+ ├── README.md
681
+ ├── LICENSE
682
+ └── test/
683
+ └── plugin.test.js
684
+ ```
685
+
686
+ ### 2. Package.json
687
+
688
+ ```json
689
+ {
690
+ "name": "@my-org/Schemantic-plugin-name",
691
+ "version": "1.0.0",
692
+ "description": "Description of your plugin",
693
+ "main": "index.js",
694
+ "keywords": ["Schemantic", "plugin", "openapi", "typescript"],
695
+ "peerDependencies": {
696
+ "schemantic": "^1.0.0"
697
+ },
698
+ "files": ["index.js", "README.md", "LICENSE"]
699
+ }
700
+ ```
701
+
702
+ ### 3. Plugin Registry
703
+
704
+ Consider adding your plugin to the community registry by submitting a PR to the Schemantic plugins repository.
705
+
706
+ ## Community Plugins
707
+
708
+ Popular community plugins include:
709
+
710
+ - `@Schemantic/plugin-zod` - Generates Zod validation schemas
711
+ - `@Schemantic/plugin-react-query` - Generates React Query hooks
712
+ - `@Schemantic/plugin-swagger-ui` - Generates Swagger UI documentation
713
+ - `@Schemantic/plugin-mock-data` - Generates mock data factories
714
+
715
+ ## Support
716
+
717
+ - 📖 [Plugin API Reference](./API.md#plugin-api)
718
+ - 💬 [Plugin Development Discussions](https://github.com/Cstannahill/schemantic/discussions)
719
+ - 🐛 [Report Plugin Issues](https://github.com/Cstannahill/schemantic/issues)
720
+ - 📧 [Plugin Development Support](mailto:plugins@Schemantic.dev)