Skip to content

Commit 3f7dac1

Browse files
committed
Date-time should validate timezone
1 parent 8827acb commit 3f7dac1

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

src/formats.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ export const fastFormats: DefinedFormats = {
106106
email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,
107107
}
108108

109+
export const strictFormats: Partial<DefinedFormats> = {
110+
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
111+
time: fmtDef(strict_time, compareTime),
112+
"date-time": fmtDef(strict_date_time, compareDateTime),
113+
}
114+
109115
export const formatNames = Object.keys(fullFormats) as FormatName[]
110116

111117
function isLeapYear(year: number): boolean {
@@ -139,8 +145,11 @@ function compareDate(d1: string, d2: string): number | undefined {
139145
}
140146

141147
const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i
148+
const PLUS_MINUS = /^[+-]/
149+
const TIMEZONE = /^[Zz]$/
150+
const ISO_8601_TIME = /^[+-](?:[01][0-9]|2[0-4])(?::?[0-5][0-9])?$/
142151

143-
function time(str: string, withTimeZone?: boolean): boolean {
152+
function time(str: string, withTimeZone?: boolean, strict?: boolean): boolean {
144153
const matches: string[] | null = TIME.exec(str)
145154
if (!matches) return false
146155

@@ -151,10 +160,19 @@ function time(str: string, withTimeZone?: boolean): boolean {
151160
return (
152161
((hour <= 23 && minute <= 59 && second <= 59) ||
153162
(hour === 23 && minute === 59 && second === 60)) &&
154-
(!withTimeZone || timeZone !== "")
163+
(!withTimeZone ||
164+
(strict
165+
? TIMEZONE.test(timeZone) ||
166+
(PLUS_MINUS.test(timeZone) && time(timeZone.slice(1) + ":00")) ||
167+
ISO_8601_TIME.test(timeZone)
168+
: timeZone !== ""))
155169
)
156170
}
157171

172+
function strict_time(str: string, withTimeZone?: boolean): boolean {
173+
return time(str, withTimeZone, true)
174+
}
175+
158176
function compareTime(t1: string, t2: string): number | undefined {
159177
if (!(t1 && t2)) return undefined
160178
const a1 = TIME.exec(t1)
@@ -174,6 +192,11 @@ function date_time(str: string): boolean {
174192
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1], true)
175193
}
176194

195+
function strict_date_time(str: string): boolean {
196+
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
197+
return dateTime.length === 2 && date(dateTime[0]) && strict_time(dateTime[1], true)
198+
}
199+
177200
function compareDateTime(dt1: string, dt2: string): number | undefined {
178201
if (!(dt1 && dt2)) return undefined
179202
const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR)

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
formatNames,
66
fastFormats,
77
fullFormats,
8+
strictFormats,
89
} from "./formats"
910
import formatLimit from "./limit"
1011
import type Ajv from "ajv"
@@ -17,6 +18,7 @@ export interface FormatOptions {
1718
mode?: FormatMode
1819
formats?: FormatName[]
1920
keywords?: boolean
21+
strictDate?: boolean
2022
}
2123

2224
export type FormatsPluginOptions = FormatName[] | FormatOptions
@@ -30,7 +32,7 @@ const fastName = new Name("fastFormats")
3032

3133
const formatsPlugin: FormatsPlugin = (
3234
ajv: Ajv,
33-
opts: FormatsPluginOptions = {keywords: true}
35+
opts: FormatsPluginOptions = {keywords: true, strictDate: false}
3436
): Ajv => {
3537
if (Array.isArray(opts)) {
3638
addFormats(ajv, opts, fullFormats, fullName)
@@ -39,7 +41,7 @@ const formatsPlugin: FormatsPlugin = (
3941
const [formats, exportName] =
4042
opts.mode === "fast" ? [fastFormats, fastName] : [fullFormats, fullName]
4143
const list = opts.formats || formatNames
42-
addFormats(ajv, list, formats, exportName)
44+
addFormats(ajv, list, opts.strictDate ? {...formats, ...strictFormats} : formats, exportName)
4345
if (opts.keywords) formatLimit(ajv)
4446
return ajv
4547
}

tests/strictDate.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Ajv from "ajv"
2+
import addFormats from "../dist"
3+
4+
const ajv = new Ajv({$data: true, strictTypes: false, formats: {allowedUnknown: true}})
5+
addFormats(ajv, {strictDate: true})
6+
7+
describe("strictDate option", () => {
8+
it("a valid date-time string with time offset", () => {
9+
expect(
10+
ajv.validate(
11+
{
12+
type: "string",
13+
format: "date-time",
14+
},
15+
"2020-06-19T12:13:14+05:00"
16+
)
17+
).toBe(true)
18+
})
19+
20+
it("an invalid date-time string (no time offset)", () => {
21+
expect(
22+
ajv.validate(
23+
{
24+
type: "string",
25+
format: "date-time",
26+
},
27+
"2020-06-19T12:13:14"
28+
)
29+
).toBe(false)
30+
})
31+
32+
it("an invalid date-time string (invalid time offset)", () => {
33+
expect(
34+
ajv.validate(
35+
{
36+
type: "string",
37+
format: "date-time",
38+
},
39+
"2020-06-19T12:13:14+26:00"
40+
)
41+
).toBe(false)
42+
})
43+
})

0 commit comments

Comments
 (0)