| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent bfec8ba commit 5aaad0a
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -61,19 +61,24 @@ export class SchemaValidationException extends BaseException { | |||
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | 63 | const messages = errors.map((err) => { | |
| 64 | + if (err.keyword === 'additionalProperties') { | ||
| 65 | + const unknown = err.params?.additionalProperty; | ||
| 66 | + // `parentSchema` is the schema that rejected the property, which ajv only attaches when | ||
| 67 | + // the validator was created with `verbose: true`. A schema that declares no `properties` | ||
| 68 | + // of its own, such as one using only `patternProperties`, has no options to offer. | ||
| 69 | + const known = Object.keys(err.parentSchema?.properties ?? {}); | ||
| 70 | + | ||
| 71 | + return ( | ||
| 72 | + `Unknown option "${unknown}"${err.instancePath ? ` at "${err.instancePath}"` : ''}.` + | ||
| 73 | + (known.length ? ` Valid options are: ${known.join(', ')}.` : '') | ||
| 74 | + ); | ||
| 75 | + } | ||
| 76 | + | ||
| 64 | 77 | let message = `Data path ${JSON.stringify(err.instancePath)} ${err.message}`; | |
| 65 | - if (err.params) { | ||
| 66 | - switch (err.keyword) { | ||
| 67 | - case 'additionalProperties': | ||
| 68 | - message += `(${err.params.additionalProperty})`; | ||
| 69 | - break; | ||
| 70 | - | ||
| 71 | - case 'enum': | ||
| 72 | - message += `. Allowed values are: ${(err.params.allowedValues as string[] | undefined) | ||
| 73 | - ?.map((v) => `"${v}"`) | ||
| 74 | - .join(', ')}`; | ||
| 75 | - break; | ||
| 76 | - } | ||
| 78 | + if (err.keyword === 'enum' && err.params) { | ||
| 79 | + message += `. Allowed values are: ${(err.params.allowedValues as string[] | undefined) | ||
| 80 | + ?.map((v) => `"${v}"`) | ||
| 81 | + .join(', ')}`; | ||
| 77 | 82 | } | |
| 78 | 83 | ||
| 79 | 84 | return message + '.'; | |
@@ -106,6 +111,8 @@ export class CoreSchemaRegistry implements SchemaRegistry { | |||
| 106 | 111 | strict: false, | |
| 107 | 112 | loadSchema: (uri: string) => this._fetch(uri), | |
| 108 | 113 | passContext: true, | |
| 114 | + // Needed to list the valid options of the object an unknown option was found in. | ||
| 115 | + verbose: true, | ||
| 109 | 116 | }); | |
| 110 | 117 | ||
| 111 | 118 | ajvAddFormats(this._ajv); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,8 +7,10 @@ | |||
| 7 | 7 | */ | |
| 8 | 8 | ||
| 9 | 9 | /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| 10 | + import { JsonValue } from '../utils'; | ||
| 10 | 11 | import { SchemaFormat } from './interface'; | |
| 11 | 12 | import { CoreSchemaRegistry, SchemaValidationException } from './registry'; | |
| 13 | + import { JsonSchema } from './schema'; | ||
| 12 | 14 | import { addUndefinedDefaults } from './transforms'; | |
| 13 | 15 | ||
| 14 | 16 | describe('CoreSchemaRegistry', () => { | |
@@ -340,4 +342,93 @@ describe('CoreSchemaRegistry', () => { | |||
| 340 | 342 | expect(deprecatedMessages[1]).toBe('Option "bar" is deprecated.'); | |
| 341 | 343 | expect(result.success).toBe(true, result.errors); | |
| 342 | 344 | }); | |
| 345 | + | ||
| 346 | + describe('error messages', () => { | ||
| 347 | + async function messagesFor(schema: JsonSchema, data: JsonValue): Promise<string[]> { | ||
| 348 | + const registry = new CoreSchemaRegistry(); | ||
| 349 | + const validator = await registry.compile(schema); | ||
| 350 | + const result = await validator(data); | ||
| 351 | + expect(result.success).toBe(false); | ||
| 352 | + | ||
| 353 | + return SchemaValidationException.createMessages(result.errors); | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + it('names an unknown option and the options that are valid there', async () => { | ||
| 357 | + const messages = await messagesFor( | ||
| 358 | + { | ||
| 359 | + properties: { version: { type: 'number' }, projects: { type: 'object' } }, | ||
| 360 | + additionalProperties: false, | ||
| 361 | + }, | ||
| 362 | + { allowedCommonJsDependencies: [] }, | ||
| 363 | + ); | ||
| 364 | + | ||
| 365 | + expect(messages).toEqual([ | ||
| 366 | + 'Unknown option "allowedCommonJsDependencies". Valid options are: version, projects.', | ||
| 367 | + ]); | ||
| 368 | + }); | ||
| 369 | + | ||
| 370 | + it('points at the object an unknown option was found in', async () => { | ||
| 371 | + const messages = await messagesFor( | ||
| 372 | + { | ||
| 373 | + properties: { | ||
| 374 | + cli: { | ||
| 375 | + type: 'object', | ||
| 376 | + properties: { cache: { type: 'object' }, packageManager: { type: 'string' } }, | ||
| 377 | + additionalProperties: false, | ||
| 378 | + }, | ||
| 379 | + }, | ||
| 380 | + }, | ||
| 381 | + { cli: { completion: true } }, | ||
| 382 | + ); | ||
| 383 | + | ||
| 384 | + expect(messages).toEqual([ | ||
| 385 | + 'Unknown option "completion" at "/cli". Valid options are: cache, packageManager.', | ||
| 386 | + ]); | ||
| 387 | + }); | ||
| 388 | + | ||
| 389 | + it('looks through a $ref for the valid options', async () => { | ||
| 390 | + const messages = await messagesFor( | ||
| 391 | + { | ||
| 392 | + $ref: '#/definitions/global', | ||
| 393 | + definitions: { | ||
| 394 | + global: { | ||
| 395 | + type: 'object', | ||
| 396 | + properties: { cli: { type: 'object' }, schematics: { type: 'object' } }, | ||
| 397 | + additionalProperties: false, | ||
| 398 | + }, | ||
| 399 | + }, | ||
| 400 | + }, | ||
| 401 | + { version: 1 }, | ||
| 402 | + ); | ||
| 403 | + | ||
| 404 | + expect(messages).toEqual(['Unknown option "version". Valid options are: cli, schematics.']); | ||
| 405 | + }); | ||
| 406 | + | ||
| 407 | + it('omits the valid options when the schema does not list any', async () => { | ||
| 408 | + const messages = await messagesFor( | ||
| 409 | + { | ||
| 410 | + $ref: '#/definitions/cli', | ||
| 411 | + definitions: { | ||
| 412 | + cli: { | ||
| 413 | + type: 'object', | ||
| 414 | + patternProperties: { '^x-': { type: 'string' } }, | ||
| 415 | + additionalProperties: false, | ||
| 416 | + }, | ||
| 417 | + }, | ||
| 418 | + }, | ||
| 419 | + { completion: true }, | ||
| 420 | + ); | ||
| 421 | + | ||
| 422 | + expect(messages).toEqual(['Unknown option "completion".']); | ||
| 423 | + }); | ||
| 424 | + | ||
| 425 | + it('leaves an error that is not about an unknown option alone', async () => { | ||
| 426 | + const messages = await messagesFor( | ||
| 427 | + { properties: { outputPath: { type: 'string' } } }, | ||
| 428 | + { outputPath: 42 }, | ||
| 429 | + ); | ||
| 430 | + | ||
| 431 | + expect(messages).toEqual(['Data path "/outputPath" must be string.']); | ||
| 432 | + }); | ||
| 433 | + }); | ||
| 343 | 434 | }); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,21 +9,15 @@ export default async function () { | |||
| 9 | 9 | let ngError: Error; | |
| 10 | 10 | ||
| 11 | 11 | ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted', 'true')); | |
| 12 | - assert.match( | ||
| 13 | - ngError.message, | ||
| 14 | - /Data path "\/cli" must NOT have additional properties\(completion\)\./, | ||
| 15 | - ); | ||
| 12 | + assert.match(ngError.message, /Unknown option "completion" at "\/cli"\./); | ||
| 16 | 13 | ||
| 17 | 14 | ngError = await expectToFail(() => | |
| 18 | 15 | silentNg('config', '--global', 'cli.completion.invalid', 'true'), | |
| 19 | 16 | ); | |
| 20 | - assert.match( | ||
| 21 | - ngError.message, | ||
| 22 | - /Data path "\/cli\/completion" must NOT have additional properties\(invalid\)\./, | ||
| 23 | - ); | ||
| 17 | + assert.match(ngError.message, /Unknown option "invalid" at "\/cli\/completion"\./); | ||
| 24 | 18 | ||
| 25 | 19 | ngError = await expectToFail(() => silentNg('config', '--global', 'cli.cache.enabled', 'true')); | |
| 26 | - assert.match(ngError.message, /Data path "\/cli" must NOT have additional properties\(cache\)\./); | ||
| 20 | + assert.match(ngError.message, /Unknown option "cache" at "\/cli"\./); | ||
| 27 | 21 | ||
| 28 | 22 | ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted')); | |
| 29 | 23 | assert.match(ngError.message, /Value cannot be found\./); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,10 +6,7 @@ export default async function () { | |||
| 6 | 6 | let ngError: Error; | |
| 7 | 7 | ||
| 8 | 8 | ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz', 'true')); | |
| 9 | - assert.match( | ||
| 10 | - ngError.message, | ||
| 11 | - /Data path "\/cli\/warnings" must NOT have additional properties\(zzzz\)\./, | ||
| 12 | - ); | ||
| 9 | + assert.match(ngError.message, /Unknown option "zzzz" at "\/cli\/warnings"\./); | ||
| 13 | 10 | ||
| 14 | 11 | ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz')); | |
| 15 | 12 | assert.match(ngError.message, /Value cannot be found\./); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments