• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zaggino / z-schema / 21795322571

08 Feb 2026 08:45AM UTC coverage: 91.87% (+1.0%) from 90.868%
21795322571

Pull #336

github

web-flow
Merge 62fba363e into 87b824610
Pull Request #336: feat!: new api, see docs for changes

485 of 495 branches covered (97.98%)

Branch coverage included in aggregate %.

132 of 154 new or added lines in 6 files covered. (85.71%)

1 existing line in 1 file now uncovered.

1436 of 1596 relevant lines covered (89.97%)

25313.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.71
/src/format-validators.ts
1
import isEmailModule from 'validator/lib/isEmail.js';
2
import isIPModule from 'validator/lib/isIP.js';
3
import isURLModule from 'validator/lib/isURL.js';
4
import { sortedKeys } from './utils/json.js';
5

6
export type FormatValidatorFn = (input: unknown) => boolean | Promise<boolean>;
7

8
const dateValidator: FormatValidatorFn = (date: unknown) => {
73✔
9
  if (typeof date !== 'string') {
×
10
    return true;
×
11
  }
12
  // full-date from http://tools.ietf.org/html/rfc3339#section-5.6
13
  const matches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(date);
×
14
  if (matches === null) {
×
15
    return false;
×
16
  }
17
  // var year = matches[1];
18
  // var month = matches[2];
19
  // var day = matches[3];
20
  if (matches[2] < '01' || matches[2] > '12' || matches[3] < '01' || matches[3] > '31') {
×
21
    return false;
×
22
  }
23
  return true;
×
24
};
25

26
const dateTimeValidator: FormatValidatorFn = (dateTime: unknown) => {
73✔
27
  if (typeof dateTime !== 'string') {
128✔
28
    return true;
48✔
29
  }
30
  // date-time from http://tools.ietf.org/html/rfc3339#section-5.6
31
  const s = dateTime.toLowerCase().split('t');
80✔
32
  if (s.length !== 2) {
80✔
33
    return false;
×
34
  }
35
  const datePart = s[0];
80✔
36
  const timePart = s[1];
80✔
37
  // Check date
38
  const dateMatches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(datePart);
80✔
39
  if (dateMatches === null) {
80✔
40
    return false;
24✔
41
  }
42
  const year = parseInt(dateMatches[1], 10);
56✔
43
  const month = parseInt(dateMatches[2], 10);
56✔
44
  const day = parseInt(dateMatches[3], 10);
56✔
45
  if (month < 1 || month > 12 || day < 1 || day > 31) {
56✔
NEW
46
    return false;
×
47
  }
48
  // Check if date is valid
49
  const date = new Date(year, month - 1, day);
56✔
50
  if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
56✔
51
    return false;
4✔
52
  }
53
  // Check time
54
  const timeMatches = /^([0-9]{2}):([0-9]{2}):([0-9]{2})(.[0-9]+)?(z|([+-][0-9]{2}:[0-9]{2}))$/.exec(timePart);
52✔
55
  if (timeMatches === null) {
52✔
56
    return false;
8✔
57
  }
58
  const hour = parseInt(timeMatches[1], 10);
44✔
59
  const minute = parseInt(timeMatches[2], 10);
44✔
60
  const second = parseInt(timeMatches[3], 10);
44✔
61
  if (hour > 23 || minute > 59 || second > 60) {
44✔
62
    return false;
4✔
63
  }
64
  // Check offset
65
  let utcHour = hour;
40✔
66
  if (timeMatches[5] !== 'z') {
40✔
67
    const offset = timeMatches[5];
16✔
68
    const offsetMatches = /^([+-])([0-9]{2}):([0-9]{2})$/.exec(offset);
16✔
69
    if (offsetMatches === null) {
16✔
NEW
70
      return false;
×
71
    }
72
    const offsetSign = offsetMatches[1];
16✔
73
    const offsetHour = parseInt(offsetMatches[2], 10);
16✔
74
    const offsetMinute = parseInt(offsetMatches[3], 10);
16✔
75
    if (offsetHour > 23 || offsetMinute > 59) {
16✔
76
      return false;
4✔
77
    }
78
    if (offsetSign === '+') {
12✔
79
      utcHour = hour - offsetHour;
4✔
80
    } else {
81
      utcHour = hour + offsetHour;
8✔
82
    }
83
    utcHour = ((utcHour % 24) + 24) % 24;
12✔
84
  }
85
  // Leap second only at 23:59:60 UTC
86
  if (second === 60) {
36✔
87
    if (utcHour !== 23 || minute !== 59) {
16✔
88
      return false;
8✔
89
    }
90
  }
91
  return true;
28✔
92
};
93

94
const emailValidator: FormatValidatorFn = (email: unknown) => {
73✔
95
  if (typeof email !== 'string') {
120✔
96
    return true;
48✔
97
  }
98
  return isEmailModule.default(email, { require_tld: true });
72✔
99
};
100

