Improve script/category name validation

- Use better error messages with more context.
- Unify their validation logic and share tests.
- Validate also type of the name.
- Refactor node (Script/Category) parser tests for easier future
  changes and cleaner test code (using `TestBuilder` to do dirty work in
  unified way).
- Add more tests. Custom `Error` properties are compared manually due to
  `chai` not supporting deep equality checks (chaijs/chai#1065,
  chaijs/chai#1405).
This commit is contained in:
undergroundwires
2021-12-16 02:40:56 +01:00
parent 65902e5b72
commit b210aaddf2
17 changed files with 857 additions and 196 deletions

View File

@@ -5,22 +5,31 @@ import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { parseDocUrls } from '../DocumentationParser';
import { createEnumParser, IEnumParser } from '../../Common/Enum';
import { NodeType } from '../NodeValidation/NodeType';
import { NodeValidator } from '../NodeValidation/NodeValidator';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
// eslint-disable-next-line consistent-return
export function parseScript(
data: ScriptData,
context: ICategoryCollectionParseContext,
levelParser = createEnumParser(RecommendationLevel),
scriptFactory: ScriptFactoryType = ScriptFactory,
): Script {
validateScript(data);
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
validateScript(data, validator);
if (!context) { throw new Error('missing context'); }
const script = new Script(
/* name: */ data.name,
/* code: */ parseCode(data, context),
/* docs: */ parseDocUrls(data),
/* level: */ parseLevel(data.recommend, levelParser),
);
return script;
try {
const script = scriptFactory(
/* name: */ data.name,
/* code: */ parseCode(data, context),
/* docs: */ parseDocUrls(data),
/* level: */ parseLevel(data.recommend, levelParser),
);
return script;
} catch (err) {
validator.throw(err.message);
}
}
function parseLevel(
@@ -40,21 +49,24 @@ function parseCode(script: ScriptData, context: ICategoryCollectionParseContext)
return new ScriptCode(script.code, script.revertCode, context.syntax);
}
function ensureNotBothCallAndCode(script: ScriptData) {
if (script.code && script.call) {
throw new Error('cannot define both "call" and "code"');
}
if (script.revertCode && script.call) {
throw new Error('cannot define "revertCode" if "call" is defined');
}
function validateScript(script: ScriptData, validator: NodeValidator) {
validator
.assertDefined(script)
.assertValidName(script.name)
.assert(
() => Boolean(script.code || script.call),
'Must define either "call" or "code".',
)
.assert(
() => !(script.code && script.call),
'Cannot define both "call" and "code".',
)
.assert(
() => !(script.revertCode && script.call),
'Cannot define "revertCode" if "call" is defined.',
);
}
function validateScript(script: ScriptData) {
if (!script) {
throw new Error('missing script');
}
if (!script.code && !script.call) {
throw new Error('must define either "call" or "code"');
}
ensureNotBothCallAndCode(script);
}
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);