Skip to content

Commit d746c03

Browse files
authored
Merge pull request #3080 from xballoy/xba/standard-schema
feat: implement standard schema spec
2 parents bdefbd2 + 8c8ee52 commit d746c03

File tree

8 files changed

+216
-3
lines changed

8 files changed

+216
-3
lines changed

lib/base.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,53 @@ internals.Base = class {
10721072

10731073
return obj;
10741074
}
1075+
1076+
// Standard Schema
1077+
1078+
get '~standard'() {
1079+
1080+
const mapToStandardError = (error) => {
1081+
1082+
let issues;
1083+
if (Errors.ValidationError.isError(error)) {
1084+
issues = error.details.map(({ message, path }) => ({
1085+
message,
1086+
path
1087+
}));
1088+
}
1089+
else {
1090+
issues = [{
1091+
message: error.message
1092+
}];
1093+
}
1094+
1095+
return {
1096+
issues
1097+
};
1098+
};
1099+
1100+
const mapToStandardValue = (value) => ({ value });
1101+
1102+
return {
1103+
version: 1,
1104+
vendor: 'joi',
1105+
validate: (value) => {
1106+
1107+
const result = Validator.standard(value, this);
1108+
1109+
if (result instanceof Promise) {
1110+
return result
1111+
.then(mapToStandardValue, mapToStandardError);
1112+
}
1113+
1114+
if (!result.error) {
1115+
return mapToStandardValue(result.value);
1116+
}
1117+
1118+
return mapToStandardError(result.error);
1119+
}
1120+
};
1121+
}
10751122
};
10761123

10771124

lib/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// TypeScript Version: 2.8
99

1010
// TODO express type of Schema in a type-parameter (.default, .valid, .example etc)
11+
import type { StandardSchemaV1 } from '@standard-schema/spec';
1112

