Skip to content

Commit

Permalink
Implement lens
Browse files Browse the repository at this point in the history
  • Loading branch information
zerobias committed Aug 11, 2024
1 parent 361c365 commit 78aae9e
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 4 deletions.
79 changes: 79 additions & 0 deletions packages/core/src/__tests__/keyval/lens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect, test, describe } from 'vitest';
import { createStore, createEvent, sample } from 'effector';
import { model, keyval, define, lens } from '@effector/model';

function createUpdatableEntities(
fill?: Array<{ id: string; count: number; tag: string }>,
) {
const entities = keyval({
getKey: 'id',
model: model({
props: {
id: define.store<string>(),
count: define.store<number>(),
tag: define.store<string>(),
},
create() {
return {};
},
}),
});
if (fill) {
entities.edit.add(fill);
}
return entities;
}

describe('lens read value of field in keyval', () => {
test('with store', () => {
const entities = createUpdatableEntities([
{ id: 'foo', count: 0, tag: 'x' },
{ id: 'bar', count: 0, tag: 'y' },
{ id: 'baz', count: 0, tag: 'z' },
]);

const $currentKey = createStore('bar');

const $currentTag = lens(entities, $currentKey).tag.store;
expect($currentTag.getState()).toBe('y');
});
test('with constant', () => {
const entities = createUpdatableEntities([
{ id: 'foo', count: 0, tag: 'x' },
{ id: 'bar', count: 0, tag: 'y' },
]);

const $currentTag = lens(entities, 'bar').tag.store;
expect($currentTag.getState()).toBe('y');
});
});

test('lens store change value when entity changed', () => {
const entities = createUpdatableEntities([
{ id: 'foo', count: 0, tag: 'x' },
{ id: 'bar', count: 0, tag: 'y' },
]);

const $currentTag = lens(entities, 'bar').tag.store;
expect($currentTag.getState()).toBe('y');

entities.edit.update({ id: 'bar', tag: 'z' });
expect($currentTag.getState()).toBe('z');
});

test('lens store change value when key changed', () => {
const entities = createUpdatableEntities([
{ id: 'foo', count: 0, tag: 'x' },
{ id: 'bar', count: 0, tag: 'y' },
]);

const changeKey = createEvent<string>();
const $currentKey = createStore('bar');
sample({ clock: changeKey, target: $currentKey });

const $currentTag = lens(entities, $currentKey).tag.store;
expect($currentTag.getState()).toBe('y');

changeKey('foo');
expect($currentTag.getState()).toBe('x');
});
31 changes: 31 additions & 0 deletions packages/core/src/keyval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import type {
InstanceOf,
Show,
ConvertToLensShape,
StructKeyval,
} from './types';
import { spawn } from './spawn';
import { isDefine } from './define';

