| /** | |
| * The `JsonPatch` module computes and applies deterministic patch documents for | |
| * JSON values. A patch is an ordered list of `add`, `remove`, and `replace` | |
| * operations addressed by JSON Pointer paths. Use it to describe the structural | |
| * difference between two JSON documents, serialize that difference, and replay | |
| * it without mutating the original input. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import { format } from "./Formatter.ts" | |
| import { escapeToken, unescapeToken } from "./JsonPointer.ts" | |
| import * as Predicate from "./Predicate.ts" | |
| import type * as Schema from "./Schema.ts" | |
| /** | |
| * A single JSON Patch operation. | |
| * | |
| * **When to use** | |
| * | |
| * Use to manually construct patch operations, accept patch operations from | |
| * callers, or type-check patch operation structures. | |
| * | |
| * **Details** | |
| * | |
| * Represents one transformation step in a JSON Patch document. This is a subset | |
| * of RFC 6902, restricted to operations that can be applied deterministically | |
| * without additional context. All fields are readonly, paths use JSON Pointer | |
| * syntax, and the empty string `""` refers to the root document. Operations are | |
| * discriminated by the `op` field, and the optional `description` field can be | |
| * used for documentation. | |
| * | |
| * **Example** (All operation types) | |
| * | |
| * ```ts | |
| * import { JsonPatch } from "effect" | |
| * | |
| * const addOp: JsonPatch.JsonPatchOperation = { | |
| * op: "add", | |
| * path: "/users/-", | |
| * value: { id: 1, name: "Alice" } | |
| * } | |
| * | |
| * const removeOp: JsonPatch.JsonPatchOperation = { | |
| * op: "remove", | |
| * path: "/users/0" | |
| * } | |
| * | |
| * const replaceOp: JsonPatch.JsonPatchOperation = { | |
| * op: "replace", | |
| * path: "/users/0/name", | |
| * value: "Bob" | |
| * } | |
| * ``` | |
| * | |
| * @see {@link JsonPatch} for the array of operations forming a complete patch | |
| * @see {@link get} to compute operations automatically from value differences | |
| * @see {@link apply} to apply operations to transform documents | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type JsonPatchOperation = | |
| | { | |
| readonly op: "add" | |
| /** | |
| * JSON Pointer to the target location. For arrays, the last token may be `-` | |
| * to append. | |
| * | |
| * **When to use** | |
| * | |
| * Use to identify where the `add` operation inserts its value. | |
| */ | |
| readonly path: string | |
| readonly value: Schema.Json | |
| readonly description?: string | |
| } | |
| | { | |
| readonly op: "remove" | |
| /** | |
| * JSON Pointer to the target location. | |
| * | |
| * **When to use** | |
| * | |
| * Use to identify which location the `remove` operation deletes. | |
| */ | |
| readonly path: string | |
| readonly description?: string | |
| } | |
| | { | |
| readonly op: "replace" | |
| /** | |
| * JSON Pointer to the target location. Use `""` to replace the root document. | |
| * | |
| * **When to use** | |
| * | |
| * Use to identify which location the `replace` operation overwrites. | |
| */ | |
| readonly path: string | |
| readonly value: Schema.Json | |
| readonly description?: string | |
| } | |
| /** | |
| * A JSON Patch document (an ordered list of operations). | |
| * | |
| * **When to use** | |
| * | |
| * Use to store, serialize, pass, or validate complete patch documents. | |
| * | |
| * **Details** | |
| * | |
| * Represents a complete transformation as a readonly sequence of immutable | |
| * operations. Operations are applied sequentially from first to last, and later | |
| * operations observe the document state produced by earlier operations. An empty | |
| * array represents a no-op patch and returns the original document. | |
| * | |
| * **Example** (Multi-operation patch) | |
| * | |
| * ```ts | |
| * import { JsonPatch } from "effect" | |
| * | |
| * const patch: JsonPatch.JsonPatch = [ | |
| * { op: "add", path: "/items/-", value: "apple" }, | |
| * { op: "replace", path: "/count", value: 5 }, | |
| * { op: "remove", path: "/oldField" } | |
| * ] | |
| * | |
| * const result = JsonPatch.apply(patch, { count: 3, oldField: "value" }) | |
| * // { count: 5, items: ["apple"] } | |
| * ``` | |
| * | |
| * @see {@link JsonPatchOperation} for individual operation types | |
| * @see {@link get} to generate patches from value differences | |
| * @see {@link apply} to execute patches to transform documents | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type JsonPatch = ReadonlyArray<JsonPatchOperation> | |
| /** | |
| * Computes a structural patch that transforms `oldValue` into `newValue`. | |
| * | |
| * **When to use** | |
| * | |
| * Use to compute a JSON Patch from before and after JSON documents, detect | |
| * structural changes, or create deterministic update operations. | |
| * | |
| * **Details** | |
| * | |
| * Generates a structural diff between two JSON values, producing a patch that | |
| * yields `newValue` when applied to `oldValue`. It returns an empty array when | |
| * values are identical, recursively diffs nested structures, emits root | |
| * `replace` operations for primitive changes, and processes object keys in | |
| * sorted order for stable output. | |
| * | |
| * **Gotchas** | |
| * | |
| * Arrays are compared by index position, with no move or copy detection. Array | |
| * removals are emitted from highest to lowest index to prevent index shifting. | |
| * The output is deterministic but not guaranteed to be minimal. | |
| * | |
| * **Example** (Computing object diff) | |
| * | |
| * ```ts | |
| * import { JsonPatch } from "effect" | |
| * | |
| * const oldValue = { users: [{ id: 1, name: "Alice" }], count: 1 } | |
| * const newValue = { users: [{ id: 1, name: "Bob" }, { id: 2, name: "Charlie" }], count: 2 } | |
| * | |
| * const patch = JsonPatch.get(oldValue, newValue) | |
| * // [ | |
| * // { op: "replace", path: "/users/0/name", value: "Bob" }, | |
| * // { op: "add", path: "/users/1", value: { id: 2, name: "Charlie" } }, | |
| * // { op: "replace", path: "/count", value: 2 } | |
| * // ] | |
| * ``` | |
| * | |
| * @see {@link apply} to apply the generated patch to a document | |
| * @see {@link JsonPatchOperation} for the operation types in the patch | |
| * @category transforming | |
| * @since 4.0.0 | |
| */ | |
| export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { | |
| if (Object.is(oldValue, newValue)) return [] | |
| const patches: Array<JsonPatchOperation> = [] | |
| if (Array.isArray(oldValue) && Array.isArray(newValue)) { | |
| const len1 = oldValue.length | |
| const len2 = newValue.length | |
| // Compare shared prefix by index | |
| const shared = Math.min(len1, len2) | |
| for (let i = 0; i < shared; i++) { | |
| const path = `/${i}` | |
| const patch = get(oldValue[i], newValue[i]) | |
| for (const op of patch) { | |
| prefixPathInPlace(op, path) | |
| patches.push(op) | |
| } | |
| } | |
| // Remove from end to start so later indices do not shift. | |
| for (let i = len1 - 1; i >= len2; i--) { | |
| patches.push({ op: "remove", path: `/${i}` }) | |
| } | |
| // Add from beginning to end. | |
| for (let i = len1; i < len2; i++) { | |
| patches.push({ op: "add", path: `/${i}`, value: newValue[i] }) | |
| } | |
| return patches | |
| } | |
| if (isJsonObject(oldValue) && isJsonObject(newValue)) { | |
| const keys1 = Object.keys(oldValue) | |
| const keys2 = Object.keys(newValue) | |
| const allKeys = Array.from(new Set([...keys1, ...keys2])).sort() | |
| for (const key of allKeys) { | |
| const esc = escapeToken(key) | |
| const path = `/${esc}` | |
| const hasKey1 = Object.hasOwn(oldValue, key) | |
| const hasKey2 = Object.hasOwn(newValue, key) | |
| if (hasKey1 && hasKey2) { | |
| const patch = get(oldValue[key], newValue[key]) | |
| for (const op of patch) { | |
| prefixPathInPlace(op, path) | |
| patches.push(op) | |
| } | |
| } else if (!hasKey1 && hasKey2) { | |
| patches.push({ op: "add", path, value: newValue[key] }) | |
| } else if (hasKey1 && !hasKey2) { | |
| patches.push({ op: "remove", path }) | |
| } | |
| } | |
| return patches | |
| } | |
| patches.push({ op: "replace", path: "", value: newValue }) | |
| return patches | |
| } | |
| /** | |
| * Applies a JSON Patch to a JSON document. | |
| * | |
| * **When to use** | |
| * | |
| * Use to execute patches generated by {@link get}, transform documents | |
| * with manually constructed patches, or process patch operations from external | |
| * sources. | |
| * | |
| * **Details** | |
| * | |
| * Executes patch operations sequentially, so later operations see changes made | |
| * by earlier operations. It never mutates the input document; array and object | |
| * operations copy the affected containers. An empty patch returns the original | |
| * reference, and a root replace (`path: ""`) returns the provided value | |
| * directly. | |
| * | |
| * **Gotchas** | |
| * | |
| * Invalid paths, missing properties, and out-of-bounds array indices throw | |
| * errors. | |
| * | |
| * **Example** (Applying a patch) | |
| * | |
| * ```ts | |
| * import { JsonPatch } from "effect" | |
| * | |
| * const document = { items: [1, 2, 3], total: 6 } | |
| * const patch: JsonPatch.JsonPatch = [ | |
| * { op: "add", path: "/items/-", value: 4 }, | |
| * { op: "replace", path: "/total", value: 10 } | |
| * ] | |
| * | |
| * const result = JsonPatch.apply(patch, document) | |
| * // { items: [1, 2, 3, 4], total: 10 } | |
| * ``` | |
| * | |
| * @see {@link get} to generate patches from value differences | |
| * @see {@link JsonPatchOperation} for the operation types being applied | |
| * @category transforming | |
| * @since 4.0.0 | |
| */ | |
| export function apply(patch: JsonPatch, oldValue: Schema.Json): Schema.Json { | |
| let doc = oldValue | |
| for (const op of patch) { | |
| switch (op.op) { | |
| case "replace": { | |
| doc = op.path === "" ? op.value : setAt(doc, op.path, op.value, "replace") | |
| break | |
| } | |
| case "add": { | |
| doc = addAt(doc, op.path, op.value) | |
| break | |
| } | |
| case "remove": { | |
| doc = setAt(doc, op.path, undefined, "remove") | |
| break | |
| } | |
| } | |
| } | |
| return doc | |
| } | |
| // Mutates op.path in place for perf; safe because child ops are freshly created and not shared. | |
| function prefixPathInPlace(op: JsonPatchOperation, parent: string): void { | |
| ;(op as any).path = op.path === "" ? parent : parent + op.path | |
| } | |
| function isJsonObject(value: unknown): value is Schema.JsonObject { | |
| return Predicate.isObject(value) | |
| } | |
| /** | |
| * Tokenize a JSON Pointer into unescaped reference tokens. | |
| * | |
| * - `""` (empty pointer) refers to the root and returns `[]` | |
| * - Non-empty pointers must start with `/` | |
| */ | |
| function tokenize(pointer: string): Array<string> { | |
| if (pointer === "") return [] | |
| if (pointer.charCodeAt(0) !== 47 /* "/" */) { | |
| throw new Error(`Invalid JSON Pointer, it must start with "/": ${format(pointer)}`) | |
| } | |
| return pointer.split("/").slice(1).map(unescapeToken) | |
| } | |
| /** Convert a reference token to a non-negative array index (rejects `-` and negatives). */ | |
| function toIndex(token: string): number { | |
| if (!/^(0|[1-9]\d*)$/.test(token)) { | |
| throw new Error(`Invalid array index: "${token}"`) | |
| } | |
| return Number(token) | |
| } | |
| function addAt(doc: Schema.Json, pointer: string, val: Schema.Json): Schema.Json { | |
| if (pointer === "") return val | |
| const resolved = resolveParent(doc, pointer) | |
| if (resolved === null) { | |
| throw new Error(`Cannot add at "${pointer}" (parent not found or not a container).`) | |
| } | |
| const { lastToken, parent, stack } = resolved | |
| if (Array.isArray(parent)) { | |
| const idx = lastToken === "-" ? parent.length : toIndex(lastToken) | |
| if (idx < 0 || idx > parent.length) throw new Error(`Array index out of bounds at "${pointer}".`) | |
| const updated = parent.slice() | |
| updated.splice(idx, 0, val) | |
| return rebuildFromStack(stack, updated) | |
| } | |
| if (isJsonObject(parent)) { | |
| const updated = { ...parent } | |
| updated[lastToken] = val | |
| return rebuildFromStack(stack, updated) | |
| } | |
| throw new Error(`Cannot add at "${pointer}" (parent not found or not a container).`) | |
| } | |
| function setAt( | |
| doc: Schema.Json, | |
| pointer: string, | |
| val: Schema.Json | undefined, | |
| mode: "replace" | "remove" | |
| ): Schema.Json { | |
| if (pointer === "") { | |
| if (mode === "remove" || val === undefined) throw new Error("Unsupported operation at the root") | |
| return val | |
| } | |
| const resolved = resolveParent(doc, pointer) | |
| if (resolved === null) { | |
| throw new Error(`Cannot ${mode} at "${pointer}" (parent not found or not a container).`) | |
| } | |
| const { lastToken, parent, stack } = resolved | |
| if (Array.isArray(parent)) { | |
| if (lastToken === "-") throw new Error(`"-" is not valid for ${mode} at "${pointer}".`) | |
| const idx = toIndex(lastToken) | |
| if (idx < 0 || idx >= parent.length) throw new Error(`Array index out of bounds at "${pointer}".`) | |
| const updated = parent.slice() | |
| if (mode === "remove") updated.splice(idx, 1) | |
| else updated[idx] = val | |
| return rebuildFromStack(stack, updated) | |
| } | |
| if (isJsonObject(parent)) { | |
| if (!Object.hasOwn(parent, lastToken)) { | |
| throw new Error(`Property "${lastToken}" does not exist at "${pointer}".`) | |
| } | |
| const updated = { ...parent } | |
| if (mode === "remove") delete updated[lastToken] | |
| else updated[lastToken] = val! | |
| return rebuildFromStack(stack, updated) | |
| } | |
| throw new Error(`Cannot ${mode} at "${pointer}" (parent not found or not a container).`) | |
| } | |
| type StackEntry = { readonly container: unknown; readonly token: number | string } | |
| // Walk to the parent of `pointer`, recording the path. | |
| // Returns null if the parent path cannot be resolved. | |
| function resolveParent( | |
| doc: Schema.Json, | |
| pointer: string | |
| ): { readonly stack: ReadonlyArray<StackEntry>; readonly parent: unknown; readonly lastToken: string } | null { | |
| const tokens = tokenize(pointer) | |
| if (tokens.length === 0) return null // caller handles root | |
| const lastToken = tokens[tokens.length - 1] | |
| const stack: Array<StackEntry> = [] | |
| let cur: unknown = doc | |
| for (let i = 0; i < tokens.length - 1; i++) { | |
| const token = tokens[i] | |
| if (cur == null) return null | |
| if (Array.isArray(cur)) { | |
| const idx = toIndex(token) | |
| if (idx < 0 || idx >= cur.length) return null | |
| stack.push({ container: cur, token: idx }) | |
| cur = cur[idx] | |
| continue | |
| } | |
| if (cur && typeof cur === "object") { | |
| if (!Object.hasOwn(cur, token)) return null | |
| stack.push({ container: cur, token }) | |
| cur = (cur as any)[token] | |
| continue | |
| } | |
| return null | |
| } | |
| return { stack, parent: cur, lastToken } | |
| } | |
| // Rebuild the document by writing `newParent` back through `stack`. | |
| function rebuildFromStack(stack: ReadonlyArray<StackEntry>, newParent: Schema.Json): Schema.Json { | |
| let acc: Schema.Json = newParent | |
| for (let i = stack.length - 1; i >= 0; i--) { | |
| const { container, token } = stack[i] | |
| if (Array.isArray(container)) { | |
| const copy = container.slice() | |
| copy[token as number] = acc | |
| acc = copy | |
| } else { | |
| const copy = { ...(container as Schema.JsonObject) } | |
| copy[token as string] = acc | |
| acc = copy | |
| } | |
| } | |
| return acc | |
| } | |
Xet Storage Details
- Size:
- 14.3 kB
- Xet hash:
- f862011aa2abd5d608b6aff4dab269771524b7f9136e65118fe02978cf253d1f
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.