-
Notifications
You must be signed in to change notification settings - Fork 0
feat: allow object format for range parameters #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
af33ddb
bf7d4d1
cd3a0a6
0c85cd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ import type { | |
| DateParam, | ||
| } from "./params.js"; | ||
| import { BooleanNumber, StopParam } from "./params.js"; | ||
| import type { Join } from "./util/type.js"; | ||
| import type { Join, RangeParam } from "./util/type.js"; | ||
|
|
||
| export type DefaultSearchResultFields = keyof Omit< | ||
| NarouSearchResult, | ||
|
|
@@ -54,6 +54,31 @@ export abstract class SearchBuilderBase< | |
| return Array.from(new Set(array)); | ||
| } | ||
|
|
||
| /** | ||
| * 範囲指定や配列をハイフン区切りの文字列に変換する | ||
| * @protected | ||
| * @static | ||
| * @param n 範囲指定オブジェクト、数値の配列、あるいは単一の数値 | ||
| * @returns ハイフン区切りの文字列 | ||
| */ | ||
| protected static range2string<T extends number>( | ||
| n: T | readonly T[] | RangeParam<T> | ||
| ): Join<T | ""> | undefined { | ||
| if (typeof n === "object" && n !== null && !Array.isArray(n)) { | ||
| if ("equal" in n && typeof n.equal === "number") { | ||
| return n.equal.toString() as Join<T>; | ||
| } else if ("min" in n || "max" in n) { | ||
| const obj = n as Extract<RangeParam<T>, { min?: T, max?: T }>; | ||
| if (obj.min === undefined && obj.max === undefined) { | ||
| return undefined; | ||
| } | ||
| return `${obj.min ?? ""}-${obj.max ?? ""}` as Join<T | "">; | ||
| } | ||
| return undefined; | ||
| } | ||
| return SearchBuilderBase.array2string(n) as Join<T>; | ||
| } | ||
|
|
||
| /** | ||
| * 配列をハイフン区切りの文字列に変換する | ||
| * @protected | ||
|
|
@@ -294,11 +319,15 @@ export abstract class NovelSearchBuilderBase< | |
| /** | ||
| * 抽出する作品の文字数を指定します (length)。 | ||
| * 範囲指定する場合は、最小文字数と最大文字数をハイフン(-)記号で区切ってください。 | ||
| * @param length 文字数、または[最小文字数, 最大文字数] | ||
| * オブジェクトによる指定 ({ min?: number, max?: number } または { equal: number }) も可能です。 | ||
| * @param length 文字数、または[最小文字数, 最大文字数]、またはオブジェクト指定 | ||
| * @return {this} | ||
| */ | ||
| length(length: number | readonly number[]): this { | ||
| this.set({ length: NovelSearchBuilderBase.array2string(length) }); | ||
| length(length: number | readonly number[] | RangeParam<number>): this { | ||
| const val = NovelSearchBuilderBase.range2string(length); | ||
| if (val !== undefined) { | ||
| this.set({ length: val }); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
|
|
@@ -315,8 +344,23 @@ export abstract class NovelSearchBuilderBase< | |
| * @return {this} | ||
| */ | ||
| kaiwaritu(min: number, max: number): this; | ||
| /** | ||
| * 抽出する作品の会話率を%単位で範囲指定またはオブジェクトで指定します (kaiwaritu)。 | ||
| * @param range 範囲指定オブジェクト | ||
| * @return {this} | ||
| */ | ||
| kaiwaritu(range: RangeParam<number>): this; | ||
|
|
||
| kaiwaritu(min: number, max?: number): this { | ||
| kaiwaritu(minOrRange: number | RangeParam<number>, max?: number): this { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency with kaiwaritu(
minOrRange: number | readonly number[] | RangeParam<number>,
max?: number
): this {
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 会話率も同じシグネチャのほうがいい |
||
| if (typeof minOrRange === "object" && minOrRange !== null) { | ||
| const val = NovelSearchBuilderBase.range2string(minOrRange); | ||
| if (val !== undefined) { | ||
| this.set({ kaiwaritu: val }); | ||
| } | ||
| return this; | ||
|
Comment on lines
+357
to
+363
|
||
| } | ||
|
|
||
| const min = minOrRange; | ||
| let n: number | string; | ||
| if (max != null) { | ||
| n = `${min}-${max}`; | ||
|
|
@@ -329,21 +373,29 @@ export abstract class NovelSearchBuilderBase< | |
|
|
||
| /** | ||
| * 抽出する作品の挿絵数を指定します (sasie)。 | ||
| * @param num 挿絵数、または[最小挿絵数, 最大挿絵数] | ||
| * オブジェクトによる指定 ({ min?: number, max?: number } または { equal: number }) も可能です。 | ||
| * @param num 挿絵数、または[最小挿絵数, 最大挿絵数]、またはオブジェクト指定 | ||
| * @return {this} | ||
| */ | ||
| sasie(num: number | readonly number[]): this { | ||
| this.set({ sasie: NovelSearchBuilderBase.array2string(num) }); | ||
| sasie(num: number | readonly number[] | RangeParam<number>): this { | ||
| const val = NovelSearchBuilderBase.range2string(num); | ||
| if (val !== undefined) { | ||
| this.set({ sasie: val }); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * 抽出する作品の予想読了時間を分単位で指定します (time)。 | ||
| * @param num 読了時間(分)、または[最小読了時間, 最大読了時間] | ||
| * オブジェクトによる指定 ({ min?: number, max?: number } または { equal: number }) も可能です。 | ||
| * @param num 読了時間(分)、または[最小読了時間, 最大読了時間]、またはオブジェクト指定 | ||
| * @return {this} | ||
| */ | ||
| time(num: number | readonly number[]): this { | ||
| this.set({ time: NovelSearchBuilderBase.array2string(num) }); | ||
| time(num: number | readonly number[] | RangeParam<number>): this { | ||
| const val = NovelSearchBuilderBase.range2string(num); | ||
| if (val !== undefined) { | ||
| this.set({ time: val }); | ||
| } | ||
| return this; | ||
|
Comment on lines
377
to
402
|
||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,3 +24,12 @@ type Stringable = string | number | bigint | boolean | null | undefined; | |||||||||||||||||||||||
| * type JoinedNumbers = Join<Numbers>; // '1' | '2' | '3' | '1-1' | '1-2' | '1-3' | '2-1' | '2-2' | '2-3' | '3-1' | '3-2' | '3-3' | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| export type Join<T extends Stringable> = `${T}-${T}` | `${T}`; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * 範囲指定のためのオブジェクトの型。 | ||||||||||||||||||||||||
| * @template T - 範囲指定する値の型 | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| export type RangeParam<T extends number> = | ||||||||||||||||||||||||
| | { min: T; max?: T } | ||||||||||||||||||||||||
| | { min?: T; max: T } | ||||||||||||||||||||||||
|
Comment on lines
+30
to
+34
|
||||||||||||||||||||||||
| * @template T - 範囲指定する値の型 | |
| */ | |
| export type RangeParam<T extends number> = | |
| | { min: T; max?: T } | |
| | { min?: T; max: T } | |
| * `min` / `max` による範囲指定、または `equal` による完全一致を表します。 | |
| * `min` と `max` はどちらも省略可能です。 | |
| * @template T - 範囲指定する値の型 | |
| */ | |
| export type RangeParam<T extends number> = | |
| | { min?: T; max?: T } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -787,6 +787,46 @@ describe("SearchBuilder", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`0-${length}`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| test("if length = { min: 1000 }", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockFn = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setupMockHandler(mockFn, ["length", "gzip", "out"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await NarouAPI.search().length({ min: 1000 }).execute(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.allcount).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`1000-`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| test("if length = { max: 1000 }", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockFn = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setupMockHandler(mockFn, ["length", "gzip", "out"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await NarouAPI.search().length({ max: 1000 }).execute(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.allcount).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`-1000`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| test("if length = { min: 100, max: 1000 }", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockFn = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setupMockHandler(mockFn, ["length", "gzip", "out"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await NarouAPI.search().length({ min: 100, max: 1000 }).execute(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.allcount).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`100-1000`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| test("if length = { equal: 1000 }", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockFn = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setupMockHandler(mockFn, ["length", "gzip", "out"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await NarouAPI.search().length({ equal: 1000 }).execute(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.allcount).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`1000`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("kaiwaritu", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -823,6 +863,26 @@ describe("SearchBuilder", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`${min}-${max}`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| test("if kaiwaritu = { min: 10 }", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockFn = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setupMockHandler(mockFn, ["kaiwaritu", "gzip", "out"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await NarouAPI.search().kaiwaritu({ min: 10 }).execute(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.allcount).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`10-`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| test("if kaiwaritu = { max: 50 }", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockFn = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setupMockHandler(mockFn, ["kaiwaritu", "gzip", "out"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await NarouAPI.search().kaiwaritu({ max: 50 }).execute(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.allcount).toBe(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockFn).toHaveBeenCalledWith(`-50`, "5", "json", 3); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |
| }); | |
| test("if kaiwaritu = { min: 10, max: 50 }", async () => { | |
| const mockFn = vi.fn(); | |
| setupMockHandler(mockFn, ["kaiwaritu", "gzip", "out"]); | |
| const result = await NarouAPI.search() | |
| .kaiwaritu({ min: 10, max: 50 }) | |
| .execute(); | |
| expect(result.allcount).toBe(1); | |
| expect(mockFn).toHaveBeenCalledTimes(1); | |
| expect(mockFn).toHaveBeenCalledWith(`10-50`, "5", "json", 3); | |
| }); | |
| test("if kaiwaritu = { equal: 10 }", async () => { | |
| const mockFn = vi.fn(); | |
| setupMockHandler(mockFn, ["kaiwaritu", "gzip", "out"]); | |
| const result = await NarouAPI.search().kaiwaritu({ equal: 10 }).execute(); | |
| expect(result.allcount).toBe(1); | |
| expect(mockFn).toHaveBeenCalledTimes(1); | |
| expect(mockFn).toHaveBeenCalledWith(`10`, "5", "json", 3); | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
range2stringは配列入力をそのままarray2stringに委譲しているため、数値配列の要素数が 2 以外(例:[1,2,3])でも1-2-3のような文字列になり、range 系パラメータとしては不正なクエリを生成します。range 用ユーティリティとしては、配列はタプル[min, max]のみ受け付ける(型をreadonly [T, T]にする)か、ランタイムで長さチェックしてエラーにするなどの防止策を入れた方が安全です。There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array2stringが意図しているタプルを返すかを(型かランタイムで)検証をしっかりすること
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
range2string は配列入力をそのまま array2string に委譲しているため、数値配列の要素数が 2 以外(例: [1,2,3])でも 1-2-3 のような文字列になり、range 系パラメータとしては不正なクエリを生成します。range 用ユーティリティとしては、配列はタプル [min, max] のみ受け付ける(型を readonly [T, T] にする)か、ランタイムで長さチェックしてエラーにするなどの防止策を入れた方が安全です。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ご指摘ありがとうございます!
range2stringが不適切な要素数(3以上)の配列を受け付けないようにランタイムチェックを追加し、さらに安全性を高めるため、TypeScriptの型定義でもreadonly [number, number]のタプル型を要求するように変更しました。