Skip to content

Commit

Permalink
refactor(config/validation): move helper fns to separate file (#33206)
Browse files Browse the repository at this point in the history
  • Loading branch information
RahulGautamSingh authored Dec 19, 2024
1 parent 94ccb91 commit f98db74
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 148 deletions.
17 changes: 17 additions & 0 deletions lib/config/validation-helpers/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getParentName } from './utils';

describe('config/validation-helpers/utils', () => {
describe('getParentName()', () => {
it('ignores encrypted in root', () => {
expect(getParentName('encrypted')).toBeEmptyString();
});

it('handles array types', () => {
expect(getParentName('hostRules[1]')).toBe('hostRules');
});

it('handles encrypted within array types', () => {
expect(getParentName('hostRules[0].encrypted')).toBe('hostRules');
});
});
});
138 changes: 138 additions & 0 deletions lib/config/validation-helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import is from '@sindresorhus/is';
import { logger } from '../../logger';
import type {
RegexManagerConfig,
RegexManagerTemplates,
} from '../../modules/manager/custom/regex/types';
import { regEx } from '../../util/regex';
import type { ValidationMessage } from '../types';

export function getParentName(parentPath: string | undefined): string {
return parentPath
? parentPath
.replace(regEx(/\.?encrypted$/), '')
.replace(regEx(/\[\d+\]$/), '')
.split('.')
.pop()!
: '.';
}

export function validatePlainObject(
val: Record<string, unknown>,
): true | string {
for (const [key, value] of Object.entries(val)) {
if (!is.string(value)) {
return key;
}
}
return true;
}

export function validateNumber(
key: string,
val: unknown,
allowsNegative: boolean,
currentPath?: string,
subKey?: string,
): ValidationMessage[] {
const errors: ValidationMessage[] = [];
const path = `${currentPath}${subKey ? '.' + subKey : ''}`;
if (is.number(val)) {
if (val < 0 && !allowsNegative) {
errors.push({
topic: 'Configuration Error',
message: `Configuration option \`${path}\` should be a positive integer. Found negative value instead.`,
});
}
} else {
errors.push({
topic: 'Configuration Error',
message: `Configuration option \`${path}\` should be an integer. Found: ${JSON.stringify(
val,
)} (${typeof val}).`,
});
}

return errors;
}

/** An option is a false global if it has the same name as a global only option
* but is actually just the field of a non global option or field an children of the non global option
* eg. token: it's global option used as the bot's token as well and
* also it can be the token used for a platform inside the hostRules configuration
*/
export function isFalseGlobal(
optionName: string,
parentPath?: string,
): boolean {
if (parentPath?.includes('hostRules')) {
if (
optionName === 'token' ||
optionName === 'username' ||
optionName === 'password'
) {
return true;
}
}

return false;
}

function hasField(
customManager: Partial<RegexManagerConfig>,
field: string,
): boolean {
const templateField = `${field}Template` as keyof RegexManagerTemplates;
return !!(
customManager[templateField] ??
customManager.matchStrings?.some((matchString) =>
matchString.includes(`(?<${field}>`),
)
);
}

export function validateRegexManagerFields(
customManager: Partial<RegexManagerConfig>,
currentPath: string,
errors: ValidationMessage[],
): void {
if (is.nonEmptyArray(customManager.matchStrings)) {
for (const matchString of customManager.matchStrings) {
try {
regEx(matchString);
} catch (err) {
logger.debug(
{ err },
'customManager.matchStrings regEx validation error',
);
errors.push({
topic: 'Configuration Error',
message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
});
}
}
} else {
errors.push({
topic: 'Configuration Error',
message: `Each Custom Manager must contain a non-empty matchStrings array`,
});
}

const mandatoryFields = ['currentValue', 'datasource'];
for (const field of mandatoryFields) {
if (!hasField(customManager, field)) {
errors.push({
topic: 'Configuration Error',
message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
});
}
}

const nameFields = ['depName', 'packageName'];
if (!nameFields.some((field) => hasField(customManager, field))) {
errors.push({
topic: 'Configuration Error',
message: `Regex Managers must contain depName or packageName regex groups or templates`,
});
}
}
16 changes: 0 additions & 16 deletions lib/config/validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,6 @@ import type { RenovateConfig } from './types';
import * as configValidation from './validation';