1213
declare namespace Joi {
1314
type Types =
@@ -961,7 +962,7 @@ declare namespace Joi {
961962
$_validate(value: any, state: State, prefs: ValidationOptions): ValidationResult;
962963
}
963964

964-
interface AnySchema<TSchema = any> extends SchemaInternals {
965+
interface AnySchema<TSchema = any> extends SchemaInternals, StandardSchemaV1<TSchema> {
965966
/**
966967
* Flags of current schema.
967968
*/

lib/validator.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ exports.entryAsync = async function (value, schema, prefs) {
186186
};
187187

188188

189+
exports.standard = function (value, schema) {
190+
191+
192+
if (schema.isAsync()) {
193+
return exports.entryAsync(value, schema);
194+
}
195+
196+
return exports.entry(value, schema);
197+
};
198+
199+
189200
internals.Mainstay = class {
190201

191202
constructor(tracer, debug, links) {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"@hapi/hoek": "^11.0.7",
2727
"@hapi/pinpoint": "^2.0.1",
2828
"@hapi/tlds": "^1.1.1",
29-
"@hapi/topo": "^6.0.2"
29+
"@hapi/topo": "^6.0.2",
30+
"@standard-schema/spec": "^1.0.0"
3031
},
3132
"devDependencies": {
3233
"@hapi/bourne": "^3.0.0",

test/base.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4020,4 +4020,56 @@ describe('any', () => {
40204020
expect(() => Joi.any().$_addRule({ name: 5 })).to.throw('Invalid rule name');
40214021
});
40224022
});
4023+
4024+
describe('~standard', () => {
4025+
4026+
it('returns the version number of the standard', () => {
4027+
4028+
const schema = Joi.number();
4029+
expect(schema['~standard'].version).to.equal(1);
4030+
});
4031+
4032+
it('returns the vendor name of the schema library', () => {
4033+
4034+
const schema = Joi.number();
4035+
expect(schema['~standard'].vendor).to.equal('joi');
4036+
});
4037+
4038+
describe('validate', () => {
4039+
4040+
it('return only value when passing', () => {
4041+
4042+
const schema = Joi.string();
4043+
expect(schema['~standard'].validate('3')).to.equal({
4044+
value: '3'
4045+
});
4046+
});
4047+
4048+
it('return only issues when not passing', () => {
4049+
4050+
const schema = Joi.string();
4051+
expect(schema['~standard'].validate(3)).to.equal({
4052+
issues: [{ message: '"value" must be a string', path: [] }]
4053+
});
4054+
});
4055+
4056+
it('return only issues when not passing (custom error is Error)', () => {
4057+
4058+
const schema = Joi.string().error(new Error('Was REALLY expecting a string'));
4059+
expect(schema['~standard'].validate(3)).to.equal({
4060+
issues: [{ message: 'Was REALLY expecting a string' }]
4061+
});
4062+
});
4063+
4064+
it('return only issues when not passing (custom error is validation error function that return string)', () => {
4065+
4066+
const schema = Joi.object({
4067+
foo: Joi.number().min(0).error((errors) => new Error('"foo" requires a positive number'))
4068+
});
4069+
expect(schema['~standard'].validate({ foo: -2 })).to.equal({
4070+
issues: [{ message: '"foo" requires a positive number' }]
4071+
});
4072+
});
4073+
});
4074+
});
40234075
});

test/helper.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ exports.validate = function (schema, prefs, tests) {
4242
}
4343

4444
const { error: errord, value: valued } = schema.validate(input, Object.assign({ debug: true }, prefs));
45+
46+
if (prefs === null) {
47+
internals.standardValidate(schema, test, { errord, valued });
48+
}
49+
4550
const { error, value } = schema.validate(input, prefs);
4651

4752
expect(error).to.equal(errord);
@@ -114,3 +119,27 @@ internals.thrownAt = function () {
114119
column: at[3]
115120
};
116121
};
122+
123+
124+
internals.standardValidate = function (schema, test, { errord, valued }) {
125+
126+
const [input, pass] = test;
127+
const { issues, value } = schema['~standard'].validate(input);
128+
129+
if (pass) {
130+
expect(issues).to.equal(undefined);
131+
expect(value).to.equal(valued);
132+
}
133+
134+
if (!pass) {
135+
expect(value).to.equal(undefined);
136+
}
137+
138+
if (!pass && !errord.details) {
139+
expect(issues.length).to.equal(1);
140+
}
141+
142+
if (!pass && errord.details) {
143+
expect(issues.length).to.equal(errord.details.length);
144+
}
145+
};

test/index.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Lab from '@hapi/lab';
22
import * as Joi from '..';
3-
3+
import { StandardSchemaV1 } from "@standard-schema/spec";
44

55
const { expect } = Lab.types;
66

@@ -1417,3 +1417,44 @@ const commentWithAlternativesSchemaObject = Joi.object<
14171417
expect.error(userSchema2.keys({ height: Joi.number() }));
14181418

14191419
expect.error(Joi.string('x'));
1420+
1421+
// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1422+
// Test Standard Schema Types
1423+
{
1424+
Joi.any()['~standard'].version
1425+
Joi.any()['~standard'].vendor
1426+
1427+
{
1428+
// Standard Validate
1429+
let value = { username: 'example', password: 'example' };
1430+
type TResult = { username: string; password: string };
1431+
const schema = Joi.object<TResult>().keys({
1432+
username: Joi.string().max(255).required(),
1433+
password: Joi.string()
1434+
.pattern(/^[a-zA-Z0-9]{3,255}$/)
1435+
.required(),
1436+
});
1437+
let result: StandardSchemaV1.Result<TResult> | Promise<StandardSchemaV1.Result<TResult>>;
1438+
1439+
result = schema['~standard'].validate(value);
1440+
if (result instanceof Promise) {
1441+
throw Error("Expected sync result");
1442+
}
1443+
1444+
if (result.issues) {
1445+
throw Error('issues should not be set')
1446+
}
1447+
expect.type<TResult>(result.value)
1448+
1449+
const falsyValue = { username: 'example' };
1450+
result = schema['~standard'].validate(falsyValue);
1451+
if (result instanceof Promise) {
1452+
throw new Error("Expected sync result");
1453+
}
1454+
1455+
if (!result.issues) {
1456+
throw Error('issues should be set')
1457+
}
1458+
expect.error(result.value)
1459+
}
1460+
}

