@ygorazambuja/sauron 1.0.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.
@@ -0,0 +1,174 @@
1
+ import { dirname } from "node:path";
2
+ import { mkdirSync } from "node:fs";
3
+ import { createDefaultPluginRegistry, type PluginRegistry } from "./registry";
4
+ import type {
5
+ PluginCanRunFailure,
6
+ PluginContext,
7
+ PluginExecutionResult,
8
+ PluginFile,
9
+ SauronPlugin,
10
+ } from "./types";
11
+
12
+ /**
13
+ * Run HTTP plugins.
14
+ * @param requestedPluginIds Input parameter `requestedPluginIds`.
15
+ * @param context Input parameter `context`.
16
+ * @param registry Input parameter `registry`.
17
+ * @returns Run HTTP plugins output as `Promise<PluginExecutionResult[]>`.
18
+ * @example
19
+ * ```ts
20
+ * const result = await runHttpPlugins([], {} as PluginContext);
21
+ * // result: PluginExecutionResult[]
22
+ * ```
23
+ */
24
+ export async function runHttpPlugins(
25
+ requestedPluginIds: string[],
26
+ context: PluginContext,
27
+ registry = createDefaultPluginRegistry(),
28
+ ): Promise<PluginExecutionResult[]> {
29
+ if (requestedPluginIds.length === 0) {
30
+ return [];
31
+ }
32
+
33
+ const results: PluginExecutionResult[] = [];
34
+ for (const requestedPluginId of requestedPluginIds) {
35
+ const requestedPlugin = resolvePluginOrThrow(requestedPluginId, registry);
36
+ const executablePlugin = resolveRunnablePlugin(
37
+ requestedPlugin,
38
+ context,
39
+ registry,
40
+ new Set<string>(),
41
+ );
42
+ const outputPaths = executablePlugin.resolveOutputs(context);
43
+ const generated = await executablePlugin.generate(context);
44
+ await writePluginFiles(generated.files, context);
45
+ const result: PluginExecutionResult = {
46
+ requestedPluginId,
47
+ executedPluginId: executablePlugin.id,
48
+ methodCount: generated.methodCount,
49
+ servicePath: outputPaths.servicePath,
50
+ reportPath: outputPaths.reportPath,
51
+ };
52
+ if (outputPaths.typeCoverageReportPath) {
53
+ result.typeCoverageReportPath = outputPaths.typeCoverageReportPath;
54
+ }
55
+ results.push(result);
56
+ }
57
+
58
+ return results;
59
+ }
60
+
61
+ /**
62
+ * Resolve runnable plugin.
63
+ * @param plugin Input parameter `plugin`.
64
+ * @param context Input parameter `context`.
65
+ * @param registry Input parameter `registry`.
66
+ * @param visitedPluginIds Input parameter `visitedPluginIds`.
67
+ * @returns Resolve runnable plugin output as `SauronPlugin`.
68
+ * @example
69
+ * ```ts
70
+ * const result = resolveRunnablePlugin(
71
+ * { id: "fetch", kind: "http-client", canRun: () => ({ ok: true }), resolveOutputs: () => ({ servicePath: "", reportPath: "" }), generate: async () => ({ files: [], methodCount: 0 }) },
72
+ * {} as PluginContext,
73
+ * createDefaultPluginRegistry(),
74
+ * new Set<string>(),
75
+ * );
76
+ * // result: SauronPlugin
77
+ * ```
78
+ */
79
+ function resolveRunnablePlugin(
80
+ plugin: SauronPlugin,
81
+ context: PluginContext,
82
+ registry: PluginRegistry,
83
+ visitedPluginIds: Set<string>,
84
+ ): SauronPlugin {
85
+ if (visitedPluginIds.has(plugin.id)) {
86
+ throw new Error(
87
+ `Circular fallback detected while resolving plugin "${plugin.id}".`,
88
+ );
89
+ }
90
+
91
+ visitedPluginIds.add(plugin.id);
92
+ const canRunResult = plugin.canRun(context);
93
+ if (canRunResult.ok) {
94
+ return plugin;
95
+ }
96
+
97
+ warnPluginFailure(canRunResult);
98
+ if (!canRunResult.fallbackPluginId) {
99
+ throw new Error(
100
+ `Plugin "${plugin.id}" cannot run: ${canRunResult.reason}`,
101
+ );
102
+ }
103
+
104
+ const fallbackPlugin = resolvePluginOrThrow(
105
+ canRunResult.fallbackPluginId,
106
+ registry,
107
+ );
108
+ return resolveRunnablePlugin(
109
+ fallbackPlugin,
110
+ context,
111
+ registry,
112
+ visitedPluginIds,
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Resolve plugin or throw.
118
+ * @param pluginId Input parameter `pluginId`.
119
+ * @param registry Input parameter `registry`.
120
+ * @returns Resolve plugin or throw output as `SauronPlugin`.
121
+ * @example
122
+ * ```ts
123
+ * const result = resolvePluginOrThrow("fetch", createDefaultPluginRegistry());
124
+ * // result: SauronPlugin
125
+ * ```
126
+ */
127
+ function resolvePluginOrThrow(
128
+ pluginId: string,
129
+ registry: PluginRegistry,
130
+ ): SauronPlugin {
131
+ const plugin = registry.resolve(pluginId);
132
+ if (plugin) {
133
+ return plugin;
134
+ }
135
+
136
+ throw new Error(`Unknown plugin "${pluginId}".`);
137
+ }
138
+
139
+ /**
140
+ * Warn plugin failure.
141
+ * @param canRunFailure Input parameter `canRunFailure`.
142
+ * @example
143
+ * ```ts
144
+ * warnPluginFailure({ ok: false, reason: "fallback", fallbackPluginId: "fetch" });
145
+ * ```
146
+ */
147
+ function warnPluginFailure(canRunFailure: PluginCanRunFailure): void {
148
+ if (!canRunFailure.reason) {
149
+ return;
150
+ }
151
+
152
+ console.warn(canRunFailure.reason);
153
+ }
154
+
155
+ /**
156
+ * Write plugin files.
157
+ * @param files Input parameter `files`.
158
+ * @param context Input parameter `context`.
159
+ * @returns Write plugin files output as `Promise<void>`.
160
+ * @example
161
+ * ```ts
162
+ * const result = await writePluginFiles([], {} as PluginContext);
163
+ * // result: void
164
+ * ```
165
+ */
166
+ async function writePluginFiles(
167
+ files: PluginFile[],
168
+ context: PluginContext,
169
+ ): Promise<void> {
170
+ for (const file of files) {
171
+ mkdirSync(dirname(file.path), { recursive: true });
172
+ await context.writeFormattedFile(file.path, file.content);
173
+ }
174
+ }
@@ -0,0 +1,97 @@
1
+ import type { z } from "zod";
2
+ import type { CliOptions } from "../cli/types";
3
+ import type { SwaggerOrOpenAPISchema } from "../schemas/swagger";
4
+ import type { OperationTypeMap, TypeNameMap } from "../utils";
5
+
6
+ /**
7
+ * Plugin kind.
8
+ */
9
+ export type PluginKind = "http-client";
10
+
11
+ /**
12
+ * Plugin can run success result.
13
+ */
14
+ export type PluginCanRunSuccess = {
15
+ ok: true;
16
+ };
17
+
18
+ /**
19
+ * Plugin can run failure result.
20
+ */
21
+ export type PluginCanRunFailure = {
22
+ ok: false;
23
+ reason: string;
24
+ fallbackPluginId?: string;
25
+ };
26
+
27
+ /**
28
+ * Plugin can run result.
29
+ */
30
+ export type PluginCanRunResult = PluginCanRunSuccess | PluginCanRunFailure;
31
+
32
+ /**
33
+ * Generated plugin file.
34
+ */
35
+ export type PluginFile = {
36
+ path: string;
37
+ content: string;
38
+ };
39
+
40
+ /**
41
+ * Plugin output paths.
42
+ */
43
+ export type PluginOutputPaths = {
44
+ servicePath: string;
45
+ reportPath: string;
46
+ typeCoverageReportPath?: string;
47
+ };
48
+
49
+ /**
50
+ * Plugin generation result.
51
+ */
52
+ export type PluginGenerateResult = {
53
+ files: PluginFile[];
54
+ methodCount: number;
55
+ };
56
+
57
+ /**
58
+ * Plugin context.
59
+ */
60
+ export type PluginContext = {
61
+ schema: z.infer<typeof SwaggerOrOpenAPISchema>;
62
+ options: CliOptions;
63
+ baseOutputPath: string;
64
+ modelsPath: string;
65
+ fileHeader: string;
66
+ operationTypes: OperationTypeMap;
67
+ typeNameMap: TypeNameMap;
68
+ isAngularProject: boolean;
69
+ writeFormattedFile: (
70
+ filePath: string,
71
+ content: string,
72
+ ) => Promise<void>;
73
+ };
74
+
75
+ /**
76
+ * Sauron plugin definition.
77
+ */
78
+ export interface SauronPlugin {
79
+ id: string;
80
+ aliases?: string[];
81
+ kind: PluginKind;
82
+ canRun(context: PluginContext): PluginCanRunResult;
83
+ resolveOutputs(context: PluginContext): PluginOutputPaths;
84
+ generate(context: PluginContext): Promise<PluginGenerateResult>;
85
+ }
86
+
87
+ /**
88
+ * Plugin execution result.
89
+ */
90
+ export type PluginExecutionResult = {
91
+ requestedPluginId: string;
92
+ executedPluginId: string;
93
+ methodCount: number;
94
+ servicePath: string;
95
+ reportPath: string;
96
+ typeCoverageReportPath?: string;
97
+ };
@@ -0,0 +1,134 @@
1
+ import { z } from "zod";
2
+
3
+ // --- 1. Schemas de Nível Mais Baixo (Responses, Methods) ---
4
+
5
+ /**
6
+ * Representa o Response Object (OpenAPI 3.0)
7
+ * Foca apenas na descrição, que é obrigatória.
8
+ */
9
+ const ResponseSchema = z
10
+ .object({
11
+ description: z.string().min(1, "A descrição da resposta é obrigatória."),
12
+ // Campos como 'content' e 'headers' seriam adicionados aqui
13
+ })
14
+ .passthrough();
15
+
16
+ /**
17
+ * Representa o Operation Object (OpenAPI 3.0), como 'get', 'post', etc.
18
+ * Foca em 'responses' (opcional para tolerar specs incompletos).
19
+ */
20
+ const HttpMethodSchema = z
21
+ .object({
22
+ tags: z.array(z.string()).optional(),
23
+ responses: z
24
+ .record(
25
+ z.string().regex(/^(\d{3}|default)$/), // Chave é o código de status HTTP (ex: "200") ou "default" para Swagger 2.0
26
+ ResponseSchema,
27
+ )
28
+ .optional(),
29
+ // Campos como 'parameters' e 'requestBody' seriam opcionais aqui
30
+ })
31
+ .passthrough();
32
+
33
+ /**
34
+ * Representa o Path Item Object (OpenAPI 3.0)
35
+ * Contém os métodos HTTP permitidos para um único caminho (path).
36
+ */
37
+ const PathItemSchema = z
38
+ .object({
39
+ get: HttpMethodSchema.optional(),
40
+ post: HttpMethodSchema.optional(),
41
+ put: HttpMethodSchema.optional(),
42
+ delete: HttpMethodSchema.optional(),
43
+ // Adicione outros métodos se necessário, como head, patch, etc.
44
+ })
45
+ .partial(); // Usamos .partial() para que os métodos sejam opcionais no Path Item
46
+
47
+ // --- 2. Schemas de Nível Superior (Info, Paths) ---
48
+
49
+ /**
50
+ * Representa o Info Object (OpenAPI 3.0)
51
+ * Requer 'title' e 'version'.
52
+ */
53
+ const InfoSchema = z
54
+ .object({
55
+ title: z.string().min(1, "O título da API é obrigatório."),
56
+ version: z.string().min(1, "A versão da API é obrigatória."),
57
+ // Campos como 'description' e 'license' seriam opcionais
58
+ })
59
+ .passthrough();
60
+
61
+ /**
62
+ * Representa o Paths Object (OpenAPI 3.0)
63
+ * É um mapa (record) onde a chave é o path e o valor é o PathItemSchema.
64
+ */
65
+ export const PathsSchema = z.record(
66
+ z.string().startsWith("/"), // A chave deve ser uma string de path (ex: "/users")
67
+ PathItemSchema,
68
+ );
69
+
70
+ export const ComponentsSchema = z
71
+ .object({
72
+ schemas: z.record(z.string(), z.any()), // Onde os DTOs reais seriam validados
73
+ securitySchemes: z.record(z.string(), z.any()).optional(),
74
+ // Simplificado. Adicione mais componentes se precisar de validação rigorosa.
75
+ })
76
+ .passthrough();
77
+
78
+ // --- 3. Schemas Raiz (OpenAPI e Swagger Objects) ---
79
+
80
+ /**
81
+ * Representa o Objeto Swagger 2.0 Raiz (swagger.json)
82
+ * Valida os campos obrigatórios e essenciais para Swagger 2.0.
83
+ */
84
+ export const SwaggerBasicSchema = z
85
+ .object({
86
+ swagger: z.string().startsWith("2.0", "Deve ser uma versão Swagger 2.0."), // Garante a versão 2.0
87
+ info: InfoSchema,
88
+ host: z.string().optional(),
89
+ basePath: z.string().optional(),
90
+ schemes: z.array(z.string()).optional(),
91
+ consumes: z.array(z.string()).optional(),
92
+ produces: z.array(z.string()).optional(),
93
+ paths: PathsSchema,
94
+ definitions: z.record(z.string(), z.any()).optional(), // Swagger 2.0 usa 'definitions' em vez de 'components.schemas'
95
+ parameters: z.record(z.string(), z.any()).optional(),
96
+ responses: z.record(z.string(), z.any()).optional(),
97
+ securityDefinitions: z.record(z.string(), z.any()).optional(),
98
+ security: z.array(z.any()).optional(),
99
+ tags: z
100
+ .array(
101
+ z.object({
102
+ name: z.string(),
103
+ description: z.string().optional(),
104
+ externalDocs: z.any().optional(),
105
+ }),
106
+ )
107
+ .optional(),
108
+ externalDocs: z.any().optional(),
109
+ // Outros campos opcionais do Swagger 2.0
110
+ })
111
+ .passthrough();
112
+
113
+ /**
114
+ * Representa o Objeto OpenAPI 3.0+ Raiz (swagger.json)
115
+ * Valida os campos obrigatórios e essenciais.
116
+ */
117
+ export const OpenAPIBasicSchema = z
118
+ .object({
119
+ openapi: z.string().startsWith("3.0", "Deve ser uma versão OpenAPI 3.x.x."), // Garante a versão 3.x.x
120
+ info: InfoSchema,
121
+ paths: PathsSchema,
122
+ components: ComponentsSchema.optional(),
123
+ // components: ... Adicione a validação dos componentes se necessário
124
+ // security: ...
125
+ })
126
+ .passthrough();
127
+
128
+ /**
129
+ * Union schema que aceita tanto OpenAPI 3.0+ quanto Swagger 2.0
130
+ */
131
+ export const SwaggerOrOpenAPISchema = z.union([
132
+ OpenAPIBasicSchema,
133
+ SwaggerBasicSchema,
134
+ ]);