describe('config/validation', () => {
describe('getParentName()', () => {
it('ignores encrypted in root', () => {
expect(configValidation.getParentName('encrypted')).toBeEmptyString();
});

it('handles array types', () => {
expect(configValidation.getParentName('hostRules[1]')).toBe('hostRules');
});

it('handles encrypted within array types', () => {
expect(configValidation.getParentName('hostRules[0].encrypted')).toBe(
'hostRules',
);
});
});

describe('validateConfig(config)', () => {
it('returns deprecation warnings', async () => {
const config = {
Expand Down
151 changes: 19 additions & 132 deletions lib/config/validation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import is from '@sindresorhus/is';
import { logger } from '../logger';
import { allManagersList, getManagerList } from '../modules/manager';
import { isCustomManager } from '../modules/manager/custom';
import type {
RegexManagerConfig,
RegexManagerTemplates,
} from '../modules/manager/custom/regex/types';
import type { CustomManager } from '../modules/manager/custom/types';
import type { HostRule } from '../types';
import { getExpression } from '../util/jsonata';
Expand Down Expand Up @@ -39,6 +34,13 @@ import { allowedStatusCheckStrings } from './types';
import * as managerValidator from './validation-helpers/managers';
import * as matchBaseBranchesValidator from './validation-helpers/match-base-branches';
import * as regexOrGlobValidator from './validation-helpers/regex-glob-matchers';
import {
getParentName,
isFalseGlobal,
validateNumber,
validatePlainObject,
validateRegexManagerFields,
} from './validation-helpers/utils';

const options = getOptions();

Expand Down Expand Up @@ -84,42 +86,6 @@ function isIgnored(key: string): boolean {
return ignoredNodes.includes(key);
}

function validatePlainObject(val: Record<string, unknown>): true | string {
for (const [key, value] of Object.entries(val)) {
if (!is.string(value)) {
return key;
}
}
return true;
}

function validateNumber(
key: string,
val: unknown,
currentPath?: string,
subKey?: string,
): ValidationMessage[] {
const errors: ValidationMessage[] = [];
const path = `${currentPath}${subKey ? '.' + subKey : ''}`;
if (is.number(val)) {
if (val < 0 && !optionAllowsNegativeIntegers.has(key)) {
errors.push({
topic: 'Configuration Error',
message: `Configuration option \`${path}\` should be a positive integer. Found negative value instead.`,
});
}
} else {
errors.push({
topic: 'Configuration Error',
message: `Configuration option \`${path}\` should be an integer. Found: ${JSON.stringify(
val,
)} (${typeof val}).`,
});
}

return errors;
}

function getUnsupportedEnabledManagers(enabledManagers: string[]): string[] {
return enabledManagers.filter(
(manager) => !allManagersList.includes(manager.replace('custom.', '')),
Expand Down Expand Up @@ -186,16 +152,6 @@ function initOptions(): void {
optionsInitialized = true;
}

export function getParentName(parentPath: string | undefined): string {
return parentPath
? parentPath
.replace(regEx(/\.?encrypted$/), '')
.replace(regEx(/\[\d+\]$/), '')
.split('.')
.pop()!
: '.';
}

export async function validateConfig(
configType: 'global' | 'inherit' | 'repo',
config: RenovateConfig,
Expand Down Expand Up @@ -370,7 +326,8 @@ export async function validateConfig(
});
}
} else if (type === 'integer') {
errors.push(...validateNumber(key, val, currentPath));
const allowsNegative = optionAllowsNegativeIntegers.has(key);
errors.push(...validateNumber(key, val, allowsNegative, currentPath));
} else if (type === 'array' && val) {
if (is.array(val)) {
for (const [subIndex, subval] of val.entries()) {
Expand Down Expand Up @@ -865,65 +822,6 @@ export async function validateConfig(
return { errors, warnings };
}

function hasField(
customManager: Partial<RegexManagerConfig>,
field: string,
): boolean {
const templateField = `${field}Template` as keyof RegexManagerTemplates;
return !!(
customManager[templateField] ??
customManager.matchStrings?.some((matchString) =>
matchString.includes(`(?<${field}>`),
)
);
}

function validateRegexManagerFields(
customManager: Partial<RegexManagerConfig>,
currentPath: string,
errors: ValidationMessage[],
): void {
if (is.nonEmptyArray(customManager.matchStrings)) {
for (const matchString of customManager.matchStrings) {
try {
regEx(matchString);
} catch (err) {
logger.debug(
{ err },
'customManager.matchStrings regEx validation error',
);
errors.push({
topic: 'Configuration Error',
message: `Invalid regExp for ${currentPath}: \`${matchString}\``,
});
}
}
} else {
errors.push({
topic: 'Configuration Error',
message: `Each Custom Manager must contain a non-empty matchStrings array`,
});
}

const mandatoryFields = ['currentValue', 'datasource'];
for (const field of mandatoryFields) {
if (!hasField(customManager, field)) {
errors.push({
topic: 'Configuration Error',
message: `Regex Managers must contain ${field}Template configuration or regex group named ${field}`,
});
}
}

const nameFields = ['depName', 'packageName'];
if (!nameFields.some((field) => hasField(customManager, field))) {
errors.push({
topic: 'Configuration Error',
message: `Regex Managers must contain depName or packageName regex groups or templates`,
});
}
}

/**
* Basic validation for global config options
*/
Expand Down Expand Up @@ -1013,7 +911,8 @@ async function validateGlobalConfig(
});
}
} else if (type === 'integer') {
warnings.push(...validateNumber(key, val, currentPath));
const allowsNegative = optionAllowsNegativeIntegers.has(key);
warnings.push(...validateNumber(key, val, allowsNegative, currentPath));
} else if (type === 'boolean') {
if (val !== true && val !== false) {
warnings.push({
Expand Down Expand Up @@ -1079,8 +978,15 @@ async function validateGlobalConfig(
}
} else if (key === 'cacheTtlOverride') {
for (const [subKey, subValue] of Object.entries(val)) {
const allowsNegative = optionAllowsNegativeIntegers.has(key);
warnings.push(
...validateNumber(key, subValue, currentPath, subKey),
...validateNumber(
key,
subValue,
allowsNegative,
currentPath,
subKey,
),
);
}
} else {
Expand All @@ -1101,22 +1007,3 @@ async function validateGlobalConfig(
}
}
}

/** An option is a false global if it has the same name as a global only option
* but is actually just the field of a non global option or field an children of the non global option
* eg. token: it's global option used as the bot's token as well and
* also it can be the token used for a platform inside the hostRules configuration
*/
function isFalseGlobal(optionName: string, parentPath?: string): boolean {
if (parentPath?.includes('hostRules')) {
if (
optionName === 'token' ||
optionName === 'username' ||
optionName === 'password'
) {
return true;
}
}

return false;
}

0 comments on commit f98db74

Please sign in to comment.