generated from stagas/ts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
139 lines (118 loc) · 3.94 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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { defineAccessors } from 'define-accessors'
export type Constructor<T> = new (...args: any[]) => T
// modified from: https://stackoverflow.com/a/69322301/175416
type Join<A, B> = {
[K in keyof (A & B)]: K extends keyof A ? A[K] : K extends keyof B ? B[K] : never
}
// Returns R if T is a function, otherwise returns Fallback
type IsFunction<T, R, Fallback = T> = T extends (...args: any[]) => any ? R : Fallback
// Returns R if T is an object, otherwise returns Fallback
type IsObject<T, R, Fallback = T> = IsFunction<T, Fallback, T extends object ? R : Fallback>
// "a.b.c" => "b.c"
type Tail<S> = S extends `${string}.${infer T}` ? Tail<T> : S
// typeof Object.values(T)
type Value<T> = T[keyof T]
// {a: {b: 1, c: 2}} => {"a.b": {b: 1, c: 2}, "a.c": {b: 1, c: 2}}
type FlattenStepOne<T> = {
[K in keyof T as K extends string
? IsObject<T[K], `${K}.${keyof T[K] & string}`, K>
: K]: IsObject<T[K], { [key in keyof T[K]]: T[K][key] }>
}
// {"a.b": {b: 1, c: 2}, "a.c": {b: 1, c: 2}} => {"a.b": {b: 1}, "a.c": {c: 2}}
type FlattenStepTwo<T> = {
[a in keyof T]: IsObject<
T[a],
Value<{ [M in keyof T[a] as M extends Tail<a> ? M : never]: T[a][M] }>
>
}
// {a: {b: 1, c: {d: 1}}} => {"a.b": 1, "a.c": {d: 1}}
type FlattenOneLevel<T> = FlattenStepTwo<FlattenStepOne<T>>
// {a: {b: 1, c: {d: 1}}} => {"a.b": 1, "a.b.c.d": 1}
type Flatten<T> = T extends FlattenOneLevel<T> ? T : Join<T, Flatten<FlattenOneLevel<T>>>
// "a.b.c" => "abc"
type ToCamelCase<S> = S extends `${infer H}.${infer T}` ? ToCamelCase<`${H}${Capitalize<T>}`> : S
export type FlattenCamelCase<T> = { [K in keyof Flatten<T> as ToCamelCase<K>]: Flatten<T>[K] }
/**
* Mixins `ctor` with `parent` and camelCase flattens
* and observes its properties mapping them back to the actual values.
* Best used in conjuction with https://github.com/stagas/with-properties
*
* ```ts
* const Foo = withFlatten(
* SomeParent,
* class {
* deep = {
* foo: 2,
* }
* }
* )
* const foo = new Foo()
* foo.deepFoo = 4
* expect(foo.deep.foo).toBe(4)
* ```
*/
export const withFlatten = <P extends Constructor<any>, T extends object>(
parent: P,
ctor: Constructor<T>
) =>
class extends parent {
constructor(...args: any[]) {
super(...args)
const self = this
const data = new ctor() as any
const paths = getPaths(data)
const get = (path: string[]) => path.reduce((o, k) => o[k], data)
const set = (path: string[], value: any) =>
path.reduce((o, k, i, { length }) => (i === length - 1 ? (o[k] = value) : o[k]), data)
// map paths omitting single level
const map = mapPaths(paths.filter(x => x.length > 1))
// create schema
const schema = Object.fromEntries(
[...map.entries()].map(([key, path]: [string, string[]]) => [key, get(path)])
)
// single level assigned as they are
;[...new Set(paths.map(x => x[0]))].forEach(key => (self[key] = data[key]))
defineAccessors(self, schema, (key: any) => ({
configurable: false,
enumerable: true,
get() {
return get(map.get(key)!)
},
set(value: any) {
const p = map.get(key)!
set(p, value)
;(self as any).propertyChangedCallback?.(p[0], top, top)
},
}))
}
} as P & Constructor<FlattenCamelCase<T>>
const getPaths = (obj: any) => {
const paths: string[][] = []
const recurse = (obj: any, path: string[]) => {
for (const key in obj) {
const value = obj[key]
const newPath = path.concat(key)
if (typeof value === 'object') {
paths.push(newPath)
recurse(value, newPath)
} else {
paths.push(newPath)
}
}
}
recurse(obj, [])
return paths
}
const mapPaths = (paths: string[][]) =>
new Map(
paths.map(path => {
return [
path[0] +
path
.slice(1)
.map(p => p[0].toUpperCase() + p.slice(1))
.join(''),
path,
]
})
)