test/validator.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,10 @@ describe('Validator', () => {
191191
});
192192

193193
expect(await schema.validateAsync({ id: 'valid' })).to.equal({ id: 'verified!' });
194+
expect(await schema['~standard'].validate({ id: 'valid' })).to.equal({ value: { id: 'verified!' } });
195+
194196
expect(await schema.validateAsync({ id: 'skip' })).to.equal({ id: 'skip!' });
197+
expect(await schema['~standard'].validate({ id: 'skip' })).to.equal({ value: { id: 'skip!' } });
195198
});
196199

197200
it('executes externals on nested object child', async () => {
@@ -228,9 +231,16 @@ describe('Validator', () => {
228231
});
229232

230233
expect(await schema.validateAsync({ user: { id: 'valid' } })).to.equal({ user: { id: 'verified!' } });
234+
expect(await schema['~standard'].validate({ user: { id: 'valid' } })).to.equal({ value: { user: { id: 'verified!' } } });
235+
231236
expect(await schema.validateAsync({ user: { id: 'skip' } })).to.equal({ user: { id: 'skip!' } });
237+
expect(await schema['~standard'].validate({ user: { id: 'skip' } })).to.equal({ value: { user: { id: 'skip!' } } });
238+
232239
expect(await schema.validateAsync({ user: { id: 'unchanged' } })).to.equal({ user: { id: 'unchanged!' } });
240+
expect(await schema['~standard'].validate({ user: { id: 'unchanged' } })).to.equal({ value: { user: { id: 'unchanged!' } } });
241+
233242
await expect(schema.validateAsync({ user: { id: 'other' } })).to.reject('Invalid id (user.id)');
243+
expect(await schema['~standard'].validate({ user: { id: 'other' } })).to.equal({ issues: [{ message: 'Invalid id (user.id)' }] });
234244
});
235245

