Skip to content

Commit fdbd714

Browse files
committed
Merge branch 'stoplightio-fix/date-time'
2 parents c1cb46c + 34df8db commit fdbd714

File tree

4 files changed

+80
-17
lines changed

4 files changed

+80
-17
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,13 @@ addFormats(ajv, {mode: "fast"})
105105
or
106106

107107
```javascript
108-
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true})
108+
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true, strictTime: true})
109109
```
110110

111111
In `"fast"` mode the following formats are simplified: `"date"`, `"time"`, `"date-time"`, `"uri"`, `"uri-reference"`, `"email"`. For example `"date"`, `"time"` and `"date-time"` do not validate ranges in `"fast"` mode, only string structure, and other formats have simplified regular expressions.
112112

113+
With `strictTime: true` option timezone becomes required in `time` and `date-time` formats, and (it also implies `full` mode for these formats).
114+
113115
## Tests
114116

115117
```bash

src/formats.ts

+30-14
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ export const fastFormats: DefinedFormats = {
111111
/^[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,
112112
}
113113

114+
export const strictFormats: Partial<DefinedFormats> = {
115+
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
116+
time: fmtDef(strict_time, compareTime),
117+
"date-time": fmtDef(strict_date_time, compareDateTime),
118+
}
119+
114120
export const formatNames = Object.keys(fullFormats) as FormatName[]
115121

116122
function isLeapYear(year: number): boolean {
@@ -143,40 +149,50 @@ function compareDate(d1: string, d2: string): number | undefined {
143149
return 0
144150
}
145151

146-
const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i
152+
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-]\d\d)(?::?(\d\d))?)?$/i
147153

148-
function time(str: string, withTimeZone?: boolean): boolean {
154+
function time(str: string, withTimeZone?: boolean, strictTime?: boolean): boolean {
149155
const matches: string[] | null = TIME.exec(str)
150156
if (!matches) return false
151-
152-
const hour: number = +matches[1]
153-
const minute: number = +matches[2]
154-
const second: number = +matches[3]
155-
const timeZone: string = matches[5]
157+
const hr: number = +matches[1]
158+
const min: number = +matches[2]
159+
const sec: number = +matches[3]
160+
const tz: string | undefined = matches[4]
161+
const tzH: number = +(matches[5] || 0)
162+
const tzM: number = +(matches[6] || 0)
156163
return (
157-
((hour <= 23 && minute <= 59 && second <= 59) ||
158-
(hour === 23 && minute === 59 && second === 60)) &&
159-
(!withTimeZone || timeZone !== "")
164+
((hr <= 23 && min <= 59 && sec < 60 && tzH <= 24 && tzM < 60) ||
165+
// leap second
166+
(hr - tzH === 23 && min - tzM === 59 && sec < 61 && tzH <= 24 && tzM < 60)) &&
167+
(!withTimeZone || (tz !== "" && (!strictTime || !!tz)))
160168
)
161169
}
162170

171+
function strict_time(str: string): boolean {
172+
return time(str, true, true)
173+
}
174+
163175
function compareTime(t1: string, t2: string): number | undefined {
164176
if (!(t1 && t2)) return undefined
165177
const a1 = TIME.exec(t1)
166178
const a2 = TIME.exec(t2)
167179
if (!(a1 && a2)) return undefined
168-
t1 = a1[1] + a1[2] + a1[3] + (a1[4] || "")
169-
t2 = a2[1] + a2[2] + a2[3] + (a2[4] || "")
180+
t1 = a1[1] + a1[2] + a1[3]
181+
t2 = a2[1] + a2[2] + a2[3]
170182
if (t1 > t2) return 1
171183
if (t1 < t2) return -1
172184
return 0
173185
}
174186

175187
const DATE_TIME_SEPARATOR = /t|\s/i
176-
function date_time(str: string): boolean {
188+
function date_time(str: string, strictTime?: boolean): boolean {
177189
// http://tools.ietf.org/html/rfc3339#section-5.6
178190
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
179-
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1], true)
191+
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1], true, strictTime)
192+
}
193+
194+
function strict_date_time(str: string): boolean {
195+
return date_time(str, true)
180196
}
181197

182198
function compareDateTime(dt1: string, dt2: string): number | undefined {

src/index.ts

+4-2
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+
strictTime?: 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, strictTime: 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.strictTime ? {...formats, ...strictFormats} : formats, exportName)
4345
if (opts.keywords) formatLimit(ajv)
4446
return ajv
4547
}

tests/strictTime.spec.ts

+43
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, {mode: "full", strictTime: true})
6+
7+
describe("strictTime 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)