Repository: Omochice/deno-redmine
Pull request #194: feat: wiki endpoint
Author: login: Omochice
Created: 2025-05-31T17:16:56Z | Merged: 2025-05-31T17:32:06Z

ssia

- **feat: add type and validator**
- **refactor: add assertResponse wrapper**
- **test: add mock**
- **feat: add show**
- **feat: add create**
- **feat: add update**
- **feat: add list**
- **feat: add delete**
- **feat: add client**
- **docs: update readme**


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added comprehensive support for managing wiki pages via REST API, including listing, viewing, creating, updating, and deleting wiki pages within a project.
  - Introduced a client interface for streamlined wiki page operations.
- **Bug Fixes**
  - Improved error handling and validation for wiki page operations, ensuring clearer error messages and robust request processing.
- **Documentation**
  - Updated documentation to reflect the current implementation status of the Wiki Pages REST API features.
- **Tests**
  - Added extensive tests covering all major wiki page operations, including success and error scenarios.
- **Chores**
  - Introduced data validation and transformation utilities for wiki page data.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/README.md b/README.md
index fcd3a3d..76bee52 100644
--- a/README.md
+++ b/README.md
@@ -65,10 +65,10 @@ Redmine rest api wrapper for deno.
     - [ ] [`PUT`](https://www.redmine.org/projects/redmine/wiki/Rest_Versions#PUT)
     - [ ] [`DELETE`](https://www.redmine.org/projects/redmine/wiki/Rest_Versions#DELETE)
 - [ ] [Wiki Pages](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages)
-  - [ ] [`Getting the pages list of a wiki`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-the-pages-list-of-a-wiki)
-  - [ ] [`Getting a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-a-wiki-page)
-  - [ ] [`Getting an old version of a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-an-old-version-of-a-wiki-page)
-  - [ ] [`Creating or updating a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Creating-or-updating-a-wiki-page)
+  - [x] [`Getting the pages list of a wiki`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-the-pages-list-of-a-wiki)
+  - [x] [`Getting a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-a-wiki-page)
+  - [x] [`Getting an old version of a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-an-old-version-of-a-wiki-page)
+  - [x] [`Creating or updating a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Creating-or-updating-a-wiki-page)
     - [ ] [`Attaching files`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Attaching-files)
   - [ ] [`Deleting a wiki page`](https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Deleting-a-wiki-page)
 - [ ] [Queries](https://www.redmine.org/projects/redmine/wiki/Rest_Queries)
diff --git a/error.ts b/error.ts
index 4f7f9ad..4673d51 100644
--- a/error.ts
+++ b/error.ts
@@ -15,3 +15,11 @@ export class UnprocessableEntityError extends Error {
     super(response.statusText, { cause: response });
   }
 }
+
+export function assertResponse(
+  response: Response,
+): asserts response is Response & { ok: true } {
+  if (!response.ok) {
+    throw new UnprocessableEntityError(response);
+  }
+}
diff --git a/mod.ts b/mod.ts
index df1c218..2f2b955 100644
--- a/mod.ts
+++ b/mod.ts
@@ -3,6 +3,7 @@ import { Client as Issue } from "./issues/mod.ts";
 import { Client as Project } from "./projects/mod.ts";
 import { Client as Tracker } from "./trackers/mod.ts";
 import { Client as IssueTemplate } from "./issue-templates/mod.ts";
+import { Client as Wiki } from "./wiki-pages/mod.ts";
 
 export class Redmine {
   readonly #context: Context;
@@ -10,6 +11,7 @@ export class Redmine {
   readonly project: Project;
   readonly tracker: Tracker;
   readonly issueTemplate: IssueTemplate;
+  readonly wiki: Wiki;
 
   constructor(context: Context) {
     this.#context = context;
@@ -17,5 +19,6 @@ export class Redmine {
     this.project = new Project(this.#context);
     this.tracker = new Tracker(this.#context);
     this.issueTemplate = new IssueTemplate(this.#context);
+    this.wiki = new Wiki(this.#context);
   }
 }
diff --git a/wiki-pages/_mock.ts b/wiki-pages/_mock.ts
new file mode 100644
index 0000000..a2b37c9
--- /dev/null
+++ b/wiki-pages/_mock.ts
@@ -0,0 +1,182 @@
+import { http, HttpResponse } from "npm:msw@2.8.5";
+import { STATUS_CODE } from "jsr:@std/http@1.0.17/status";
+
+export const context = {
+  apiKey: "sample",
+  endpoint: "http://redmine.example.com",
+};
+
+export const validResponseHandelers = [
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/index.json`,
+    () => {
+      return HttpResponse.json({
+        wiki_pages: [
+          {
+            title: "CookBook_documentation",
+            version: 3,
+            created_on: "2023-01-01T00:00:00Z",
+            updated_on: "2023-01-15T10:30:00Z",
+          },
+          {
+            title: "Page_with_an_inline_image",
+            version: 1,
+            created_on: "2023-01-02T00:00:00Z",
+            updated_on: "2023-01-02T00:00:00Z",
+            parent: { title: "CookBook_documentation" },
+          },
+        ],
+      });
+    },
+  ),
+  http.get(`${context.endpoint}/projects/:id/wiki/:page.json`, ({ params }) => {
+    const { id, page } = params;
+    return HttpResponse.json({
+      wiki_page: {
+        title: page,
+        version: 3,
+        text: `# sample page on project ${id}`,
+        author: {
+          id: 1,
+          name: "Admin User",
+          login: "admin",
+        },
+        comments: "Updated via API",
+        created_on: "2023-01-01T00:00:00Z",
+        updated_on: "2023-01-02T00:00:00Z",
+      },
+    });
+  }),
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/:page/:version.json`,
+    ({ params }) => {
+      const { id, page, version } = params;
+      return HttpResponse.json({
+        wiki_page: {
+          title: page,
+          version: Number(version),
+          text: `# sample page on project ${id}`,
+          author: {
+            id: 1,
+            name: "Admin User",
+            login: "admin",
+          },
+          comments: "Updated via API",
+          created_on: "2023-01-01T00:00:00Z",
+          updated_on: "2023-01-02T00:00:00Z",
+        },
+      });
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/create.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      // NOTE: If page is not found, it will create a new page and return 200.
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/update.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
+  http.delete(
+    `${context.endpoint}/projects/:id/wiki/:page.json`,
+    () => {
+      return HttpResponse.json({
+        status: STATUS_CODE.NoContent,
+      });
+    },
+  ),
+];
+
+export const invalidResponseHandlers = [
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/index.json`,
+    () => {
+      return HttpResponse.json({
+        errors: ["sample error"],
+      }, {
+        status: STATUS_CODE.UnprocessableEntity,
+        statusText: "Unprocessable Entity",
+      });
+    },
+  ),
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/:page.json`,
+    () => {
+      return HttpResponse.json({
+        errors: ["sample error"],
+      }, {
+        status: STATUS_CODE.UnprocessableEntity,
+        statusText: "Unprocessable Entity",
+      });
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/:page.json`,
+    () => {
+      return HttpResponse.json({
+        errors: ["sample error"],
+      }, {
+        status: 422,
+        statusText: "Unprocessable Entity",
+      });
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/conflict.json`,
+    () => {
+      return HttpResponse.json({
+        status: STATUS_CODE.Conflict,
+      });
+    },
+  ),
+  http.delete(
+    `${context.endpoint}/projects/:id/wiki/forbidden.json`,
+    () => {
+      return HttpResponse.json({}, {
+        status: STATUS_CODE.Forbidden,
+      });
+    },
+  ),
+  http.delete(
+    `${context.endpoint}/projects/:id/wiki/notfound.json`,
+    () => {
+      return HttpResponse.json({}, {
+        status: STATUS_CODE.NotFound,
+      });
+    },
+  ),
+];
diff --git a/wiki-pages/create.test.ts b/wiki-pages/create.test.ts
new file mode 100644
index 0000000..6ad5b96
--- /dev/null
+++ b/wiki-pages/create.test.ts
@@ -0,0 +1,30 @@
+import { create } from "./create.ts";
+import { assert } from "jsr:@std/assert@1.0.13";
+import {
+  context,
+  invalidResponseHandlers,
+  validResponseHandelers,
+} from "./_mock.ts";
+import { setupServer } from "npm:msw@2.8.5/node";
+
+const server = setupServer();
+server.listen();
+
+Deno.test("POST /project/:id/wiki/:page.json", async (t) => {
+  await t.step("if got 200, should be success", async () => {
+    server.resetHandlers(...validResponseHandelers);
+    const r = await create(context, 1, {
+      title: "create",
+      text: "sample text",
+    });
+    assert(r.isOk());
+  });
+  await t.step("If got 422, should be error", async () => {
+    server.resetHandlers(...invalidResponseHandlers);
+    const r = await create(context, 1, {
+      title: "create",
+      text: "sample text",
+    });
+    assert(r.isErr());
+  });
+});
diff --git a/wiki-pages/create.ts b/wiki-pages/create.ts
new file mode 100644
index 0000000..5b515dd
--- /dev/null
+++ b/wiki-pages/create.ts
@@ -0,0 +1,52 @@
+import { Context } from "../context.ts";
+import { join } from "jsr:@std/path@1.1.0/posix/join";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { assertResponse, convertError } from "../error.ts";
+import { makeWikiPutRequest } from "./validator.ts";
+import { sanitizeTitle, type WikiContent } from "./type.ts";
+
+/**
+ * Create a wiki page in the project
+ * This may throw `Error`
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param wiki Wiki page object
+ */
+export async function createWithError(
+  context: Context,
+  projectId: number,
+  wiki: WikiContent,
+): Promise<void> {
+  const body = makeWikiPutRequest(wiki);
+  const opts = {
+    method: "PUT",
+    headers: {
+      "Content-Type": "application/json",
+      "X-Redmine-API-Key": context.apiKey,
+    },
+    body: JSON.stringify(body),
+  } as const satisfies RequestInit;
+  const url = new URL(
+    join(
+      context.endpoint,
+      "projects",
+      `${projectId}`,
+      "wiki",
+      `${sanitizeTitle(wiki.title)}.json`,
+    ),
+  );
+  assertResponse(await fetch(url, opts));
+}
+
+/**
+ * Create a wiki page in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param wiki Wiki page object
+ */
+export const create = ResultAsync.fromThrowable(
+  createWithError,
+  convertError("unknown error create a wiki page"),
+);
diff --git a/wiki-pages/delete.test.ts b/wiki-pages/delete.test.ts
new file mode 100644
index 0000000..6174562
--- /dev/null
+++ b/wiki-pages/delete.test.ts
@@ -0,0 +1,29 @@
+import { deleteWiki } from "./delete.ts";
+import { assert } from "jsr:@std/assert@1.0.13";
+import {
+  context,
+  invalidResponseHandlers,
+  validResponseHandelers,
+} from "./_mock.ts";
+import { setupServer } from "npm:msw@2.8.5/node";
+
+const server = setupServer();
+server.listen();
+
+Deno.test("DELETE /projects/:id/wiki/:page.json", async (t) => {
+  await t.step("if got 200, should be success", async () => {
+    server.resetHandlers(...validResponseHandelers);
+    const r = await deleteWiki(context, 1, "sample-title");
+    assert(r.isOk());
+  });
+  await t.step("If got 403, should be error", async () => {
+    server.resetHandlers(...invalidResponseHandlers);
+    const r = await deleteWiki(context, 1, "forbidden");
+    assert(r.isErr());
+  });
+  await t.step("If got 404, should be error", async () => {
+    server.resetHandlers(...invalidResponseHandlers);
+    const r = await deleteWiki(context, 1, "notfound");
+    assert(r.isErr());
+  });
+});
diff --git a/wiki-pages/delete.ts b/wiki-pages/delete.ts
new file mode 100644
index 0000000..e06f326
--- /dev/null
+++ b/wiki-pages/delete.ts
@@ -0,0 +1,49 @@
+import { Context } from "../context.ts";
+import { join } from "jsr:@std/path@1.1.0/posix/join";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { assertResponse, convertError } from "../error.ts";
+import { sanitizeTitle } from "./type.ts";
+
+/**
+ * Delete a wiki page in the project
+ * This may throw `Error`
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param title Title for wiki page
+ */
+export async function deleteWikiWithError(
+  context: Context,
+  projectId: number,
+  title: string,
+): Promise<void> {
+  const opts = {
+    method: "DELETE",
+    headers: {
+      "Content-Type": "application/json",
+      "X-Redmine-API-Key": context.apiKey,
+    },
+  } as const satisfies RequestInit;
+  const url = new URL(
+    join(
+      context.endpoint,
+      "projects",
+      `${projectId}`,
+      "wiki",
+      `${sanitizeTitle(title)}.json`,
+    ),
+  );
+  assertResponse(await fetch(url, opts));
+}
+
+/**
+ * Delete a wiki page in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param title Title for wiki page
+ */
+export const deleteWiki = ResultAsync.fromThrowable(
+  deleteWikiWithError,
+  convertError("unknown error delete a wiki page"),
+);
diff --git a/wiki-pages/list.test.ts b/wiki-pages/list.test.ts
new file mode 100644
index 0000000..222dcb9
--- /dev/null
+++ b/wiki-pages/list.test.ts
@@ -0,0 +1,27 @@
+import { fetchList } from "./list.ts";
+import { assert } from "jsr:@std/assert@1.0.13";
+
+import {
+  context,
+  invalidResponseHandlers,
+  validResponseHandelers,
+} from "./_mock.ts";
+import { setupServer } from "npm:msw@2.8.5/node";
+
+const server = setupServer();
+server.listen();
+
+Deno.test("GET /projects/:id/wiki/index.json", async (t) => {
+  await t.step("if got 200, should be success", async () => {
+    server.resetHandlers(...validResponseHandelers);
+    const e = await fetchList(context, 1);
+    assert(e.isOk());
+    assert(e.value.length === 2);
+  });
+  await t.step("If got 422, should be error", async () => {
+    server.resetHandlers(...invalidResponseHandlers);
+    const e = await fetchList(context, 2);
+    assert(e.isErr());
+    assert(e.error.message === "Unprocessable Entity");
+  });
+});
diff --git a/wiki-pages/list.ts b/wiki-pages/list.ts
new file mode 100644
index 0000000..54d0cd9
--- /dev/null
+++ b/wiki-pages/list.ts
@@ -0,0 +1,47 @@
+import { Context } from "../context.ts";
+import { join } from "jsr:@std/path@1.1.0/posix/join";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { assertResponse, convertError } from "../error.ts";
+import { parse } from "jsr:@valibot/valibot@1.1.0";
+import type { Wiki } from "./type.ts";
+import { wikis } from "./validator.ts";
+
+/**
+ * List wiki pages included in the project
+ * This may throw `Error`
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @returns Wiki pages
+ */
+async function fetchListWithError(
+  context: Context,
+  projectId: number,
+): Promise<Wiki[]> {
+  const opts = {
+    method: "GET",
+    headers: {
+      "Content-Type": "application/json",
+      "X-Redmine-API-Key": context.apiKey,
+    },
+  } as const satisfies RequestInit;
+  const url = new URL(
+    join(context.endpoint, "projects", `${projectId}`, "wiki", "index.json"),
+  );
+
+  const r = await fetch(url, opts);
+  assertResponse(r);
+  return parse(wikis, await r.json());
+}
+
+/**
+ * List wiki pages included in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @returns Wiki pages
+ */
+export const fetchList = ResultAsync.fromThrowable(
+  fetchListWithError,
+  convertError("unknown error fetching wiki pages"),
+);
diff --git a/wiki-pages/mod.ts b/wiki-pages/mod.ts
new file mode 100644
index 0000000..d3fbba6
--- /dev/null
+++ b/wiki-pages/mod.ts
@@ -0,0 +1,75 @@
+import type { Context } from "../context.ts";
+import type { WikiContent } from "./type.ts";
+import { fetchList } from "./list.ts";
+import { show } from "./show.ts";
+import { update } from "./update.ts";
+import { create } from "./create.ts";
+import { deleteWiki } from "./delete.ts";
+
+export class Client {
+  readonly #context: Context;
+  constructor(context: Context) {
+    this.#context = context;
+  }
+
+  /**
+   * Fetch all wiki pages included in the project
+   *
+   * @param projectId The project ID
+   * @returns Wiki pages
+   */
+  list(projectId: number): ReturnType<typeof fetchList> {
+    return fetchList(this.#context, projectId);
+  }
+
+  /**
+   * Fetch a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param title The title for queriying
+   * @returns Wiki page
+   */
+  show(projectId: number, title: string): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version: number,
+  ): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version?: number,
+  ): ReturnType<typeof show> {
+    return show(this.#context, projectId, title, version);
+  }
+
+  /**
+   * Update a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  update(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return update(this.#context, projectId, wiki);
+  }
+
+  /**
+   * Create a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  create(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return create(this.#context, projectId, wiki);
+  }
+
+  /**
+   * Delete a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param title Title for wiki page
+   */
+  delete(projectId: number, title: string): ReturnType<typeof deleteWiki> {
+    return deleteWiki(this.#context, projectId, title);
+  }
+}
diff --git a/wiki-pages/show.test.ts b/wiki-pages/show.test.ts
new file mode 100644
index 0000000..8569ccf
--- /dev/null
+++ b/wiki-pages/show.test.ts
@@ -0,0 +1,36 @@
+import { show } from "./show.ts";
+import { assert } from "jsr:@std/assert@1.0.13";
+import {
+  context,
+  invalidResponseHandlers,
+  validResponseHandelers,
+} from "./_mock.ts";
+import { setupServer } from "npm:msw@2.8.5/node";
+
+const server = setupServer();
+server.listen();
+
+Deno.test("GET /projects/:id/wiki/:page.json", async (t) => {
+  await t.step("if got 200, should be success", async () => {
+    server.resetHandlers(...validResponseHandelers);
+    const e = await show(context, 1, "sample-title");
+    assert(e.isOk());
+    assert(e.value.title === "sample-title");
+  });
+  await t.step("If got 422, should be error", async () => {
+    server.resetHandlers(...invalidResponseHandlers);
+    const e = await show(context, 2, "sample-title");
+    assert(e.isErr());
+    assert(e.error.message === "Unprocessable Entity");
+  });
+});
+
+Deno.test("GET /projects/:id/wiki/:page/:version.json", async (t) => {
+  await t.step("if got 200, should be success", async () => {
+    server.resetHandlers(...validResponseHandelers);
+    const e = await show(context, 1, "sample-title", 3);
+    assert(e.isOk());
+    assert(e.value.title === "sample-title");
+    assert(e.value.version === 3);
+  });
+});
diff --git a/wiki-pages/show.ts b/wiki-pages/show.ts
new file mode 100644
index 0000000..e7e97b4
--- /dev/null
+++ b/wiki-pages/show.ts
@@ -0,0 +1,66 @@
+import { Context } from "../context.ts";
+import { join } from "jsr:@std/path@1.1.0/posix/join";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { assertResponse, convertError } from "../error.ts";
+import { parse } from "jsr:@valibot/valibot@1.1.0";
+import { sanitizeTitle, type WikiDetail } from "./type.ts";
+import { wikiDetail } from "./validator.ts";
+
+/**
+ * Show the wiki page in the project
+ * This may throw `Error`
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param title Title for wiki page
+ * @returns Wiki page object
+ */
+async function showWithError(
+  context: Context,
+  projectId: number,
+  title: string,
+  version?: number,
+): Promise<WikiDetail> {
+  const opts = {
+    method: "GET",
+    headers: {
+      "Content-Type": "application/json",
+      "X-Redmine-API-Key": context.apiKey,
+    },
+  } as const satisfies RequestInit;
+  const url = new URL(
+    version == null
+      ? join(
+        context.endpoint,
+        "projects",
+        `${projectId}`,
+        "wiki",
+        `${sanitizeTitle(title)}.json`,
+      )
+      : join(
+        context.endpoint,
+        "projects",
+        `${projectId}`,
+        "wiki",
+        sanitizeTitle(title),
+        `${version}.json`,
+      ),
+  );
+
+  const r = await fetch(url, opts);
+  assertResponse(r);
+  return parse(wikiDetail, await r.json());
+}
+
+/**
+ * Show the wiki page in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param title Title for wiki page
+ * @returns Wiki page object
+ */
+export const show = ResultAsync.fromThrowable(
+  showWithError,
+  convertError("unknown error show a wiki page"),
+);
diff --git a/wiki-pages/type.ts b/wiki-pages/type.ts
new file mode 100644
index 0000000..65902bc
--- /dev/null
+++ b/wiki-pages/type.ts
@@ -0,0 +1,54 @@
+export type Wiki = {
+  title: string;
+  version: number;
+  createdOn: Date;
+  updatedOn: Date;
+  parent?: {
+    title: string;
+  };
+};
+
+export type WikiDetail = {
+  title: string;
+  version: number;
+  text: string;
+  author: Author;
+  comments: string;
+  createdOn: Date;
+  updatedOn: Date;
+  attachments?: Attachment[];
+};
+
+type Author = {
+  id: number;
+  name: string;
+};
+
+type Attachment = {
+  id: number;
+  filename: string;
+};
+
+export type WikiContent = {
+  title: string;
+  text: string;
+  comments?: string;
+  version?: number;
+  parentTitle?: string;
+};
+
+/** Characters that cannot be used in wiki title */
+const bannedCharacters = new Set([".", "/", "?", ";", ":", "|", " "]);
+
+/**
+ * Sanitize wiki title
+ *
+ * @param title sanitize target
+ * @returns Sanitized one
+ */
+export function sanitizeTitle(title: string): string {
+  return bannedCharacters.values().toArray().reduce(
+    (t, s) => t.replaceAll(s, "_"),
+    title,
+  );
+}
diff --git a/wiki-pages/update.ts b/wiki-pages/update.ts
new file mode 100644
index 0000000..2ac3b4a
--- /dev/null
+++ b/wiki-pages/update.ts
@@ -0,0 +1,15 @@
+import { createWithError } from "./create.ts";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { convertError } from "../error.ts";
+
+/**
+ * Update a wiki page in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param wiki Wiki page object
+ */
+export const update = ResultAsync.fromThrowable(
+  createWithError,
+  convertError("unknown error update a wiki page"),
+);
diff --git a/wiki-pages/validator.ts b/wiki-pages/validator.ts
new file mode 100644
index 0000000..0f52ff3
--- /dev/null
+++ b/wiki-pages/validator.ts
@@ -0,0 +1,108 @@
+import {
+  array,
+  custom,
+  number,
+  object,
+  optional,
+  pipe,
+  string,
+  transform,
+} from "jsr:@valibot/valibot@1.1.0";
+import type { SnakeCasedProperties } from "npm:type-fest@4.41.0";
+import {
+  sanitizeTitle,
+  type Wiki,
+  type WikiContent,
+  type WikiDetail,
+} from "./type.ts";
+// NOTE: replace valibot.toCamelCase when implements it
+import { objectToCamel } from "npm:ts-case-convert@2.1.0";
+
+const dateLikeString = pipe(
+  string(),
+  custom((input) => {
+    if (typeof input !== "string") {
+      return false;
+    }
+    return !isNaN(Date.parse(input));
+  }),
+  transform((input: string) => new Date(input)),
+);
+
+const inputWiki = object({
+  wiki_pages: array(object({
+    title: string(),
+    version: number(),
+    created_on: dateLikeString,
+    updated_on: dateLikeString,
+    parent: optional(
+      object({
+        title: string(),
+      }),
+    ),
+  })),
+});
+
+/**
+ * Validate that it is wiki object
+ */
+export const wikis = pipe(
+  inputWiki,
+  transform((input) => {
+    return input.wiki_pages
+      .map((w) => {
+        return objectToCamel(w) satisfies Wiki;
+      });
+  }),
+);
+
+const idName = object({
+  id: number(),
+  name: string(),
+});
+
+const inputWikiDetail = object({
+  wiki_page: object({
+    title: string(),
+    version: number(),
+    text: string(),
+    author: idName,
+    comments: string(),
+    created_on: dateLikeString,
+    updated_on: dateLikeString,
+    attachments: optional(
+      array(object({
+        id: number(),
+        filename: string(),
+      })),
+    ),
+  }),
+});
+
+/**
+ * Validate that it is detailed wiki object
+ */
+export const wikiDetail = pipe(
+  inputWikiDetail,
+  transform((input) => {
+    return objectToCamel(input.wiki_page) satisfies WikiDetail;
+  }),
+);
+
+/**
+ * Make wiki content to object for create/update
+ *
+ * @param wiki Wiki content
+ * @returns Converted one
+ */
+export function makeWikiPutRequest(wiki: WikiContent) {
+  const page = {
+    text: wiki.text,
+    comments: wiki.comments,
+    version: wiki.version,
+    parent_title: wiki.parentTitle == null
+      ? undefined
+      : sanitizeTitle(wiki.parentTitle),
+  } satisfies Omit<SnakeCasedProperties<WikiContent>, "title">;
+  return { wiki_page: page };
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds a new wiki endpoint feature with validators, client methods, and tests for creating, updating, listing, showing, and deleting wiki pages. The changes include new validation definitions and helper functions, endpoint implementations with error conversion wrappers, and updated documentation as well as tests and mocks.

### Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

<details>
<summary>Show a summary per file</summary>

| File                           | Description                                                      |
| ------------------------------ | ---------------------------------------------------------------- |
| wiki-pages/validator.ts        | New schema validators and helper for wiki page validation.      |
| wiki-pages/update.ts           | Implements an update endpoint (currently invoking create logic). |
| wiki-pages/type.ts             | Type definitions and the sanitizeTitle helper for wiki pages.    |
| wiki-pages/show.ts             | Implements the show endpoint with version handling.            |
| wiki-pages/show.test.ts        | Tests for the show endpoint.                                     |
| wiki-pages/mod.ts              | Client wrapper methods for wiki operations.                      |
| wiki-pages/list.ts             | Implements the list endpoint for wiki pages.                     |
| wiki-pages/list.test.ts        | Tests for the list endpoint.                                     |
| wiki-pages/delete.ts          | Implements the delete endpoint for wiki pages.                   |
| wiki-pages/delete.test.ts     | Tests for the delete endpoint.                                   |
| wiki-pages/create.ts          | Implements the create endpoint for wiki pages.                   |
| wiki-pages/create.test.ts     | Tests for the create endpoint.                                   |
| wiki-pages/_mock.ts           | Mocks for responses used in testing wiki endpoints.              |
| mod.ts                        | Integration of the wiki client into the overall Redmine client.    |
| error.ts                      | Error handling helper methods.                                   |
| README.md                     | Documentation updates reflecting the current wiki capabilities.  |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**wiki-pages/_mock.ts:9**
* [nitpick] The variable name 'validResponseHandelrs' appears to be misspelled. Consider renaming it to 'validResponseHandlers' for clarity.
```
export const validResponseHandelrs = [
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-31T17:18:31Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>wiki-pages/delete.test.ts (1)</summary><blockquote>

`14-14`: **Update comment to reflect correct HTTP status for DELETE operations.**

The comment mentions "if got 200, should be success" but DELETE operations typically return HTTP 204 (No Content) for successful deletions, not 200.



```diff
-  await t.step("if got 200, should be success", async () => {
+  await t.step("if got 204, should be success", async () => {
```

</blockquote></details>
<details>
<summary>wiki-pages/list.test.ts (1)</summary><blockquote>

`7-7`: **Fix typo in import name.**

There's a typo in the imported variable name. It should be `validResponseHandlers` instead of `validResponseHandelrs`.



```diff
-  validResponseHandelrs,
+  validResponseHandlers,
```

</blockquote></details>
<details>
<summary>wiki-pages/create.test.ts (2)</summary><blockquote>

`6-6`: **Fix typo in import name.**

Same typo as in other test files. It should be `validResponseHandlers` instead of `validResponseHandelrs`.



```diff
-  validResponseHandelrs,
+  validResponseHandlers,
```

---

`13-13`: **Correct the test description to match actual HTTP method.**

The test description mentions "POST" but based on the mock handlers, wiki page creation uses PUT method. Update the description for accuracy.



```diff
-Deno.test("POST /project/:id/wiki/:page.json", async (t) => {
+Deno.test("PUT /projects/:id/wiki/:page.json", async (t) => {
```

</blockquote></details>
<details>
<summary>wiki-pages/update.ts (1)</summary><blockquote>

`5-11`: **Fix JSDoc comment to reflect update operation.**

The JSDoc comment says "Create a wiki page" but this function is for updating. Update the comment to accurately describe the function's purpose.



```diff
 /**
- * Create a wiki page in the project
+ * Update a wiki page in the project
  *
  * @param context REST endpoint context
  * @param projectId Project identifier
  * @param wiki Wiki page object
  */
```

</blockquote></details>
<details>
<summary>wiki-pages/show.test.ts (1)</summary><blockquote>

`6-6`: **Fix typo in import name.**

Same typo as in other test files. It should be `validResponseHandlers` instead of `validResponseHandelrs`.



```diff
-  validResponseHandelrs,
+  validResponseHandlers,
```

</blockquote></details>
<details>
<summary>wiki-pages/mod.ts (1)</summary><blockquote>

`29-29`: **Fix typo in JSDoc comment.**

There's a typo in the parameter description.



```diff
-   * @param title The title for queriying
+   * @param title The title for querying
```

</blockquote></details>
<details>
<summary>wiki-pages/type.ts (1)</summary><blockquote>

`40-41`: **Consider expanding banned characters set.**

The current banned characters list covers basic problematic characters, but consider if additional characters like `#`, `%`, `&`, or other URL-unsafe characters should be included based on Redmine's wiki title requirements.


```diff
-const bannedCharacters = new Set([".", "/", "?", ";", ":", "|", " "]);
+const bannedCharacters = new Set([".", "/", "?", ";", ":", "|", " ", "#", "%", "&", "*", "+", "=", "@", "[", "]", "{", "}", "\\", "^", "`", "~"]);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9 and 0936ecba7f6f4f2d371a099df896df5d64325772.

</details>

<details>
<summary>📒 Files selected for processing (16)</summary>

* `README.md` (1 hunks)
* `error.ts` (1 hunks)
* `mod.ts` (1 hunks)
* `wiki-pages/_mock.ts` (1 hunks)
* `wiki-pages/create.test.ts` (1 hunks)
* `wiki-pages/create.ts` (1 hunks)
* `wiki-pages/delete.test.ts` (1 hunks)
* `wiki-pages/delete.ts` (1 hunks)
* `wiki-pages/list.test.ts` (1 hunks)
* `wiki-pages/list.ts` (1 hunks)
* `wiki-pages/mod.ts` (1 hunks)
* `wiki-pages/show.test.ts` (1 hunks)
* `wiki-pages/show.ts` (1 hunks)
* `wiki-pages/type.ts` (1 hunks)
* `wiki-pages/update.ts` (1 hunks)
* `wiki-pages/validator.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (11)</summary>

<details>
<summary>mod.ts (2)</summary><blockquote>

<details>
<summary>context.ts (1)</summary>

* `Context` (1-4)

</details>
<details>
<summary>wiki-pages/type.ts (1)</summary>

* `Wiki` (1-9)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/create.test.ts (2)</summary><blockquote>

<details>
<summary>wiki-pages/_mock.ts (2)</summary>

* `validResponseHandelrs` (9-101)
* `invalidResponseHandlers` (103-161)

</details>
<details>
<summary>wiki-pages/create.ts (1)</summary>

* `create` (49-52)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/show.test.ts (2)</summary><blockquote>

<details>
<summary>wiki-pages/_mock.ts (2)</summary>

* `validResponseHandelrs` (9-101)
* `invalidResponseHandlers` (103-161)

</details>
<details>
<summary>wiki-pages/show.ts (1)</summary>

* `show` (63-66)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/update.ts (2)</summary><blockquote>

<details>
<summary>wiki-pages/create.ts (1)</summary>

* `createWithError` (16-40)

</details>
<details>
<summary>error.ts (1)</summary>

* `convertError` (1-11)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/list.test.ts (2)</summary><blockquote>

<details>
<summary>wiki-pages/_mock.ts (2)</summary>

* `validResponseHandelrs` (9-101)
* `invalidResponseHandlers` (103-161)

</details>
<details>
<summary>wiki-pages/list.ts (1)</summary>

* `fetchList` (44-47)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/mod.ts (3)</summary><blockquote>

<details>
<summary>context.ts (1)</summary>

* `Context` (1-4)

</details>
<details>
<summary>wiki-pages/type.ts (1)</summary>

* `WikiContent` (32-38)

</details>
<details>
<summary>wiki-pages/delete.ts (1)</summary>

* `deleteWiki` (46-49)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/show.ts (4)</summary><blockquote>

<details>
<summary>context.ts (1)</summary>

* `Context` (1-4)

</details>
<details>
<summary>wiki-pages/type.ts (2)</summary>

* `WikiDetail` (11-20)
* `sanitizeTitle` (49-54)

</details>
<details>
<summary>error.ts (2)</summary>

* `assertResponse` (19-25)
* `convertError` (1-11)

</details>
<details>
<summary>wiki-pages/validator.ts (1)</summary>

* `wikiDetail` (85-90)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/create.ts (4)</summary><blockquote>

<details>
<summary>context.ts (1)</summary>

* `Context` (1-4)

</details>
<details>
<summary>wiki-pages/type.ts (2)</summary>

* `WikiContent` (32-38)
* `sanitizeTitle` (49-54)

</details>
<details>
<summary>wiki-pages/validator.ts (1)</summary>

* `makeWikiPutRequest` (98-106)

</details>
<details>
<summary>error.ts (2)</summary>

* `assertResponse` (19-25)
* `convertError` (1-11)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/list.ts (4)</summary><blockquote>

<details>
<summary>context.ts (1)</summary>

* `Context` (1-4)

</details>
<details>
<summary>wiki-pages/type.ts (1)</summary>

* `Wiki` (1-9)

</details>
<details>
<summary>error.ts (2)</summary>

* `assertResponse` (19-25)
* `convertError` (1-11)

</details>
<details>
<summary>wiki-pages/validator.ts (1)</summary>

* `wikis` (49-57)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/delete.ts (3)</summary><blockquote>

<details>
<summary>context.ts (1)</summary>

* `Context` (1-4)

</details>
<details>
<summary>wiki-pages/type.ts (1)</summary>

* `sanitizeTitle` (49-54)

</details>
<details>
<summary>error.ts (2)</summary>

* `assertResponse` (19-25)
* `convertError` (1-11)

</details>

</blockquote></details>
<details>
<summary>wiki-pages/validator.ts (1)</summary><blockquote>

<details>
<summary>wiki-pages/type.ts (4)</summary>

* `Wiki` (1-9)
* `WikiDetail` (11-20)
* `WikiContent` (32-38)
* `sanitizeTitle` (49-54)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (26)</summary><blockquote>

<details>
<summary>error.ts (1)</summary>

`19-25`: **LGTM! Well-implemented response assertion function.**

The function properly uses TypeScript's assertion signature to narrow the response type, ensuring type safety while providing clean error handling. The implementation correctly throws `UnprocessableEntityError` for failed responses, maintaining consistency with the existing error handling pattern.

</details>
<details>
<summary>README.md (1)</summary>

`68-73`: **Inconsistency between documentation and implementation.**

The checklist marks several wiki features as implemented, but line 73 shows "Deleting a wiki page" as not implemented (`[ ]`). However, the presence of `wiki-pages/delete.test.ts` suggests that delete functionality has been implemented. Please verify and update the documentation to reflect the actual implementation status.




```shell
#!/bin/bash
# Description: Check if wiki delete functionality is actually implemented
# Expected: Find delete-related implementation files

# Search for delete wiki implementation
fd "delete" wiki-pages/

# Check if delete functionality is exported from wiki-pages module
rg -A 5 "delete" wiki-pages/mod.ts

# Look for delete-related functions in wiki-pages directory
ast-grep --pattern 'function $_($$) {
  $$$
}' wiki-pages/
```


> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>mod.ts (2)</summary>

`6-6`: **Clean integration of Wiki client.**

The import follows the established pattern used for other clients in the module.

---

`14-14`: **Proper client integration and initialization.**

The Wiki client property and initialization follow the same pattern as other clients (Issue, Project, Tracker, IssueTemplate), maintaining consistency in the codebase architecture.




Also applies to: 22-22

</details>
<details>
<summary>wiki-pages/delete.test.ts (1)</summary>

`10-29`: **Excellent test coverage with proper mocking setup.**

The test suite provides comprehensive coverage for success and error scenarios using MSW for HTTP mocking. The structure follows good testing practices with clear test steps and appropriate assertions.

</details>
<details>
<summary>wiki-pages/list.test.ts (1)</summary>

`14-27`: **Well-structured test implementation.**

The test cases properly cover both success and error scenarios with appropriate assertions. The use of msw for mocking HTTP requests is a good practice for isolated testing.

</details>
<details>
<summary>wiki-pages/create.test.ts (1)</summary>

`14-30`: **Test implementation looks good.**

The test logic properly validates both success and error scenarios with appropriate assertions.

</details>
<details>
<summary>wiki-pages/update.ts (1)</summary>

`12-15`: **Good implementation approach.**

Reusing `createWithError` makes sense since HTTP PUT operations can both create and update resources. The error handling pattern is consistent with other functions in the codebase.

</details>
<details>
<summary>wiki-pages/show.test.ts (1)</summary>

`13-26`: **Excellent test coverage.**

The test properly validates both success and error scenarios. The assertion on line 18 that checks `e.value.title === "sample-title"` is particularly good as it verifies the returned data matches expectations.

</details>
<details>
<summary>wiki-pages/show.ts (1)</summary>

`1-67`: **LGTM! Well-structured wiki page retrieval implementation.**

The implementation follows good practices with proper error handling, type safety, and consistent patterns. The conditional URL construction for versioned/non-versioned requests is correctly implemented, and the use of `ResultAsync` provides safe error handling.

</details>
<details>
<summary>wiki-pages/delete.ts (1)</summary>

`1-50`: **LGTM! Clean and consistent delete implementation.**

The delete functionality is correctly implemented with appropriate HTTP method, proper error handling, and consistent patterns with other modules. The use of `Promise<void>` return type is appropriate for DELETE operations.

</details>
<details>
<summary>wiki-pages/list.ts (1)</summary>

`1-48`: **LGTM! Solid implementation for listing wiki pages.**

The list functionality is well-implemented with proper GET method usage, appropriate URL construction with "index.json", and consistent error handling patterns. The use of the `wikis` validator ensures type safety for the returned array.

</details>
<details>
<summary>wiki-pages/create.ts (5)</summary>

`1-6`: **LGTM: Clean imports and dependencies.**

The imports are well-organized and all appear to be used appropriately in the implementation.

---

`30-38`: **LGTM: Proper URL construction with title sanitization.**

The URL construction correctly uses `join` for path building and applies `sanitizeTitle` to ensure the wiki title is URL-safe. The `.json` extension follows REST API conventions.

---

`39-39`: **LGTM: Appropriate error handling with assertResponse.**

Using `assertResponse` provides consistent error handling across the API client, throwing appropriate errors for non-2xx responses.

---

`49-52`: **LGTM: Functional error handling approach.**

The use of `ResultAsync.fromThrowable` with `convertError` provides a clean functional approach to error handling, converting exceptions into structured results.

---

`16-20`: <details>
<summary>✅ Verification successful</summary>

**Verify PUT method usage for wiki page creation.**

Using PUT for creation is unusual but may be correct for Redmine's wiki API. Please confirm this aligns with the Redmine API documentation, as most REST APIs use POST for creation and PUT for updates.

---

🌐 Web query:

```web
Does Redmine API use PUT method for creating wiki pages?
```

💡 Result:

## Redmine API Method for Creating Wiki Pages

**Redmine's REST API uses the PUT method to create or update wiki pages.**

- According to the official Redmine documentation, the API endpoint for creating or updating a wiki page is:

  ```
  PUT /projects/:project/wiki/:page.xml
  ```
  The body of the request should contain the `wiki_page` data, such as the text and optional comments[2].

- The documentation specifies that PUT is used both for creating a new wiki page (if the page does not exist) and for updating an existing one[2][10].

- The process is confirmed by user discussions and API examples, all of which reference using PUT for both creation and updating, rather than POST, which is more traditional for creating new resources in REST APIs[7][10].

### Example

To create a new wiki page named "UsersGuide" in the project "foo", you would send:

```
PUT /projects/foo/wiki/UsersGuide.xml
<wiki_page>
  <text>Example</text>
  <comments>Initial version</comments>
</wiki_page>
```
If the page does not exist, it will be created; if it does, it will be updated[2][10].

## Summary Table

| Action            | HTTP Method | Endpoint                                        |
|-------------------|-------------|-------------------------------------------------|
| Create Wiki Page  | PUT         | /projects/:project/wiki/:page.(xml/json)        |
| Update Wiki Page  | PUT         | /projects/:project/wiki/:page.(xml/json)        |

## Conclusion

Redmine's API uses the **PUT** method for both creating and updating wiki pages, rather than the more conventional POST for creation[2][10]. This is documented in both the official API reference and user-contributed examples.

Citations:

- 1: https://www.redmine.org/boards/1/topics/53751
- 2: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages
- 3: https://www.redmine.org/projects/redmine/wiki/rest_api
- 4: https://www.redmine.org/boards/2/topics/28499
- 5: https://github.com/peplin/redmine_create_wiki_page
- 6: https://www.redmine.org/boards/1/topics/53924
- 7: https://www.redmine.org/boards/2/topics/38673
- 8: https://www.redmine.org/projects/redmine/wiki/redminewikis
- 9: https://tomassetti.me/using-the-redmine-api-to-create-a-page-where-to-quickly-add-and-edit-tasks/
- 10: https://github.com/ADVANTECH-Corp/Redmine-Api/blob/master/RedmineManager.cs

---



</details>

**Confirmation: PUT is correct for wiki page creation**  
Redmine’s REST API uses PUT on `/projects/:project/wiki/:page.(xml|json)` to create (if missing) or update wiki pages. The implementation in `wiki-pages/create.ts` (lines 16–20) aligns with the official docs. No changes needed.

</details>
<details>
<summary>wiki-pages/type.ts (4)</summary>

`1-9`: **LGTM: Well-structured Wiki type definition.**

The `Wiki` type appropriately captures basic wiki metadata with proper optional typing for the parent relationship.

---

`11-20`: **LGTM: Comprehensive WikiDetail type.**

The `WikiDetail` type extends the basic wiki with all necessary content fields. The use of internal `Author` and `Attachment` types provides good type safety.

---

`32-38`: **LGTM: Appropriate WikiContent input type.**

The `WikiContent` type is well-designed for creation/update operations with appropriate optional fields for versioning and hierarchy.

---

`49-54`: **LGTM: Effective title sanitization implementation.**

The `sanitizeTitle` function correctly replaces all banned characters with underscores using a clean functional approach.

</details>
<details>
<summary>wiki-pages/_mock.ts (2)</summary>

`10-31`: **LGTM: Well-structured list endpoint mock.**

The mock handler for the wiki list endpoint provides appropriate test data with proper structure including optional parent relationships.

---

`103-161`: **LGTM: Comprehensive error scenario coverage.**

The invalid response handlers provide good coverage of various HTTP error scenarios (422, 409, 403, 404) that are important for testing error handling.

</details>
<details>
<summary>wiki-pages/validator.ts (3)</summary>

`21-30`: **LGTM: Robust date validation with transformation.**

The `dateLikeString` schema properly validates date-like strings and transforms them to Date objects, providing good type safety.

---

`49-57`: **LGTM: Clean validation and transformation pipeline.**

The `wikis` validator effectively combines validation with snake_case to camelCase transformation, ensuring type safety with the `satisfies` clause.

---

`85-90`: **LGTM: Consistent detailed wiki validation.**

The `wikiDetail` validator follows the same pattern as `wikis`, providing consistent validation and transformation for detailed wiki data.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-31T17:23:21Z
user: login: Omochice
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-31T17:26:03Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-31T17:26:37Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements comprehensive support for managing wiki pages via the REST API by introducing endpoints for listing, showing, updating, creating, and deleting wiki pages, along with corresponding client methods and tests. Key changes include the addition of validation utilities, new client interface methods, and updated documentation and test cases.

### Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                      | Description                                                       |
| ------------------------- | ----------------------------------------------------------------- |
| wiki-pages/validator.ts   | Added validation and transformation logic for wiki payloads.      |
| wiki-pages/update.ts      | Introduced the update endpoint (currently mapping to create logic). |
| wiki-pages/type.ts        | Defined types for wiki pages and provided a title sanitization helper. |
| wiki-pages/show.ts        | Implemented fetching a single wiki page (with version support).     |
| wiki-pages/show.test.ts   | Added tests for the show endpoint.                                  |
| wiki-pages/list.ts        | Implemented the listing of wiki pages.                              |
| wiki-pages/list.test.ts   | Added tests for the list endpoint.                                  |
| wiki-pages/delete.ts      | Implemented deletion of a wiki page.                                |
| wiki-pages/delete.test.ts | Added tests for the delete endpoint.                                |
| wiki-pages/create.ts       | Implemented the create endpoint for wiki pages.                     |
| wiki-pages/create.test.ts  | Added tests for the create endpoint.                                |
| wiki-pages/_mock.ts       | Provided mocked response handlers for testing endpoints.            |
| mod.ts                    | Updated the main module to include the new Wiki client.             |
| error.ts                  | Enhanced error handling with assertions for response objects.       |
| README.md                 | Updated documentation to reflect completed wiki endpoint features.  |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**wiki-pages/_mock.ts:9**
* [nitpick] The variable name 'validResponseHandelers' appears to have a typo. Rename it to 'validResponseHandlers' for clarity and consistency.
```
export const validResponseHandelers = [
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-31T17:29:48Z

Review comments:
diff_hunk: @@ -0,0 +1,106 @@
+import {
+  array,
+  custom,
+  number,
+  object,
+  optional,
+  pipe,
+  string,
+  transform,
+} from "jsr:@valibot/valibot@1.1.0";
+import type { SnakeCasedProperties } from "npm:type-fest@4.41.0";
+import {
+  sanitizeTitle,
+  type Wiki,
+  type WikiContent,
+  type WikiDetail,
+} from "./type.ts";
+// NOTE: replace valibot.toCamelCase when implments it
path: wiki-pages/validator.ts
user: login: Copilot
body: Typo in comment: 'implments' should be 'implements'.
```suggestion
// NOTE: replace valibot.toCamelCase when implements it
```
created_at: 2025-05-31T17:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+import { createWithError } from "./create.ts";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { convertError } from "../error.ts";
+
+/**
+ * Create a wiki page in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param wiki Wiki page object
+ */
+export const update = ResultAsync.fromThrowable(
+  createWithError,
path: wiki-pages/update.ts
user: login: Copilot
body: The update endpoint is currently using createWithError from create.ts. Consider implementing a dedicated update logic to avoid confusion between create and update operations.
```suggestion
import { updateWithError } from "./create.ts";
import { ResultAsync } from "npm:neverthrow@8.2.0";
import { convertError } from "../error.ts";

/**
 * Update a wiki page in the project
 *
 * @param context REST endpoint context
 * @param projectId Project identifier
 * @param wiki Wiki page object
 */
export const update = ResultAsync.fromThrowable(
  updateWithError,
```
created_at: 2025-05-31T17:18:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import type { Context } from "../context.ts";
+import type { WikiContent } from "./type.ts";
+import { fetchList } from "./list.ts";
+import { show } from "./show.ts";
+import { update } from "./update.ts";
+import { create } from "./create.ts";
+import { deleteWiki } from "./delete.ts";
+
+export class Client {
+  readonly #context: Context;
+  constructor(context: Context) {
+    this.#context = context;
+  }
+
+  /**
+   * Fetch all wiki pages included in the project
+   *
+   * @param projectId The project ID
+   * @returns Wiki pages
+   */
+  list(projectId: number): ReturnType<typeof fetchList> {
+    return fetchList(this.#context, projectId);
+  }
+
+  /**
+   * Fetch a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param title The title for queriying
+   * @returns Wiki page
+   */
+  show(projectId: number, title: string): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version: number,
+  ): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version?: number,
+  ): ReturnType<typeof show> {
+    return show(this.#context, projectId, title, version);
+  }
+
+  /**
+   * Create a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  update(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return update(this.#context, projectId, wiki);
+  }
+
+  /**
+   * Update a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  create(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return create(this.#context, projectId, wiki);
+  }
+
+  /**
path: wiki-pages/mod.ts
user: login: Copilot
body: The doc comment for the create method incorrectly mentions 'Update a wiki page'. Update the comment to accurately reflect that this method creates a wiki page.
created_at: 2025-05-31T17:18:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+export type Wiki = {
+  title: string;
+  version: number;
+  createdOn: Date;
+  updatedOn: Date;
+  parent?: {
+    title: string;
+  };
+};
+
+export type WikiDetail = {
+  title: string;
+  version: number;
+  text: string;
+  author: Author;
+  comments: string;
+  createdOn: Date;
+  updatedOn: Date;
+  attachments?: Attachment[];
+};
+
+type Author = {
+  id: number;
+  name: string;
+};
+
+type Attachment = {
+  id: number;
+  filename: string;
+};
+
+export type WikiContent = {
+  title: string;
+  text: string;
+  comments?: string;
+  version?: number;
+  parentTitle?: string;
+};
+
+/** Characters that cannot be used in wiki title */
+const bannedCharacters = new Set([".", "/", "?", ";", ":", "|", " "]);
+
+/**
+ * Sanitize wiki title
+ *
+ * @param title sanitize target
+ * @returns Sanitized one
+ */
+export function sanitizeTitle(title: string): string {
+  return bannedCharacters.values().toArray().reduce(
path: wiki-pages/type.ts
user: login: Copilot
body: The expression 'bannedCharacters.values().toArray()' may not work as expected since toArray() is not a standard method. Consider using Array.from(bannedCharacters) or the spread operator ([...bannedCharacters]) instead.
```suggestion
  return [...bannedCharacters.values()].reduce(
```
created_at: 2025-05-31T17:18:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+import { deleteWiki } from "./delete.ts";
+import { assert } from "jsr:@std/assert@1.0.13";
+import {
+  context,
+  invalidResponseHandlers,
+  validResponseHandelrs,
path: wiki-pages/delete.test.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix typo in variable name.**

There's a typo in the import: `validResponseHandelrs` should be `validResponseHandlers` (missing 'r').



```diff
-  validResponseHandelrs,
+  validResponseHandlers,
```

Also update line 15:

```diff
-    server.resetHandlers(...validResponseHandelrs);
+    server.resetHandlers(...validResponseHandlers);
```


Also applies to: 15-15

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/delete.test.ts at lines 6 and 15, fix the typo in the variable
name from validResponseHandelrs to validResponseHandlers by adding the missing
'r' in both import and usage to ensure the correct variable is referenced.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T17:23:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import type { Context } from "../context.ts";
+import type { WikiContent } from "./type.ts";
+import { fetchList } from "./list.ts";
+import { show } from "./show.ts";
+import { update } from "./update.ts";
+import { create } from "./create.ts";
+import { deleteWiki } from "./delete.ts";
+
+export class Client {
+  readonly #context: Context;
+  constructor(context: Context) {
+    this.#context = context;
+  }
+
+  /**
+   * Fetch all wiki pages included in the project
+   *
+   * @param projectId The project ID
+   * @returns Wiki pages
+   */
+  list(projectId: number): ReturnType<typeof fetchList> {
+    return fetchList(this.#context, projectId);
+  }
+
+  /**
+   * Fetch a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param title The title for queriying
+   * @returns Wiki page
+   */
+  show(projectId: number, title: string): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version: number,
+  ): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version?: number,
+  ): ReturnType<typeof show> {
+    return show(this.#context, projectId, title, version);
+  }
+
+  /**
+   * Create a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  update(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return update(this.#context, projectId, wiki);
+  }
path: wiki-pages/mod.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Correct the JSDoc comment for the update method.**

The JSDoc comment incorrectly describes this as "Create a wiki page" when it should describe updating.



```diff
   /**
-   * Create a wiki page in the project
+   * Update a wiki page in the project
    *
    * @param projectId The project ID
    * @param wiki wiki content object
    */
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  /**
   * Update a wiki page in the project
   *
   * @param projectId The project ID
   * @param wiki wiki content object
   */
  update(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
    return update(this.#context, projectId, wiki);
  }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/mod.ts around lines 46 to 54, the JSDoc comment for the update
method incorrectly states it creates a wiki page. Update the comment to
accurately describe that this method updates a wiki page in the project,
reflecting the method's actual functionality.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T17:23:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import type { Context } from "../context.ts";
+import type { WikiContent } from "./type.ts";
+import { fetchList } from "./list.ts";
+import { show } from "./show.ts";
+import { update } from "./update.ts";
+import { create } from "./create.ts";
+import { deleteWiki } from "./delete.ts";
+
+export class Client {
+  readonly #context: Context;
+  constructor(context: Context) {
+    this.#context = context;
+  }
+
+  /**
+   * Fetch all wiki pages included in the project
+   *
+   * @param projectId The project ID
+   * @returns Wiki pages
+   */
+  list(projectId: number): ReturnType<typeof fetchList> {
+    return fetchList(this.#context, projectId);
+  }
+
+  /**
+   * Fetch a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param title The title for queriying
+   * @returns Wiki page
+   */
+  show(projectId: number, title: string): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version: number,
+  ): ReturnType<typeof show>;
+  show(
+    projectId: number,
+    title: string,
+    version?: number,
+  ): ReturnType<typeof show> {
+    return show(this.#context, projectId, title, version);
+  }
+
+  /**
+   * Create a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  update(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return update(this.#context, projectId, wiki);
+  }
+
+  /**
+   * Update a wiki page in the project
+   *
+   * @param projectId The project ID
+   * @param wiki wiki content object
+   */
+  create(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+    return create(this.#context, projectId, wiki);
+  }
path: wiki-pages/mod.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix JSDoc comment and return type for the create method.**

The JSDoc comment incorrectly describes this as "Update a wiki page" and the return type annotation is wrong.



```diff
   /**
-   * Update a wiki page in the project
+   * Create a wiki page in the project
    *
    * @param projectId The project ID
    * @param wiki wiki content object
    */
-  create(projectId: number, wiki: WikiContent): ReturnType<typeof update> {
+  create(projectId: number, wiki: WikiContent): ReturnType<typeof create> {
     return create(this.#context, projectId, wiki);
   }
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/mod.ts around lines 56 to 64, the JSDoc comment for the create
method incorrectly states it updates a wiki page and the return type is wrongly
set as the return type of update. Change the JSDoc comment to accurately
describe that this method creates a wiki page, and update the return type
annotation to match the actual return type of the create function being called.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T17:23:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+import { http, HttpResponse } from "npm:msw@2.8.5";
+import { STATUS_CODE } from "jsr:@std/http@1.0.17/status";
+
+export const context = {
+  apiKey: "sample",
+  endpoint: "http://redmine.example.com",
+};
+
+export const validResponseHandelrs = [
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/index.json`,
+    () => {
+      return HttpResponse.json({
+        wiki_pages: [
+          {
+            title: "CookBook_documentation",
+            version: 3,
+            created_on: "2023-01-01T00:00:00Z",
+            updated_on: "2023-01-15T10:30:00Z",
+          },
+          {
+            title: "Page_with_an_inline_image",
+            version: 1,
+            created_on: "2023-01-02T00:00:00Z",
+            updated_on: "2023-01-02T00:00:00Z",
+            parent: { title: "CookBook_documentation" },
+          },
+        ],
+      });
+    },
+  ),
+  http.get(`${context.endpoint}/projects/:id/wiki/:page.json`, ({ params }) => {
+    const { id, page } = params;
+    return HttpResponse.json({
+      wiki_page: {
+        title: page,
+        version: 3,
+        text: `# sample page on project ${id}`,
+        author: {
+          id: 1,
+          name: "Admin User",
+          login: "admin",
+        },
+        comments: "Updated via API",
+        created_on: "2023-01-01T00:00:00Z",
+        updated_on: "2023-01-02T00:00:00Z",
+      },
+    });
+  }),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/create.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      // NOTE: If page is not found, it will create a new page and return 200.
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/update.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
path: wiki-pages/_mock.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inconsistent endpoint naming in update handler.**

Similar to the create handler, the update handler uses `/projects/:id/wiki/update.json` which doesn't match the actual implementation pattern.


```diff
-    `${context.endpoint}/projects/:id/wiki/update.json`,
+    `${context.endpoint}/projects/:id/wiki/:page.json`,
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  http.put(
    `${context.endpoint}/projects/:id/wiki/:page.json`,
    async ({ request }) => {
      const r = await request.json() as unknown as {
        wiki_page: {
          text: string;
          comments?: string;
          version?: number;
          parent_title?: string;
        };
      };
      return HttpResponse.json(
        (r.wiki_page.comments ?? "").length > 1024
          ? {
            errors: ["Comment is too long"],
          }
          : {},
        { status: STATUS_CODE.OK },
      );
    },
  ),
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/_mock.ts between lines 72 and 92, the update handler uses the
endpoint path `/projects/:id/wiki/update.json`, which is inconsistent with the
actual implementation pattern used elsewhere. To fix this, rename the endpoint
to match the correct pattern used in the create handler and other related
handlers, ensuring consistent RESTful naming conventions across the API routes.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-31T17:23:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+import { http, HttpResponse } from "npm:msw@2.8.5";
+import { STATUS_CODE } from "jsr:@std/http@1.0.17/status";
+
+export const context = {
+  apiKey: "sample",
+  endpoint: "http://redmine.example.com",
+};
+
+export const validResponseHandelrs = [
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/index.json`,
+    () => {
+      return HttpResponse.json({
+        wiki_pages: [
+          {
+            title: "CookBook_documentation",
+            version: 3,
+            created_on: "2023-01-01T00:00:00Z",
+            updated_on: "2023-01-15T10:30:00Z",
+          },
+          {
+            title: "Page_with_an_inline_image",
+            version: 1,
+            created_on: "2023-01-02T00:00:00Z",
+            updated_on: "2023-01-02T00:00:00Z",
+            parent: { title: "CookBook_documentation" },
+          },
+        ],
+      });
+    },
+  ),
+  http.get(`${context.endpoint}/projects/:id/wiki/:page.json`, ({ params }) => {
+    const { id, page } = params;
+    return HttpResponse.json({
+      wiki_page: {
+        title: page,
+        version: 3,
+        text: `# sample page on project ${id}`,
+        author: {
+          id: 1,
+          name: "Admin User",
+          login: "admin",
+        },
+        comments: "Updated via API",
+        created_on: "2023-01-01T00:00:00Z",
+        updated_on: "2023-01-02T00:00:00Z",
+      },
+    });
+  }),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/create.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      // NOTE: If page is not found, it will create a new page and return 200.
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
path: wiki-pages/_mock.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inconsistent endpoint naming in create handler.**

The create handler uses `/projects/:id/wiki/create.json` endpoint, but this doesn't match the URL pattern used in the actual implementation which uses `/projects/:id/wiki/:title.json`. Consider aligning the mock with the actual implementation.


```diff
-    `${context.endpoint}/projects/:id/wiki/create.json`,
+    `${context.endpoint}/projects/:id/wiki/:page.json`,
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/_mock.ts between lines 50 and 71, the create handler uses the
endpoint /projects/:id/wiki/create.json which is inconsistent with the actual
implementation that uses /projects/:id/wiki/:title.json. Update the mock
endpoint URL to match the actual pattern by replacing /create.json with
/:title.json to ensure consistency and correct routing.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T17:23:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+import { http, HttpResponse } from "npm:msw@2.8.5";
+import { STATUS_CODE } from "jsr:@std/http@1.0.17/status";
+
+export const context = {
+  apiKey: "sample",
+  endpoint: "http://redmine.example.com",
+};
+
+export const validResponseHandelrs = [
path: wiki-pages/_mock.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix typo in export name.**

There's a typo in the export name: `validResponseHandelrs` should be `validResponseHandlers`.


```diff
-export const validResponseHandelrs = [
+export const validResponseHandlers = [
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/_mock.ts at line 9, fix the typo in the export name by renaming
`validResponseHandelrs` to `validResponseHandlers` to correct the spelling.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T17:23:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+import {
+  array,
+  custom,
+  number,
+  object,
+  optional,
+  pipe,
+  string,
+  transform,
+} from "jsr:@valibot/valibot@1.1.0";
+import type { SnakeCasedProperties } from "npm:type-fest@4.41.0";
+import {
+  sanitizeTitle,
+  type Wiki,
+  type WikiContent,
+  type WikiDetail,
+} from "./type.ts";
+// NOTE: replace valibot.toCamelCase when implments it
+import { objectToCamel } from "npm:ts-case-convert@2.1.0";
+
+const dateLikeString = pipe(
+  string(),
+  custom((input) => {
+    if (typeof input !== "string") {
+      return false;
+    }
+    return !isNaN(Date.parse(input));
+  }),
+  transform((input: string) => new Date(input)),
+);
+
+const inputWiki = object({
+  wiki_pages: array(object({
+    title: string(),
+    version: number(),
+    created_on: dateLikeString,
+    updated_on: dateLikeString,
+    parent: optional(
+      object({
+        title: string(),
+      }),
+    ),
+  })),
+});
+
+/**
+ * Validate that it is wiki object
+ */
+export const wikis = pipe(
+  inputWiki,
+  transform((input) => {
+    return input.wiki_pages
+      .map((w) => {
+        return objectToCamel(w) satisfies Wiki;
+      });
+  }),
+);
+
+const idName = object({
+  id: number(),
+  name: string(),
+});
+
+const inputWikiDetail = object({
+  wiki_page: object({
+    title: string(),
+    version: number(),
+    text: string(),
+    author: idName,
+    comments: string(),
+    created_on: dateLikeString,
+    updated_on: dateLikeString,
+    attachments: optional(
+      array(object({
+        id: number(),
+        filename: string(),
+      })),
+    ),
+  }),
+});
+
+/**
+ * Validate that it is detailed wiki object
+ */
+export const wikiDetail = pipe(
+  inputWikiDetail,
+  transform((input) => {
+    return objectToCamel(input.wiki_page) satisfies WikiDetail;
+  }),
+);
+
+/**
+ * Make wiki content to object for create/update
+ *
+ * @param wiki Wiki content
+ * @returns Converted one
+ */
+export function makeWikiPutRequest(wiki: WikiContent) {
+  const page = {
+    text: sanitizeTitle(wiki.text),
+    comments: wiki.comments,
+    version: wiki.version,
+    parent_title: wiki.parentTitle,
+  } satisfies Omit<SnakeCasedProperties<WikiContent>, "title">;
+  return { wiki_page: page };
+}
path: wiki-pages/validator.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Critical bug: Wrong field being sanitized.**

The `makeWikiPutRequest` function is calling `sanitizeTitle` on `wiki.text` instead of `wiki.title`. The text content should not be sanitized, only the title should be sanitized for URL construction.


```diff
 export function makeWikiPutRequest(wiki: WikiContent) {
   const page = {
-    text: sanitizeTitle(wiki.text),
+    text: wiki.text,
     comments: wiki.comments,
     version: wiki.version,
     parent_title: wiki.parentTitle,
   } satisfies Omit<SnakeCasedProperties<WikiContent>, "title">;
   return { wiki_page: page };
 }
```

Note: The title sanitization should happen during URL construction, not in the request body preparation.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
export function makeWikiPutRequest(wiki: WikiContent) {
  const page = {
    text: wiki.text,
    comments: wiki.comments,
    version: wiki.version,
    parent_title: wiki.parentTitle,
  } satisfies Omit<SnakeCasedProperties<WikiContent>, "title">;
  return { wiki_page: page };
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In wiki-pages/validator.ts around lines 98 to 106, the function incorrectly
sanitizes wiki.text instead of wiki.title. Remove the sanitizeTitle call from
the text field and ensure that only the title is sanitized during URL
construction, not in the request body. Update the page object to assign
wiki.text directly to text and remove sanitization from this function.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T17:23:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+import { http, HttpResponse } from "npm:msw@2.8.5";
+import { STATUS_CODE } from "jsr:@std/http@1.0.17/status";
+
+export const context = {
+  apiKey: "sample",
+  endpoint: "http://redmine.example.com",
+};
+
+export const validResponseHandelrs = [
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/index.json`,
+    () => {
+      return HttpResponse.json({
+        wiki_pages: [
+          {
+            title: "CookBook_documentation",
+            version: 3,
+            created_on: "2023-01-01T00:00:00Z",
+            updated_on: "2023-01-15T10:30:00Z",
+          },
+          {
+            title: "Page_with_an_inline_image",
+            version: 1,
+            created_on: "2023-01-02T00:00:00Z",
+            updated_on: "2023-01-02T00:00:00Z",
+            parent: { title: "CookBook_documentation" },
+          },
+        ],
+      });
+    },
+  ),
+  http.get(`${context.endpoint}/projects/:id/wiki/:page.json`, ({ params }) => {
+    const { id, page } = params;
+    return HttpResponse.json({
+      wiki_page: {
+        title: page,
+        version: 3,
+        text: `# sample page on project ${id}`,
+        author: {
+          id: 1,
+          name: "Admin User",
+          login: "admin",
+        },
+        comments: "Updated via API",
+        created_on: "2023-01-01T00:00:00Z",
+        updated_on: "2023-01-02T00:00:00Z",
+      },
+    });
+  }),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/create.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      // NOTE: If page is not found, it will create a new page and return 200.
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/update.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
path: wiki-pages/_mock.ts
user: login: Omochice
body: update and create has same endpoint.
we use `update` and `create` to specify between them.
created_at: 2025-05-31T17:26:03Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,161 @@
+import { http, HttpResponse } from "npm:msw@2.8.5";
+import { STATUS_CODE } from "jsr:@std/http@1.0.17/status";
+
+export const context = {
+  apiKey: "sample",
+  endpoint: "http://redmine.example.com",
+};
+
+export const validResponseHandelrs = [
+  http.get(
+    `${context.endpoint}/projects/:id/wiki/index.json`,
+    () => {
+      return HttpResponse.json({
+        wiki_pages: [
+          {
+            title: "CookBook_documentation",
+            version: 3,
+            created_on: "2023-01-01T00:00:00Z",
+            updated_on: "2023-01-15T10:30:00Z",
+          },
+          {
+            title: "Page_with_an_inline_image",
+            version: 1,
+            created_on: "2023-01-02T00:00:00Z",
+            updated_on: "2023-01-02T00:00:00Z",
+            parent: { title: "CookBook_documentation" },
+          },
+        ],
+      });
+    },
+  ),
+  http.get(`${context.endpoint}/projects/:id/wiki/:page.json`, ({ params }) => {
+    const { id, page } = params;
+    return HttpResponse.json({
+      wiki_page: {
+        title: page,
+        version: 3,
+        text: `# sample page on project ${id}`,
+        author: {
+          id: 1,
+          name: "Admin User",
+          login: "admin",
+        },
+        comments: "Updated via API",
+        created_on: "2023-01-01T00:00:00Z",
+        updated_on: "2023-01-02T00:00:00Z",
+      },
+    });
+  }),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/create.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      // NOTE: If page is not found, it will create a new page and return 200.
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
+  http.put(
+    `${context.endpoint}/projects/:id/wiki/update.json`,
+    async ({ request }) => {
+      const r = await request.json() as unknown as {
+        wiki_page: {
+          text: string;
+          comments?: string;
+          version?: number;
+          parent_title?: string;
+        };
+      };
+      return HttpResponse.json(
+        (r.wiki_page.comments ?? "").length > 1024
+          ? {
+            errors: ["Comment is too long"],
+          }
+          : {},
+        { status: STATUS_CODE.OK },
+      );
+    },
+  ),
path: wiki-pages/_mock.ts
user: login: coderabbitai[bot]
body: @Omochice Thank you for the clarification! I understand now that using separate endpoints `/projects/:id/wiki/create.json` and `/projects/:id/wiki/update.json` is intentional to distinguish between create and update operations, even though they share the same HTTP method. This makes sense for your API design.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Omochice
PR: Omochice/deno-redmine#194
File: wiki-pages/_mock.ts:93-113
Timestamp: 2025-05-31T17:26:35.717Z
Learning: In the deno-redmine project, wiki create and update operations use separate endpoints (`/projects/:id/wiki/create.json` and `/projects/:id/wiki/update.json`) with the same HTTP PUT method to distinguish between the two operations, rather than using a single endpoint with different logic.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Omochice Thank you for the clarification! I understand now that using separate endpoints `/projects/:id/wiki/create.json` and `/projects/:id/wiki/update.json` is intentional to distinguish between create and update operations, even though they share the same HTTP method. This makes sense for your API design.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Omochice
PR: Omochice/deno-redmine#194
File: wiki-pages/_mock.ts:93-113
Timestamp: 2025-05-31T17:26:35.717Z
Learning: In the deno-redmine project, wiki create and update operations use separate endpoints (`/projects/:id/wiki/create.json` and `/projects/:id/wiki/update.json`) with the same HTTP PUT method to distinguish between the two operations, rather than using a single endpoint with different logic.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-31T17:26:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+export type Wiki = {
+  title: string;
+  version: number;
+  createdOn: Date;
+  updatedOn: Date;
+  parent?: {
+    title: string;
+  };
+};
+
+export type WikiDetail = {
+  title: string;
+  version: number;
+  text: string;
+  author: Author;
+  comments: string;
+  createdOn: Date;
+  updatedOn: Date;
+  attachments?: Attachment[];
+};
+
+type Author = {
+  id: number;
+  name: string;
+};
+
+type Attachment = {
+  id: number;
+  filename: string;
+};
+
+export type WikiContent = {
+  title: string;
+  text: string;
+  comments?: string;
+  version?: number;
+  parentTitle?: string;
+};
+
+/** Characters that cannot be used in wiki title */
+const bannedCharacters = new Set([".", "/", "?", ";", ":", "|", " "]);
+
+/**
+ * Sanitize wiki title
+ *
+ * @param title sanitize target
+ * @returns Sanitized one
+ */
+export function sanitizeTitle(title: string): string {
+  return bannedCharacters.values().toArray().reduce(
path: wiki-pages/type.ts
user: login: Copilot
body: The use of 'toArray()' on the Set iterator is invalid. Use Array.from(bannedCharacters.values()) instead to properly convert the iterator into an array.
```suggestion
  return Array.from(bannedCharacters.values()).reduce(
```
created_at: 2025-05-31T17:29:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+import { createWithError } from "./create.ts";
+import { ResultAsync } from "npm:neverthrow@8.2.0";
+import { convertError } from "../error.ts";
+
+/**
+ * Update a wiki page in the project
+ *
+ * @param context REST endpoint context
+ * @param projectId Project identifier
+ * @param wiki Wiki page object
+ */
+export const update = ResultAsync.fromThrowable(
+  createWithError,
path: wiki-pages/update.ts
user: login: Copilot
body: The update endpoint is currently using 'createWithError' from the create module. Consider implementing a separate update-specific function and endpoint that maps to '/wiki/update.json' as expected by the API.
```suggestion
const updateWithError = (context: any, projectId: string, wiki: any) => {
  // Implement update logic here
  if (!context || !projectId || !wiki) {
    throw new Error("Invalid parameters for updating a wiki page");
  }
  // Example logic: Update the wiki page in the database or API
  return { success: true, updatedWiki: wiki };
};

export const update = ResultAsync.fromThrowable(
  updateWithError,
```
created_at: 2025-05-31T17:29:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-31T17:17:02Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->

> [!WARNING]
> ## Rate limit exceeded
> 
> @Omochice has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait **16 minutes and 33 seconds** before requesting another review.
> 
> <details>
> <summary>⌛ How to resolve this issue?</summary>
> 
> After the wait time has elapsed, a review can be triggered using the `@coderabbitai review` command as a PR comment. Alternatively, push new commits to this PR.
> 
> We recommend that you space out your commits to avoid hitting the rate limit.
> 
> </details>
> 
> 
> <details>
> <summary>🚦 How do rate limits work?</summary>
> 
> CodeRabbit enforces hourly rate limits for each developer per organization.
> 
> Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
> 
> Please see our [FAQ](https://docs.coderabbit.ai/faq) for further information.
> 
> </details>
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between 0936ecba7f6f4f2d371a099df896df5d64325772 and f29bb854ba3319b6092f4dfe404d70e5e2184ff8.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (8)</summary>
> 
> * `wiki-pages/_mock.ts` (1 hunks)
> * `wiki-pages/create.test.ts` (1 hunks)
> * `wiki-pages/delete.test.ts` (1 hunks)
> * `wiki-pages/list.test.ts` (1 hunks)
> * `wiki-pages/mod.ts` (1 hunks)
> * `wiki-pages/show.test.ts` (1 hunks)
> * `wiki-pages/update.ts` (1 hunks)
> * `wiki-pages/validator.ts` (1 hunks)
> 
> </details>

<!-- end of auto-generated comment: rate limited by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a comprehensive REST API client for managing wiki pages within projects. It adds modules for listing, showing, creating, updating, and deleting wiki pages, including type definitions, validation logic, and error handling. A mock server and extensive test suites are provided for robust testing. Documentation is updated to reflect the new API coverage.

## Changes

| File(s)                                         | Change Summary                                                                                                      |
|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
| README.md                                       | Updated checklist to mark Wiki Pages API features as implemented.                                                   |
| error.ts                                        | Added `assertResponse` function for response validation and error throwing.                                         |
| mod.ts                                          | Extended `Redmine` class with a `wiki` client property for wiki page operations.                                    |
| wiki-pages/type.ts                              | Defined types: `Wiki`, `WikiDetail`, `WikiContent`, and `sanitizeTitle` utility for title sanitization.            |
| wiki-pages/validator.ts                         | Added Valibot-based validators for wiki data, a request builder, and camelCase transformation utilities.            |
| wiki-pages/list.ts, wiki-pages/show.ts          | Implemented `fetchList` and `show` functions to retrieve wiki pages and details with error handling.                |
| wiki-pages/create.ts, wiki-pages/update.ts      | Added `create` and `update` functions for creating/updating wiki pages, with error conversion and request shaping.  |
| wiki-pages/delete.ts                            | Implemented `deleteWiki` function for deleting wiki pages with error handling.                                      |
| wiki-pages/mod.ts                               | Introduced `Client` class exposing methods to list, show, create, update, and delete wiki pages.                   |
| wiki-pages/_mock.ts                             | Added MSW-based mock handlers for simulating valid and invalid wiki API responses.                                  |
| wiki-pages/list.test.ts, show.test.ts           | Introduced tests for listing and showing wiki pages, asserting success and error scenarios.                         |
| wiki-pages/create.test.ts, delete.test.ts       | Added tests for creating and deleting wiki pages, verifying correct handling of API responses.                      |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant API
    participant Validator

    Client->>API: GET /projects/:id/wiki/index.json
    API-->>Client: JSON list of wiki pages
    Client->>Validator: Validate and transform response
    Validator-->>Client: Wiki[]

    Client->>API: GET /projects/:id/wiki/:page.json
    API-->>Client: JSON wiki page detail
    Client->>Validator: Validate and transform response
    Validator-->>Client: WikiDetail

    Client->>API: PUT /projects/:id/wiki/:page.json
    API-->>Client: 200 OK or error status

    Client->>API: DELETE /projects/:id/wiki/:page.json
    API-->>Client: 204 No Content or error status
```

## Poem

> In the warren of code, new features bloom,  
> Wiki pages now dance—create, list, and exhume!  
> With tests and mocks, the burrow feels right,  
> Validation and errors keep bugs out of sight.  
> 🐇✨ The Redmine rabbit hops with glee—  
> "Wiki REST is ready! Come, code with me!"

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Omochice/deno-redmine&utm_content=194):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:17:26Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([2874b12](https://github.com/Omochice/deno-redmine/commit/2874b1227ec4ab5c27a3c95e54da7a9a4af0643e)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           92.6% | -1.4% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                             10s |   +3s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (2874b12) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          92.6% | -1.4% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1450 |  +340 |
+ |   Covered           |           1044 |           1344 |  +300 |
- | Test Execution Time |             7s |            10s |   +3s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 87.3%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2F_mock.ts)         | 80.7%    | +80.7%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2Fshow.ts)           | 62.5%    | +62.5%  | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/0936ecba7f6f4f2d371a099df896df5d64325772/wiki-pages%2Fvalidator.ts) | 96.1%    | +96.1%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:21:42Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([2c1682b](https://github.com/Omochice/deno-redmine/commit/2c1682bbf8f59553c6b1e309cd8c60e7722972e7)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           93.8% | -0.3% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                              9s |   +2s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (2c1682b) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          93.8% | -0.3% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1470 |  +360 |
+ |   Covered           |           1044 |           1379 |  +335 |
- | Test Execution Time |             7s |             9s |   +2s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 92.0%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2F_mock.ts)         | 83.3%    | +83.3%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2Fshow.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/5d3eb701c3b582cff76b969ed8247fd7a8bef2a2/wiki-pages%2Fvalidator.ts) | 96.1%    | +96.1%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:22:35Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([2293d0e](https://github.com/Omochice/deno-redmine/commit/2293d0e144a92df95d07620cc75508481c0ca51c)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           93.8% | -0.3% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                              9s |   +2s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (2293d0e) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          93.8% | -0.3% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1470 |  +360 |
+ |   Covered           |           1044 |           1379 |  +335 |
- | Test Execution Time |             7s |             9s |   +2s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 92.0%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2F_mock.ts)         | 83.3%    | +83.3%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2Fshow.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/dca5226151b15a30fd1eb336a603a11f121954d4/wiki-pages%2Fvalidator.ts) | 96.1%    | +96.1%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:24:17Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([cd711b2](https://github.com/Omochice/deno-redmine/commit/cd711b2599af3a82f7715d8ac40719ca78d892e1)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           93.8% | -0.3% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                              9s |   +2s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (cd711b2) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          93.8% | -0.3% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1470 |  +360 |
+ |   Covered           |           1044 |           1379 |  +335 |
- | Test Execution Time |             7s |             9s |   +2s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 92.0%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2F_mock.ts)         | 83.3%    | +83.3%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2Fshow.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/b31d1867583ab1277245f819782d2d7620bda5a3/wiki-pages%2Fvalidator.ts) | 96.1%    | +96.1%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:27:04Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([6e1f9c3](https://github.com/Omochice/deno-redmine/commit/6e1f9c3bd867e6cf8c8ee8756e3f7c86f6550c5d)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           93.8% | -0.3% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                              9s |   +2s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (6e1f9c3) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          93.8% | -0.3% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1470 |  +360 |
+ |   Covered           |           1044 |           1379 |  +335 |
- | Test Execution Time |             7s |             9s |   +2s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 92.0%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2F_mock.ts)         | 83.3%    | +83.3%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2Fshow.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/96b75fe94889a37beeff4129364b1c6585f7a02d/wiki-pages%2Fvalidator.ts) | 96.1%    | +96.1%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:28:51Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([d3f6e22](https://github.com/Omochice/deno-redmine/commit/d3f6e225d10b5da4c32f1a5efe8b3de75b247e62)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           93.6% | -0.5% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                              9s |   +2s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (d3f6e22) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          93.6% | -0.5% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1472 |  +362 |
+ |   Covered           |           1044 |           1378 |  +334 |
- | Test Execution Time |             7s |             9s |   +2s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 91.2%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2F_mock.ts)         | 83.3%    | +83.3%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2Fshow.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/213d1e54d949c840d8373e23d22621f384303df1/wiki-pages%2Fvalidator.ts) | 92.4%    | +92.4%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-31T17:31:02Z
body: ## Code Metrics Report
|                         | [main](https://github.com/Omochice/deno-redmine/tree/main) ([63f5424](https://github.com/Omochice/deno-redmine/commit/63f5424cd8e3e21fc05c2d9e54e1c0b7587bdec9)) | [#194](https://github.com/Omochice/deno-redmine/pull/194) ([0c7f80d](https://github.com/Omochice/deno-redmine/commit/0c7f80d487e68828a18e1a539e52d26becef1826)) |  +/-  |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------:|------:|
| **Coverage**            |                                                                                                                                                            94.0% |                                                                                                                                                           93.6% | -0.5% |
| **Test Execution Time** |                                                                                                                                                               7s |                                                                                                                                                              9s |   +2s |

<details>

<summary>Details</summary>

``` diff
  |                     | main (63f5424) | #194 (0c7f80d) |  +/-  |
  |---------------------|----------------|----------------|-------|
- | Coverage            |          94.0% |          93.6% | -0.5% |
  |   Files             |             21 |             28 |    +7 |
  |   Lines             |           1110 |           1472 |  +362 |
+ |   Covered           |           1044 |           1378 |  +334 |
- | Test Execution Time |             7s |             9s |   +2s |
```

</details>


### Code coverage of files in pull request scope (66.6% → 91.2%)

|                                                                    Files                                                                    | Coverage |   +/-   |  Status  |
|---------------------------------------------------------------------------------------------------------------------------------------------|---------:|--------:|---------:|
| [error.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/error.ts)                                 | 90.4%    | +23.8%  | modified |
| [wiki-pages/_mock.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2F_mock.ts)         | 83.3%    | +83.3%  | added    |
| [wiki-pages/create.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2Fcreate.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/delete.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2Fdelete.ts)       | 100.0%   | +100.0% | added    |
| [wiki-pages/list.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2Flist.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/show.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2Fshow.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/type.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2Ftype.ts)           | 100.0%   | +100.0% | added    |
| [wiki-pages/validator.ts](https://github.com/Omochice/deno-redmine/blob/f29bb854ba3319b6092f4dfe404d70e5e2184ff8/wiki-pages%2Fvalidator.ts) | 92.4%    | +92.4%  | added    |

---
Reported by [octocov](https://github.com/k1LoW/octocov)
<!-- octocov -->
author_association: CONTRIBUTOR