-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
107 lines (97 loc) · 2.76 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import * as _ from 'lodash';
interface SafeProxy<Initial, To> {
$: undefined | To;
$map: (f: (t: To) => To) => Initial;
$set: (value: To) => Initial;
$pmap: (f: (t: To) => Promise<To>) => Promise<Initial>;
}
type UnionToIntersection<U> = ((U extends any ? (k: U) => void : never) extends (k: infer I) => void
? { [K in Exclude<keyof I, keyof U>]: I[K] }
: never) &
U;
export type U<T> = {
[K in keyof UnionToIntersection<T>]-?: NonNullable<UnionToIntersection<T>[K]>;
};
type Safe<Initial, To> = SafeProxy<Initial, To> & { [K in keyof U<To>]: Safe<Initial, U<To>[K]> };
interface Proxied {
value: any;
path: Array<string | number>;
}
const noValue = Symbol;
function hasPath(target: any, path: string[]) {
if (path.length === 0) {
return target;
}
const value = _.get(target, path);
return value == null ? noValue : value;
}
function set(target: any, path: string[], to: any): any {
const value = hasPath(target, path);
if (value === noValue) {
return target;
}
if (path.length === 0) {
return to;
}
return _.set(clonePath(target, path), path, to);
}
function clonePath(target: any, path: any[]) {
let cursor: any = _.clone(target);
const root = cursor;
let i = 0;
while (i < path.length) {
if (!Array.isArray(cursor) && !_.isObject(cursor)) {
break;
}
const at = (cursor as any)[path[i]];
(cursor as any)[path[i]] = _.clone(at);
if (at && (Array.isArray(at) || _.isObject(at))) {
cursor = (cursor as any)[path[i]];
i++;
} else {
break;
}
}
return root;
}
function _safe<Initial, T>(v: Proxied): Safe<Initial, T> {
return new Proxy(v, {
get(target: any, key: string) {
if (key === '$') {
const value = hasPath(target.value, target.path);
return value === noValue ? undefined : value;
}
if (key === '$set') {
return (newValue: any) => {
return set(target.value, target.path, newValue);
};
}
if (key === '$map') {
return (f: (t: T) => T) => {
const value = hasPath(target.value, target.path);
if (value === noValue) {
return target.value;
}
return set(target.value, target.path, f(value));
};
}
if (key === '$pmap') {
return async (f: (t: T) => Promise<T>) => {
const value = hasPath(target.value, target.path);
if (value === noValue) {
return target.value;
}
return set(target.value, target.path, await f(value));
};
}
return _safe({ value: target.value, path: [...target.path, key] });
}
});
}
export default function safe<T>(v: T | undefined | null): Safe<T, T> {
const p = {
value: v,
path: [] as Proxied['path']
};
return _safe<T, T>(p);
}