type-route
Version:
The flexible, type safe routing library.
393 lines (356 loc) • 10.9 kB
text/typescript
import { ErrorDef, BuildPathDefErrorContext } from "./types";
import { typeOf } from "./typeOf";
function getBuildPathDefRouteNameMessage(routeName: string) {
return `This problem occurred when building the route definition for the "${routeName}" route.`;
}
function getBuildPathDefErrorMessage(context: BuildPathDefErrorContext) {
return [
getBuildPathDefRouteNameMessage(context.routeName),
`The path was constructed as \`${context.rawPath}\``,
];
}
export const TypeRouteError = buildErrorCollection({
Path_may_not_be_an_empty_string: {
errorCode: 1000,
getDetails: getBuildPathDefErrorMessage,
},
Path_must_start_with_a_forward_slash: {
errorCode: 1001,
getDetails: getBuildPathDefErrorMessage,
},
Path_may_not_end_with_a_forward_slash: {
errorCode: 1002,
getDetails: getBuildPathDefErrorMessage,
},
Path_may_not_include_characters_that_must_be_URL_encoded: {
errorCode: 1003,
getDetails: (
context: BuildPathDefErrorContext,
segment: {
leading: string;
paramId?: string;
trailing?: string;
}
) => {
const leading = segment.leading;
const trailing = segment.trailing ?? "";
const paramId = segment.paramId ?? "";
const invalidCharacters = (leading + trailing)
.split("")
.filter((character) => character !== encodeURIComponent(character));
return [
...getBuildPathDefErrorMessage(context),
`The path segment \`${
leading + paramId + trailing
}\` has the following invalid characters: ${invalidCharacters.join(
", "
)}`,
];
},
},
Path_may_not_include_empty_segments: {
errorCode: 1004,
getDetails: (context: BuildPathDefErrorContext) => {
return [
...getBuildPathDefErrorMessage(context),
"Empty segments can be spotted by finding the place in the path with two consecutive forward slashes '//'.",
];
},
},
Path_may_have_at_most_one_parameter_per_segment: {
errorCode: 1005,
getDetails: (
context: BuildPathDefErrorContext,
parameterNames: string[]
) => {
return [
...getBuildPathDefErrorMessage(context),
`A single segment of the path included the following parameters: ${parameterNames}`,
"Consider using ofType with a customer ValueSerializer for this scenario.",
];
},
},
Path_parameters_may_not_be_used_more_than_once_when_building_a_path: {
errorCode: 1005,
getDetails: (context: BuildPathDefErrorContext, parameterName: string) => {
return [
...getBuildPathDefErrorMessage(context),
`The parameter "${parameterName}" was used more than once.`,
];
},
},
Optional_path_parameters_may_not_have_any_text_around_the_parameter: {
errorCode: 1006,
getDetails: (
context: BuildPathDefErrorContext,
parameterName: string,
leadingText: string,
trailingText: string
) => {
const messages = getBuildPathDefErrorMessage(context);
if (leadingText) {
messages.push(
`The parameter "${parameterName}" cannot be preceded by "${leadingText}".`
);
}
if (trailingText) {
messages.push(
`The parameter "${parameterName}" cannot be followed by "${trailingText}".`
);
}
return messages;
},
},
Path_may_have_at_most_one_optional_or_trailing_parameter: {
errorCode: 1007,
getDetails(
context: BuildPathDefErrorContext,
numOptionalTrailingParameterNames: number
) {
return [
...getBuildPathDefErrorMessage(context),
`At most one optional/trailing parameter should be given but ${numOptionalTrailingParameterNames} were provided.`,
];
},
},
Optional_or_trailing_path_parameters_may_only_appear_in_the_last_path_segment:
{
errorCode: 1008,
getDetails: getBuildPathDefErrorMessage,
},
All_path_parameters_must_be_used_in_path_construction: {
errorCode: 1009,
getDetails(context: BuildPathDefErrorContext, unusedParameters: string[]) {
return [
...getBuildPathDefErrorMessage(context),
`The following parameters were not used: ${unusedParameters.join(
", "
)}`,
];
},
},
Path_parameter_name_must_not_include_curly_brackets_dollar_signs_or_the_forward_slash_character:
{
errorCode: 1010,
getDetails(routeName: string, paramName: string) {
return [
getBuildPathDefRouteNameMessage(routeName),
`The $ { } or / character was used in this parameter name: ${paramName}`,
];
},
},
Extension_route_definition_parameter_names_may_not_be_the_same_as_base_route_definition_parameter_names:
{
errorCode: 1011,
getDetails(duplicateParameterNames: string[]) {
return [
`The following parameter names were used in both the base route definition and the extension: ${duplicateParameterNames.join(
", "
)}`,
];
},
},
Expected_type_does_not_match_actual_type: {
errorCode: 1012,
getDetails({
context,
value,
valueName,
expectedType,
actualType,
}: {
context: string;
valueName: string;
expectedType: string | string[];
actualType: string;
value: any;
}) {
return [
`Problem found with your usage of \`${context}\``,
`\`${valueName}\` was expected to be of type \`${
Array.isArray(expectedType) ? expectedType.join(" | ") : expectedType
}\` but was of type \`${actualType}\``,
`The actual value provided was: ${
typeOf(value) === "object"
? "\n" +
JSON.stringify(value, null, 2)
.split("\n")
.map((line) => ` ${line}`)
.join("\n")
: "`" + value + "`"
}`,
];
},
},
Expected_number_of_arguments_does_match_actual_number: {
errorCode: 1013,
getDetails({
context,
args,
min,
max,
}: {
context: string;
args: any[];
min: number;
max: number;
}) {
return [
`Problem found with your usage of \`${context}\``,
`Expected ${min}${min === max ? "" : " - " + max} but received ${
args.length
} argument${args.length === 1 ? "" : "s"}`,
];
},
},
Query_string_array_format_and_custom_query_string_serializer_may_not_both_be_provided:
{
errorCode: 1014,
getDetails() {
return [
"You may not provide both options.arrayFormat.queryString and options.queryStringSerializer. These options are not compatible.",
];
},
},
Expected_length_of_array_does_match_actual_length: {
errorCode: 1015,
getDetails({
context,
array,
min,
max,
}: {
context: string;
array: any[];
min: number;
max: number;
}) {
return [
`Problem found with your usage of \`${context}\``,
`Expected array to be of length ${min}${
min === max ? "" : " - " + max
} but actual length was ${array.length}`,
];
},
},
Encountered_unexpected_parameter_when_building_route: {
errorCode: 1016,
getDetails({
routeName,
unexpectedParameterName,
allowedParameterNames,
}: {
routeName: string;
unexpectedParameterName: string;
allowedParameterNames: string[];
}) {
return [
`Problem found with your usage of routes.${routeName}( ... )`,
`Unexpected parameter passed to route builder named "${unexpectedParameterName}"`,
allowedParameterNames.length === 0
? "The route does not take any parameters"
: `This route takes the following parameters: ${allowedParameterNames
.map((name) => `"${name}"`)
.join(", ")}`,
];
},
},
Missing_required_parameter_when_building_route: {
errorCode: 1017,
getDetails({
routeName,
missingParameterName,
}: {
routeName: string;
missingParameterName: string;
}) {
return [
`Problem found with your usage of routes.${routeName}( ... )`,
`The parameter "${missingParameterName}" is required but was not provided.`,
];
},
},
Base_url_must_start_with_a_forward_slash: {
errorCode: 1018,
getDetails(baseUrl: string) {
return [
'Base URL must start with a forward slash "/"',
`The value you provided "${baseUrl}" does not start with a forward slash.`,
];
},
},
Base_url_must_not_contain_any_characters_that_must_be_url_encoded: {
errorCode: 1019,
getDetails(baseUrl: string) {
const invalidCharacters = baseUrl
.replace(/\//g, "")
.split("")
.filter((character) => character !== encodeURIComponent(character));
return [
`The following characters are invalid: ${invalidCharacters.join(
", "
)}.`,
];
},
},
App_should_be_wrapped_in_a_RouteProvider_component: {
errorCode: 1020,
getDetails() {
return [
"Your application must be wrapped in the `RouteProvider` component returned by `createRouter` in order to use the `useRoute` hook.",
];
},
},
Invalid_React_version: {
errorCode: 1021,
getDetails(version: string) {
return [
"React version must be 16.8 or greater.",
`You have version ${version} installed.`,
"If you cannot upgrade the React version try using `type-route/core`.",
];
},
},
});
function buildErrorCollection<
TErrorDefCollection extends Record<string, ErrorDef>
>(definitions: TErrorDefCollection) {
const errors: Record<
string,
{
name: string;
errorCode: number;
create(...args: any[]): Error;
}
> = {};
Object.keys(definitions).forEach((key) => {
const name = key.replace(/_/g, " ") + ".";
const { errorCode, getDetails } = definitions[key];
const messageTitle = `TR${errorCode} · ${name}`;
errors[key] = {
errorCode,
name,
create(...args: any[]) {
const message = (getDetails?.(...args) ?? [])
.map((detail) => `- ${detail}`)
.join("\n");
const error = new Error(
message
? `\n\n${messageTitle}\n\n${message}\n`
: `\n\n${messageTitle}\n`
);
error.name = `(hopefully helpful 😄) TypeRouteError`;
return error;
},
};
});
return errors as {
[TName in keyof TErrorDefCollection]: {
create(
...args: Parameters<TErrorDefCollection[TName]["getDetails"]>
): Error;
name: TName;
errorCode: TErrorDefCollection[TName]["errorCode"];
};
};
}