refactor: updater errors
This commit is contained in:
+5
-5
@@ -44,7 +44,7 @@ updated: 2026-07-12T10:00:00Z
|
|||||||
src/
|
src/
|
||||||
├── extension.ts # composition root (DI, register)
|
├── extension.ts # composition root (DI, register)
|
||||||
├── ports.ts # IFileSystem, ITaskRepository, IConfigProvider
|
├── ports.ts # IFileSystem, ITaskRepository, IConfigProvider
|
||||||
├── error.ts # AppError + factories (единый каталог ошибок)
|
├── error.ts # AppErrorVariant + AppError { variant, message }
|
||||||
├── model/task.ts
|
├── model/task.ts
|
||||||
├── storage/
|
├── storage/
|
||||||
│ ├── FsTaskRepository.ts
|
│ ├── FsTaskRepository.ts
|
||||||
@@ -132,10 +132,10 @@ use-cases + `ITaskRepository`. Ошибки — `AppError` из `error.ts`.
|
|||||||
|
|
||||||
### neverthrow + `AppError`
|
### neverthrow + `AppError`
|
||||||
|
|
||||||
- Use-cases возвращают `ResultAsync<T, AppError>` вместо кастомных
|
- Use-cases возвращают `ResultAsync<T, AppError>`.
|
||||||
`{ ok, reason }`.
|
- `AppErrorVariant` — const-object кодов (`NOT_FOUND: "not-found"`).
|
||||||
- Каталог ошибок: `src/error.ts` (`AppErrorCode`, factories `AppError.noFolder()`
|
- `AppError` — `{ variant, message }`; `message` из шаблона с `{placeholders}`
|
||||||
и т.д.).
|
через `appError(variant, params?)`.
|
||||||
|
|
||||||
### Прочее относительно v0
|
### Прочее относительно v0
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { err } from "neverthrow";
|
import { err } from "neverthrow";
|
||||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppErrorVariant } from "@/error";
|
||||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||||
import { changeStatus } from "./changeStatus";
|
import { changeStatus } from "./changeStatus";
|
||||||
|
|
||||||
@@ -38,13 +38,15 @@ describe("changeStatus", () => {
|
|||||||
expect(stored?.status).toBe("in-progress");
|
expect(stored?.status).toBe("in-progress");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns not_found for unknown id", async () => {
|
it("returns not-found for unknown id", async () => {
|
||||||
const result = await changeStatus(repo, {
|
const result = await changeStatus(repo, {
|
||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
id: "missing",
|
id: "missing",
|
||||||
status: "done",
|
status: "done",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.notFound()));
|
expect(result).toEqual(
|
||||||
|
err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing folder", async () => {
|
it("rejects missing folder", async () => {
|
||||||
@@ -53,7 +55,7 @@ describe("changeStatus", () => {
|
|||||||
id: "x",
|
id: "x",
|
||||||
status: "done",
|
status: "done",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noFolder()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing id", async () => {
|
it("rejects missing id", async () => {
|
||||||
@@ -62,7 +64,7 @@ describe("changeStatus", () => {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
status: "done",
|
status: "done",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noId()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_ID)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing status", async () => {
|
it("rejects missing status", async () => {
|
||||||
@@ -71,6 +73,6 @@ describe("changeStatus", () => {
|
|||||||
id: "x",
|
id: "x",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noStatus()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_STATUS)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppError, AppErrorVariant } from "@/error";
|
||||||
import { Task, TaskStatus } from "@/model/task";
|
import { Task, TaskStatus } from "@/model/task";
|
||||||
import { ITaskRepository } from "@/ports";
|
import { ITaskRepository } from "@/ports";
|
||||||
|
|
||||||
@@ -14,13 +14,13 @@ export function changeStatus(
|
|||||||
input: ChangeStatusInput,
|
input: ChangeStatusInput,
|
||||||
): ResultAsync<Task, AppError> {
|
): ResultAsync<Task, AppError> {
|
||||||
if (input.folderPath === undefined) {
|
if (input.folderPath === undefined) {
|
||||||
return errAsync(AppError.noFolder());
|
return errAsync(appError(AppErrorVariant.NO_FOLDER));
|
||||||
}
|
}
|
||||||
if (input.id === undefined) {
|
if (input.id === undefined) {
|
||||||
return errAsync(AppError.noId());
|
return errAsync(appError(AppErrorVariant.NO_ID));
|
||||||
}
|
}
|
||||||
if (input.status === undefined) {
|
if (input.status === undefined) {
|
||||||
return errAsync(AppError.noStatus());
|
return errAsync(appError(AppErrorVariant.NO_STATUS));
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderPath = input.folderPath;
|
const folderPath = input.folderPath;
|
||||||
@@ -30,7 +30,7 @@ export function changeStatus(
|
|||||||
return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
||||||
(existing) => {
|
(existing) => {
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return errAsync(AppError.notFound());
|
return errAsync(appError(AppErrorVariant.NOT_FOUND, { id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const next: Task = { ...existing, status };
|
const next: Task = { ...existing, status };
|
||||||
@@ -38,7 +38,9 @@ export function changeStatus(
|
|||||||
() =>
|
() =>
|
||||||
ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
||||||
(updated) =>
|
(updated) =>
|
||||||
updated ? okAsync(updated) : errAsync(AppError.notFound()),
|
updated
|
||||||
|
? okAsync(updated)
|
||||||
|
: errAsync(appError(AppErrorVariant.NOT_FOUND, { id })),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { err } from "neverthrow";
|
import { err } from "neverthrow";
|
||||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppErrorVariant } from "@/error";
|
||||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||||
import { createTask } from "./createTask";
|
import { createTask } from "./createTask";
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ describe("createTask", () => {
|
|||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
title: " ",
|
title: " ",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.emptyTitle()));
|
expect(result).toEqual(err(appError(AppErrorVariant.EMPTY_TITLE)));
|
||||||
expect(await repo.list(folder)).toEqual([]);
|
expect(await repo.list(folder)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ describe("createTask", () => {
|
|||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
title: undefined,
|
title: undefined,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.emptyTitle()));
|
expect(result).toEqual(err(appError(AppErrorVariant.EMPTY_TITLE)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing folder", async () => {
|
it("rejects missing folder", async () => {
|
||||||
@@ -56,6 +56,6 @@ describe("createTask", () => {
|
|||||||
folderPath: undefined,
|
folderPath: undefined,
|
||||||
title: "Task",
|
title: "Task",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noFolder()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { errAsync, ResultAsync } from "neverthrow";
|
import { errAsync, ResultAsync } from "neverthrow";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppError, AppErrorVariant } from "@/error";
|
||||||
import { Task } from "@/model/task";
|
import { Task } from "@/model/task";
|
||||||
import { ITaskRepository } from "@/ports";
|
import { ITaskRepository } from "@/ports";
|
||||||
|
|
||||||
@@ -13,12 +13,12 @@ export function createTask(
|
|||||||
input: CreateTaskInput,
|
input: CreateTaskInput,
|
||||||
): ResultAsync<Task, AppError> {
|
): ResultAsync<Task, AppError> {
|
||||||
if (input.folderPath === undefined) {
|
if (input.folderPath === undefined) {
|
||||||
return errAsync(AppError.noFolder());
|
return errAsync(appError(AppErrorVariant.NO_FOLDER));
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = input.title?.trim() ?? "";
|
const title = input.title?.trim() ?? "";
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return errAsync(AppError.emptyTitle());
|
return errAsync(appError(AppErrorVariant.EMPTY_TITLE));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResultAsync.fromSafePromise(
|
return ResultAsync.fromSafePromise(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { err, ok } from "neverthrow";
|
import { err, ok } from "neverthrow";
|
||||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppErrorVariant } from "@/error";
|
||||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||||
import { deleteTask } from "./deleteTask";
|
import { deleteTask } from "./deleteTask";
|
||||||
|
|
||||||
@@ -32,12 +32,14 @@ describe("deleteTask", () => {
|
|||||||
expect(await repo.list(folder)).toEqual([]);
|
expect(await repo.list(folder)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns not_found for unknown id", async () => {
|
it("returns not-found for unknown id", async () => {
|
||||||
const result = await deleteTask(repo, {
|
const result = await deleteTask(repo, {
|
||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
id: "missing",
|
id: "missing",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.notFound()));
|
expect(result).toEqual(
|
||||||
|
err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing folder", async () => {
|
it("rejects missing folder", async () => {
|
||||||
@@ -45,7 +47,7 @@ describe("deleteTask", () => {
|
|||||||
folderPath: undefined,
|
folderPath: undefined,
|
||||||
id: "x",
|
id: "x",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noFolder()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing id", async () => {
|
it("rejects missing id", async () => {
|
||||||
@@ -53,6 +55,6 @@ describe("deleteTask", () => {
|
|||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noId()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_ID)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { errAsync, ResultAsync } from "neverthrow";
|
import { errAsync, ResultAsync } from "neverthrow";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppError, AppErrorVariant } from "@/error";
|
||||||
import { ITaskRepository } from "@/ports";
|
import { ITaskRepository } from "@/ports";
|
||||||
|
|
||||||
export type DeleteTaskInput = {
|
export type DeleteTaskInput = {
|
||||||
@@ -12,10 +12,10 @@ export function deleteTask(
|
|||||||
input: DeleteTaskInput,
|
input: DeleteTaskInput,
|
||||||
): ResultAsync<void, AppError> {
|
): ResultAsync<void, AppError> {
|
||||||
if (input.folderPath === undefined) {
|
if (input.folderPath === undefined) {
|
||||||
return errAsync(AppError.noFolder());
|
return errAsync(appError(AppErrorVariant.NO_FOLDER));
|
||||||
}
|
}
|
||||||
if (input.id === undefined) {
|
if (input.id === undefined) {
|
||||||
return errAsync(AppError.noId());
|
return errAsync(appError(AppErrorVariant.NO_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderPath = input.folderPath;
|
const folderPath = input.folderPath;
|
||||||
@@ -24,7 +24,7 @@ export function deleteTask(
|
|||||||
return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
||||||
(existing) => {
|
(existing) => {
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return errAsync(AppError.notFound());
|
return errAsync(appError(AppErrorVariant.NOT_FOUND, { id }));
|
||||||
}
|
}
|
||||||
return ResultAsync.fromSafePromise(repo.delete(folderPath, id));
|
return ResultAsync.fromSafePromise(repo.delete(folderPath, id));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { err } from "neverthrow";
|
import { err } from "neverthrow";
|
||||||
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppErrorVariant } from "@/error";
|
||||||
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
import { FsTaskRepository } from "@/storage/FsTaskRepository";
|
||||||
import { openTask } from "./openTask";
|
import { openTask } from "./openTask";
|
||||||
|
|
||||||
@@ -34,12 +34,14 @@ describe("openTask", () => {
|
|||||||
expect(result.value.task).toEqual(created);
|
expect(result.value.task).toEqual(created);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns not_found for unknown id", async () => {
|
it("returns not-found for unknown id", async () => {
|
||||||
const result = await openTask(repo, {
|
const result = await openTask(repo, {
|
||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
id: "missing",
|
id: "missing",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.notFound()));
|
expect(result).toEqual(
|
||||||
|
err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing folder", async () => {
|
it("rejects missing folder", async () => {
|
||||||
@@ -47,7 +49,7 @@ describe("openTask", () => {
|
|||||||
folderPath: undefined,
|
folderPath: undefined,
|
||||||
id: "x",
|
id: "x",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noFolder()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing id", async () => {
|
it("rejects missing id", async () => {
|
||||||
@@ -55,6 +57,6 @@ describe("openTask", () => {
|
|||||||
folderPath: folder,
|
folderPath: folder,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(err(AppError.noId()));
|
expect(result).toEqual(err(appError(AppErrorVariant.NO_ID)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
import { errAsync, okAsync, ResultAsync } from "neverthrow";
|
||||||
import { AppError } from "@/error";
|
import { appError, AppError, AppErrorVariant } from "@/error";
|
||||||
import { Task } from "@/model/task";
|
import { Task } from "@/model/task";
|
||||||
import { ITaskRepository } from "@/ports";
|
import { ITaskRepository } from "@/ports";
|
||||||
|
|
||||||
@@ -18,10 +18,10 @@ export function openTask(
|
|||||||
input: OpenTaskInput,
|
input: OpenTaskInput,
|
||||||
): ResultAsync<OpenTaskValue, AppError> {
|
): ResultAsync<OpenTaskValue, AppError> {
|
||||||
if (input.folderPath === undefined) {
|
if (input.folderPath === undefined) {
|
||||||
return errAsync(AppError.noFolder());
|
return errAsync(appError(AppErrorVariant.NO_FOLDER));
|
||||||
}
|
}
|
||||||
if (input.id === undefined) {
|
if (input.id === undefined) {
|
||||||
return errAsync(AppError.noId());
|
return errAsync(appError(AppErrorVariant.NO_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderPath = input.folderPath;
|
const folderPath = input.folderPath;
|
||||||
@@ -30,7 +30,7 @@ export function openTask(
|
|||||||
return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
return ResultAsync.fromSafePromise(repo.getById(folderPath, id)).andThen(
|
||||||
(task) => {
|
(task) => {
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return errAsync(AppError.notFound());
|
return errAsync(appError(AppErrorVariant.NOT_FOUND, { id }));
|
||||||
}
|
}
|
||||||
return okAsync({
|
return okAsync({
|
||||||
path: repo.getFilePath(folderPath, task),
|
path: repo.getFilePath(folderPath, task),
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { appError, AppErrorVariant, formatMessage } from "./error";
|
||||||
|
|
||||||
|
describe("formatMessage", () => {
|
||||||
|
it("substitutes placeholders", () => {
|
||||||
|
expect(formatMessage("Task not found: {id}", { id: "abc" })).toBe(
|
||||||
|
"Task not found: abc",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops empty placeholders and cleans spacing", () => {
|
||||||
|
expect(formatMessage("Task not found: {id}", {})).toBe("Task not found:");
|
||||||
|
expect(formatMessage("Invalid message{detail}", {})).toBe(
|
||||||
|
"Invalid message",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
formatMessage("Invalid message{detail}", { detail: ": explode" }),
|
||||||
|
).toBe("Invalid message: explode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("appError", () => {
|
||||||
|
it("builds variant + message", () => {
|
||||||
|
expect(appError(AppErrorVariant.NOT_FOUND, { id: "x" })).toEqual({
|
||||||
|
variant: AppErrorVariant.NOT_FOUND,
|
||||||
|
message: "Task not found: x",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works without params", () => {
|
||||||
|
expect(appError(AppErrorVariant.NO_FOLDER)).toEqual({
|
||||||
|
variant: AppErrorVariant.NO_FOLDER,
|
||||||
|
message: "Task folder is not set",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+53
-16
@@ -1,20 +1,57 @@
|
|||||||
/** All application-level error codes. */
|
export const AppErrorVariant = {
|
||||||
export type AppErrorCode =
|
NO_FOLDER: "no-folder",
|
||||||
| "no_folder"
|
NO_ID: "no-id",
|
||||||
| "no_id"
|
NO_STATUS: "no-status",
|
||||||
| "no_status"
|
NO_DESCRIPTION: "no-description",
|
||||||
| "empty_title"
|
EMPTY_TITLE: "empty-title",
|
||||||
| "not_found";
|
NOT_FOUND: "not-found",
|
||||||
|
INVALID_MESSAGE: "invalid-message",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AppErrorVariant =
|
||||||
|
(typeof AppErrorVariant)[keyof typeof AppErrorVariant];
|
||||||
|
|
||||||
/** Discriminated app error returned via neverthrow `Result` / `ResultAsync`. */
|
|
||||||
export type AppError = {
|
export type AppError = {
|
||||||
readonly code: AppErrorCode;
|
readonly variant: AppErrorVariant;
|
||||||
|
readonly message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppError = {
|
export type AppErrorParams = Record<string, string | number | undefined>;
|
||||||
noFolder: (): AppError => ({ code: "no_folder" }),
|
|
||||||
noId: (): AppError => ({ code: "no_id" }),
|
/** Message templates. Placeholders: `{name}` — replaced via `appError(variant, params)`. */
|
||||||
noStatus: (): AppError => ({ code: "no_status" }),
|
const APP_ERROR_MESSAGES: Record<AppErrorVariant, string> = {
|
||||||
emptyTitle: (): AppError => ({ code: "empty_title" }),
|
[AppErrorVariant.NO_FOLDER]: "Task folder is not set",
|
||||||
notFound: (): AppError => ({ code: "not_found" }),
|
[AppErrorVariant.NO_ID]: "Task id is required",
|
||||||
} as const;
|
[AppErrorVariant.NO_STATUS]: "Task status is required",
|
||||||
|
[AppErrorVariant.NO_DESCRIPTION]: "Task description is required",
|
||||||
|
[AppErrorVariant.EMPTY_TITLE]: "Task title is empty",
|
||||||
|
[AppErrorVariant.NOT_FOUND]: "Task not found: {id}",
|
||||||
|
[AppErrorVariant.INVALID_MESSAGE]: "Invalid message: {detail}",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatMessage(
|
||||||
|
template: string,
|
||||||
|
params: AppErrorParams = {},
|
||||||
|
): string {
|
||||||
|
return template
|
||||||
|
.replace(/\{(\w+)\}/g, (_, key: string) => {
|
||||||
|
const value = params[key];
|
||||||
|
if (value === undefined || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
})
|
||||||
|
.replace(/[ \t]+/g, " ")
|
||||||
|
.replace(/\s+([:,;.])/g, "$1")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appError(
|
||||||
|
variant: AppErrorVariant,
|
||||||
|
params?: AppErrorParams,
|
||||||
|
): AppError {
|
||||||
|
return {
|
||||||
|
variant,
|
||||||
|
message: formatMessage(APP_ERROR_MESSAGES[variant], params),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user