forked from VOICEVOX/voicevox
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
applyPatchesを自前実装に置き換える (VOICEVOX#2052)
* Revert "applyPatchesをimmerのものを利用するように変更 (VOICEVOX#362)" This reverts commit c4f96ff. * npm install * implement applyPatches * drop rfc6902 * doc * ignore type error * fix typo * fix error by structuredClone * refactor * support Map and Set * add note comment * support Map/Set on clone * assert equals on clone * support original classes * shrink changes * use internal applyPatches for testing * simplify tests * remove tests for function * add tests for patches * add tests for Map * add tests for error * format * add test for remove from array * add test for "-" path * add tests for error * Revert "use internal applyPatches for testing" This reverts commit 7bedef3. * rename immerPatch into immerPatchUtility * use splice on remove from array * fix add operation into array * refactor tests * add test for insert element into array * drop support for userClass * drop support for un-cloneable value * 微調整 --------- Co-authored-by: Hiroshiba Kazuyuki <[email protected]>
- Loading branch information
1 parent
5ef5d65
commit 1f39897
Showing
3 changed files
with
673 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { Patch } from "immer"; | ||
import { ExhaustiveError } from "@/type/utility"; | ||
|
||
/** | ||
* produceWithPatchesにより生成された複数のパッチをオブジェクトに適用する。 | ||
* | ||
* @param {T} target パッチを適用する対象オブジェクト | ||
* @param {Patch[]} patches 適用するパッチの配列 | ||
* @template T 対象オブジェクトの型(任意) | ||
*/ | ||
export function applyPatches<T>(target: T, patches: Patch[]) { | ||
for (const patch of patches) { | ||
applyPatch(target, patch); | ||
} | ||
} | ||
|
||
function isObject(value: unknown): value is object { | ||
return typeof value === "object" && value != null; | ||
} | ||
|
||
// 値を再帰的に複製する。 | ||
// ユーザー定義クラスのインスタンスや関数など、複製しない値で例外を発生させる。 | ||
function clone<T>(value: T): T { | ||
// function等、単純にcloneできない値の取り扱いを禁止する | ||
if (!isObject(value)) return structuredClone(value); | ||
|
||
if (Array.isArray(value)) { | ||
if (Object.getPrototypeOf(value) !== Array.prototype) | ||
throw new Error("unsupported type"); | ||
return value.map((v) => clone(v)) as T; | ||
} | ||
|
||
if (value instanceof Map) { | ||
if (Object.getPrototypeOf(value) !== Map.prototype) | ||
throw new Error("unsupported type"); | ||
const result = new Map(); | ||
for (const [k, v] of value.entries()) { | ||
result.set(clone(k), clone(v)); | ||
} | ||
return result as T; | ||
} | ||
|
||
if (value instanceof Set) { | ||
if (Object.getPrototypeOf(value) !== Set.prototype) | ||
throw new Error("unsupported type"); | ||
const result = new Set(); | ||
for (const v of value.values()) { | ||
result.add(clone(v)); | ||
} | ||
return result as T; | ||
} | ||
|
||
// object以外の自前classは現状利用していないため不具合予防として一旦禁止 | ||
// 適切な動作テストの後制限を緩めることを妨げるものではない | ||
if (Object.getPrototypeOf(value) !== Object.prototype) | ||
throw new Error("unsupported type"); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const result: any = Object.create(Object.getPrototypeOf(value)); | ||
for (const [k, v] of Object.entries(value)) { | ||
result[k] = clone(v); | ||
} | ||
return result as T; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
function get(value: unknown, key: string | number): any { | ||
if (value instanceof Map) { | ||
return value.get(key); | ||
} | ||
// @ts-expect-error produceWithPatchesにより生成されたPatchを適用するため、valueはany型として扱う | ||
return value[key]; | ||
} | ||
|
||
function add(value: unknown, key: string | number, v: unknown): void { | ||
if (value instanceof Map) { | ||
value.set(key, v); | ||
} else if (value instanceof Set) { | ||
value.add(v); | ||
} else if (Array.isArray(value)) { | ||
if (typeof key === "number") { | ||
value.splice(key, 0, v); | ||
} else if (key === "-") { | ||
value.push(v); | ||
} else { | ||
throw new Error("unsupported key"); | ||
} | ||
} else { | ||
// @ts-expect-error produceWithPatchesにより生成されたPatchを適用するため、valueはany型として扱う | ||
value[key] = v; | ||
} | ||
} | ||
|
||
function replace(value: unknown, key: string | number, v: unknown): void { | ||
if (value instanceof Map) { | ||
value.set(key, v); | ||
} else if (value instanceof Set) { | ||
value.add(v); | ||
} else { | ||
// @ts-expect-error produceWithPatchesにより生成されたPatchを適用するため、valueはany型として扱う | ||
value[key] = v; | ||
} | ||
} | ||
|
||
function remove(value: unknown, key: string | number, v: unknown): void { | ||
if (value instanceof Map) { | ||
value.delete(key); | ||
} else if (value instanceof Set) { | ||
value.delete(v); | ||
} else if (Array.isArray(value) && typeof key === "number") { | ||
value.splice(key, 1); | ||
} else { | ||
// @ts-expect-error produceWithPatchesにより生成されたPatchを適用するため、valueはany型として扱う | ||
delete value[key]; | ||
} | ||
} | ||
|
||
/** | ||
* produceWithPatchesにより生成された単一のパッチをオブジェクトに適用する。 | ||
* | ||
* @param {T} target パッチを適用する対象オブジェクト | ||
* @param {Patch} patch 適用するパッチ | ||
* @template T 対象オブジェクトの型(任意) | ||
*/ | ||
function applyPatch<T>(target: T, patch: Patch) { | ||
const { path, value, op } = patch; | ||
for (const p of patch.path.slice(0, path.length - 1)) { | ||
target = get(target, p); | ||
} | ||
const v = clone(value); | ||
switch (op) { | ||
case "add": | ||
add(target, path[path.length - 1], v); | ||
break; | ||
case "replace": | ||
replace(target, path[path.length - 1], v); | ||
break; | ||
case "remove": | ||
remove(target, path[path.length - 1], v); | ||
break; | ||
default: | ||
throw new ExhaustiveError(op); | ||
} | ||
} |
Oops, something went wrong.