type ToPlainShape<Shape> = {
[K in {
Expand Down Expand Up @@ -402,10 +404,39 @@ export function keyval<Input, ModelEnhance, Api, Shape>({
return state;
});

let structShape: any = null;

if (model) {
const initShape = {} as Record<string, any>;
/** for in leads to typescript errors */
Object.entries(model.shape).forEach(([key, def]) => {
if (isDefine.store(def)) {
initShape[key] = createStore({});
} else if (isDefine.event(def)) {
initShape[key] = createEvent();
} else if (isDefine.effect(def)) {
initShape[key] = createEffect(() => {});
}
});
const consoleError = console.error;
console.error = () => {};
// @ts-expect-error type issues
const instance = spawn(model, initShape);
console.error = consoleError;
instance.unmount();
structShape = {
type: 'structKeyval',
getKey,
shape: model.__struct!.shape,
} as StructKeyval;
}

return {
type: 'keyval',
api: api as any,
__lens: shape,
__struct: structShape,
__getKey: getKey,
$items: $entities.map(({ items }) => items),
$keys: $entities.map(({ keys }) => keys),
edit: {
Expand Down
94 changes: 90 additions & 4 deletions packages/core/src/lens.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,94 @@
import { Keyval, KeyStore } from './types';
import { combine, Store } from 'effector';
import { Keyval, KeyStore, LensStore, LensEvent, StructKeyval } from './types';

type PathDecl =
| {
type: 'index';
/** position of index value in path itself */
pathIndex: number;
}
| {
type: 'field';
value: string;
};

function createLensStruct(
struct: StructKeyval,
pathDecl: PathDecl[],
path: Array<KeyStore | string | number>,
items: Store<any[]>,
) {
const shape = {} as any;
for (const key in struct.shape) {
const item = struct.shape[key];
if (item.type === 'structUnit') {
switch (item.unit) {
case 'store': {
const $value = combine(
[items, ...path],
([items, ...pathKeysRaw]) => {
const pathKeys = pathKeysRaw as Array<string | number>;
let value: any = items;
for (const segment of pathDecl) {
if (value === undefined) return undefined;
switch (segment.type) {
case 'index': {
const id = pathKeys[segment.pathIndex];
value = value.find((e: any) => struct.getKey(e) === id);
break;
}
case 'field': {
value = value[segment.value];
break;
}
}
}
return value?.[key];
},
);
shape[key] = {
__type: 'lensStore',
store: $value,
} as LensStore<any>;
break;
}
case 'event': {
shape[key] = {
__type: 'lensEvent',
__value: null,
} as LensEvent<any>;
break;
}
case 'effect': {
console.error('effects are not supported in lens');
break;
}
}
} else {
shape[key] = (childKey: KeyStore | string | number) =>
createLensStruct(
item,
[
...pathDecl,
{ type: 'field', value: key },
{ type: 'index', pathIndex: path.length },
],
[...path, childKey],
items,
);
}
}
return shape;
}

export function lens<Shape>(
keyval: Keyval<any, any, any, Shape>,
key: KeyStore,
) {
return keyval.__lens;
key: KeyStore | string | number,
): Shape {
return createLensStruct(
keyval.__struct,
[{ type: 'index', pathIndex: 0 }],
[key],
keyval.$items,
);
}
24 changes: 24 additions & 0 deletions packages/core/src/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {
EventDef,
EffectDef,
AnyDef,
StructUnit,
StructShape,
} from './types';
import { define, isDefine, isKeyval } from './define';

Expand Down Expand Up @@ -241,17 +243,39 @@ export function spawn<
}
if (!model.shapeInited) {
model.shapeInited = true;
const structShape: StructShape = {
type: 'structShape',
shape: {},
};
for (const key in normProps) {
const unit = normProps[key];
structShape.shape[key] = {
type: 'structUnit',
unit: is.store(unit) ? 'store' : is.event(unit) ? 'event' : 'effect',
};
}
for (const key in storeOutputs) {
// @ts-expect-error
model.shape[key] = define.store<any>();
structShape.shape[key] = isKeyval(storeOutputs[key])
? storeOutputs[key].__struct
: {
type: 'structUnit',
unit: 'store',
};
}
for (const key in apiOutputs) {
const value = apiOutputs[key];
// @ts-expect-error
model.shape[key] = is.event(value)
? define.event<any>()
: define.effect<any, any, any>();
structShape.shape[key] = {
type: 'structUnit',
unit: is.event(value) ? 'event' : 'effect',
};
}
model.__struct = structShape;
}
const result: Instance<Output, Api> = {
type: 'instance',
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type Model<Props, Output, Api, Shape> = {
>;
// private
shapeInited: boolean;
// private
__struct?: StructShape;
};

export type Instance<Output, Api> = {
Expand Down Expand Up @@ -117,6 +119,25 @@ export type LensEvent<T> = {
__value: T;
};

/** internal representation of model structure, unit leaf */
export type StructUnit = {
type: 'structUnit';
unit: 'store' | 'event' | 'effect';
};

/** internal representation of model structure, model shape */
export type StructShape = {
type: 'structShape';
shape: Record<string, StructUnit | StructKeyval>;
};

/** internal representation of model structure, keyval shape */
export type StructKeyval = {
type: 'structKeyval';
getKey: (input: any) => string | number;
shape: Record<string, StructUnit | StructKeyval>;
};

export type KeyStore = Store<string | number>;

export type ConvertToLensShape<Shape> = {
Expand Down Expand Up @@ -171,6 +192,10 @@ export type Keyval<Input, Enriched extends Input, Api, Shape> = {
};
// private
__lens: Shape;
// private
__struct: StructKeyval;
// private
__getKey: (input: Input) => string | number;
};

export type StoreContext<T> = {
Expand Down

0 comments on commit 78aae9e

Please sign in to comment.