Skip to content

Commit

Permalink
improved defaultHandler & allow new control properties
Browse files Browse the repository at this point in the history
  • Loading branch information
karmaniverous committed Oct 16, 2024
1 parent a675cf6 commit 0daae1b
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
## TODO

- Configurable control property
- Structured `defaultProxyFunction`
- Structured `defaultHandler`
- README tests

---
Expand Down
26 changes: 13 additions & 13 deletions src/controlledProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai';

import { control, controlledProxy } from './controlledProxy';

const sym = Symbol('symProp');
const sym = Symbol('sym');

interface TargetType extends Record<string | number | symbol, unknown> {
foo: (value: string) => string;
Expand Down Expand Up @@ -55,7 +55,7 @@ describe('controlledProxy', function () {
it('should apply default handler', function () {
const proxy = controlledProxy({
controls: { foo: true, bar: false },
defaultProxyFunction: () => 'default',
defaultHandler: () => 'default',
target,
});

Expand All @@ -67,7 +67,7 @@ describe('controlledProxy', function () {
it('should catch invalid control', function () {
controlledProxy({
controls: { foo: true, bar: false, invalid: true },
defaultProxyFunction: () => 'default',
defaultHandler: () => 'default',
// @ts-expect-error Property 'invalid' is missing in type
target,
});
Expand Down Expand Up @@ -117,10 +117,10 @@ describe('controlledProxy', function () {
expect(target.answer).to.equal(100);
});

it('should handle defaultProxyFunction for properties', function () {
it('should handle defaultHandler for properties', function () {
const proxy = controlledProxy({
controls: { answer: false },
defaultProxyFunction: () => 'default value',
defaultHandler: () => 'default value',
target,
});

Expand Down Expand Up @@ -154,7 +154,7 @@ describe('controlledProxy', function () {
it('should apply default handler for disabled methods', function () {
const proxy = controlledProxy({
controls: { increment: false },
defaultProxyFunction: () => 'default increment',
defaultHandler: () => 'default increment',
target,
});

Expand Down Expand Up @@ -217,16 +217,16 @@ describe('controlledProxy', function () {
expect(proxy[control]).to.deep.equal({ foo: true, bar: false });
});

it('should prevent adding new properties to controls', function () {
it('should allow adding new properties to controls', function () {
const proxy = controlledProxy({
controls: { foo: true },
target,
});

expect(() => {
// @ts-expect-error Property 'bar' does not exist on type 'Record<"foo", boolean>'.
proxy.controls.bar = false;
}).to.throw(TypeError);
// @ts-expect-error Property 'bar' does not exist on type 'Record<"foo", boolean>'.
proxy[control].bar = false;

expect(proxy.bar('test')).to.be.undefined;
});

it('should allow updating control values', function () {
Expand Down Expand Up @@ -304,10 +304,10 @@ describe('controlledProxy', function () {
expect(proxy.concat('Hello, ', 'World!')).to.equal('Hello, World!');
});

it('should pass arguments to defaultProxyFunction when method is disabled', function () {
it('should pass arguments to defaultHandler when method is disabled', function () {
const proxy = controlledProxy({
controls: { foo: false },
defaultProxyFunction: (...args: unknown[]) =>
defaultHandler: (target, prop, receiver, ...args: unknown[]) =>
`default: ${args.join(', ')}`,
target,
});
Expand Down
33 changes: 19 additions & 14 deletions src/controlledProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,53 @@ export type ControlledPartial<Properties extends PropertyKey> = Record<
unknown
>;

export type ControlledProxyHandler<
Properties extends PropertyKey,
Target extends ControlledPartial<Properties>,
> = (target: Target, p: PropertyKey, receiver: any, ...args: any[]) => any; // eslint-disable-line @typescript-eslint/no-explicit-any

export interface ControlledProxyOptions<
Properties extends PropertyKey,
Target extends ControlledPartial<Properties>,
> {
controls: Record<Properties, boolean>;
defaultHandler?: ControlledProxyHandler<Properties, Target>;
target: Target;
defaultProxyFunction?: ControlledMethod;
}

export const control = Symbol('control');

export function controlledProxy<
export const controlledProxy = <
Properties extends PropertyKey,
Target extends ControlledPartial<Properties>,
>({
controls,
defaultHandler,
target,
defaultProxyFunction,
}: ControlledProxyOptions<Properties, Target>) {
const sealedControls = Object.seal(controls);

return new Proxy(target, {
}: ControlledProxyOptions<Properties, Target>) =>
new Proxy(target, {
get(targetObj, prop, receiver): unknown {
// if property is control property, return it
if (prop === control) return sealedControls;
if (prop === control) return controls;
const value = Reflect.get(targetObj, prop, receiver);

return prop in sealedControls // controlled?
? sealedControls[prop as Properties] // controlled & enabled?
return prop in controls // controlled?
? controls[prop as Properties] // controlled & enabled?
? typeof value === 'function' // controlled & enabled & function?
? value.bind(targetObj) // controlled & enabled & function
: value // controlled & enabled & !function
: typeof value === 'function' // controlled & !enabled & function?
? (defaultProxyFunction ?? (() => undefined)) // controlled & !enabled & function
: (defaultProxyFunction?.() ?? undefined) // controlled & !enabled & !function
? defaultHandler // controlled & !enabled & function & defaultHandler?
? (...args: unknown[]) =>
defaultHandler(targetObj, prop, receiver, ...args) // eslint-disable-line @typescript-eslint/no-unsafe-return
: () => undefined
: defaultHandler?.(targetObj, prop, receiver) // controlled & !enabled & !function
: value; // !controlled
},

set(targetObj, prop, value, receiver): boolean {
return !(prop in sealedControls) || sealedControls[prop as Properties]
return !(prop in controls) || controls[prop as Properties]
? Reflect.set(targetObj, prop, value, receiver) // !controlled | enabled
: false;
},
}) as Target & { [control]: Record<Properties, boolean> };
}

0 comments on commit 0daae1b

Please sign in to comment.