string.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.parseStringDef = exports.zodPatterns = void 0;
  4. const errorMessages_js_1 = require("../errorMessages.js");
  5. let emojiRegex = undefined;
  6. /**
  7. * Generated from the regular expressions found here as of 2024-05-22:
  8. * https://github.com/colinhacks/zod/blob/master/src/types.ts.
  9. *
  10. * Expressions with /i flag have been changed accordingly.
  11. */
  12. exports.zodPatterns = {
  13. /**
  14. * `c` was changed to `[cC]` to replicate /i flag
  15. */
  16. cuid: /^[cC][^\s-]{8,}$/,
  17. cuid2: /^[0-9a-z]+$/,
  18. ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/,
  19. /**
  20. * `a-z` was added to replicate /i flag
  21. */
  22. email: /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-\.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/,
  23. /**
  24. * Constructed a valid Unicode RegExp
  25. *
  26. * Lazily instantiate since this type of regex isn't supported
  27. * in all envs (e.g. React Native).
  28. *
  29. * See:
  30. * https://github.com/colinhacks/zod/issues/2433
  31. * Fix in Zod:
  32. * https://github.com/colinhacks/zod/commit/9340fd51e48576a75adc919bff65dbc4a5d4c99b
  33. */
  34. emoji: () => {
  35. if (emojiRegex === undefined) {
  36. emojiRegex = RegExp("^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$", "u");
  37. }
  38. return emojiRegex;
  39. },
  40. /**
  41. * Unused
  42. */
  43. uuid: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
  44. /**
  45. * Unused
  46. */
  47. ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,
  48. ipv4Cidr: /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/,
  49. /**
  50. * Unused
  51. */
  52. ipv6: /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/,
  53. ipv6Cidr: /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,
  54. base64: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,
  55. base64url: /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/,
  56. nanoid: /^[a-zA-Z0-9_-]{21}$/,
  57. jwt: /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/,
  58. };
  59. function parseStringDef(def, refs) {
  60. const res = {
  61. type: "string",
  62. };
  63. if (def.checks) {
  64. for (const check of def.checks) {
  65. switch (check.kind) {
  66. case "min":
  67. (0, errorMessages_js_1.setResponseValueAndErrors)(res, "minLength", typeof res.minLength === "number"
  68. ? Math.max(res.minLength, check.value)
  69. : check.value, check.message, refs);
  70. break;
  71. case "max":
  72. (0, errorMessages_js_1.setResponseValueAndErrors)(res, "maxLength", typeof res.maxLength === "number"
  73. ? Math.min(res.maxLength, check.value)
  74. : check.value, check.message, refs);
  75. break;
  76. case "email":
  77. switch (refs.emailStrategy) {
  78. case "format:email":
  79. addFormat(res, "email", check.message, refs);
  80. break;
  81. case "format:idn-email":
  82. addFormat(res, "idn-email", check.message, refs);
  83. break;
  84. case "pattern:zod":
  85. addPattern(res, exports.zodPatterns.email, check.message, refs);
  86. break;
  87. }
  88. break;
  89. case "url":
  90. addFormat(res, "uri", check.message, refs);
  91. break;
  92. case "uuid":
  93. addFormat(res, "uuid", check.message, refs);
  94. break;
  95. case "regex":
  96. addPattern(res, check.regex, check.message, refs);
  97. break;
  98. case "cuid":
  99. addPattern(res, exports.zodPatterns.cuid, check.message, refs);
  100. break;
  101. case "cuid2":
  102. addPattern(res, exports.zodPatterns.cuid2, check.message, refs);
  103. break;
  104. case "startsWith":
  105. addPattern(res, RegExp(`^${escapeLiteralCheckValue(check.value, refs)}`), check.message, refs);
  106. break;
  107. case "endsWith":
  108. addPattern(res, RegExp(`${escapeLiteralCheckValue(check.value, refs)}$`), check.message, refs);
  109. break;
  110. case "datetime":
  111. addFormat(res, "date-time", check.message, refs);
  112. break;
  113. case "date":
  114. addFormat(res, "date", check.message, refs);
  115. break;
  116. case "time":
  117. addFormat(res, "time", check.message, refs);
  118. break;
  119. case "duration":
  120. addFormat(res, "duration", check.message, refs);
  121. break;
  122. case "length":
  123. (0, errorMessages_js_1.setResponseValueAndErrors)(res, "minLength", typeof res.minLength === "number"
  124. ? Math.max(res.minLength, check.value)
  125. : check.value, check.message, refs);
  126. (0, errorMessages_js_1.setResponseValueAndErrors)(res, "maxLength", typeof res.maxLength === "number"
  127. ? Math.min(res.maxLength, check.value)
  128. : check.value, check.message, refs);
  129. break;
  130. case "includes": {
  131. addPattern(res, RegExp(escapeLiteralCheckValue(check.value, refs)), check.message, refs);
  132. break;
  133. }
  134. case "ip": {
  135. if (check.version !== "v6") {
  136. addFormat(res, "ipv4", check.message, refs);
  137. }
  138. if (check.version !== "v4") {
  139. addFormat(res, "ipv6", check.message, refs);
  140. }
  141. break;
  142. }
  143. case "base64url":
  144. addPattern(res, exports.zodPatterns.base64url, check.message, refs);
  145. break;
  146. case "jwt":
  147. addPattern(res, exports.zodPatterns.jwt, check.message, refs);
  148. break;
  149. case "cidr": {
  150. if (check.version !== "v6") {
  151. addPattern(res, exports.zodPatterns.ipv4Cidr, check.message, refs);
  152. }
  153. if (check.version !== "v4") {
  154. addPattern(res, exports.zodPatterns.ipv6Cidr, check.message, refs);
  155. }
  156. break;
  157. }
  158. case "emoji":
  159. addPattern(res, exports.zodPatterns.emoji(), check.message, refs);
  160. break;
  161. case "ulid": {
  162. addPattern(res, exports.zodPatterns.ulid, check.message, refs);
  163. break;
  164. }
  165. case "base64": {
  166. switch (refs.base64Strategy) {
  167. case "format:binary": {
  168. addFormat(res, "binary", check.message, refs);
  169. break;
  170. }
  171. case "contentEncoding:base64": {
  172. (0, errorMessages_js_1.setResponseValueAndErrors)(res, "contentEncoding", "base64", check.message, refs);
  173. break;
  174. }
  175. case "pattern:zod": {
  176. addPattern(res, exports.zodPatterns.base64, check.message, refs);
  177. break;
  178. }
  179. }
  180. break;
  181. }
  182. case "nanoid": {
  183. addPattern(res, exports.zodPatterns.nanoid, check.message, refs);
  184. }
  185. case "toLowerCase":
  186. case "toUpperCase":
  187. case "trim":
  188. break;
  189. default:
  190. /* c8 ignore next */
  191. ((_) => { })(check);
  192. }
  193. }
  194. }
  195. return res;
  196. }
  197. exports.parseStringDef = parseStringDef;
  198. function escapeLiteralCheckValue(literal, refs) {
  199. return refs.patternStrategy === "escape"
  200. ? escapeNonAlphaNumeric(literal)
  201. : literal;
  202. }
  203. const ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
  204. function escapeNonAlphaNumeric(source) {
  205. let result = "";
  206. for (let i = 0; i < source.length; i++) {
  207. if (!ALPHA_NUMERIC.has(source[i])) {
  208. result += "\\";
  209. }
  210. result += source[i];
  211. }
  212. return result;
  213. }
  214. // Adds a "format" keyword to the schema. If a format exists, both formats will be joined in an allOf-node, along with subsequent ones.
  215. function addFormat(schema, value, message, refs) {
  216. if (schema.format || schema.anyOf?.some((x) => x.format)) {
  217. if (!schema.anyOf) {
  218. schema.anyOf = [];
  219. }
  220. if (schema.format) {
  221. schema.anyOf.push({
  222. format: schema.format,
  223. ...(schema.errorMessage &&
  224. refs.errorMessages && {
  225. errorMessage: { format: schema.errorMessage.format },
  226. }),
  227. });
  228. delete schema.format;
  229. if (schema.errorMessage) {
  230. delete schema.errorMessage.format;
  231. if (Object.keys(schema.errorMessage).length === 0) {
  232. delete schema.errorMessage;
  233. }
  234. }
  235. }
  236. schema.anyOf.push({
  237. format: value,
  238. ...(message &&
  239. refs.errorMessages && { errorMessage: { format: message } }),
  240. });
  241. }
  242. else {
  243. (0, errorMessages_js_1.setResponseValueAndErrors)(schema, "format", value, message, refs);
  244. }
  245. }
  246. // Adds a "pattern" keyword to the schema. If a pattern exists, both patterns will be joined in an allOf-node, along with subsequent ones.
  247. function addPattern(schema, regex, message, refs) {
  248. if (schema.pattern || schema.allOf?.some((x) => x.pattern)) {
  249. if (!schema.allOf) {
  250. schema.allOf = [];
  251. }
  252. if (schema.pattern) {
  253. schema.allOf.push({
  254. pattern: schema.pattern,
  255. ...(schema.errorMessage &&
  256. refs.errorMessages && {
  257. errorMessage: { pattern: schema.errorMessage.pattern },
  258. }),
  259. });
  260. delete schema.pattern;
  261. if (schema.errorMessage) {
  262. delete schema.errorMessage.pattern;
  263. if (Object.keys(schema.errorMessage).length === 0) {
  264. delete schema.errorMessage;
  265. }
  266. }
  267. }
  268. schema.allOf.push({
  269. pattern: stringifyRegExpWithFlags(regex, refs),
  270. ...(message &&
  271. refs.errorMessages && { errorMessage: { pattern: message } }),
  272. });
  273. }
  274. else {
  275. (0, errorMessages_js_1.setResponseValueAndErrors)(schema, "pattern", stringifyRegExpWithFlags(regex, refs), message, refs);
  276. }
  277. }
  278. // Mutate z.string.regex() in a best attempt to accommodate for regex flags when applyRegexFlags is true
  279. function stringifyRegExpWithFlags(regex, refs) {
  280. if (!refs.applyRegexFlags || !regex.flags) {
  281. return regex.source;
  282. }
  283. // Currently handled flags
  284. const flags = {
  285. i: regex.flags.includes("i"),
  286. m: regex.flags.includes("m"),
  287. s: regex.flags.includes("s"), // `.` matches newlines
  288. };
  289. // The general principle here is to step through each character, one at a time, applying mutations as flags require. We keep track when the current character is escaped, and when it's inside a group /like [this]/ or (also) a range like /[a-z]/. The following is fairly brittle imperative code; edit at your peril!
  290. const source = flags.i ? regex.source.toLowerCase() : regex.source;
  291. let pattern = "";
  292. let isEscaped = false;
  293. let inCharGroup = false;
  294. let inCharRange = false;
  295. for (let i = 0; i < source.length; i++) {
  296. if (isEscaped) {
  297. pattern += source[i];
  298. isEscaped = false;
  299. continue;
  300. }
  301. if (flags.i) {
  302. if (inCharGroup) {
  303. if (source[i].match(/[a-z]/)) {
  304. if (inCharRange) {
  305. pattern += source[i];
  306. pattern += `${source[i - 2]}-${source[i]}`.toUpperCase();
  307. inCharRange = false;
  308. }
  309. else if (source[i + 1] === "-" && source[i + 2]?.match(/[a-z]/)) {
  310. pattern += source[i];
  311. inCharRange = true;
  312. }
  313. else {
  314. pattern += `${source[i]}${source[i].toUpperCase()}`;
  315. }
  316. continue;
  317. }
  318. }
  319. else if (source[i].match(/[a-z]/)) {
  320. pattern += `[${source[i]}${source[i].toUpperCase()}]`;
  321. continue;
  322. }
  323. }
  324. if (flags.m) {
  325. if (source[i] === "^") {
  326. pattern += `(^|(?<=[\r\n]))`;
  327. continue;
  328. }
  329. else if (source[i] === "$") {
  330. pattern += `($|(?=[\r\n]))`;
  331. continue;
  332. }
  333. }
  334. if (flags.s && source[i] === ".") {
  335. pattern += inCharGroup ? `${source[i]}\r\n` : `[${source[i]}\r\n]`;
  336. continue;
  337. }
  338. pattern += source[i];
  339. if (source[i] === "\\") {
  340. isEscaped = true;
  341. }
  342. else if (inCharGroup && source[i] === "]") {
  343. inCharGroup = false;
  344. }
  345. else if (!inCharGroup && source[i] === "[") {
  346. inCharGroup = true;
  347. }
  348. }
  349. try {
  350. new RegExp(pattern);
  351. }
  352. catch {
  353. console.warn(`Could not convert regex pattern at ${refs.currentPath.join("/")} to a flag-independent form! Falling back to the flag-ignorant source`);
  354. return regex.source;
  355. }
  356. return pattern;
  357. }