@alextheman/utility 5.13.0 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -180,7 +180,7 @@ var CodeError = class CodeError extends Error {
180
180
  *
181
181
  * @template DataType - The type of the data that caused the error.
182
182
  */
183
- var DataError = class DataError extends CodeError {
183
+ var DataError$1 = class DataError$1 extends CodeError {
184
184
  data;
185
185
  /**
186
186
  * @param data - The data that caused the error.
@@ -205,7 +205,7 @@ var DataError = class DataError extends CodeError {
205
205
  * @returns `true` if the input is a DataError, and `false` otherwise. The type of the input will also be narrowed down to DataError if `true`.
206
206
  */
207
207
  static check(input) {
208
- if (input instanceof DataError) return true;
208
+ if (input instanceof DataError$1) return true;
209
209
  return typeof input === "object" && input !== null && "message" in input && typeof input.message === "string" && "code" in input && typeof input.code === "string" && "data" in input;
210
210
  }
211
211
  /**
@@ -254,18 +254,18 @@ var DataError = class DataError extends CodeError {
254
254
  function parseIntStrict(string, radix) {
255
255
  const trimmedString = string.trim();
256
256
  const maxAllowedAlphabeticalCharacter = radix && radix > 10 && radix <= 36 ? String.fromCharCode(87 + radix - 1) : void 0;
257
- if (!(radix && radix > 10 && radix <= 36 ? new RegExp(`^[+-]?[0-9a-${maxAllowedAlphabeticalCharacter}]+$`, "i") : /^[+-]?\d+$/).test(trimmedString)) throw new DataError(radix ? {
257
+ if (!(radix && radix > 10 && radix <= 36 ? new RegExp(`^[+-]?[0-9a-${maxAllowedAlphabeticalCharacter}]+$`, "i") : /^[+-]?\d+$/).test(trimmedString)) throw new DataError$1(radix ? {
258
258
  string,
259
259
  radix
260
260
  } : { string }, "INTEGER_PARSING_ERROR", `Only numeric values${radix && radix > 10 && radix <= 36 ? ` or character${radix !== 11 ? "s" : ""} A${radix !== 11 ? `-${maxAllowedAlphabeticalCharacter?.toUpperCase()} ` : " "}` : " "}are allowed.`);
261
261
  if (radix && radix < 10 && [...trimmedString.replace(/^[+-]/, "")].some((character) => {
262
262
  return parseInt(character) >= radix;
263
- })) throw new DataError({
263
+ })) throw new DataError$1({
264
264
  string,
265
265
  radix
266
266
  }, "INTEGER_PARSING_ERROR", "Value contains one or more digits outside of the range of the given radix.");
267
267
  const parseIntResult = parseInt(trimmedString, radix);
268
- if (isNaN(parseIntResult)) throw new DataError({ string }, "INTEGER_PARSING_ERROR", "Value is not a valid integer.");
268
+ if (isNaN(parseIntResult)) throw new DataError$1({ string }, "INTEGER_PARSING_ERROR", "Value is not a valid integer.");
269
269
  return parseIntResult;
270
270
  }
271
271
  //#endregion
@@ -328,16 +328,16 @@ function randomiseArray(array) {
328
328
  */
329
329
  function range(start, stop, step = 1) {
330
330
  const numbers = [];
331
- if (step === 0) throw new DataError({ step }, "ZERO_STEP_SIZE", "Step size cannot be zero.");
331
+ if (step === 0) throw new DataError$1({ step }, "ZERO_STEP_SIZE", "Step size cannot be zero.");
332
332
  else if (step > 0) {
333
- if (start > stop) throw new DataError({
333
+ if (start > stop) throw new DataError$1({
334
334
  start,
335
335
  stop,
336
336
  step
337
337
  }, "INVALID_BOUNDARIES", "The starting value cannot be bigger than the final value if step is positive");
338
338
  for (let i = start; i < stop; i += step) numbers.push(i);
339
339
  } else if (step < 0) {
340
- if (start < stop) throw new DataError({
340
+ if (start < stop) throw new DataError$1({
341
341
  start,
342
342
  stop,
343
343
  step
@@ -537,7 +537,7 @@ function convertFileToBase64(file) {
537
537
  reader.readAsDataURL(file);
538
538
  reader.onload = () => {
539
539
  if (reader.result === null) {
540
- reject(new DataError({ result: reader.result }, "FILE_CONVERSION_ERROR", "Could not convert the given file."));
540
+ reject(new DataError$1({ result: reader.result }, "FILE_CONVERSION_ERROR", "Could not convert the given file."));
541
541
  return;
542
542
  }
543
543
  resolve(reader.result);
@@ -604,10 +604,10 @@ function createFormData(data, options = {
604
604
  if (Array.isArray(value)) {
605
605
  if (value.some((item) => {
606
606
  return item instanceof Blob;
607
- }) && (options.arrayResolution === "stringify" || typeof options.arrayResolution === "object" && options.arrayResolution[key] === "stringify")) throw new DataError({ value }, "CANNOT_STRINGIFY_BLOB", "Files/blobs cannot be stringified. Please change the resolution option for this item to \"multiple\" instead.");
607
+ }) && (options.arrayResolution === "stringify" || typeof options.arrayResolution === "object" && options.arrayResolution[key] === "stringify")) throw new DataError$1({ value }, "CANNOT_STRINGIFY_BLOB", "Files/blobs cannot be stringified. Please change the resolution option for this item to \"multiple\" instead.");
608
608
  if (options.arrayResolution === "multiple" || typeof options.arrayResolution === "object" && options.arrayResolution[key] === "multiple") {
609
609
  for (const item of value) {
610
- if (!isPrimitive(item) && !(item instanceof Blob)) throw new DataError({ item }, "NON_PRIMITIVE_ARRAY_ITEMS_FOUND", "Cannot directly add non-primitive data to FormData. Please change the resolution option for this item to \"stringify\" instead.");
610
+ if (!isPrimitive(item) && !(item instanceof Blob)) throw new DataError$1({ item }, "NON_PRIMITIVE_ARRAY_ITEMS_FOUND", "Cannot directly add non-primitive data to FormData. Please change the resolution option for this item to \"stringify\" instead.");
611
611
  if (item instanceof Blob) formData.append(String(key), item);
612
612
  else formData.append(String(key), String(item));
613
613
  }
@@ -763,8 +763,8 @@ function stringifyDotenv(contents, options) {
763
763
  const { quoteStyle = "double" } = options ?? {};
764
764
  let result = "";
765
765
  for (const key in contentsCopy) {
766
- if (/[ \t\r\n]/.test(key)) throw new DataError({ [key]: contentsCopy[key] }, "INVALID_KEY", "Environment variables are not allowed to have whitespace.");
767
- if (quoteStyle === "none") if (/[ \t\r\n]/.test(contentsCopy[key]) || contentsCopy[key].includes("#") || contentsCopy[key].includes("=") || contentsCopy[key].includes("\\")) throw new DataError({ [key]: contentsCopy[key] }, "INCOMPATIBLE_QUOTE_STYLE", "Cannot use `{ quoteStyle: \"none\" }` when value has whitespace, #, =, or \\");
766
+ if (/[ \t\r\n]/.test(key)) throw new DataError$1({ [key]: contentsCopy[key] }, "INVALID_KEY", "Environment variables are not allowed to have whitespace.");
767
+ if (quoteStyle === "none") if (/[ \t\r\n]/.test(contentsCopy[key]) || contentsCopy[key].includes("#") || contentsCopy[key].includes("=") || contentsCopy[key].includes("\\")) throw new DataError$1({ [key]: contentsCopy[key] }, "INCOMPATIBLE_QUOTE_STYLE", "Cannot use `{ quoteStyle: \"none\" }` when value has whitespace, #, =, or \\");
768
768
  else {
769
769
  result += `${key}=${contentsCopy[key]}\n`;
770
770
  continue;
@@ -889,11 +889,221 @@ function removeUndefinedFromObject(object) {
889
889
  */
890
890
  function parseBoolean(inputString) {
891
891
  const normalisedString = inputString.toLowerCase();
892
- if (!["true", "false"].includes(normalisedString)) throw new DataError({ inputString }, "INVALID_BOOLEAN_STRING", "The provided boolean string must be one of `true | false`");
892
+ if (!["true", "false"].includes(normalisedString)) throw new DataError$1({ inputString }, "INVALID_BOOLEAN_STRING", "The provided boolean string must be one of `true | false`");
893
893
  return normalisedString === "true";
894
894
  }
895
895
  //#endregion
896
- //#region src/root/functions/parsers/zod/_parseZodSchema.ts
896
+ //#region src/root/types/APIError.ts
897
+ const httpErrorCodeLookup = {
898
+ 400: "BAD_REQUEST",
899
+ 401: "UNAUTHORISED",
900
+ 403: "FORBIDDEN",
901
+ 404: "NOT_FOUND",
902
+ 418: "I_AM_A_TEAPOT",
903
+ 500: "INTERNAL_SERVER_ERROR"
904
+ };
905
+ /**
906
+ * Represents common errors you may get from a HTTP API request.
907
+ *
908
+ * @category Types
909
+ */
910
+ var APIError = class APIError extends Error {
911
+ status;
912
+ /**
913
+ * @param status - A HTTP status code. Can be any number, but numbers between 400 and 600 are encouraged to fit with HTTP status code conventions.
914
+ * @param message - An error message to display alongside the status code.
915
+ * @param options - Extra options to be passed to super Error constructor.
916
+ */
917
+ constructor(status = 500, message, options) {
918
+ super(message, options);
919
+ this.status = status;
920
+ if (message) this.message = message;
921
+ else this.message = httpErrorCodeLookup[this.status] ?? "API_ERROR";
922
+ Object.defineProperty(this, "message", { enumerable: true });
923
+ Object.setPrototypeOf(this, new.target.prototype);
924
+ }
925
+ /**
926
+ * Checks whether the given input may have been caused by an APIError.
927
+ *
928
+ * @param input - The input to check.
929
+ *
930
+ * @returns `true` if the input is an APIError, and `false` otherwise. The type of the input will also be narrowed down to APIError if `true`.
931
+ */
932
+ static check(input) {
933
+ if (input instanceof APIError) return true;
934
+ const data = input;
935
+ return typeof data === "object" && data !== null && typeof data?.status === "number" && typeof data?.message === "string";
936
+ }
937
+ };
938
+ //#endregion
939
+ //#region src/root/types/VersionNumber.ts
940
+ /**
941
+ * Represents a software version number, considered to be made up of a major, minor, and patch part.
942
+ *
943
+ * @category Types
944
+ */
945
+ var VersionNumber = class VersionNumber {
946
+ static NON_NEGATIVE_TUPLE_ERROR = "Input array must be a tuple of three non-negative integers.";
947
+ /** The major number. Increments when a feature is removed or changed in a way that is not backwards-compatible with the previous release. */
948
+ major = 0;
949
+ /** The minor number. Increments when a new feature is added/deprecated and is expected to be backwards-compatible with the previous release. */
950
+ minor = 0;
951
+ /** The patch number. Increments when the next release is fixing a bug or doing a small refactor that should not be noticeable in practice. */
952
+ patch = 0;
953
+ /**
954
+ * @param input - The input to create a new instance of `VersionNumber` from.
955
+ */
956
+ constructor(input) {
957
+ if (input instanceof VersionNumber) {
958
+ this.major = input.major;
959
+ this.minor = input.minor;
960
+ this.patch = input.patch;
961
+ } else if (typeof input === "string") {
962
+ if (!VERSION_NUMBER_REGEX.test(input)) throw new DataError$1({ input }, "INVALID_VERSION", `"${input}" is not a valid version number. Version numbers must be of the format "X.Y.Z" or "vX.Y.Z", where X, Y, and Z are non-negative integers.`);
963
+ const [major, minor, patch] = VersionNumber.formatString(input, { omitPrefix: true }).split(".").map((number) => {
964
+ return parseIntStrict(number);
965
+ });
966
+ this.major = major;
967
+ this.minor = minor;
968
+ this.patch = patch;
969
+ } else if (Array.isArray(input)) {
970
+ if (input.length !== 3) throw new DataError$1({ input }, "INVALID_LENGTH", VersionNumber.NON_NEGATIVE_TUPLE_ERROR);
971
+ const [major, minor, patch] = input.map((number) => {
972
+ const parsedInteger = parseIntStrict(number?.toString());
973
+ if (parsedInteger < 0) throw new DataError$1({ input }, "NEGATIVE_INPUTS", VersionNumber.NON_NEGATIVE_TUPLE_ERROR);
974
+ return parsedInteger;
975
+ });
976
+ this.major = major;
977
+ this.minor = minor;
978
+ this.patch = patch;
979
+ } else throw new DataError$1({ input }, "INVALID_INPUT", normaliseIndents`
980
+ The provided input can not be parsed into a valid version number.
981
+ Expected either a string of format X.Y.Z or vX.Y.Z, a tuple of three numbers, or another \`VersionNumber\` instance.
982
+ `);
983
+ }
984
+ /**
985
+ * Gets the current version type of the current instance of `VersionNumber`.
986
+ *
987
+ * @returns Either `"major"`, `"minor"`, or `"patch"`, depending on the version type.
988
+ */
989
+ get type() {
990
+ if (this.minor === 0 && this.patch === 0) return VersionType.MAJOR;
991
+ if (this.patch === 0) return VersionType.MINOR;
992
+ return VersionType.PATCH;
993
+ }
994
+ static formatString(input, options) {
995
+ if (options?.omitPrefix) return input.startsWith("v") ? input.slice(1) : input;
996
+ return input.startsWith("v") ? input : `v${input}`;
997
+ }
998
+ /**
999
+ * Checks if the provided version numbers have the exact same major, minor, and patch numbers.
1000
+ *
1001
+ * @param firstVersion - The first version number to compare.
1002
+ * @param secondVersion - The second version number to compare.
1003
+ *
1004
+ * @returns `true` if the provided version numbers have exactly the same major, minor, and patch numbers, and returns `false` otherwise.
1005
+ */
1006
+ static isEqual(firstVersion, secondVersion) {
1007
+ return firstVersion.major === secondVersion.major && firstVersion.minor === secondVersion.minor && firstVersion.patch === secondVersion.patch;
1008
+ }
1009
+ /**
1010
+ * Get a formatted string representation of the current version number
1011
+ *
1012
+ * @param options - Options to apply to the string formatting.
1013
+ *
1014
+ * @returns A formatted string representation of the current version number with the options applied.
1015
+ */
1016
+ format(options) {
1017
+ let baseOutput = `${this.major}`;
1018
+ if (!options?.omitMinor) {
1019
+ baseOutput += `.${this.minor}`;
1020
+ if (!options?.omitPatch) baseOutput += `.${this.patch}`;
1021
+ }
1022
+ return VersionNumber.formatString(baseOutput, { omitPrefix: options?.omitPrefix });
1023
+ }
1024
+ /**
1025
+ * Increments the current version number by the given increment type, returning the result as a new reference in memory.
1026
+ *
1027
+ * @param incrementType - The type of increment. Can be one of the following:
1028
+ * - `"major"`: Change the major version `v1.2.3` → `v2.0.0`
1029
+ * - `"minor"`: Change the minor version `v1.2.3` → `v1.3.0`
1030
+ * - `"patch"`: Change the patch version `v1.2.3` → `v1.2.4`
1031
+ * @param incrementAmount - The amount to increment by (defaults to 1).
1032
+ *
1033
+ * @returns A new instance of `VersionNumber` with the increment applied.
1034
+ */
1035
+ increment(incrementType, incrementAmount = 1) {
1036
+ const incrementBy = parseIntStrict(String(incrementAmount));
1037
+ const calculatedRawVersion = {
1038
+ major: [
1039
+ this.major + incrementBy,
1040
+ 0,
1041
+ 0
1042
+ ],
1043
+ minor: [
1044
+ this.major,
1045
+ this.minor + incrementBy,
1046
+ 0
1047
+ ],
1048
+ patch: [
1049
+ this.major,
1050
+ this.minor,
1051
+ this.patch + incrementBy
1052
+ ]
1053
+ }[incrementType];
1054
+ try {
1055
+ return new VersionNumber(calculatedRawVersion);
1056
+ } catch (error) {
1057
+ if (DataError$1.check(error) && error.code === "NEGATIVE_INPUTS") throw new DataError$1({
1058
+ currentVersion: this.toString(),
1059
+ calculatedRawVersion: `v${calculatedRawVersion.join(".")}`,
1060
+ incrementAmount
1061
+ }, "NEGATIVE_VERSION", "Cannot apply this increment amount as it would lead to a negative version number.");
1062
+ else throw error;
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Ensures that the VersionNumber behaves correctly when attempted to be coerced to a string.
1067
+ *
1068
+ * @param hint - Not used as of now, but generally used to help with numeric coercion, I think (which we most likely do not need for version numbers).
1069
+ *
1070
+ * @returns A stringified representation of the current version number, prefixed with `v`.
1071
+ */
1072
+ [Symbol.toPrimitive](hint) {
1073
+ if (hint === "number") throw new DataError$1({ thisVersion: this.toString() }, "INVALID_COERCION", "VersionNumber cannot be coerced to a number type.");
1074
+ return this.toString();
1075
+ }
1076
+ /**
1077
+ * Ensures that the VersionNumber behaves correctly when attempted to be converted to JSON.
1078
+ *
1079
+ * @returns A stringified representation of the current version number, prefixed with `v`.
1080
+ */
1081
+ toJSON() {
1082
+ return this.toString();
1083
+ }
1084
+ /**
1085
+ * Get a string representation of the current version number.
1086
+ *
1087
+ * @returns A stringified representation of the current version number with the prefix.
1088
+ */
1089
+ toString() {
1090
+ const rawString = `${this.major}.${this.minor}.${this.patch}`;
1091
+ return VersionNumber.formatString(rawString, { omitPrefix: false });
1092
+ }
1093
+ };
1094
+ const zodVersionNumber = zod.default.union([
1095
+ zod.default.string(),
1096
+ zod.default.tuple([
1097
+ zod.default.number(),
1098
+ zod.default.number(),
1099
+ zod.default.number()
1100
+ ]),
1101
+ zod.default.instanceof(VersionNumber)
1102
+ ]).transform((rawVersionNumber) => {
1103
+ return new VersionNumber(rawVersionNumber);
1104
+ });
1105
+ //#endregion
1106
+ //#region src/root/zod/_parseZodSchema.ts
897
1107
  function _parseZodSchema(parsedResult, input, onError) {
898
1108
  if (!parsedResult.success) {
899
1109
  if (onError) {
@@ -908,7 +1118,7 @@ function _parseZodSchema(parsedResult, input, onError) {
908
1118
  const code = issue.code.toUpperCase();
909
1119
  allErrorCodes[code] = (allErrorCodes[code] ?? 0) + 1;
910
1120
  }
911
- throw new DataError({ input }, Object.entries(allErrorCodes).toSorted(([_, firstCount], [__, secondCount]) => {
1121
+ throw new DataError$1({ input }, Object.entries(allErrorCodes).toSorted(([_, firstCount], [__, secondCount]) => {
912
1122
  return secondCount - firstCount;
913
1123
  }).map(([code, count], _, allErrorCodes) => {
914
1124
  return allErrorCodes.length === 1 && count === 1 ? code : `${code}×${count}`;
@@ -917,7 +1127,7 @@ function _parseZodSchema(parsedResult, input, onError) {
917
1127
  return parsedResult.data;
918
1128
  }
919
1129
  //#endregion
920
- //#region src/root/functions/parsers/zod/parseZodSchema.ts
1130
+ //#region src/root/zod/parseZodSchema.ts
921
1131
  /**
922
1132
  * An alternative function to zodSchema.parse() that can be used to strictly parse Zod schemas.
923
1133
  *
@@ -925,6 +1135,8 @@ function _parseZodSchema(parsedResult, input, onError) {
925
1135
  *
926
1136
  * @category Parsers
927
1137
  *
1138
+ * @deprecated Please use `az.with(schema).parse(input)` instead.
1139
+ *
928
1140
  * @template SchemaType - The Zod schema type.
929
1141
  * @template ErrorType - The type of error to throw on invalid data.
930
1142
  *
@@ -940,6 +1152,60 @@ function parseZodSchema(schema, input, onError) {
940
1152
  return _parseZodSchema(schema.safeParse(input), input, onError);
941
1153
  }
942
1154
  //#endregion
1155
+ //#region src/root/zod/parseZodSchemaAsync.ts
1156
+ /**
1157
+ * An alternative function to zodSchema.parseAsync() that can be used to strictly parse asynchronous Zod schemas.
1158
+ *
1159
+ * @category Parsers
1160
+ *
1161
+ * @deprecated Please use `az.with(schema).parseAsync(input)` instead.
1162
+ *
1163
+ * @template SchemaType - The Zod schema type.
1164
+ * @template ErrorType - The type of error to throw on invalid data.
1165
+ *
1166
+ * @param schema - The Zod schema to use in parsing.
1167
+ * @param input - The data to parse.
1168
+ * @param onError - A custom error to throw on invalid data (defaults to `DataError`). May either be the error itself, or a function that returns the error or nothing. If nothing is returned, the default error is thrown instead.
1169
+ *
1170
+ * @throws {DataError} If the given data cannot be parsed according to the schema.
1171
+ *
1172
+ * @returns The parsed data from the Zod schema.
1173
+ */
1174
+ async function parseZodSchemaAsync(schema, input, onError) {
1175
+ return _parseZodSchema(await schema.safeParseAsync(input), input, onError);
1176
+ }
1177
+ //#endregion
1178
+ //#region src/root/zod/zodFieldWrapper.ts
1179
+ function zodFieldWrapper(schema) {
1180
+ return zod.default.string().trim().transform((value) => {
1181
+ return value === "" ? null : value;
1182
+ }).pipe(schema);
1183
+ }
1184
+ //#endregion
1185
+ //#region src/root/zod/az.ts
1186
+ const az = {
1187
+ field: zodFieldWrapper,
1188
+ fieldNumber: () => {
1189
+ return zod.default.coerce.number();
1190
+ },
1191
+ versionNumber: () => {
1192
+ return zodVersionNumber;
1193
+ },
1194
+ fieldDate: () => {
1195
+ return zod.default.coerce.date();
1196
+ },
1197
+ with: (schema) => {
1198
+ return {
1199
+ parse: (input, error) => {
1200
+ return parseZodSchema(schema, input, error);
1201
+ },
1202
+ parseAsync: async (input, error) => {
1203
+ return await parseZodSchemaAsync(schema, input, error);
1204
+ }
1205
+ };
1206
+ }
1207
+ };
1208
+ //#endregion
943
1209
  //#region src/root/functions/parsers/parseEnv.ts
944
1210
  /**
945
1211
  * Represents the three common development environments.
@@ -963,7 +1229,7 @@ const Env = {
963
1229
  * @returns The specified environment if allowed.
964
1230
  */
965
1231
  function parseEnv(input) {
966
- return parseZodSchema(zod.z.enum(Env), input, new DataError({ input }, "INVALID_ENV", "The provided environment type must be one of `test | development | production`"));
1232
+ return az.with(zod.z.enum(Env)).parse(input, new DataError$1({ input }, "INVALID_ENV", "The provided environment type must be one of `test | development | production`"));
967
1233
  }
968
1234
  //#endregion
969
1235
  //#region src/root/functions/parsers/parseFormData.ts
@@ -1001,8 +1267,8 @@ function parseFormData(formData, dataParser) {
1001
1267
  * @returns The UUID again if successful.
1002
1268
  */
1003
1269
  function parseUUID(input) {
1004
- if (!(typeof input === "string")) throw new DataError({ input }, "INVALID_TYPE", "Invalid type - expected string.");
1005
- if (!UUID_REGEX.test(input)) throw new DataError({ input }, "INVALID_UUID", "The provided input does not match the expected shape for a UUID.");
1270
+ if (!(typeof input === "string")) throw new DataError$1({ input }, "INVALID_TYPE", "Invalid type - expected string.");
1271
+ if (!UUID_REGEX.test(input)) throw new DataError$1({ input }, "INVALID_UUID", "The provided input does not match the expected shape for a UUID.");
1006
1272
  return input;
1007
1273
  }
1008
1274
  //#endregion
@@ -1029,28 +1295,7 @@ const VersionType = {
1029
1295
  * @returns The given version type if allowed.
1030
1296
  */
1031
1297
  function parseVersionType(input) {
1032
- return parseZodSchema(zod.default.enum(VersionType), input, new DataError({ input }, "INVALID_VERSION_TYPE", "The provided version type must be one of `major | minor | patch`"));
1033
- }
1034
- //#endregion
1035
- //#region src/root/functions/parsers/zod/parseZodSchemaAsync.ts
1036
- /**
1037
- * An alternative function to zodSchema.parseAsync() that can be used to strictly parse asynchronous Zod schemas.
1038
- *
1039
- * @category Parsers
1040
- *
1041
- * @template SchemaType - The Zod schema type.
1042
- * @template ErrorType - The type of error to throw on invalid data.
1043
- *
1044
- * @param schema - The Zod schema to use in parsing.
1045
- * @param input - The data to parse.
1046
- * @param onError - A custom error to throw on invalid data (defaults to `DataError`). May either be the error itself, or a function that returns the error or nothing. If nothing is returned, the default error is thrown instead.
1047
- *
1048
- * @throws {DataError} If the given data cannot be parsed according to the schema.
1049
- *
1050
- * @returns The parsed data from the Zod schema.
1051
- */
1052
- async function parseZodSchemaAsync(schema, input, onError) {
1053
- return _parseZodSchema(await schema.safeParseAsync(input), input, onError);
1298
+ return az.with(zod.default.enum(VersionType)).parse(input, new DataError$1({ input }, "INVALID_VERSION_TYPE", "The provided version type must be one of `major | minor | patch`"));
1054
1299
  }
1055
1300
  //#endregion
1056
1301
  //#region src/root/functions/recursive/deepCopy.ts
@@ -1116,7 +1361,7 @@ function deepFreeze(object) {
1116
1361
  * @returns A string with the semicolon appended.
1117
1362
  */
1118
1363
  function appendSemicolon(stringToAppendTo) {
1119
- if (stringToAppendTo.includes("\n")) throw new DataError({ stringToAppendTo }, "MULTIPLE_LINE_ERROR", "Cannot append semicolon to multi-line string.");
1364
+ if (stringToAppendTo.includes("\n")) throw new DataError$1({ stringToAppendTo }, "MULTIPLE_LINE_ERROR", "Cannot append semicolon to multi-line string.");
1120
1365
  const stringWithNoTrailingWhitespace = stringToAppendTo.trimEnd();
1121
1366
  if (stringWithNoTrailingWhitespace === "") return "";
1122
1367
  return stringWithNoTrailingWhitespace[stringWithNoTrailingWhitespace.length - 1] === ";" ? stringWithNoTrailingWhitespace : `${stringWithNoTrailingWhitespace};`;
@@ -1184,9 +1429,9 @@ function escapeRegexPattern(regexPattern) {
1184
1429
  * @returns The string converted to camelCase.
1185
1430
  */
1186
1431
  function kebabToCamel(input, options) {
1187
- if (input !== input.toLowerCase()) throw new DataError({ input }, "UPPERCASE_INPUT", "Kebab-case must be purely lowercase.");
1188
- if (input.startsWith("-") || input.endsWith("-")) throw new DataError({ input }, "TRAILING_DASHES", "Dashes at the start and/or end are not allowed.");
1189
- if (input.includes("--")) throw new DataError({ input }, "CONSECUTIVE_DASHES", "Consecutive dashes are not allowed.");
1432
+ if (input !== input.toLowerCase()) throw new DataError$1({ input }, "UPPERCASE_INPUT", "Kebab-case must be purely lowercase.");
1433
+ if (input.startsWith("-") || input.endsWith("-")) throw new DataError$1({ input }, "TRAILING_DASHES", "Dashes at the start and/or end are not allowed.");
1434
+ if (input.includes("--")) throw new DataError$1({ input }, "CONSECUTIVE_DASHES", "Consecutive dashes are not allowed.");
1190
1435
  let outputString = "";
1191
1436
  let skip = false;
1192
1437
  for (const stringIndex in [...input]) {
@@ -1299,7 +1544,7 @@ function createTemplateStringsArray(strings) {
1299
1544
  * ```
1300
1545
  */
1301
1546
  function getStringsAndInterpolations(strings, ...interpolations) {
1302
- if (strings.length !== interpolations.length + 1) throw new DataError({
1547
+ if (strings.length !== interpolations.length + 1) throw new DataError$1({
1303
1548
  stringsLength: strings.length,
1304
1549
  interpolationsLength: interpolations.length,
1305
1550
  strings,
@@ -1465,217 +1710,99 @@ function normaliseIndents(first, ...args) {
1465
1710
  */
1466
1711
  const normalizeIndents = normaliseIndents;
1467
1712
  //#endregion
1468
- //#region src/root/types/APIError.ts
1469
- const httpErrorCodeLookup = {
1470
- 400: "BAD_REQUEST",
1471
- 401: "UNAUTHORISED",
1472
- 403: "FORBIDDEN",
1473
- 404: "NOT_FOUND",
1474
- 418: "I_AM_A_TEAPOT",
1475
- 500: "INTERNAL_SERVER_ERROR"
1476
- };
1713
+ //#region src/root/deprecated/DataError.ts
1477
1714
  /**
1478
- * Represents common errors you may get from a HTTP API request.
1715
+ * Represents errors you may get that may've been caused by a specific piece of data.
1479
1716
  *
1480
1717
  * @category Types
1718
+ *
1719
+ * @deprecated Please use `DataError` from `@alextheman/utility/v6` instead.
1720
+ *
1721
+ * @template DataType - The type of the data that caused the error.
1481
1722
  */
1482
- var APIError = class APIError extends Error {
1483
- status;
1723
+ var DataError = class DataError extends Error {
1724
+ code;
1725
+ data;
1484
1726
  /**
1485
- * @param status - A HTTP status code. Can be any number, but numbers between 400 and 600 are encouraged to fit with HTTP status code conventions.
1486
- * @param message - An error message to display alongside the status code.
1487
- * @param options - Extra options to be passed to super Error constructor.
1727
+ * @param data - The data that caused the error.
1728
+ * @param code - A standardised code (e.g. UNEXPECTED_DATA).
1729
+ * @param message - A human-readable error message (e.g. The data provided is invalid).
1730
+ * @param options - Extra options to pass to super Error constructor.
1488
1731
  */
1489
- constructor(status = 500, message, options) {
1732
+ constructor(data, code = "INVALID_DATA", message = "The data provided is invalid", options) {
1490
1733
  super(message, options);
1491
- this.status = status;
1492
- if (message) this.message = message;
1493
- else this.message = httpErrorCodeLookup[this.status] ?? "API_ERROR";
1734
+ if (Error.captureStackTrace) Error.captureStackTrace(this, new.target);
1735
+ this.name = new.target.name;
1736
+ this.code = code;
1737
+ this.data = data;
1494
1738
  Object.defineProperty(this, "message", { enumerable: true });
1495
1739
  Object.setPrototypeOf(this, new.target.prototype);
1496
1740
  }
1741
+ static checkCaughtError(error, options) {
1742
+ if (DataError.check(error)) {
1743
+ if (options?.expectedCode && error.code !== options.expectedCode) throw new Error(normaliseIndents`The error code on the thrown error does not match the expected error code.
1744
+
1745
+ Expected: ${options.expectedCode}
1746
+ Received: ${error.code}
1747
+ `, { cause: error });
1748
+ return error;
1749
+ }
1750
+ throw error;
1751
+ }
1497
1752
  /**
1498
- * Checks whether the given input may have been caused by an APIError.
1753
+ * Checks whether the given input may have been caused by a DataError.
1499
1754
  *
1500
1755
  * @param input - The input to check.
1501
1756
  *
1502
- * @returns `true` if the input is an APIError, and `false` otherwise. The type of the input will also be narrowed down to APIError if `true`.
1757
+ * @returns `true` if the input is a DataError, and `false` otherwise. The type of the input will also be narrowed down to DataError if `true`.
1503
1758
  */
1504
1759
  static check(input) {
1505
- if (input instanceof APIError) return true;
1760
+ if (input instanceof DataError) return true;
1506
1761
  const data = input;
1507
- return typeof data === "object" && data !== null && typeof data?.status === "number" && typeof data?.message === "string";
1762
+ return typeof data === "object" && data !== null && typeof data.message === "string" && typeof data.code === "string" && "data" in data;
1508
1763
  }
1509
- };
1510
- //#endregion
1511
- //#region src/root/types/VersionNumber.ts
1512
- /**
1513
- * Represents a software version number, considered to be made up of a major, minor, and patch part.
1514
- *
1515
- * @category Types
1516
- */
1517
- var VersionNumber = class VersionNumber {
1518
- static NON_NEGATIVE_TUPLE_ERROR = "Input array must be a tuple of three non-negative integers.";
1519
- /** The major number. Increments when a feature is removed or changed in a way that is not backwards-compatible with the previous release. */
1520
- major = 0;
1521
- /** The minor number. Increments when a new feature is added/deprecated and is expected to be backwards-compatible with the previous release. */
1522
- minor = 0;
1523
- /** The patch number. Increments when the next release is fixing a bug or doing a small refactor that should not be noticeable in practice. */
1524
- patch = 0;
1525
1764
  /**
1526
- * @param input - The input to create a new instance of `VersionNumber` from.
1527
- */
1528
- constructor(input) {
1529
- if (input instanceof VersionNumber) {
1530
- this.major = input.major;
1531
- this.minor = input.minor;
1532
- this.patch = input.patch;
1533
- } else if (typeof input === "string") {
1534
- if (!VERSION_NUMBER_REGEX.test(input)) throw new DataError({ input }, "INVALID_VERSION", `"${input}" is not a valid version number. Version numbers must be of the format "X.Y.Z" or "vX.Y.Z", where X, Y, and Z are non-negative integers.`);
1535
- const [major, minor, patch] = VersionNumber.formatString(input, { omitPrefix: true }).split(".").map((number) => {
1536
- return parseIntStrict(number);
1537
- });
1538
- this.major = major;
1539
- this.minor = minor;
1540
- this.patch = patch;
1541
- } else if (Array.isArray(input)) {
1542
- if (input.length !== 3) throw new DataError({ input }, "INVALID_LENGTH", VersionNumber.NON_NEGATIVE_TUPLE_ERROR);
1543
- const [major, minor, patch] = input.map((number) => {
1544
- const parsedInteger = parseIntStrict(number?.toString());
1545
- if (parsedInteger < 0) throw new DataError({ input }, "NEGATIVE_INPUTS", VersionNumber.NON_NEGATIVE_TUPLE_ERROR);
1546
- return parsedInteger;
1547
- });
1548
- this.major = major;
1549
- this.minor = minor;
1550
- this.patch = patch;
1551
- } else throw new DataError({ input }, "INVALID_INPUT", normaliseIndents`
1552
- The provided input can not be parsed into a valid version number.
1553
- Expected either a string of format X.Y.Z or vX.Y.Z, a tuple of three numbers, or another \`VersionNumber\` instance.
1554
- `);
1555
- }
1556
- /**
1557
- * Gets the current version type of the current instance of `VersionNumber`.
1558
- *
1559
- * @returns Either `"major"`, `"minor"`, or `"patch"`, depending on the version type.
1560
- */
1561
- get type() {
1562
- if (this.minor === 0 && this.patch === 0) return VersionType.MAJOR;
1563
- if (this.patch === 0) return VersionType.MINOR;
1564
- return VersionType.PATCH;
1565
- }
1566
- static formatString(input, options) {
1567
- if (options?.omitPrefix) return input.startsWith("v") ? input.slice(1) : input;
1568
- return input.startsWith("v") ? input : `v${input}`;
1569
- }
1570
- /**
1571
- * Checks if the provided version numbers have the exact same major, minor, and patch numbers.
1572
- *
1573
- * @param firstVersion - The first version number to compare.
1574
- * @param secondVersion - The second version number to compare.
1575
- *
1576
- * @returns `true` if the provided version numbers have exactly the same major, minor, and patch numbers, and returns `false` otherwise.
1577
- */
1578
- static isEqual(firstVersion, secondVersion) {
1579
- return firstVersion.major === secondVersion.major && firstVersion.minor === secondVersion.minor && firstVersion.patch === secondVersion.patch;
1580
- }
1581
- /**
1582
- * Get a formatted string representation of the current version number
1583
- *
1584
- * @param options - Options to apply to the string formatting.
1765
+ * Gets the thrown `DataError` from a given function if one was thrown, and re-throws any other errors, or throws a default `DataError` if no error thrown.
1585
1766
  *
1586
- * @returns A formatted string representation of the current version number with the options applied.
1587
- */
1588
- format(options) {
1589
- let baseOutput = `${this.major}`;
1590
- if (!options?.omitMinor) {
1591
- baseOutput += `.${this.minor}`;
1592
- if (!options?.omitPatch) baseOutput += `.${this.patch}`;
1593
- }
1594
- return VersionNumber.formatString(baseOutput, { omitPrefix: options?.omitPrefix });
1595
- }
1596
- /**
1597
- * Increments the current version number by the given increment type, returning the result as a new reference in memory.
1767
+ * @param errorFunction - The function expected to throw the error.
1768
+ * @param options - Extra options to apply.
1598
1769
  *
1599
- * @param incrementType - The type of increment. Can be one of the following:
1600
- * - `"major"`: Change the major version `v1.2.3` `v2.0.0`
1601
- * - `"minor"`: Change the minor version `v1.2.3` → `v1.3.0`
1602
- * - `"patch"`: Change the patch version `v1.2.3` → `v1.2.4`
1603
- * @param incrementAmount - The amount to increment by (defaults to 1).
1770
+ * @throws {Error} Any other errors thrown by the `errorFunction` that are not a `DataError`.
1771
+ * @throws {Error} If no `DataError` was thrown by the `errorFunction`
1604
1772
  *
1605
- * @returns A new instance of `VersionNumber` with the increment applied.
1773
+ * @returns The `DataError` that was thrown by the `errorFunction`
1606
1774
  */
1607
- increment(incrementType, incrementAmount = 1) {
1608
- const incrementBy = parseIntStrict(String(incrementAmount));
1609
- const calculatedRawVersion = {
1610
- major: [
1611
- this.major + incrementBy,
1612
- 0,
1613
- 0
1614
- ],
1615
- minor: [
1616
- this.major,
1617
- this.minor + incrementBy,
1618
- 0
1619
- ],
1620
- patch: [
1621
- this.major,
1622
- this.minor,
1623
- this.patch + incrementBy
1624
- ]
1625
- }[incrementType];
1775
+ static expectError(errorFunction, options) {
1626
1776
  try {
1627
- return new VersionNumber(calculatedRawVersion);
1777
+ errorFunction();
1628
1778
  } catch (error) {
1629
- if (DataError.check(error) && error.code === "NEGATIVE_INPUTS") throw new DataError({
1630
- currentVersion: this.toString(),
1631
- calculatedRawVersion: `v${calculatedRawVersion.join(".")}`,
1632
- incrementAmount
1633
- }, "NEGATIVE_VERSION", "Cannot apply this increment amount as it would lead to a negative version number.");
1634
- else throw error;
1779
+ return DataError.checkCaughtError(error, options);
1635
1780
  }
1781
+ throw new Error("Expected a DataError to be thrown but none was thrown");
1636
1782
  }
1637
1783
  /**
1638
- * Ensures that the VersionNumber behaves correctly when attempted to be coerced to a string.
1639
- *
1640
- * @param hint - Not used as of now, but generally used to help with numeric coercion, I think (which we most likely do not need for version numbers).
1784
+ * Gets the thrown `DataError` from a given asynchronous function if one was thrown, and re-throws any other errors, or throws a default `DataError` if no error thrown.
1641
1785
  *
1642
- * @returns A stringified representation of the current version number, prefixed with `v`.
1643
- */
1644
- [Symbol.toPrimitive](hint) {
1645
- if (hint === "number") throw new DataError({ thisVersion: this.toString() }, "INVALID_COERCION", "VersionNumber cannot be coerced to a number type.");
1646
- return this.toString();
1647
- }
1648
- /**
1649
- * Ensures that the VersionNumber behaves correctly when attempted to be converted to JSON.
1786
+ * @param errorFunction - The function expected to throw the error.
1787
+ * @param options - Extra options to apply.
1650
1788
  *
1651
- * @returns A stringified representation of the current version number, prefixed with `v`.
1652
- */
1653
- toJSON() {
1654
- return this.toString();
1655
- }
1656
- /**
1657
- * Get a string representation of the current version number.
1789
+ * @throws {Error} Any other errors thrown by the `errorFunction` that are not a `DataError`.
1790
+ * @throws {Error} If no `DataError` was thrown by the `errorFunction`
1658
1791
  *
1659
- * @returns A stringified representation of the current version number with the prefix.
1792
+ * @returns The `DataError` that was thrown by the `errorFunction`
1660
1793
  */
1661
- toString() {
1662
- const rawString = `${this.major}.${this.minor}.${this.patch}`;
1663
- return VersionNumber.formatString(rawString, { omitPrefix: false });
1794
+ static async expectErrorAsync(errorFunction, options) {
1795
+ try {
1796
+ await errorFunction();
1797
+ } catch (error) {
1798
+ return DataError.checkCaughtError(error, options);
1799
+ }
1800
+ throw new Error("Expected a DataError to be thrown but none was thrown");
1664
1801
  }
1665
1802
  };
1666
- const zodVersionNumber = zod.default.union([
1667
- zod.default.string(),
1668
- zod.default.tuple([
1669
- zod.default.number(),
1670
- zod.default.number(),
1671
- zod.default.number()
1672
- ]),
1673
- zod.default.instanceof(VersionNumber)
1674
- ]).transform((rawVersionNumber) => {
1675
- return new VersionNumber(rawVersionNumber);
1676
- });
1677
1803
  //#endregion
1678
1804
  exports.APIError = APIError;
1805
+ exports.DataError = DataError;
1679
1806
  exports.Env = Env;
1680
1807
  exports.FILE_PATH_PATTERN = FILE_PATH_PATTERN;
1681
1808
  exports.FILE_PATH_REGEX = FILE_PATH_REGEX;
@@ -1688,6 +1815,7 @@ exports.VersionNumber = VersionNumber;
1688
1815
  exports.VersionType = VersionType;
1689
1816
  exports.addDaysToDate = addDaysToDate;
1690
1817
  exports.appendSemicolon = appendSemicolon;
1818
+ exports.az = az;
1691
1819
  exports.calculateMonthlyDifference = calculateMonthlyDifference;
1692
1820
  exports.camelToKebab = camelToKebab;
1693
1821
  exports.convertFileToBase64 = convertFileToBase64;