forked from asyncapi/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSpecificationFile.ts
More file actions
331 lines (289 loc) · 8.47 KB
/
Copy pathSpecificationFile.ts
File metadata and controls
331 lines (289 loc) · 8.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import { promises as fs } from 'fs';
import path from 'path';
import { URL } from 'url';
import yaml from 'js-yaml';
import { loadContext } from './Context';
import { ErrorLoadingSpec } from '@errors/specification-file';
import { MissingContextFileError } from '@errors/context-error';
import { fileFormat } from '@cli/internal/flags/format.flags';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { logger } from '@utils/logger';
import { getErrorMessage } from '@utils/error-handler';
const { readFile, lstat } = fs;
const allowedFileNames: string[] = [
'asyncapi.json',
'asyncapi.yml',
'asyncapi.yaml',
];
const TYPE_CONTEXT_NAME = 'context-name';
const TYPE_FILE_PATH = 'file-path';
const TYPE_URL = 'url-path';
export class Specification {
private readonly spec: string;
private readonly filePath?: string;
private readonly fileURL?: string;
private readonly kind?: 'file' | 'url';
constructor(
spec: string,
options: { filepath?: string; fileURL?: string } = {},
) {
this.spec = spec;
if (options.filepath) {
this.filePath = options.filepath;
this.kind = 'file';
} else if (options.fileURL) {
this.fileURL = options.fileURL;
this.kind = 'url';
}
}
isAsyncAPI3() {
const jsObj = this.toJson();
return jsObj.asyncapi.startsWith('3.');
}
toJson(): Record<string, any> {
try {
return yaml.load(this.spec, { json: true }) as Record<string, any>;
} catch {
return JSON.parse(this.spec);
}
}
text() {
return this.spec;
}
getFilePath() {
return this.filePath;
}
getFileURL() {
return this.fileURL;
}
getKind() {
return this.kind;
}
getSource() {
return this.getFilePath() ?? this.getFileURL();
}
toSourceString() {
if (this.kind === 'file') {
return `File ${this.filePath}`;
}
return `URL ${this.fileURL}`;
}
static async fromFile(filepath: string) {
let spec;
try {
spec = await readFile(filepath, { encoding: 'utf8' });
} catch {
throw new ErrorLoadingSpec('file', filepath);
}
return new Specification(spec, { filepath });
}
static async fromURL(URLpath: string) {
let response;
const delimiter = '+';
let targetUrl = URLpath;
let proxyUrl = '';
// Check if URLpath contains a proxy URL
if (URLpath.includes(delimiter)) {
[targetUrl, proxyUrl] = URLpath.split(delimiter);
}
try {
// Validate the target URL
new URL(targetUrl);
const fetchOptions: RequestInit & { agent?: HttpsProxyAgent<string> } = { method: 'GET' };
// If proxy URL is provided, create a proxy agent
if (proxyUrl) {
try {
new URL(proxyUrl);
const proxyAgent = new HttpsProxyAgent(proxyUrl);
fetchOptions.agent = proxyAgent;
response = await fetch(targetUrl, fetchOptions);
} catch (err: unknown) {
logger.error(`Proxy connection error: ${getErrorMessage(err)}`);
throw new Error(
'Proxy Connection Error: Unable to establish a connection to the proxy check hostName or PortNumber',
);
}
} else {
response = await fetch(targetUrl);
if (!response.ok) {
throw new ErrorLoadingSpec('url', targetUrl);
}
}
} catch (error: unknown) {
logger.error(`Error loading spec from URL: ${getErrorMessage(error)}`);
throw new ErrorLoadingSpec('url', targetUrl);
}
return new Specification((await response?.text()) as string, {
fileURL: targetUrl,
});
}
}
export default class SpecificationFile {
private readonly pathToFile: string;
constructor(filePath: string) {
this.pathToFile = filePath;
}
getPath(): string {
return this.pathToFile;
}
async read(): Promise<string> {
return readFile(this.pathToFile, { encoding: 'utf8' });
}
}
interface LoadType {
file?: boolean;
url?: boolean;
context?: boolean;
}
/* eslint-disable sonarjs/cognitive-complexity */
export async function load(
filePathOrContextName?: string,
loadType?: LoadType,
): Promise<Specification> {
// NOSONAR
try {
if (filePathOrContextName) {
if (loadType?.file) {
return Specification.fromFile(filePathOrContextName);
}
if (loadType?.context) {
return loadFromContext(filePathOrContextName);
}
if (loadType?.url) {
return Specification.fromURL(filePathOrContextName);
}
const type = await nameType(filePathOrContextName);
if (type === TYPE_CONTEXT_NAME) {
return loadFromContext(filePathOrContextName);
}
if (type === TYPE_URL) {
return Specification.fromURL(filePathOrContextName);
}
await fileExists(filePathOrContextName);
return Specification.fromFile(filePathOrContextName);
}
return await loadFromContext();
} catch (e) {
const autoDetectedSpecFile = await detectSpecFile();
if (autoDetectedSpecFile) {
return Specification.fromFile(autoDetectedSpecFile);
}
if (e instanceof MissingContextFileError) {
throw new ErrorLoadingSpec();
}
throw e;
}
}
export async function nameType(name: string): Promise<string> {
if (name.startsWith('.')) {
return TYPE_FILE_PATH;
}
try {
if (await fileExists(name)) {
return TYPE_FILE_PATH;
}
return TYPE_CONTEXT_NAME;
} catch {
if (await isURL(name)) {
return TYPE_URL;
}
return TYPE_CONTEXT_NAME;
}
}
export async function isURL(urlpath: string): Promise<boolean> {
try {
const url = new URL(urlpath);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
export async function fileExists(name: string): Promise<boolean> {
try {
if ((await lstat(name)).isFile()) {
return true;
}
// Use path.extname to correctly handle multi-dot filenames like "spec.test.yaml".
// `name.split('.')[1]` only returned the *second* segment, so "spec.test.yaml"
// yielded "test" instead of "yaml".
const extension = path.extname(name).slice(1).toLowerCase();
const allowedExtenstion = ['yml', 'yaml', 'json'];
if (!allowedExtenstion.includes(extension)) {
throw new ErrorLoadingSpec('invalid file', name);
}
throw new ErrorLoadingSpec('file', name);
} catch {
throw new ErrorLoadingSpec('file', name);
}
}
async function loadFromContext(contextName?: string): Promise<Specification> {
try {
const context = await loadContext(contextName);
return Specification.fromFile(context);
} catch (error) {
if (error instanceof MissingContextFileError) {
throw new ErrorLoadingSpec();
}
throw error;
}
}
async function detectSpecFile(): Promise<string | undefined> {
const existingFileNames = await Promise.all(
allowedFileNames.map(async (filename) => {
try {
const exists = await fileExists(path.resolve(process.cwd(), filename));
return exists ? filename : undefined;
} catch {
// We did our best...
}
}),
);
return existingFileNames.find((filename) => filename !== undefined);
}
export function retrieveFileFormat(content: string): fileFormat | undefined {
try {
if (content.trimStart()[0] === '{') {
JSON.parse(content);
return 'json';
}
// below yaml.load is not a definitive way to determine if a file is yaml or not.
// it is able to load .txt text files also.
yaml.load(content);
return 'yaml';
} catch {
return undefined;
}
}
/**
* Converts a JSON or YAML specification to YAML format.
*
* @param spec - The specification content as a string
* @returns The YAML formatted string, or undefined if conversion fails
*/
export function convertToYaml(spec: string): string | undefined {
try {
// JS object -> YAML string
const jsonContent = yaml.load(spec);
return yaml.dump(jsonContent);
} catch (err: unknown) {
logger.error(`Failed to convert spec to YAML: ${getErrorMessage(err)}`);
return undefined;
}
}
/**
* Converts a JSON or YAML specification to JSON format.
*
* @param spec - The specification content as a string
* @returns The JSON formatted string, or undefined if conversion fails
*/
export function convertToJSON(spec: string): string | undefined {
try {
// JSON or YAML String -> JS object
const jsonContent = yaml.load(spec);
// JS Object -> pretty JSON string
return JSON.stringify(jsonContent, null, 2);
} catch (err: unknown) {
logger.error(`Failed to convert spec to JSON: ${getErrorMessage(err)}`);
return undefined;
}
}