Skip to content

Commit

Permalink
better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
AmmarHalees committed Dec 24, 2023
1 parent b1c733c commit 2e1b46f
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 86 deletions.
141 changes: 101 additions & 40 deletions src/core/validateField.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MappingError, handleError } from "../internal/error-management";
import {
RulesObject,
FieldError,
Expand All @@ -14,46 +15,102 @@ export function validateField(
rulesObject: RulesObject
): Array<FieldError> {
const errors: Array<FieldError> = [];
try {
const configMap: ConfigMap = {
minLength: (value: string, criterion: number, message: string) => {
/*--- Error Guards ---*/

const configMap: ConfigMap = {
minLength: (value: string, criterion: number, message: string) => {
if (!_utils.isString(value))
throw new Error(
"File value must be a string to be validated with minLength"
);
if (!_utils.isString(value))
throw new TypeError("'value' must be a string");

if (value && value.length > 0 && !(value.length >= criterion)) {
errors.push({ name, message });
}
},
maxLength: (value: string, criterion: number, message: string) => {
if (value && !(value.length <= criterion)) {
errors.push({ name, message });
}
},
pattern: (value: string, criterion: RegExp, message: string) => {
if (value.length > 0 && !value.match(criterion)) {
errors.push({ name, message });
}
},
custom: (value: string, criterion: CustomFunction, message: string) => {
// if the custom function criterion(value) returns true: push an error
if (criterion(value)) {
errors.push({ name, message });
}
},
required: (value: string, criterion, message: string) => {
if (value.replace(/\s/g, "") === "" && criterion) {
errors.push({ name, message });
}
},
};
if (!_utils.isNumber(criterion))
throw new TypeError("'criterion' must be a number");

function isKeyOfConfigMap(key: string): key is keyof ConfigMap {
return key in configMap;
}
if (!_utils.isString(message))
throw new TypeError("'message' must be a string");

/*--- Functionality ---*/

if (value && value.length > 0 && !(value.length >= criterion)) {
errors.push({ name, message });
}
},
maxLength: (value: string, criterion: number, message: string) => {
/*--- Error Guards ---*/

if (!_utils.isString(value))
throw new TypeError("'value' must be a string");

if (!_utils.isNumber(criterion))
throw new TypeError("'criterion' must be a number");

if (!_utils.isString(message))
throw new TypeError("'message' must be a string");

/*--- Functionality ---*/

if (value && !(value.length <= criterion)) {
errors.push({ name, message });
}
},
pattern: (value: string, criterion: RegExp, message: string) => {
/*--- Error Guards ---*/

if (!_utils.isString(value))
throw new TypeError("'value' must be a string");

// if (!_utils.isRegExp(criterion))
// throw new TypeError("'criterion' must be a RegExp");

if (!_utils.isString(message))
throw new TypeError("'message' must be a string");

if (value.length > 0 && !value.match(criterion)) {
errors.push({ name, message });
}
},
custom: (value: string, criterion: CustomFunction, message: string) => {
/*--- Error Guards ---*/

if (!_utils.isString(value))
throw new TypeError("'value' must be a string");

// if (!_utils.isFunction(criterion))
// throw new TypeError("'criterion' must be a function");

if (!_utils.isString(message))
throw new TypeError("'message' must be a string");

/*--- Functionality ---*/

if (criterion(value)) {
errors.push({ name, message });
}
},
required: (value: string, criterion: boolean, message: string) => {
/*--- Error Guards ---*/

if (!_utils.isString(value))
throw new TypeError("'value' must be a string");

// if (!_utils.isBoolean(criterion))
// throw new TypeError("'criterion' must be a boolean");

if (!_utils.isString(message))
throw new TypeError("'message' must be a string");

/*--- Functionality ---*/

if (value.replace(/\s/g, "") === "" && criterion) {
errors.push({ name, message });
}
},
} as const;

function isKeyOfConfigMap(key: string): key is keyof ConfigMap {
return key in configMap;
}

try {
Object.entries(rulesObject).forEach(([key, { criterion, message }]) => {
if (isKeyOfConfigMap(key)) {
const validationFunction = configMap[
Expand All @@ -63,14 +120,18 @@ export function validateField(
if (validationFunction) {
validationFunction(value, criterion, message);
} else {
throw new Error(
`Validation function for ${key} does not exist in configMap`
throw new MappingError(
`No validation function found for ${key} in configMap`
);
}
}
});
} catch (e) {
console.error(e);
} catch (error) {
if (error instanceof Error) {
handleError(error);
} else {
console.log(error);
}
}

return errors;
Expand Down
8 changes: 1 addition & 7 deletions src/core/validateForm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { doKeysMatch } from "../internal/do-keys-match";
import {
FieldError,
NameRuleMap,
KeyValuePair,
RulesObject,
FormDataShape,
} from "../types";
import { FieldError, NameRuleMap, RulesObject, FormDataShape } from "../types";
import _utils from "../utils";
import { validateField } from "./validateField";

Expand Down
64 changes: 28 additions & 36 deletions src/internal/error-management.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
// Custom error types
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = "NetworkError";
}
}
type ERROR_NAMES = "TYPE_ERROR" | "MAPPING_ERROR";

class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
class MappingError extends Error {
constructor(
message: string = "Validation Error",
name: ERROR_NAMES = "MAPPING_ERROR"
) {
super();
this.name = name;
this.message = message;
}
}

class DatabaseError extends Error {
constructor(message: string) {
super(message);
this.name = "DatabaseError";
class TypeError extends Error {
constructor(
message: string = "Type Error",
name: ERROR_NAMES = "TYPE_ERROR"
) {
super();
this.name = name;
this.message = message;
}
}

Expand All @@ -25,31 +27,21 @@ class DatabaseError extends Error {
// Error Handling Utility
function handleError(error: Error): void {
switch (error.constructor) {
case NetworkError:
console.log("Handle network error:", error.message);
// specific logic for NetworkError
break;
case ValidationError:
console.log("Handle validation error:", error.message);
// specific logic for ValidationError
case TypeError:
console.log("Type Error:", error.message);
break;
case DatabaseError:
console.log("Handle database error:", error.message);
// specific logic for DatabaseError
case MappingError:
console.log("Mapping Error:", error.message);
break;
default:
console.log("Handle general error:", error.message);
// logic for unrecognized error types
console.log(
"Internal Error",
error.message,
"Please report this at" +
"https://github.com/AmmarHalees/onsubmit/issues"
);
break;
}
}

// Example usage
try {
// Code that may throw errors
throw new ValidationError("Invalid input data");
} catch (error) {
if (error instanceof Error) {
handleError(error);
}
}
export { MappingError, handleError };
7 changes: 7 additions & 0 deletions src/mocks/validateField.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const minLength = {
input: "abc",
expectedOutput: [],
},
case: {
rules: {
minLength: { criterion: 3, message: "Minimum length is 3" },
},
input: "",
expectedOutput: [],
},
};

const maxLength = {
Expand Down
9 changes: 9 additions & 0 deletions src/tests/validateField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ describe("validateField", () => {
mocks.minLength.fail.rules
)
).toEqual([]);

expect(
validateField(
mocks.minLength.case.input,
"testField",
mocks.minLength.case.rules
)
).toEqual([]);

});

it("should validate maximum length", () => {
Expand Down
4 changes: 1 addition & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
/* Strictness */
"strict": true,
"noUnusedLocals": true,
"noUncheckedIndexedAccess": true,
"noUnusedParameters": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
/* If NOT transpiling with TypeScript: */
"moduleResolution": "Node",
"module": "ESNext",
"noEmit": true,
Expand Down

0 comments on commit 2e1b46f

Please sign in to comment.