236246
it('executes externals on root', async () => {
@@ -255,6 +265,7 @@ describe('Validator', () => {
255265

256266
const result = await schema.validateAsync('valid');
257267
expect(result).to.equal('verified!');
268+
expect(await schema['~standard'].validate('valid')).to.equal({ value: 'verified!' });
258269
});
259270

260271
it('executes externals on array item', async () => {
@@ -283,14 +294,18 @@ describe('Validator', () => {
283294
const schema = Joi.array().items(Joi.string().external(check).external(append));
284295

285296
expect(await schema.validateAsync(['valid'])).to.equal(['verified!']);
297+
expect(await schema['~standard'].validate(['valid'])).to.equal({ value: ['verified!'] });
298+
286299
expect(await schema.validateAsync(['skip'])).to.equal(['skip!']);
300+
expect(await schema['~standard'].validate(['skip'])).to.equal({ value: ['skip!'] });
287301
});
288302

289303
it('executes externals on array', async () => {
290304

291305
const schema = Joi.array().items(Joi.string()).external((value) => [...value, 'extra']);
292306

293307
expect(await schema.validateAsync(['valid'])).to.equal(['valid', 'extra']);
308+
expect(await schema['~standard'].validate(['valid'])).to.equal({ value: ['valid', 'extra'] });
294309
});
295310

296311
it('skips externals when prefs is false', async () => {
@@ -305,6 +320,8 @@ describe('Validator', () => {
305320
});
306321

307322
await expect(schema.validateAsync({ id: 'valid' })).to.reject('Invalid id (id)');
323+
expect(await schema['~standard'].validate({ id: 'valid' })).to.equal({ issues: [{ message: 'Invalid id (id)' }] });
324+
308325
expect(() => schema.validate({ id: 'valid' }, { externals: false })).to.not.throw();
309326
expect(() => schema.validate({ id: 'valid' })).to.throw('Schema with external rules must use validateAsync()');
310327
});
@@ -322,6 +339,7 @@ describe('Validator', () => {
322339
});
323340

324341
await expect(schema.validateAsync({ id: 'valid' })).to.reject('"id" length must be at least 10 characters long');
342+
expect(await schema['~standard'].validate({ id: 'valid' })).to.equal({ issues: [{ message: '"id" length must be at least 10 characters long', path: ['id'] }] });
325343
expect(called).to.be.false();
326344
});
327345

@@ -350,6 +368,8 @@ describe('Validator', () => {
350368

351369
const result = await schema.validateAsync(['valid']);
352370
expect(result).to.equal(['valid']);
371+
expect(await schema['~standard'].validate(['valid'])).to.equal({ value: ['valid'] });
372+
353373
expect(called).to.be.false();
354374
});
355375

@@ -365,6 +385,8 @@ describe('Validator', () => {
365385

366386
const result = await schema.validateAsync(input);
367387
expect(result).to.equal({ x: true });
388+
expect(await schema['~standard'].validate(input)).to.equal({ value: { x: true } });
389+
368390
expect(input).to.equal({ x: false });
369391
});
370392

@@ -380,6 +402,8 @@ describe('Validator', () => {
380402

381403
const result = await schema.validateAsync(input);
382404
expect(result).to.equal({ a: { x: true } });
405+
expect(await schema['~standard'].validate(input)).to.equal({ value: { a: { x: true } } });
406+
383407
expect(input).to.equal({ a: { x: false } });
384408
});
385409

@@ -395,6 +419,8 @@ describe('Validator', () => {
395419

396420
const result = await schema.validateAsync(input);
397421
expect(result).to.equal([1, 'x']);
422+
expect(await schema['~standard'].validate(input)).to.equal({ value: [1, 'x'] });
423+
398424
expect(input).to.equal([1]);
399425
});
400426

@@ -410,6 +436,7 @@ describe('Validator', () => {
410436

411437
const result = await schema.validateAsync(input);
412438
expect(result).to.equal([[1, 'x']]);
439+
expect(await schema['~standard'].validate(input)).to.equal({ value: [[1, 'x']] });
413440
expect(input).to.equal([[1]]);
414441
});
415442

@@ -559,6 +586,7 @@ describe('Validator', () => {
559586
value: 'my stringmy string'
560587
}
561588
}]);
589+
expect(await schema['~standard'].validate(input)).to.equal({ issues: [{ message: '"value" length must be less than or equal to 10 characters long', path: [] }] });
562590
});
563591

564592
it('should add multiple errors when errorsArray helper is used', async () => {
@@ -598,6 +626,7 @@ describe('Validator', () => {
598626
value: 'my stringmy string'
599627
}
600628
}]);
629+
expect(await schema['~standard'].validate(input)).to.equal({ issues: [{ message: '"value" length must be less than or equal to 10 characters long', path: [] }, { message: '"value" length must be at least 1 characters long', path: [] }] });
601630
});
602631

603632
it('should add a custom error when message helper is used', async () => {
@@ -621,6 +650,7 @@ describe('Validator', () => {
621650
custom: 'denied'
622651
}
623652
}]);
653+
expect(await schema['~standard'].validate(input)).to.equal({ issues: [{ message: '"value" has an invalid value my string (denied)', path: [] }] });
624654
});
625655

626656
it('should add warnings when warn helper is used on a link', async () => {
@@ -708,6 +738,7 @@ describe('Validator', () => {
708738
sign: '>'
709739
}
710740
}]);
741+
expect(await schema['~standard'].validate({ a: 1, b: 4 })).to.equal({ issues: [{ message: '"b" should be > 4', path: ['b'] }] });
711742
});
712743

713744
it('should call multiple externals when abortEarly is false and error helper is used', async () => {

0 commit comments

Comments
 (0)