Skip to content

Commit a69ad25

Browse files
authored
refactor: validators return errors, not throw them (#691)
--------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 080b63b commit a69ad25

12 files changed

+72
-105
lines changed

HISTORY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ All notable changes to this project will be documented in this file.
55
## unreleased
66

77
* Added
8-
* Formal validators for JSON string and XML string ([#620] via [#652])
8+
* Formal validators for JSON string and XML string ([#620] via [#652], [#691])
99
Currently available only for _Node.js_. Requires [optional dependencies](README.md#optional-dependencies).
1010
* Related new validator classes:
1111
* `Validation.JsonValidator`
1212
* `Validation.JsonStrictValidator`
1313
* `Validation.XmlValidator`
1414
* Related new error classes:
15-
* `Validation.ValidationError`
1615
* `Validation.NotImplementedError`
1716
* `Validation.MissingOptionalDependencyError`
1817
* Build
@@ -24,6 +23,7 @@ All notable changes to this project will be documented in this file.
2423
[#644]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/644
2524
[#652]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/652
2625
[#686]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/686
26+
[#691]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/691
2727

2828
## 1.13.3 - 2023-04-05
2929

src/validation/baseValidator.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

2020
import type { Version } from '../spec'
21-
22-
export interface Validator {
23-
/**
24-
* validate the data.
25-
*
26-
* Promise may reject with one of the following:
27-
* - {@link Validation.NotImplementedError | NotImplementedError} when there is no validator available for `this.version`
28-
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError} when a required dependency was not installed
29-
* - {@link Validation.ValidationError | ValidationError} when `data` was invalid to the schema
30-
*/
31-
validate: (data: string) => Promise<void>
32-
}
21+
import type { ValidationError, Validator } from './types'
3322

3423
export abstract class BaseValidator implements Validator {
3524
readonly #version: Version
@@ -42,6 +31,6 @@ export abstract class BaseValidator implements Validator {
4231
return this.#version
4332
}
4433

45-
/** {@inheritDoc Validator.validate} */
46-
abstract validate (data: string): Promise<void>
34+
/** {@inheritDoc Validation.Types.Validator.validate} */
35+
abstract validate (data: string): Promise<null | ValidationError>
4736
}

src/validation/errors.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
1919

2020
import { type Version } from '../spec'
2121

22-
export class ValidationError extends Error {
23-
readonly details: any | undefined
24-
25-
constructor (message: string, details?: any) {
26-
super(message)
27-
this.details = details
28-
}
29-
}
30-
3122
export class NotImplementedError extends Error {
3223
constructor (version: Version) {
3324
super(`not implemented for CycloneDX version: ${version}`)

src/validation/index.common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

2020
export * from './errors'
21-
export * from './types'
21+
export * as Types from './types'

src/validation/jsonValidator.node.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import { readFile } from 'fs/promises'
2323

2424
import { FILES } from '../resources.node'
2525
import { BaseValidator } from './baseValidator'
26-
import { MissingOptionalDependencyError, NotImplementedError, ValidationError } from './errors'
26+
import { MissingOptionalDependencyError, NotImplementedError } from './errors'
27+
import type { ValidationError } from './types'
2728

2829
let _ajv: Ajv | undefined
2930

@@ -101,19 +102,22 @@ abstract class BaseJsonValidator extends BaseValidator {
101102
/**
102103
* Validate the data against CycloneDX spec of `this.version`.
103104
*
104-
* Promise may reject with one of the following:
105-
* - {@link Validation.NotImplementedError | NotImplementedError} when there is no validator available for `this.version`
106-
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError} when a required dependency was not installed
107-
* - {@link Validation.ValidationError | ValidationError} when `data` was invalid to the schema
105+
* Promise completes with one of the following:
106+
* - `null`, when data was valid
107+
* - {@link Validation.Types.ValidationError | something} representing the error details, when data was invalid representing the error details, when data was invalid
108+
*
109+
* Promise rejects with one of the following:
110+
* - {@link Validation.NotImplementedError | NotImplementedError}, when there is no validator available for `this.version`
111+
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError}, when a required dependency was not installed
108112
*/
109-
async validate (data: string): Promise<void> {
113+
async validate (data: string): Promise<null | ValidationError> {
110114
const [doc, validator] = await Promise.all([
111115
(async () => JSON.parse(data))(),
112116
this.#getValidator()
113117
])
114-
if (!validator(doc)) {
115-
throw new ValidationError(`invalid to CycloneDX ${this.version}`, validator.errors)
116-
}
118+
return validator(doc)
119+
? null
120+
: validator.errors
117121
}
118122
}
119123
export class JsonValidator extends BaseJsonValidator {

src/validation/types.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@ SPDX-License-Identifier: Apache-2.0
1717
Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

20+
/**
21+
* Details and information describing a validation error.
22+
*/
23+
export type ValidationError = NonNullable<any>
24+
2025
export interface Validator {
2126
/**
22-
* Promise rejects with one of the following
23-
* - {@link Validation.NotImplementedError | NotImplementedError} when there is no validator available for `this.version`
24-
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError} when a required dependency was not installed
25-
* - {@link Validation.ValidationError | ValidationError} when `data` was invalid to the schema
27+
* Promise completes with one of the following:
28+
* - `null`, when data was valid
29+
* - {@link ValidationError | something} representing the error details, when data was invalid
30+
*
31+
* Promise rejects with one of the following:
32+
* - {@link Validation.NotImplementedError | NotImplementedError}, when there is no validator available
33+
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError}, when a required dependency was not installed
2634
*/
27-
validate: (data: string) => Promise<void>
35+
validate: (data: string) => Promise<null | ValidationError>
2836
}

src/validation/xmlValidator.node.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import { pathToFileURL } from 'url'
2323

2424
import { FILES } from '../resources.node'
2525
import { BaseValidator } from './baseValidator'
26-
import { MissingOptionalDependencyError, NotImplementedError, ValidationError } from './errors'
26+
import { MissingOptionalDependencyError, NotImplementedError } from './errors'
27+
import type { ValidationError } from './types'
2728

2829
let _parser: typeof parseXml | undefined
2930

@@ -75,19 +76,22 @@ export class XmlValidator extends BaseValidator {
7576
/**
7677
* Validate the data against CycloneDX spec of `this.version`.
7778
*
78-
* Promise may reject with one of the following:
79-
* - {@link Validation.NotImplementedError | NotImplementedError} when there is no validator available for `this.version`
80-
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError} when a required dependency was not installed
81-
* - {@link Validation.ValidationError | ValidationError} when `data` was invalid to the schema
79+
* Promise completes with one of the following:
80+
* - `null`, when data was valid
81+
* - {@link Validation.Types.ValidationError | something} representing the error details, when data was invalid representing the error details, when data was invalid
82+
*
83+
* Promise rejects with one of the following:
84+
* - {@link Validation.NotImplementedError | NotImplementedError}, when there is no validator available for `this.version`
85+
* - {@link Validation.MissingOptionalDependencyError | MissingOptionalDependencyError}, when a required dependency was not installed
8286
*/
83-
async validate (data: string): Promise<void> {
87+
async validate (data: string): Promise<null | ValidationError> {
8488
const [parse, schema] = await Promise.all([
8589
getParser(),
8690
this.#getSchema()
8791
])
8892
const doc = parse(data, xmlParseOptions)
89-
if (!doc.validate(schema)) {
90-
throw new ValidationError(`invalid to CycloneDX ${this.version}`, doc.validationErrors)
91-
}
93+
return doc.validate(schema)
94+
? null
95+
: doc.validationErrors
9296
}
9397
}

tests/integration/Serialize.JsonSerialize.test.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const {
3131
},
3232
Spec: { Spec1dot2, Spec1dot3, Spec1dot4 },
3333
Validation: {
34-
ValidationError, MissingOptionalDependencyError,
34+
MissingOptionalDependencyError,
3535
JsonStrictValidator
3636
}
3737
} = require('../../')
@@ -65,11 +65,9 @@ describe('Serialize.JsonSerialize', function () {
6565

6666
const validator = new JsonStrictValidator(spec.version)
6767
try {
68-
await validator.validate(serialized)
68+
const validationError = await validator.validate(serialized)
69+
assert.strictEqual(validationError, null)
6970
} catch (err) {
70-
if (err instanceof ValidationError) {
71-
assert.fail(`unexpected ValidationError: ${err.message}\n` + JSON.stringify(err.details))
72-
}
7371
if (!(err instanceof MissingOptionalDependencyError)) {
7472
// unexpected error
7573
assert.fail(err)

tests/integration/Serialize.XmlSerialize.test.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const {
3131
},
3232
Spec: { Spec1dot2, Spec1dot3, Spec1dot4 },
3333
Validation: {
34-
ValidationError, MissingOptionalDependencyError,
34+
MissingOptionalDependencyError,
3535
XmlValidator
3636
}
3737
} = require('../../')
@@ -72,11 +72,9 @@ describe('Serialize.XmlSerialize', function () {
7272

7373
const validator = new XmlValidator(spec.version)
7474
try {
75-
await validator.validate(serialized)
75+
const validationError = await validator.validate(serialized)
76+
assert.strictEqual(validationError, null)
7677
} catch (err) {
77-
if (err instanceof ValidationError) {
78-
assert.fail(`unexpected ValidationError: ${err.message}\n` + JSON.stringify(err.details))
79-
}
8078
if (!(err instanceof MissingOptionalDependencyError)) {
8179
// unexpected error
8280
assert.fail(err)

tests/integration/Validation.JsonStrictValidator.test.js

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
2020
const assert = require('assert')
2121
const { describe, it } = require('mocha')
2222

23-
const { escapeRegExp } = require('../_helpers/stringFunctions')
24-
2523
let hasDep = true
2624
try {
2725
require('ajv')
@@ -32,7 +30,7 @@ try {
3230
const {
3331
Spec: { Version },
3432
Validation: {
35-
ValidationError, NotImplementedError, MissingOptionalDependencyError,
33+
NotImplementedError, MissingOptionalDependencyError,
3634
JsonStrictValidator
3735
}
3836
} = require('../../')
@@ -45,7 +43,7 @@ describe('Validation.JsonStrictValidator', () => {
4543
it('throws not implemented', async () => {
4644
const validator = new JsonStrictValidator(version)
4745
await assert.rejects(
48-
() => validator.validate('{}'),
46+
validator.validate('{}'),
4947
(err) => err instanceof NotImplementedError
5048
)
5149
})
@@ -60,7 +58,7 @@ describe('Validation.JsonStrictValidator', () => {
6058
it('throws MissingOptionalDependencyError', async () => {
6159
const validator = new JsonStrictValidator(version)
6260
await assert.rejects(
63-
() => validator.validate('{}'),
61+
validator.validate('{}'),
6462
(err) => err instanceof MissingOptionalDependencyError
6563
)
6664
})
@@ -78,15 +76,8 @@ describe('Validation.JsonStrictValidator', () => {
7876
unknown: 'undefined' // << undefined/additional property
7977
}]
8078
})
81-
await assert.rejects(
82-
() => validator.validate(input),
83-
(err) => {
84-
assert.ok(err instanceof ValidationError)
85-
assert.match(err.message, new RegExp(`invalid.* CycloneDX ${escapeRegExp(version)}`, 'i'))
86-
assert.notStrictEqual(err.details, undefined)
87-
return true
88-
}
89-
)
79+
const validationError = await validator.validate(input)
80+
assert.notStrictEqual(validationError, null)
9081
})
9182

9283
it('valid passes', async () => {
@@ -100,7 +91,8 @@ describe('Validation.JsonStrictValidator', () => {
10091
version: '1.337'
10192
}]
10293
})
103-
await validator.validate(input)
94+
const validationError = await validator.validate(input)
95+
assert.strictEqual(validationError, null)
10496
})
10597
}))
10698
})

0 commit comments

Comments
 (0)