Skip to content

Commit

Permalink
fix: replace date-time validator with our bug fixed version (#1856)
Browse files Browse the repository at this point in the history
* fix: replace date-time validator with our bug fixed version

* chore: add comment to explain ajv-formats code copy

* chore: cleanup
  • Loading branch information
chohmann committed Jul 16, 2021
1 parent 5456334 commit 44186db
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`HttpValidator validateInput() all validations are turned on properly validate date-time format ("") 1`] = `
Object {
"_tag": "Right",
"right": Object {
"method": "get",
"url": Object {
"path": "/todos",
"query": Object {
"updated_since": "",
},
},
},
}
`;

exports[`HttpValidator validateInput() all validations are turned on properly validate date-time format ("2021-02-18T12:02:16.49") 1`] = `
Object {
"_tag": "Left",
"left": Array [
Object {
"code": "format",
"message": "must match format \\"date-time\\"",
"path": Array [
"query",
"updated_since",
],
"severity": 0,
},
],
}
`;

exports[`HttpValidator validateInput() all validations are turned on properly validate date-time format ("2021-02-18T12:02:16.49Z") 1`] = `
Object {
"_tag": "Right",
"right": Object {
"method": "get",
"url": Object {
"path": "/todos",
"query": Object {
"updated_since": "2021-02-18T12:02:16.49Z",
},
},
},
}
`;

exports[`HttpValidator validateInput() all validations are turned on properly validate date-time format ("yesterday") 1`] = `
Object {
"_tag": "Left",
"left": Array [
Object {
"code": "format",
"message": "must match format \\"date-time\\"",
"path": Array [
"query",
"updated_since",
],
"severity": 0,
},
],
}
`;

exports[`HttpValidator validateInput() all validations are turned on returns validation errors for whole request structure 1`] = `
Object {
"_tag": "Left",
Expand Down
39 changes: 39 additions & 0 deletions packages/http/src/validator/__tests__/functional.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,45 @@ describe('HttpValidator', () => {
expect(validateInput({ resource: httpOperations[2], element: BAD_INPUT })).toMatchSnapshot();
});

it.each(['yesterday', '', '2021-02-18T12:02:16.49Z', '2021-02-18T12:02:16.49'])(
'properly validate date-time format ("%s")',
(dateValue: string) => {
expect(
validateInput({
resource: {
id: '?http-operation-id?',
method: 'get',
path: '/todos',
responses: [
{
code: '200',
},
],
request: {
query: [
{
name: 'updated_since',
schema: {
type: 'string',
format: 'date-time',
$schema: 'http://json-schema.org/draft-07/schema#',
},
style: HttpParamStyles.Form,
},
],
cookie: [],
path: [],
},
},
element: {
method: 'get',
url: { path: '/todos', query: { updated_since: dateValue } },
},
})
).toMatchSnapshot();
}
);

describe('when all required params are provided', () => {
it('returns no validation errors', () => {
assertRight(validateInput({ resource: httpOperations[0], element: GOOD_INPUT }));
Expand Down
85 changes: 85 additions & 0 deletions packages/http/src/validator/validators/dateTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
This file contains code copied from the ajv-formats project with a small bug fix.
This was done because https://github.com/ajv-validator/ajv-formats/pull/33 does not look like it will be accepted soon, if at all.
We needed this to address the regression in date-time validation when we switch from ajv-oai to ajv-formats.
Regression initially reported in https://github.com/stoplightio/prism/issues/1830
*/

import { FormatCompare, FormatDefinition, FormatValidator } from 'ajv/dist/types';

export function fmtDef(
validate: RegExp | FormatValidator<string>,
compare: FormatCompare<string>
): FormatDefinition<string> {
return { validate, compare };
}

const DATE_TIME_SEPARATOR = /t|\s/i;
export function date_time(str: string): boolean {
// http://tools.ietf.org/html/rfc3339#section-5.6
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR);
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1], true);
}

const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function isLeapYear(year: number): boolean {
// https://tools.ietf.org/html/rfc3339#appendix-C
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
function date(str: string): boolean {
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
const matches: string[] | null = DATE.exec(str);
if (!matches) return false;
const year: number = +matches[1];
const month: number = +matches[2];
const day: number = +matches[3];
return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]);
}

const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i;
const PLUS_MINUS = /^[+-]/;
const TIMEZONE = /^[Zz]$/;
const ISO_8601_TIME = /^[+-](?:[01][0-9]|2[0-4])(?::?[0-5][0-9])?$/;
function time(str: string, withTimeZone?: boolean): boolean {
const matches: string[] | null = TIME.exec(str);
if (!matches) return false;

const hour: number = +matches[1];
const minute: number = +matches[2];
const second: number = +matches[3];
const timeZone: string = matches[5];
return (
((hour <= 23 && minute <= 59 && second <= 59) || (hour === 23 && minute === 59 && second === 60)) &&
(!withTimeZone ||
TIMEZONE.test(timeZone) ||
(PLUS_MINUS.test(timeZone) && time(timeZone.slice(1) + ':00')) ||
ISO_8601_TIME.test(timeZone))
);
}

export function compareDateTime(dt1: string, dt2: string): number | undefined {
if (!(dt1 && dt2)) return undefined;
const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR);
const [d2, t2] = dt2.split(DATE_TIME_SEPARATOR);
const res = compareDate(d1, d2);
if (res === undefined) return undefined;
return res || compareTime(t1, t2);
}
function compareDate(d1: string, d2: string): number | undefined {
if (!(d1 && d2)) return undefined;
if (d1 > d2) return 1;
if (d1 < d2) return -1;
return 0;
}
function compareTime(t1: string, t2: string): number | undefined {
if (!(t1 && t2)) return undefined;
const a1 = TIME.exec(t1);
const a2 = TIME.exec(t2);
if (!(a1 && a2)) return undefined;
t1 = a1[1] + a1[2] + a1[3] + (a1[4] || '');
t2 = a2[1] + a2[2] + a2[3] + (a2[4] || '');
if (t1 > t2) return 1;
if (t1 < t2) return -1;
return 0;
}
4 changes: 4 additions & 0 deletions packages/http/src/validator/validators/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Ajv2019 from 'ajv/dist/2019';
import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import type { JSONSchema } from '../../';
import { compareDateTime, date_time, fmtDef } from './dateTime';

const baseAjvOptions: Partial<Options> = {
allErrors: true,
Expand All @@ -24,6 +25,9 @@ function createAjvInstances(Ajv: typeof AjvCore) {
addFormats(ajv);
addFormats(ajvNoCoerce);

ajv.addFormat('date-time', fmtDef(date_time, compareDateTime));
ajvNoCoerce.addFormat('date-time', fmtDef(date_time, compareDateTime));

return {
coerce: ajv,
noCoerce: ajvNoCoerce,
Expand Down

0 comments on commit 44186db

Please sign in to comment.