Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions lib/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,53 @@ internals.Base = class {

return obj;
}

// Standard Schema

get '~standard'() {

const mapToStandardError = (error) => {

let issues;
if (Errors.ValidationError.isError(error)) {
issues = error.details.map(({ message, path }) => ({
message,
path
}));
}
else {
issues = [{
message: error.message
}];
}

return {
issues
};
};

const mapToStandardValue = (value) => ({ value });

return {
version: 1,
vendor: 'joi',
validate: (value) => {

const result = Validator.standard(value, this);

if (result instanceof Promise) {
return result
.then(mapToStandardValue, mapToStandardError);
}

if (!result.error) {
return mapToStandardValue(result.value);
}

return mapToStandardError(result.error);
}
};
}
};


Expand Down
3 changes: 2 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// TypeScript Version: 2.8

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

declare namespace Joi {
type Types =
Expand Down Expand Up @@ -961,7 +962,7 @@ declare namespace Joi {
$_validate(value: any, state: State, prefs: ValidationOptions): ValidationResult;
}

interface AnySchema<TSchema = any> extends SchemaInternals {
interface AnySchema<TSchema = any> extends SchemaInternals, StandardSchemaV1<TSchema> {
/**
* Flags of current schema.
*/
Expand Down
11 changes: 11 additions & 0 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ exports.entryAsync = async function (value, schema, prefs) {
};


exports.standard = function (value, schema) {


if (schema.isAsync()) {
return exports.entryAsync(value, schema);
}

return exports.entry(value, schema);
};


internals.Mainstay = class {

constructor(tracer, debug, links) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"@hapi/hoek": "^11.0.7",
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2"
"@hapi/topo": "^6.0.2",
"@standard-schema/spec": "^1.0.0"
},
"devDependencies": {
"@hapi/bourne": "^3.0.0",
Expand Down
52 changes: 52 additions & 0 deletions test/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4020,4 +4020,56 @@ describe('any', () => {
expect(() => Joi.any().$_addRule({ name: 5 })).to.throw('Invalid rule name');
});
});

describe('~standard', () => {

it('returns the version number of the standard', () => {

const schema = Joi.number();
expect(schema['~standard'].version).to.equal(1);
});

it('returns the vendor name of the schema library', () => {

const schema = Joi.number();
expect(schema['~standard'].vendor).to.equal('joi');
});

describe('validate', () => {

it('return only value when passing', () => {

const schema = Joi.string();
expect(schema['~standard'].validate('3')).to.equal({
value: '3'
});
});

it('return only issues when not passing', () => {

const schema = Joi.string();
expect(schema['~standard'].validate(3)).to.equal({
issues: [{ message: '"value" must be a string', path: [] }]
});
});

it('return only issues when not passing (custom error is Error)', () => {

const schema = Joi.string().error(new Error('Was REALLY expecting a string'));
expect(schema['~standard'].validate(3)).to.equal({
issues: [{ message: 'Was REALLY expecting a string' }]
});
});

it('return only issues when not passing (custom error is validation error function that return string)', () => {

const schema = Joi.object({
foo: Joi.number().min(0).error((errors) => new Error('"foo" requires a positive number'))
});
expect(schema['~standard'].validate({ foo: -2 })).to.equal({
issues: [{ message: '"foo" requires a positive number' }]
});
});
});
});
});
29 changes: 29 additions & 0 deletions test/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ exports.validate = function (schema, prefs, tests) {
}

const { error: errord, value: valued } = schema.validate(input, Object.assign({ debug: true }, prefs));

if (prefs === null) {
internals.standardValidate(schema, test, { errord, valued });
}

const { error, value } = schema.validate(input, prefs);

expect(error).to.equal(errord);
Expand Down Expand Up @@ -114,3 +119,27 @@ internals.thrownAt = function () {
column: at[3]
};
};


internals.standardValidate = function (schema, test, { errord, valued }) {

const [input, pass] = test;
const { issues, value } = schema['~standard'].validate(input);

if (pass) {
expect(issues).to.equal(undefined);
expect(value).to.equal(valued);
}

if (!pass) {
expect(value).to.equal(undefined);
}

if (!pass && !errord.details) {
expect(issues.length).to.equal(1);
}

if (!pass && errord.details) {
expect(issues.length).to.equal(errord.details.length);
}
};
43 changes: 42 additions & 1 deletion test/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as Lab from '@hapi/lab';
import * as Joi from '..';

import { StandardSchemaV1 } from "@standard-schema/spec";

const { expect } = Lab.types;

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

expect.error(Joi.string('x'));

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
// Test Standard Schema Types
{
Joi.any()['~standard'].version
Joi.any()['~standard'].vendor

{
// Standard Validate
let value = { username: 'example', password: 'example' };
type TResult = { username: string; password: string };
const schema = Joi.object<TResult>().keys({
username: Joi.string().max(255).required(),
password: Joi.string()
.pattern(/^[a-zA-Z0-9]{3,255}$/)
.required(),
});
let result: StandardSchemaV1.Result<TResult> | Promise<StandardSchemaV1.Result<TResult>>;

result = schema['~standard'].validate(value);
if (result instanceof Promise) {
throw Error("Expected sync result");
}

if (result.issues) {
throw Error('issues should not be set')
}
expect.type<TResult>(result.value)

const falsyValue = { username: 'example' };
result = schema['~standard'].validate(falsyValue);
if (result instanceof Promise) {
throw new Error("Expected sync result");
}

if (!result.issues) {
throw Error('issues should be set')
}
expect.error(result.value)
}
}
31 changes: 31 additions & 0 deletions test/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,10 @@ describe('Validator', () => {
});

expect(await schema.validateAsync({ id: 'valid' })).to.equal({ id: 'verified!' });
expect(await schema['~standard'].validate({ id: 'valid' })).to.equal({ value: { id: 'verified!' } });

expect(await schema.validateAsync({ id: 'skip' })).to.equal({ id: 'skip!' });
expect(await schema['~standard'].validate({ id: 'skip' })).to.equal({ value: { id: 'skip!' } });
});

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

expect(await schema.validateAsync({ user: { id: 'valid' } })).to.equal({ user: { id: 'verified!' } });
expect(await schema['~standard'].validate({ user: { id: 'valid' } })).to.equal({ value: { user: { id: 'verified!' } } });

expect(await schema.validateAsync({ user: { id: 'skip' } })).to.equal({ user: { id: 'skip!' } });
expect(await schema['~standard'].validate({ user: { id: 'skip' } })).to.equal({ value: { user: { id: 'skip!' } } });

expect(await schema.validateAsync({ user: { id: 'unchanged' } })).to.equal({ user: { id: 'unchanged!' } });
expect(await schema['~standard'].validate({ user: { id: 'unchanged' } })).to.equal({ value: { user: { id: 'unchanged!' } } });

await expect(schema.validateAsync({ user: { id: 'other' } })).to.reject('Invalid id (user.id)');
expect(await schema['~standard'].validate({ user: { id: 'other' } })).to.equal({ issues: [{ message: 'Invalid id (user.id)' }] });
});

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

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

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

expect(await schema.validateAsync(['valid'])).to.equal(['verified!']);
expect(await schema['~standard'].validate(['valid'])).to.equal({ value: ['verified!'] });

expect(await schema.validateAsync(['skip'])).to.equal(['skip!']);
expect(await schema['~standard'].validate(['skip'])).to.equal({ value: ['skip!'] });
});

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

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

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

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

await expect(schema.validateAsync({ id: 'valid' })).to.reject('Invalid id (id)');
expect(await schema['~standard'].validate({ id: 'valid' })).to.equal({ issues: [{ message: 'Invalid id (id)' }] });

expect(() => schema.validate({ id: 'valid' }, { externals: false })).to.not.throw();
expect(() => schema.validate({ id: 'valid' })).to.throw('Schema with external rules must use validateAsync()');
});
Expand All @@ -322,6 +339,7 @@ describe('Validator', () => {
});

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

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

const result = await schema.validateAsync(['valid']);
expect(result).to.equal(['valid']);
expect(await schema['~standard'].validate(['valid'])).to.equal({ value: ['valid'] });

expect(called).to.be.false();
});

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

const result = await schema.validateAsync(input);
expect(result).to.equal({ x: true });
expect(await schema['~standard'].validate(input)).to.equal({ value: { x: true } });

expect(input).to.equal({ x: false });
});

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

const result = await schema.validateAsync(input);
expect(result).to.equal({ a: { x: true } });
expect(await schema['~standard'].validate(input)).to.equal({ value: { a: { x: true } } });

expect(input).to.equal({ a: { x: false } });
});

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

const result = await schema.validateAsync(input);
expect(result).to.equal([1, 'x']);
expect(await schema['~standard'].validate(input)).to.equal({ value: [1, 'x'] });

expect(input).to.equal([1]);
});

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

const result = await schema.validateAsync(input);
expect(result).to.equal([[1, 'x']]);
expect(await schema['~standard'].validate(input)).to.equal({ value: [[1, 'x']] });
expect(input).to.equal([[1]]);
});

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

it('should add multiple errors when errorsArray helper is used', async () => {
Expand Down Expand Up @@ -598,6 +626,7 @@ describe('Validator', () => {
value: 'my stringmy string'
}
}]);
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: [] }] });
});

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

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

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