Skip to content

Commit

Permalink
refactor(i18n-validation): replace errors with error codes (#1972)
Browse files Browse the repository at this point in the history
[skip ci]
  • Loading branch information
bigopon committed May 15, 2024
1 parent 3a63cde commit f91f31c
Show file tree
Hide file tree
Showing 19 changed files with 370 additions and 65 deletions.
6 changes: 3 additions & 3 deletions packages/__tests__/src/i18n/t/translation-integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ describe('i18n/t/translation-integration.spec.ts', function () {
@customElement({ name: 'app', template: `<p t.bind="key" id="undefined"></p>` })
class App { private readonly key: boolean | number = value; }
$it(`throws error if the key expression is evaluated to ${value}`, function ({ error }: I18nIntegrationTestContext<App>) {
assert.match(error?.message, new RegExp(`Expected the i18n key to be a string, but got ${value} of type (boolean|number)`));
assert.match(error?.message, /AUR4002/);
}, { component: App });
}

Expand All @@ -278,7 +278,7 @@ describe('i18n/t/translation-integration.spec.ts', function () {
try {
app.changeKey();
} catch (e) {
assert.match(e.message, new RegExp(`Expected the i18n key to be a string, but got ${value} of type (boolean|number)`));
assert.match(e.message, /AUR4002/);
}
}, { component: App });
}
Expand Down Expand Up @@ -321,7 +321,7 @@ describe('i18n/t/translation-integration.spec.ts', function () {
@customElement({ name: 'app', template: `<span t-params.bind="{context: 'dispatched'}"></span>` })
class App { }
$it('throws error if used without `t` attribute', function ({ error }: I18nIntegrationTestContext<App>) {
assert.equal(error?.message, 'key expression is missing');
assert.includes(error?.message, 'AUR4000');
}, { component: App });
}
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ describe('validation-html/validate-binding-behavior.spec.ts', function () {
{ template: `<input id="target" type="text" value.two-way="person.age | toNumber & validate:'${trigger}'">` }
);

$it(`GH#1470 - multiple round of validations involving multiple fields - **${trigger}** validation trigger`,
$it(`GH#1470 - multiple rounds of validations involving multiple fields - **${trigger}** validation trigger`,
async function ({ app, host, platform, ctx }: TestExecutionContext<App>) {
const controller = app.controller;

Expand Down Expand Up @@ -681,16 +681,16 @@ describe('validation-html/validate-binding-behavior.spec.ts', function () {

// #region argument parsing
const negativeTestData = [
{ args: `'chaos'`, expectedError: 'is not a supported validation trigger' },
{ args: `controller`, expectedError: 'is not a supported validation trigger' },
{ args: `ageMinRule`, expectedError: 'is not a supported validation trigger' },
{ args: `controller:'change'`, expectedError: 'is not a supported validation trigger' },
{ args: `'change':'foo'`, expectedError: 'foo is not of type ValidationController' },
{ args: `'change':{}`, expectedError: 'is not of type ValidationController' },
{ args: `'change':ageMinRule`, expectedError: 'is not of type ValidationController' },
{ args: `'change':controller:ageMinRule:'foo'`, expectedError: 'Unconsumed argument#4 for validate binding behavior: foo' },
{ args: `'chaos'`, code: 'AUR4202', expectedError: 'is not a supported validation trigger' },
{ args: `controller`, code: 'AUR4202', expectedError: 'is not a supported validation trigger' },
{ args: `ageMinRule`, code: 'AUR4202', expectedError: 'is not a supported validation trigger' },
{ args: `controller:'change'`, code: 'AUR4202', expectedError: 'is not a supported validation trigger' },
{ args: `'change':'foo'`, code: 'AUR4203', expectedError: 'foo is not of type ValidationController' },
{ args: `'change':{}`, code: 'AUR4203', expectedError: 'is not of type ValidationController' },
{ args: `'change':ageMinRule`, code: 'AUR4203', expectedError: 'is not of type ValidationController' },
{ args: `'change':controller:ageMinRule:'foo'`, code: 'AUR4201', expectedError: 'Unconsumed argument#4 for validate binding behavior: foo' },
];
for (const { args, expectedError } of negativeTestData) {
for (const { args, code } of negativeTestData) {
it(`throws error if the arguments are not provided in correct order - ${args}`, async function () {
const ctx = TestContext.create();
const container = ctx.container;
Expand All @@ -710,7 +710,7 @@ describe('validation-html/validate-binding-behavior.spec.ts', function () {
})
.start();
} catch (e) {
assert.equal(e.message.endsWith(expectedError), true);
assert.includes(e.message, code);
}
await au.stop();
ctx.doc.body.removeChild(host);
Expand Down Expand Up @@ -1373,7 +1373,7 @@ describe('validation-html/validate-binding-behavior.spec.ts', function () {
})
.start();
} catch (e) {
assert.equal(e.message, 'Validate behavior used on non property binding');
assert.includes(e.message, 'AUR4200');
}
await au.stop();
ctx.doc.body.removeChild(host);
Expand Down
2 changes: 1 addition & 1 deletion packages/__tests__/src/validation/rule-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ describe('validation/rule-provider.spec.ts', function () {
() => {
parsePropertyName(property as any, parser);
},
/Unable to parse accessor function/);
/AUR4102/);
});
}
});
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,4 @@ function createI18nConfiguration(optionsProvider: I18NConfigOptionsProvider) {
};
}

export const I18nConfiguration = createI18nConfiguration(() => { /* noop */ });
export const I18nConfiguration = /*@__PURE__*/ createI18nConfiguration(() => { /* noop */ });
89 changes: 89 additions & 0 deletions packages/i18n/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable prefer-template */

/** @internal */
export const createMappedError: CreateError = __DEV__
? (code: ErrorNames, ...details: unknown[]) => new Error(`AUR${String(code).padStart(4, '0')}: ${getMessageByCode(code, ...details)}`)
: (code: ErrorNames, ...details: unknown[]) => new Error(`AUR${String(code).padStart(4, '0')}:${details.map(String)}`);

_START_CONST_ENUM();
/** @internal */
export const enum ErrorNames {
method_not_implemented = 99,

i18n_translation_key_not_found = 4000,
i18n_translation_parameter_existed = 4001,
i18n_translation_key_invalid = 4002,
}
_END_CONST_ENUM();

const errorsMap: Record<ErrorNames, string> = {
[ErrorNames.method_not_implemented]: 'Method {{0}} not implemented',

[ErrorNames.i18n_translation_key_not_found]: 'Translation key not found',
[ErrorNames.i18n_translation_parameter_existed]: 'Translation parameter already existed',
[ErrorNames.i18n_translation_key_invalid]: `Expected the i18n key to be a string, but got {{0}} of type {{1}}`,
};

const getMessageByCode = (name: ErrorNames, ...details: unknown[]) => {
let cooked: string = errorsMap[name];
for (let i = 0; i < details.length; ++i) {
const regex = new RegExp(`{{${i}(:.*)?}}`, 'g');
let matches = regex.exec(cooked);
while (matches != null) {
const method = matches[1]?.slice(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let value = details[i] as any;
if (value != null) {
switch (method) {
case 'nodeName': value = (value as Node).nodeName.toLowerCase(); break;
case 'name': value = (value as { name: string }).name; break;
case 'typeof': value = typeof value; break;
case 'ctor': value = (value as object).constructor.name; break;
case 'controller': value = value.controller.name; break;
case 'target@property': value = `${value.target}@${value.targetProperty}`; break;
case 'toString': value = Object.prototype.toString.call(value); break;
case 'join(!=)': value = (value as unknown[]).join('!='); break;
case 'bindingCommandHelp': value = getBindingCommandHelp(value); break;
case 'element': value = value === '*' ? 'all elements' : `<${value} />`; break;
default: {
// property access
if (method?.startsWith('.')) {
value = String(value[method.slice(1)]);
} else {
value = String(value);
}
}
}
}
cooked = cooked.slice(0, matches.index) + value + cooked.slice(regex.lastIndex);
matches = regex.exec(cooked);
}
}
return cooked;
};

type CreateError = (code: ErrorNames, ...details: unknown[]) => Error;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function pleaseHelpCreateAnIssue(title: string, body?: string) {
return `\nThis is likely an issue with Aurelia.\n Please help create an issue by clicking the following link\n`
+ `https://github.com/aurelia/aurelia/issues/new?title=${encodeURIComponent(title)}`
+ (body != null ? `&body=${encodeURIComponent(body)}` : '&template=bug_report.md');
}

function getBindingCommandHelp(name: string) {
switch (name) {
case 'delegate':
return `\nThe ".delegate" binding command has been removed in v2.`
+ ` Binding command ".trigger" should be used instead.`
+ ` If you are migrating v1 application, install compat package`
+ ` to add back the ".delegate" binding command for ease of migration.`;
case 'call':
return `\nThe ".call" binding command has been removed in v2.`
+ ` If you want to pass a callback that preserves the context of the function call,`
+ ` you can use lambda instead. Refer to lambda expression doc for more details.`;
default:
return '';
}
}
2 changes: 1 addition & 1 deletion packages/i18n/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class I18nService implements I18N {
if (this.options.skipTranslationOnMissingKey && translation === key) {
// TODO change this once the logging infra is there.
// eslint-disable-next-line no-console
console.warn(`Couldn't find translation for key: ${key}`);
console.warn(`[DEV:aurelia] Couldn't find translation for key: ${key}`);
} else {
result.value = translation;
results.push(result);
Expand Down
7 changes: 4 additions & 3 deletions packages/i18n/src/t/translation-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
import type { TranslationBindBindingInstruction, TranslationBindingInstruction } from './translation-renderer';
import type { TranslationParametersBindingInstruction } from './translation-parameters-renderer';
import { etInterpolation, etIsProperty, stateActivating } from '../utils';
import { ErrorNames, createMappedError } from '../errors';

interface TranslationBindingCreationContext {
parser: IExpressionParser;
Expand Down Expand Up @@ -163,7 +164,7 @@ export class TranslationBinding implements IBinding {
return;
}
const ast = this.ast;
if (ast == null) { throw new Error('key expression is missing'); }
if (ast == null) throw createMappedError(ErrorNames.i18n_translation_key_not_found);
this._scope = _scope;
this.i18n.subscribeLocaleChange(this);

Expand Down Expand Up @@ -211,7 +212,7 @@ export class TranslationBinding implements IBinding {

public useParameter(expr: IsExpression) {
if (this.parameter != null) {
throw new Error('This translation parameter has already been specified.');
throw createMappedError(ErrorNames.i18n_translation_parameter_existed);
}
this.parameter = new ParameterBinding(this, expr, () => this.updateTranslations());
}
Expand Down Expand Up @@ -350,7 +351,7 @@ export class TranslationBinding implements IBinding {
const expr = this._keyExpression ??= '';
const exprType = typeof expr;
if (exprType !== 'string') {
throw new Error(`Expected the i18n key to be a string, but got ${expr} of type ${exprType}`); // TODO use reporter/logger
throw createMappedError(ErrorNames.i18n_translation_key_invalid, expr, exprType);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/validation-html/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ function createConfiguration(optionsProvider: ValidationConfigurationProvider) {
};
}

export const ValidationHtmlConfiguration = createConfiguration(noop);
export const ValidationHtmlConfiguration = /*@__PURE__*/ createConfiguration(noop);

99 changes: 99 additions & 0 deletions packages/validation-html/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable prefer-template */

/** @internal */
export const createMappedError: CreateError = __DEV__
? (code: ErrorNames, ...details: unknown[]) => new Error(`AUR${String(code).padStart(4, '0')}: ${getMessageByCode(code, ...details)}`)
: (code: ErrorNames, ...details: unknown[]) => new Error(`AUR${String(code).padStart(4, '0')}:${details.map(String)}`);

_START_CONST_ENUM();
/** @internal */
export const enum ErrorNames {
method_not_implemented = 99,

validate_binding_behavior_on_invalid_binding_type = 4200,
validate_binding_behavior_extraneous_args = 4201,
validate_binding_behavior_invalid_trigger_name = 4202,
validate_binding_behavior_invalid_controller = 4203,
validate_binding_behavior_invalid_binding_target = 4204,

validation_controller_unknown_expression = 4205,
validation_controller_unable_to_parse_expression = 4206,
}
_END_CONST_ENUM();

const errorsMap: Record<ErrorNames, string> = {
[ErrorNames.method_not_implemented]: 'Method {{0}} not implemented',

[ErrorNames.validate_binding_behavior_on_invalid_binding_type]: 'Validate behavior used on non property binding',
[ErrorNames.validate_binding_behavior_extraneous_args]: `Unconsumed argument#{{0}} for validate binding behavior: {{1}}`,
[ErrorNames.validate_binding_behavior_invalid_trigger_name]: `{{0}} is not a supported validation trigger`,
[ErrorNames.validate_binding_behavior_invalid_controller]: `{{0}} is not of type ValidationController`,
[ErrorNames.validate_binding_behavior_invalid_binding_target]: 'Invalid binding target',

[ErrorNames.validation_controller_unknown_expression]: `Unknown expression of type {{0}}`,
[ErrorNames.validation_controller_unable_to_parse_expression]: `Unable to parse binding expression: {{0}}`,
};

const getMessageByCode = (name: ErrorNames, ...details: unknown[]) => {
let cooked: string = errorsMap[name];
for (let i = 0; i < details.length; ++i) {
const regex = new RegExp(`{{${i}(:.*)?}}`, 'g');
let matches = regex.exec(cooked);
while (matches != null) {
const method = matches[1]?.slice(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let value = details[i] as any;
if (value != null) {
switch (method) {
case 'nodeName': value = (value as Node).nodeName.toLowerCase(); break;
case 'name': value = (value as { name: string }).name; break;
case 'typeof': value = typeof value; break;
case 'ctor': value = (value as object).constructor.name; break;
case 'controller': value = value.controller.name; break;
case 'target@property': value = `${value.target}@${value.targetProperty}`; break;
case 'toString': value = Object.prototype.toString.call(value); break;
case 'join(!=)': value = (value as unknown[]).join('!='); break;
case 'bindingCommandHelp': value = getBindingCommandHelp(value); break;
case 'element': value = value === '*' ? 'all elements' : `<${value} />`; break;
default: {
// property access
if (method?.startsWith('.')) {
value = String(value[method.slice(1)]);
} else {
value = String(value);
}
}
}
}
cooked = cooked.slice(0, matches.index) + value + cooked.slice(regex.lastIndex);
matches = regex.exec(cooked);
}
}
return cooked;
};

type CreateError = (code: ErrorNames, ...details: unknown[]) => Error;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function pleaseHelpCreateAnIssue(title: string, body?: string) {
return `\nThis is likely an issue with Aurelia.\n Please help create an issue by clicking the following link\n`
+ `https://github.com/aurelia/aurelia/issues/new?title=${encodeURIComponent(title)}`
+ (body != null ? `&body=${encodeURIComponent(body)}` : '&template=bug_report.md');
}

function getBindingCommandHelp(name: string) {
switch (name) {
case 'delegate':
return `\nThe ".delegate" binding command has been removed in v2.`
+ ` Binding command ".trigger" should be used instead.`
+ ` If you are migrating v1 application, install compat package`
+ ` to add back the ".delegate" binding command for ease of migration.`;
case 'call':
return `\nThe ".call" binding command has been removed in v2.`
+ ` If you want to pass a callback that preserves the context of the function call,`
+ ` you can use lambda instead. Refer to lambda expression doc for more details.`;
default:
return '';
}
}
13 changes: 7 additions & 6 deletions packages/validation-html/src/validate-binding-behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { PropertyRule } from '@aurelia/validation';
import { BindingInfo, BindingWithBehavior, IValidationController, ValidationController, ValidationEvent, ValidationResultsSubscriber } from './validation-controller';
import { BindingBehaviorExpression } from '@aurelia/expression-parser';
import { ErrorNames, createMappedError } from './errors';

/**
* Validation triggers.
Expand Down Expand Up @@ -74,7 +75,7 @@ export class ValidateBindingBehavior implements BindingBehaviorInstance {

public bind(scope: Scope, binding: IBinding) {
if (!(binding instanceof PropertyBinding)) {
throw new Error('Validate behavior used on non property binding');
throw createMappedError(ErrorNames.validate_binding_behavior_on_invalid_binding_type);
}
let connector = validationConnectorMap.get(binding);
if (connector == null) {
Expand Down Expand Up @@ -241,7 +242,7 @@ class ValidatitionConnector implements ValidationResultsSubscriber {
rules = this._ensureRules(astEvaluate(arg, scope, this, this._rulesMediator));
break;
default:
throw new Error(`Unconsumed argument#${i + 1} for validate binding behavior: ${astEvaluate(arg, scope, this, null)}`);
throw createMappedError(ErrorNames.validate_binding_behavior_extraneous_args, i + 1, astEvaluate(arg, scope, this, null));
}
}

Expand Down Expand Up @@ -301,17 +302,17 @@ class ValidatitionConnector implements ValidationResultsSubscriber {
if (trigger === (void 0) || trigger === null) {
trigger = this.defaultTrigger;
} else if (!Object.values(ValidationTrigger).includes(trigger as ValidationTrigger)) {
throw new Error(`${trigger} is not a supported validation trigger`); // TODO: use reporter
throw createMappedError(ErrorNames.validate_binding_behavior_invalid_trigger_name, trigger);
}
return trigger as ValidationTrigger;
}

/** @internal */
private _ensureController(controller: unknown): ValidationController {
if (controller === (void 0) || controller === null) {
if (controller == null) {
controller = this.scopedController;
} else if (!(controller instanceof ValidationController)) {
throw new Error(`${controller} is not of type ValidationController`); // TODO: use reporter
throw createMappedError(ErrorNames.validate_binding_behavior_invalid_controller, controller);
}
return controller as ValidationController;
}
Expand All @@ -331,7 +332,7 @@ class ValidatitionConnector implements ValidationResultsSubscriber {
} else {
const controller = (target as ICustomElementViewModel)?.$controller;
if (controller === void 0) {
throw new Error('Invalid binding target'); // TODO: use reporter
throw createMappedError(ErrorNames.validate_binding_behavior_invalid_binding_target);
}
return controller.host;
}
Expand Down
Loading

0 comments on commit f91f31c

Please sign in to comment.