101
const hostnameValidator: FormatValidatorFn = (hostname: unknown) => {
73✔
102
  if (typeof hostname !== 'string') {
136✔
103
    return true;
48✔
104
  }
105
  /*
106
          http://json-schema.org/latest/json-schema-validation.html#anchor114
107
          A string instance is valid against this attribute if it is a valid
108
          representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034].
109

110
          http://tools.ietf.org/html/rfc1034#section-3.5
111

112
          <digit> ::= any one of the ten digits 0 through 9
113
          var digit = /[0-9]/;
114

115
          <letter> ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case
116
          var letter = /[a-zA-Z]/;
117

118
          <let-dig> ::= <letter> | <digit>
119
          var letDig = /[0-9a-zA-Z]/;
120

121
          <let-dig-hyp> ::= <let-dig> | "-"
122
          var letDigHyp = /[-0-9a-zA-Z]/;
123

124
          <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
125
          var ldhStr = /[-0-9a-zA-Z]+/;
126

127
          <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
128
          var label = /[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?/;
129

130
          <subdomain> ::= <label> | <subdomain> "." <label>
131
          var subdomain = /^[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?(\.[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?)*$/;
132

133
          <domain> ::= <subdomain> | " "
134
          var domain = null;
135
      */
136
  const valid = /^[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?(\.[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?)*$/.test(hostname);
88✔
137
  if (valid) {
88✔
138
    // the sum of all label octets and label lengths is limited to 255.
139
    if (hostname.length > 255) {
36✔
140
      return false;
×
141
    }
142
    // Each node has a label, which is zero to 63 octets in length
143
    const labels = hostname.split('.');
36✔
144
    for (let i = 0; i < labels.length; i++) {
36✔
145
      if (labels[i].length > 63) {
52✔
146
        return false;
8✔
147
      }
148
    }
149
  }
150
  return valid;
80✔
151
};
152

153
const ipv4Validator: FormatValidatorFn = (ipv4: unknown) => {
73✔
154
  if (typeof ipv4 !== 'string') {
92✔
155
    return true;
48✔
156
  }
157
  return isIPModule.default(ipv4, 4);
44✔
158
};
159

160
const ipv6Validator: FormatValidatorFn = (ipv6: unknown) => {
73✔
161
  if (typeof ipv6 !== 'string') {
188✔
162
    return true;
48✔
163
  }
164
  if (ipv6.includes('%')) {
140✔
165
    return false;
4✔
166
  }
167
  return isIPModule.default(ipv6, 6);
136✔
168
};
169

170
const regexValidator: FormatValidatorFn = (input: unknown) => {
73✔
171
  if (typeof input !== 'string') {
72✔
172
    return false;
×
173
  }
174
  try {
72✔
175
    RegExp(input);
72✔
176
    return true;
72✔
177
  } catch (_e) {
178
    return false;
×
179
  }
180
};
181

182
const strictUriValidator: FormatValidatorFn = (uri: unknown) => typeof uri !== 'string' || isURLModule.default(uri);
73✔
183

184
const uriValidator: FormatValidatorFn = function (uri: unknown) {
73✔
185
  if (typeof uri !== 'string') return true;
168✔
186
  // eslint-disable-next-line no-control-regex
187
  if (/[^\x00-\x7F]/.test(uri)) return false;
120✔
188
  const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/]*)/);
116✔
189
  if (match) {
116✔
190
    const authority = match[2];
76✔
191
    const atIndex = authority.indexOf('@');
76✔
192
    if (atIndex > 0) {
76✔
193
      const userinfo = authority.substring(0, atIndex);
8✔
194
      if (userinfo.includes('[') || userinfo.includes(']')) {
8✔
195
        return false;
4✔
196
      }
197
    }
198
  }
199
  return /^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/.test(uri);
112✔
200
};
201

202
export interface FormatValidatorsOptions {
203
  strictUris?: boolean;
204
  customFormats?: Record<string, FormatValidatorFn | null>;
205
}
206

207
const inbuiltValidators: Record<string, FormatValidatorFn> = {
73✔
208
  date: dateValidator,
209
  'date-time': dateTimeValidator,
210
  email: emailValidator,
211
  hostname: hostnameValidator,
212
  'host-name': hostnameValidator,
213
  ipv4: ipv4Validator,
214
  ipv6: ipv6Validator,
215
  regex: regexValidator,
216
  uri: uriValidator,
217
  'strict-uri': strictUriValidator,
218
} as const;
219

220
const customValidators: Record<string, FormatValidatorFn> = {};
73✔
221

222
export function getFormatValidators(options?: FormatValidatorsOptions): Record<string, FormatValidatorFn> {
223
  return {
1,250✔
224
    ...inbuiltValidators,
225
    ...(options?.strictUris ? { uri: strictUriValidator } : {}),
226
    ...customValidators,
227
    ...(options?.customFormats || {}),
228
  };
229
}
230

231
export function registerFormat(name: string, validatorFunction: FormatValidatorFn) {
232
  customValidators[name] = validatorFunction;
116✔
233
}
234

235
export function unregisterFormat(name: string) {
236
  delete customValidators[name];
×
237
}
238

239
export function getSupportedFormats(customFormats?: Record<string, FormatValidatorFn | null>) {
240
  const merged = {
5,113✔
241
    ...inbuiltValidators,
242
    ...customValidators,
243
    ...customFormats,
244
  };
245
  const keys = sortedKeys(merged);
5,113✔
246
  return keys.filter((key) => merged[key] != null);
55,946✔
247
}
248

249
export function isFormatSupported(name: string, customFormats?: Record<string, FormatValidatorFn | null>): boolean {
250
  const supported = getSupportedFormats(customFormats);
5,101✔
251
  return supported.includes(name);
5,101✔
252
}
253

254
export function getRegisteredFormats() {
255
  return sortedKeys(customValidators);
×
256
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc