Skip to content

Commit

Permalink
Merge branch 'stoplightio-fix/date-time'
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Nov 6, 2021
2 parents c1cb46c + 34df8db commit fdbd714
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 17 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,13 @@ addFormats(ajv, {mode: "fast"})
or

```javascript
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true})
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true, strictTime: true})
```

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.

With `strictTime: true` option timezone becomes required in `time` and `date-time` formats, and (it also implies `full` mode for these formats).

## Tests

```bash
Expand Down
44 changes: 30 additions & 14 deletions src/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export const fastFormats: DefinedFormats = {
/^[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,
}

export const strictFormats: Partial<DefinedFormats> = {
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
time: fmtDef(strict_time, compareTime),
"date-time": fmtDef(strict_date_time, compareDateTime),
}

export const formatNames = Object.keys(fullFormats) as FormatName[]

function isLeapYear(year: number): boolean {
Expand Down Expand Up @@ -143,40 +149,50 @@ function compareDate(d1: string, d2: string): number | undefined {
return 0
}

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

function time(str: string, withTimeZone?: boolean): boolean {
function time(str: string, withTimeZone?: boolean, strictTime?: 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]
const hr: number = +matches[1]
const min: number = +matches[2]
const sec: number = +matches[3]
const tz: string | undefined = matches[4]
const tzH: number = +(matches[5] || 0)
const tzM: number = +(matches[6] || 0)
return (
((hour <= 23 && minute <= 59 && second <= 59) ||
(hour === 23 && minute === 59 && second === 60)) &&
(!withTimeZone || timeZone !== "")
((hr <= 23 && min <= 59 && sec < 60 && tzH <= 24 && tzM < 60) ||
// leap second
(hr - tzH === 23 && min - tzM === 59 && sec < 61 && tzH <= 24 && tzM < 60)) &&
(!withTimeZone || (tz !== "" && (!strictTime || !!tz)))
)
}

function strict_time(str: string): boolean {
return time(str, true, true)
}

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] || "")
t1 = a1[1] + a1[2] + a1[3]
t2 = a2[1] + a2[2] + a2[3]
if (t1 > t2) return 1
if (t1 < t2) return -1
return 0
}

const DATE_TIME_SEPARATOR = /t|\s/i
function date_time(str: string): boolean {
function date_time(str: string, strictTime?: boolean): 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)
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1], true, strictTime)
}

function strict_date_time(str: string): boolean {
return date_time(str, true)
}

function compareDateTime(dt1: string, dt2: string): number | undefined {
Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
formatNames,
fastFormats,
fullFormats,
strictFormats,
} from "./formats"
import formatLimit from "./limit"
import type Ajv from "ajv"
Expand All @@ -17,6 +18,7 @@ export interface FormatOptions {
mode?: FormatMode
formats?: FormatName[]
keywords?: boolean
strictTime?: boolean
}

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

const formatsPlugin: FormatsPlugin = (
ajv: Ajv,
opts: FormatsPluginOptions = {keywords: true}
opts: FormatsPluginOptions = {keywords: true, strictTime: false}
): Ajv => {
if (Array.isArray(opts)) {
addFormats(ajv, opts, fullFormats, fullName)
Expand All @@ -39,7 +41,7 @@ const formatsPlugin: FormatsPlugin = (
const [formats, exportName] =
opts.mode === "fast" ? [fastFormats, fastName] : [fullFormats, fullName]
const list = opts.formats || formatNames
addFormats(ajv, list, formats, exportName)
addFormats(ajv, list, opts.strictTime ? {...formats, ...strictFormats} : formats, exportName)
if (opts.keywords) formatLimit(ajv)
return ajv
}
Expand Down
43 changes: 43 additions & 0 deletions tests/strictTime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Ajv from "ajv"
import addFormats from "../dist"

const ajv = new Ajv({$data: true, strictTypes: false, formats: {allowedUnknown: true}})
addFormats(ajv, {mode: "full", strictTime: true})

describe("strictTime option", () => {
it("a valid date-time string with time offset", () => {
expect(
ajv.validate(
{
type: "string",
format: "date-time",
},
"2020-06-19T12:13:14+05:00"
)
).toBe(true)
})

it("an invalid date-time string (no time offset)", () => {
expect(
ajv.validate(
{
type: "string",
format: "date-time",
},
"2020-06-19T12:13:14"
)
).toBe(false)
})

it("an invalid date-time string (invalid time offset)", () => {
expect(
ajv.validate(
{
type: "string",
format: "date-time",
},
"2020-06-19T12:13:14+26:00"
)
).toBe(false)
})
})

0 comments on commit fdbd714

Please sign in to comment.