import * as fs from 'fs'; import { extname } from 'path'; import * as minimatch from 'minimatch'; import { parseYaml } from './js-yaml'; import { env } from './env'; import { logger, colorize } from './logger'; import { HttpsProxyAgent } from 'https-proxy-agent'; import * as pluralizeOne from 'pluralize'; import type { HttpResolveConfig } from './config'; import type { UserContext } from './walk'; export { parseYaml, stringifyYaml } from './js-yaml'; export type StackFrame = { prev: StackFrame | null; value: T; }; export type Stack = StackFrame | null; export type StackNonEmpty = StackFrame; export function pushStack = Stack>(head: P, value: T) { return { prev: head, value }; } export function pluralize(sentence: string, count?: number, inclusive?: boolean) { return sentence .split(' ') .map((word) => pluralizeOne(word, count, inclusive)) .join(' '); } export function popStack>(head: P) { return head?.prev ?? null; } export type BundleOutputFormat = 'json' | 'yml' | 'yaml'; export async function loadYaml(filename: string): Promise { const contents = await fs.promises.readFile(filename, 'utf-8'); return parseYaml(contents) as T; } export function isDefined(x: T | undefined): x is T { return x !== undefined; } export function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value); } export function isEmptyObject(value: unknown): value is Record { return isPlainObject(value) && Object.keys(value).length === 0; } export function isNotEmptyObject(obj: unknown): boolean { return isPlainObject(obj) && !isEmptyObject(obj); } export function isEmptyArray(value: unknown) { return Array.isArray(value) && value.length === 0; } export function isNotEmptyArray(args?: T[]): boolean { return !!args && Array.isArray(args) && !!args.length; } export async function readFileFromUrl(url: string, config: HttpResolveConfig) { const headers: Record = {}; for (const header of config.headers) { if (match(url, header.matches)) { headers[header.name] = header.envVariable !== undefined ? env[header.envVariable] || '' : header.value; } } const req = await (config.customFetch || fetch)(url, { headers: headers, }); if (!req.ok) { throw new Error(`Failed to load ${url}: ${req.status} ${req.statusText}`); } return { body: await req.text(), mimeType: req.headers.get('content-type') }; } function match(url: string, pattern: string) { if (!pattern.match(/^https?:\/\//)) { // if pattern doesn't specify protocol directly, do not match against it url = url.replace(/^https?:\/\//, ''); } return minimatch(url, pattern); } export function pickObjectProps>( object: T, keys: Array ): T { return Object.fromEntries( keys.filter((key: string) => key in object).map((key: string) => [key, object[key]]) ) as T; } export function omitObjectProps>( object: T, keys: Array ): T { return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key))) as T; } export function splitCamelCaseIntoWords(str: string) { const camel = str .split(/(?:[-._])|([A-Z][a-z]+)/) .filter(isTruthy) .map((item) => item.toLocaleLowerCase()); const caps = str .split(/([A-Z]{2,})/) .filter((e: string) => e && e === e.toUpperCase()) .map((item) => item.toLocaleLowerCase()); return new Set([...camel, ...caps]); } export function validateMimeType( { type, value }: any, { report, location }: UserContext, allowedValues: string[] ) { const ruleType = type === 'consumes' ? 'request' : 'response'; if (!allowedValues) throw new Error(`Parameter "allowedValues" is not provided for "${ruleType}-mime-type" rule`); if (!value[type]) return; for (const mime of value[type]) { if (!allowedValues.includes(mime)) { report({ message: `Mime type "${mime}" is not allowed`, location: location.child(value[type].indexOf(mime)).key(), }); } } } export function validateMimeTypeOAS3( { type, value }: any, { report, location }: UserContext, allowedValues: string[] ) { const ruleType = type === 'consumes' ? 'request' : 'response'; if (!allowedValues) throw new Error(`Parameter "allowedValues" is not provided for "${ruleType}-mime-type" rule`); if (!value.content) return; for (const mime of Object.keys(value.content)) { if (!allowedValues.includes(mime)) { report({ message: `Mime type "${mime}" is not allowed`, location: location.child('content').child(mime).key(), }); } } } export function readFileAsStringSync(filePath: string) { return fs.readFileSync(filePath, 'utf-8'); } export function yamlAndJsonSyncReader(filePath: string): T { const content = fs.readFileSync(filePath, 'utf-8'); return parseYaml(content) as T; } export function isPathParameter(pathSegment: string) { return pathSegment.startsWith('{') && pathSegment.endsWith('}'); } /** * Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar */ export function slash(path: string): string { const isExtendedLengthPath = /^\\\\\?\\/.test(path); if (isExtendedLengthPath) { return path; } return path.replace(/\\/g, '/'); } // TODO: use it everywhere export function isString(value: unknown): value is string { return typeof value === 'string'; } export function isNotString(value: string | T): value is T { return !isString(value); } export const assignConfig = ( target: Record, obj?: Record ) => { if (!obj) return; for (const k of Object.keys(obj)) { if (isPlainObject(target[k]) && typeof obj[k] === 'string') { target[k].severity = obj[k]; } else { target[k] = obj[k]; } } }; export function assignOnlyExistingConfig( target: Record, obj?: Record ) { if (!obj) return; for (const k of Object.keys(obj)) { if (!target.hasOwnProperty(k)) continue; if (isPlainObject(target[k]) && typeof obj[k] === 'string') { target[k].severity = obj[k]; } else { target[k] = obj[k]; } } } export function getMatchingStatusCodeRange(code: number | string): string { return `${code}`.replace(/^(\d)\d\d$/, (_, firstDigit) => `${firstDigit}XX`); } export function isCustomRuleId(id: string) { return id.includes('/'); } export function doesYamlFileExist(filePath: string): boolean { return ( (extname(filePath) === '.yaml' || extname(filePath) === '.yml') && fs?.hasOwnProperty?.('existsSync') && fs.existsSync(filePath) ); } export function showWarningForDeprecatedField( deprecatedField: string, updatedField?: string, updatedObject?: string, link?: string ) { const readMoreText = link ? `Read more about this change: ${link}` : ''; logger.warn( `The '${colorize.red(deprecatedField)}' field is deprecated. ${ updatedField ? `Use ${colorize.green(getUpdatedFieldName(updatedField, updatedObject))} instead. ` : '' }${readMoreText}\n` ); } export function showErrorForDeprecatedField( deprecatedField: string, updatedField?: string, updatedObject?: string ) { throw new Error( `Do not use '${deprecatedField}' field. ${ updatedField ? `Use '${getUpdatedFieldName(updatedField, updatedObject)}' instead. ` : '' }\n` ); } export type Falsy = undefined | null | false | '' | 0; export function isTruthy(value: Truthy | Falsy): value is Truthy { return !!value; } export function identity(value: T): T { return value; } export function keysOf(obj: T) { if (!obj) return []; return Object.keys(obj) as (keyof T)[]; } export function pickDefined>( obj?: T ): Record | undefined { if (!obj) return undefined; const res: Record = {}; for (const key in obj) { if (obj[key] !== undefined) { res[key] = obj[key]; } } return res; } export function nextTick() { return new Promise((resolve) => { setTimeout(resolve); }); } export async function pause(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function getUpdatedFieldName(updatedField: string, updatedObject?: string) { return `${typeof updatedObject !== 'undefined' ? `${updatedObject}.` : ''}${updatedField}`; } export function getProxyAgent() { const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; return proxy ? new HttpsProxyAgent(proxy) : undefined; } /** * Checks if two objects are deeply equal. * Borrowed the source code from https://github.com/lukeed/dequal. */ export function dequal(foo: any, bar: any): boolean { let ctor, len; if (foo === bar) return true; if (foo && bar && (ctor = foo.constructor) === bar.constructor) { if (ctor === Date) return foo.getTime() === bar.getTime(); if (ctor === RegExp) return foo.toString() === bar.toString(); if (ctor === Array) { if ((len = foo.length) === bar.length) { while (len-- && dequal(foo[len], bar[len])); } return len === -1; } if (!ctor || typeof foo === 'object') { len = 0; for (ctor in foo) { if ( Object.prototype.hasOwnProperty.call(foo, ctor) && ++len && !Object.prototype.hasOwnProperty.call(bar, ctor) ) return false; if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false; } return Object.keys(bar).length === len; } } return foo !== foo && bar !== bar; } export type CollectFn = (value: unknown) => void; export type StrictObject = T & { [key: string]: undefined };