Skip to content

Commit 772714d

Browse files
committed
add initializer to marks
1 parent 389bf55 commit 772714d

3 files changed

Lines changed: 153 additions & 65 deletions

File tree

src/get-property-descriptor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const getPropertyDescriptor = (instance: unknown, key: PropertyKey): PropertyDescriptor | undefined => {
2+
while (instance) {
3+
const descriptor = Object.getOwnPropertyDescriptor(instance, key)
4+
if (descriptor) return descriptor
5+
instance = Object.getPrototypeOf(instance)
6+
}
7+
}

src/mark.ts

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,67 @@
1+
import {getPropertyDescriptor} from './get-property-descriptor.js'
2+
13
type PropertyType = 'field' | 'getter' | 'setter' | 'method'
2-
interface PropertyDecorator {
4+
export interface PropertyDecorator {
35
(proto: object, key: PropertyKey, descriptor?: PropertyDescriptor): void
46
readonly static: unique symbol
57
}
6-
type GetMarks = (instance: object) => Set<PropertyKey>
7-
export function createMark(validate: (key: PropertyKey, type: PropertyType) => void): [PropertyDecorator, GetMarks] {
8+
type GetMarks<T> = (instance: T) => Set<PropertyKey>
9+
type InitializeMarks<T> = (instance: T) => void
10+
11+
type Context = {
12+
kind: PropertyType
13+
name: PropertyKey
14+
access: PropertyDescriptor
15+
}
16+
17+
const getType = (descriptor?: PropertyDescriptor): PropertyType => {
18+
if (descriptor) {
19+
if (typeof descriptor.value === 'function') return 'method'
20+
if (typeof descriptor.get === 'function') return 'getter'
21+
if (typeof descriptor.set === 'function') return 'setter'
22+
}
23+
return 'field'
24+
}
25+
26+
export function createMark<T extends object>(
27+
validate: (context: {name: PropertyKey; kind: PropertyType}) => void,
28+
initialize: (instance: T, context: Context) => PropertyDescriptor | void
29+
): [PropertyDecorator, GetMarks<T>, InitializeMarks<T>] {
830
const marks = new WeakMap<object, Set<PropertyKey>>()
9-
function get(proto: object): Set<PropertyKey> {
31+
const get = (proto: object): Set<PropertyKey> => {
1032
if (!marks.has(proto)) {
1133
const parent = Object.getPrototypeOf(proto)
12-
marks.set(proto, new Set(marks.get(parent) || []))
34+
marks.set(proto, new Set(parent ? get(parent) || [] : []))
1335
}
1436
return marks.get(proto)!
1537
}
16-
const marker = (proto: object, key: PropertyKey, descriptor?: PropertyDescriptor): void => {
17-
if (get(proto).has(key)) return
18-
let type: PropertyType = 'field'
19-
if (descriptor) {
20-
if (typeof descriptor.value === 'function') type = 'method'
21-
if (typeof descriptor.get === 'function') type = 'getter'
22-
if (typeof descriptor.set === 'function') type = 'setter'
23-
}
24-
validate(key, type)
25-
get(proto).add(key)
38+
const marker = (proto: object, name: PropertyKey, descriptor?: PropertyDescriptor): void => {
39+
if (get(proto).has(name)) return
40+
validate({name, kind: getType(descriptor)})
41+
get(proto).add(name)
2642
}
2743
marker.static = Symbol()
28-
44+
const getMarks = (instance: T): Set<PropertyKey> => {
45+
const proto = Object.getPrototypeOf(instance)
46+
for (const key of proto.constructor[marker.static] || []) {
47+
marker(proto, key, Object.getOwnPropertyDescriptor(proto, key))
48+
}
49+
return new Set(get(proto))
50+
}
2951
return [
3052
marker as PropertyDecorator,
31-
(instance: object): Set<PropertyKey> => {
32-
const proto = Object.getPrototypeOf(instance)
33-
for (const key of proto.constructor[marker.static] || []) {
34-
marker(proto, key, Object.getOwnPropertyDescriptor(proto, key))
53+
getMarks,
54+
(instance: T): void => {
55+
for (const name of getMarks(instance)) {
56+
const access: PropertyDescriptor = getPropertyDescriptor(instance, name) || {
57+
value: void 0,
58+
configurable: true,
59+
writable: true,
60+
enumerable: true
61+
}
62+
const newDescriptor = initialize(instance, {name, kind: getType(access), access}) || access
63+
Object.defineProperty(instance, name, newDescriptor)
3564
}
36-
return new Set(get(proto))
3765
}
3866
]
3967
}

test/mark.ts

Lines changed: 97 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import {fake} from 'sinon'
33
import {createMark} from '../src/mark.js'
44

55
describe('createMark', () => {
6-
it('returns a tuple of functions: a mark and getMarks', () => {
7-
const mark = createMark(() => {})
8-
expect(mark).to.be.an('array').with.lengthOf(2)
6+
it('returns a tuple of functions: mark, getMarks, initializeMarks', () => {
7+
const mark = createMark(
8+
() => {},
9+
() => ({})
10+
)
11+
expect(mark).to.be.an('array').with.lengthOf(3)
912
expect(mark).to.have.property('0').a('function')
1013
expect(mark).to.have.property('1').a('function')
14+
expect(mark).to.have.property('2').a('function')
1115
})
1216

1317
it('attaches a `static` unique symbol to the first function', () => {
14-
const mark = createMark(() => {})
18+
const mark = createMark(
19+
() => {},
20+
() => ({})
21+
)
1522
expect(mark).to.have.nested.property('0.static').a('symbol')
16-
const otherMark = createMark(() => {})
23+
const otherMark = createMark(
24+
() => {},
25+
() => ({})
26+
)
1727
expect(otherMark).to.have.nested.property('0.static').a('symbol').not.equal(mark[0].static)
1828
})
1929

2030
it('can be added to class fields without errors', () => {
21-
const [mark] = createMark(() => {})
31+
const [mark] = createMark(
32+
() => {},
33+
() => ({})
34+
)
2235
class FooBar {
2336
@mark foo: unknown
2437
@mark bar = 1
@@ -28,7 +41,10 @@ describe('createMark', () => {
2841
})
2942

3043
it('can be added to getters or setters without errors', () => {
31-
const [mark] = createMark(() => {})
44+
const [mark] = createMark(
45+
() => {},
46+
() => ({})
47+
)
3248
class FooBar {
3349
@mark get foo() {
3450
return 1
@@ -44,15 +60,21 @@ describe('createMark', () => {
4460
})
4561

4662
it('can be added to methods without errors', () => {
47-
const [mark] = createMark(() => {})
63+
const [mark] = createMark(
64+
() => {},
65+
() => ({})
66+
)
4867
class Foo {
4968
@mark foo() {}
5069
}
5170
new Foo()
5271
})
5372

5473
it('retrieves all marked fields with the get mark function', () => {
55-
const [mark, getMark] = createMark(() => {})
74+
const [mark, getMark] = createMark(
75+
() => {},
76+
() => ({})
77+
)
5678
class FooBar {
5779
@mark foo: unknown
5880
@mark bar = 1
@@ -71,7 +93,10 @@ describe('createMark', () => {
7193
})
7294

7395
it('retrieves marked symbol methods correctly', () => {
74-
const [mark, getMark] = createMark(() => {})
96+
const [mark, getMark] = createMark(
97+
() => {},
98+
() => ({})
99+
)
75100
const sym = Symbol('foo')
76101
class FooBar {
77102
@mark [sym]() {}
@@ -80,7 +105,10 @@ describe('createMark', () => {
80105
})
81106

82107
it('retrieves fields declared using the `mark.static` symbol as a static class field', () => {
83-
const [mark, getMark] = createMark(() => {})
108+
const [mark, getMark] = createMark(
109+
() => {},
110+
() => ({})
111+
)
84112
class FooBar {
85113
static [mark.static] = ['bar', 'bing', 'quuz', 'grault']
86114
@mark foo: unknown
@@ -101,7 +129,10 @@ describe('createMark', () => {
101129
})
102130

103131
it('will not contain duplicates', () => {
104-
const [mark, getMark] = createMark(() => {})
132+
const [mark, getMark] = createMark(
133+
() => {},
134+
() => ({})
135+
)
105136
class FooBar {
106137
static [mark.static] = ['bar', 'bing', 'quuz', 'grault']
107138
@mark foo: unknown
@@ -120,63 +151,85 @@ describe('createMark', () => {
120151
expect(getMark(new FooBar())).to.eql(new Set(['foo', 'bar', 'baz', 'bing', 'qux', 'quuz', 'corge', 'grault']))
121152
})
122153

123-
it('calls the given function for each field, with name and type', () => {
154+
it('calls the given validate function for each field, with name and kind', () => {
124155
const validate = fake()
125-
const [mark] = createMark(validate)
156+
const [mark, getMarks] = createMark(validate, () => {})
126157
const sym = Symbol('garply')
127158
class FooBar {
159+
static [mark.static] = ['bar', 'bing', 'quuz', 'grault']
128160
@mark foo: unknown
129-
@mark bar = 1
161+
bar = 1
130162
@mark baz = 'hi'
131-
@mark get bing() {
163+
get bing() {
132164
return 1
133165
}
134166
@mark get qux() {
135167
return 1
136168
}
137-
@mark set quuz(v: number) {}
169+
set quuz(v: number) {}
138170
@mark set corge(v: number) {}
139-
@mark grault() {}
171+
grault() {}
140172
@mark [sym]() {}
141173
}
142-
expect(validate).to.be.calledWith('foo', 'field')
143-
expect(validate).to.be.calledWith('bar', 'field')
144-
expect(validate).to.be.calledWith('baz', 'field')
145-
expect(validate).to.be.calledWith('bing', 'getter')
146-
expect(validate).to.be.calledWith('qux', 'getter')
147-
expect(validate).to.be.calledWith('quuz', 'setter')
148-
expect(validate).to.be.calledWith('corge', 'setter')
149-
expect(validate).to.be.calledWith('grault', 'method')
150-
expect(validate).to.be.calledWith(sym, 'method')
151-
return new FooBar()
174+
getMarks(new FooBar())
175+
expect(validate).to.be.calledWithExactly({name: 'foo', kind: 'field'})
176+
expect(validate).to.be.calledWithExactly({name: 'bar', kind: 'field'})
177+
expect(validate).to.be.calledWithExactly({name: 'baz', kind: 'field'})
178+
expect(validate).to.be.calledWithExactly({name: 'bing', kind: 'getter'})
179+
expect(validate).to.be.calledWithExactly({name: 'qux', kind: 'getter'})
180+
expect(validate).to.be.calledWithExactly({name: 'quuz', kind: 'setter'})
181+
expect(validate).to.be.calledWithExactly({name: 'corge', kind: 'setter'})
182+
expect(validate).to.be.calledWithExactly({name: 'grault', kind: 'method'})
183+
expect(validate).to.be.calledWithExactly({name: sym, kind: 'method'})
152184
})
153185

154-
it('calls the given function for each static defined field once initialized, with name and type', () => {
186+
it('calls the given initialize function for each static defined field once initialized, with name, kind and access', () => {
155187
const validate = fake()
156-
const [mark, getMark] = createMark(validate)
188+
const initialize = fake(({access}) => access)
189+
const [mark, getMarks, initializeMarks] = createMark(validate, initialize)
190+
const sym = Symbol('garply')
157191
class FooBar {
158-
static [mark.static] = ['foo', 'bar', 'baz', 'bing', 'qux', 'quuz', 'corge', 'grault']
159-
foo: unknown
192+
static [mark.static] = ['bar', 'bing', 'quuz', 'grault']
193+
@mark foo: unknown
160194
bar = 1
161-
baz = 'hi'
195+
@mark baz = 'hi'
162196
get bing() {
163197
return 1
164198
}
165-
get qux() {
199+
@mark get qux() {
166200
return 1
167201
}
168202
set quuz(v: number) {}
169-
set corge(v: number) {}
203+
@mark set corge(v: number) {}
170204
grault() {}
205+
@mark [sym]() {}
171206
}
172-
getMark(new FooBar())
173-
expect(validate).to.be.calledWith('foo', 'field')
174-
expect(validate).to.be.calledWith('bar', 'field')
175-
expect(validate).to.be.calledWith('baz', 'field')
176-
expect(validate).to.be.calledWith('bing', 'getter')
177-
expect(validate).to.be.calledWith('qux', 'getter')
178-
expect(validate).to.be.calledWith('quuz', 'setter')
179-
expect(validate).to.be.calledWith('corge', 'setter')
180-
expect(validate).to.be.calledWith('grault', 'method')
207+
const fooBar = new FooBar()
208+
getMarks(fooBar)
209+
expect(initialize).to.have.callCount(0)
210+
console.log(...getMarks(fooBar))
211+
initializeMarks(fooBar)
212+
const accessFor = (field: PropertyKey) => Object.getOwnPropertyDescriptor(FooBar.prototype, field)
213+
expect(initialize).to.be.calledWithExactly(fooBar, {
214+
name: 'foo',
215+
kind: 'field',
216+
access: {value: void 0, configurable: true, writable: true, enumerable: true}
217+
})
218+
expect(initialize).to.be.calledWithExactly(fooBar, {
219+
name: 'bar',
220+
kind: 'field',
221+
access: {value: 1, configurable: true, writable: true, enumerable: true}
222+
})
223+
expect(initialize).to.be.calledWithExactly(fooBar, {
224+
name: 'baz',
225+
kind: 'field',
226+
access: {value: 'hi', configurable: true, writable: true, enumerable: true}
227+
})
228+
expect(initialize).to.be.calledWithExactly(fooBar, {name: 'bing', kind: 'getter', access: accessFor('bing')})
229+
expect(initialize).to.be.calledWithExactly(fooBar, {name: 'qux', kind: 'getter', access: accessFor('qux')})
230+
expect(initialize).to.be.calledWithExactly(fooBar, {name: 'quuz', kind: 'setter', access: accessFor('quuz')})
231+
expect(initialize).to.be.calledWithExactly(fooBar, {name: 'corge', kind: 'setter', access: accessFor('corge')})
232+
expect(initialize).to.be.calledWithExactly(fooBar, {name: 'grault', kind: 'method', access: accessFor('grault')})
233+
expect(initialize).to.be.calledWithExactly(fooBar, {name: sym, kind: 'method', access: accessFor(sym)})
181234
})
182235
})

0 commit comments

Comments
 (0)