From 452c0c151ba8cfa91d43d8d02669bb9fdf5d0813 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Mon, 28 Oct 2024 21:00:46 +0200 Subject: [PATCH 1/9] Support registering graphs --- packages/react-obsidian/.eslintrc.json | 4 +- packages/react-obsidian/src/Obsidian.ts | 12 ++++-- .../src/graph/ServiceLocatorFactory.ts | 4 +- .../src/graph/registry/GraphRegistry.test.ts | 23 ++++++++++++ .../src/graph/registry/GraphRegistry.ts | 37 ++++++++++++++++++- packages/react-obsidian/src/utils/isString.ts | 3 ++ .../test/acceptance/obtain.test.ts | 5 +++ 7 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 packages/react-obsidian/src/utils/isString.ts diff --git a/packages/react-obsidian/.eslintrc.json b/packages/react-obsidian/.eslintrc.json index b926c9fb..9570fb6b 100644 --- a/packages/react-obsidian/.eslintrc.json +++ b/packages/react-obsidian/.eslintrc.json @@ -28,6 +28,7 @@ "obsidian" ], "rules": { + "global-require": "off", "no-console":"off", "obsidian/unresolved-provider-dependencies": "error", "obsidian/no-circular-dependencies": "warn", @@ -37,7 +38,8 @@ { "code": 115, "comments": 200, - "ignoreRegExpLiterals": true + "ignoreRegExpLiterals": true, + "ignorePattern": "throw new Error\\(.*\\);" } ], "@stylistic/no-extra-semi": "error", diff --git a/packages/react-obsidian/src/Obsidian.ts b/packages/react-obsidian/src/Obsidian.ts index 0767e777..ea9d6e0c 100644 --- a/packages/react-obsidian/src/Obsidian.ts +++ b/packages/react-obsidian/src/Obsidian.ts @@ -1,16 +1,20 @@ import graphRegistry from './graph/registry/GraphRegistry'; import { ObjectGraph } from './graph/ObjectGraph'; -import { GraphInternals, ServiceLocator } from './types'; +import { GraphInternals, ServiceLocator, type Constructable } from './types'; import { GraphMiddleware } from './graph/registry/GraphMiddleware'; import lateInjector from './injectors/class/LateInjector'; import serviceLocatorFactory from './graph/ServiceLocatorFactory'; export default class Obsidian { - obtain, P>( - Graph: new(...args: P[]) => T, + registerGraph(key: string, generator: () => Constructable) { + graphRegistry.registerGraphGenerator(key, generator); + } + + obtain, P = unknown>( + keyOrGraph: string | (new(...args: P[]) => T), props?: P, ): ServiceLocator> { - return serviceLocatorFactory.fromGraph(Graph, props); + return serviceLocatorFactory.fromGraph(keyOrGraph, props); } inject(target: T, graph?: ObjectGraph) { diff --git a/packages/react-obsidian/src/graph/ServiceLocatorFactory.ts b/packages/react-obsidian/src/graph/ServiceLocatorFactory.ts index 4f5638d9..5563541b 100644 --- a/packages/react-obsidian/src/graph/ServiceLocatorFactory.ts +++ b/packages/react-obsidian/src/graph/ServiceLocatorFactory.ts @@ -3,8 +3,8 @@ import { Constructable, ServiceLocator as ServiceLocatorType } from '../types'; import graphRegistry from './registry/GraphRegistry'; export default class ServiceLocatorFactory { - static fromGraph, P = any>(Graph: Constructable, props?: P) { - const resolved = graphRegistry.resolve(Graph, 'serviceLocator', props); + static fromGraph, P = any>(keyOrGraph: string | Constructable, props?: P) { + const resolved = graphRegistry.resolve(keyOrGraph, 'serviceLocator', props); const wrapped = new Proxy(resolved, { get(_target: any, property: string, receiver: any) { return () => resolved.retrieve(property, receiver); diff --git a/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts b/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts index 0edf4259..36554cd2 100644 --- a/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts +++ b/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts @@ -59,4 +59,27 @@ describe('GraphRegistry', () => { expect(uut.resolve(ScopedLifecycleBoundGraph, 'lifecycleOwner', undefined, 'token')).not.toBe(graph); }); + + it('resolves graph by key', () => { + uut.registerGraphGenerator('main', () => MainGraph); + expect(uut.resolve('main')).toBeInstanceOf(MainGraph); + }); + + it('throws an error when resolving a graph by key that is not registered', () => { + expect(() => uut.resolve('main')).toThrow('Attempted to resolve a graph by key "main" that is not registered. Did you forget to call Obsidian.registerGraph?'); + }); + + it('clears graph generators', () => { + uut.registerGraphGenerator('main', () => MainGraph); + uut.clearAll(); + expect(() => uut.resolve('main')).toThrow(); + }); + + it('clears graph generator for a specific graph', () => { + uut.registerGraphGenerator('main', () => MainGraph); + const graph = uut.resolve('main'); + + uut.clear(graph); + expect(() => uut.resolve('main')).toThrow(); + }); }); diff --git a/packages/react-obsidian/src/graph/registry/GraphRegistry.ts b/packages/react-obsidian/src/graph/registry/GraphRegistry.ts index bff0770e..eba03ac5 100644 --- a/packages/react-obsidian/src/graph/registry/GraphRegistry.ts +++ b/packages/react-obsidian/src/graph/registry/GraphRegistry.ts @@ -4,6 +4,7 @@ import { Middleware } from './Middleware'; import GraphMiddlewareChain from './GraphMiddlewareChain'; import { ObtainLifecycleBoundGraphException } from './ObtainLifecycleBoundGraphException'; import { getGlobal } from '../../utils/getGlobal'; +import { isString } from '../../utils/isString'; export class GraphRegistry { private readonly constructorToInstance = new Map, Set>(); @@ -13,11 +14,17 @@ export class GraphRegistry { private readonly nameToInstance = new Map(); private readonly graphToSubgraphs = new Map, Set>>(); private readonly graphMiddlewares = new GraphMiddlewareChain(); + private readonly keyToGenerator = new Map Constructable>(); + private readonly keyToGraph = new Map>(); register(constructor: Constructable, subgraphs: Constructable[] = []) { this.graphToSubgraphs.set(constructor, new Set(subgraphs)); } + registerGraphGenerator(key: string, generator: () => Constructable) { + this.keyToGenerator.set(key, generator); + } + ensureRegistered(graph: Graph) { if (this.instanceToConstructor.get(graph)) return; this.set(graph.constructor as any, graph); @@ -34,12 +41,15 @@ export class GraphRegistry { } resolve( - Graph: Constructable, + keyOrGraph: String | Constructable, source: 'lifecycleOwner' | 'classInjection' | 'serviceLocator' = 'lifecycleOwner', props: any = undefined, injectionToken?: string, ): T { - if ((this.isSingleton(Graph) || this.isBoundToReactLifecycle(Graph)) && this.has(Graph, injectionToken)) { + const Graph = isString(keyOrGraph) ? + this.getGraphConstructorByKey(keyOrGraph) : + keyOrGraph as Constructable; + if (( this.isSingleton(Graph) || this.isBoundToReactLifecycle(Graph)) && this.has(Graph, injectionToken)) { return this.isComponentScopedLifecycleBound(Graph) ? this.getByInjectionToken(Graph, injectionToken) : this.getFirst(Graph); @@ -52,6 +62,15 @@ export class GraphRegistry { return graph as T; } + private getGraphConstructorByKey(key: string): Constructable { + if (this.keyToGraph.has(key)) return this.keyToGraph.get(key) as Constructable; + const generator = this.keyToGenerator.get(key); + if (!generator) throw new Error(`Attempted to resolve a graph by key "${key}" that is not registered. Did you forget to call Obsidian.registerGraph?`); + const constructor = generator(); + this.keyToGraph.set(key, constructor); + return constructor as Constructable; + } + private has(Graph: Constructable, injectionToken?: string): boolean { const instances = this.constructorToInstance.get(Graph); if (!instances) return false; @@ -133,6 +152,18 @@ export class GraphRegistry { this.injectionTokenToInstance.delete(token); this.instanceToInjectionToken.delete(graph); } + + this.clearGraphsRegisteredByKey(Graph); + } + + private clearGraphsRegisteredByKey(Graph: Constructable) { + [...this.keyToGraph.keys()] + .map((key) => [key, this.keyToGraph.get(key)!] as [string, Constructable]) + .filter(([_, $Graph]) => $Graph === Graph) + .forEach(([key, _]) => { + this.keyToGraph.delete(key); + this.keyToGenerator.delete(key); + }); } addGraphMiddleware(middleware: Middleware) { @@ -149,6 +180,8 @@ export class GraphRegistry { this.nameToInstance.clear(); this.injectionTokenToInstance.clear(); this.instanceToInjectionToken.clear(); + this.keyToGenerator.clear(); + this.keyToGraph.clear(); } } diff --git a/packages/react-obsidian/src/utils/isString.ts b/packages/react-obsidian/src/utils/isString.ts new file mode 100644 index 00000000..aa2d9904 --- /dev/null +++ b/packages/react-obsidian/src/utils/isString.ts @@ -0,0 +1,3 @@ +export function isString(value: any): value is string { + return typeof value === 'string'; +} \ No newline at end of file diff --git a/packages/react-obsidian/test/acceptance/obtain.test.ts b/packages/react-obsidian/test/acceptance/obtain.test.ts index c4b70ff5..5de82954 100644 --- a/packages/react-obsidian/test/acceptance/obtain.test.ts +++ b/packages/react-obsidian/test/acceptance/obtain.test.ts @@ -32,4 +32,9 @@ describe('obtain', () => { /Could not resolve dep1 from CircularDependencyFromSubgraph\d because of a circular dependency: dep1 -> dep2 -> dep3 -> dep2/, ); }); + + it('should be able to obtain a graph by key', () => { + Obsidian.registerGraph('MainGraph', () => require('../fixtures/MainGraph').default); + expect(Obsidian.obtain('MainGraph').someString()).toBe(injectedValues.fromStringProvider); + }); }); From 7ef16fa8f97dd978b7b0f97041b82ed3e777c4a3 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Tue, 29 Oct 2024 18:59:43 +0200 Subject: [PATCH 2/9] inject hooks from a registered graph --- .../src/injectors/components/useGraph.ts | 4 ++-- .../src/injectors/hooks/HookInjector.ts | 4 ++-- .../src/injectors/hooks/InjectHook.test.ts | 15 +++++++++++++++ .../src/injectors/hooks/InjectHook.ts | 8 ++++---- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/react-obsidian/src/injectors/components/useGraph.ts b/packages/react-obsidian/src/injectors/components/useGraph.ts index b7be1cb0..045d3b12 100644 --- a/packages/react-obsidian/src/injectors/components/useGraph.ts +++ b/packages/react-obsidian/src/injectors/components/useGraph.ts @@ -5,14 +5,14 @@ import graphRegistry from '../../graph/registry/GraphRegistry'; import referenceCounter from '../../ReferenceCounter'; export default

( - Graph: Constructable, + keyOrGraph: string | Constructable, target: any, props?: Partial

, injectionToken?: string, ) => { const [graph] = useState(() => { - const resolvedGraph = graphRegistry.resolve(Graph, 'lifecycleOwner', props, injectionToken); + const resolvedGraph = graphRegistry.resolve(keyOrGraph, 'lifecycleOwner', props, injectionToken); resolvedGraph.onBind(target); return resolvedGraph; }); diff --git a/packages/react-obsidian/src/injectors/hooks/HookInjector.ts b/packages/react-obsidian/src/injectors/hooks/HookInjector.ts index 540f6dfc..4768b89d 100644 --- a/packages/react-obsidian/src/injectors/hooks/HookInjector.ts +++ b/packages/react-obsidian/src/injectors/hooks/HookInjector.ts @@ -6,10 +6,10 @@ import { Constructable } from '../../types'; export default class HookInjector { inject( hook: (args: Args) => Result, - Graph: Constructable, + keyOrGraph: string | Constructable, ): (args?: Partial) => Result { return (args?: Partial): Result => { - const graph = useGraph(Graph, hook, args); + const graph = useGraph(keyOrGraph, hook, args); return hook(new Proxy(args ?? {}, new Injector(graph))); }; } diff --git a/packages/react-obsidian/src/injectors/hooks/InjectHook.test.ts b/packages/react-obsidian/src/injectors/hooks/InjectHook.test.ts index 1efe7b47..1b8a6430 100644 --- a/packages/react-obsidian/src/injectors/hooks/InjectHook.test.ts +++ b/packages/react-obsidian/src/injectors/hooks/InjectHook.test.ts @@ -3,6 +3,7 @@ import MainGraph from '../../../test/fixtures/MainGraph'; import Subgraph from '../../../test/fixtures/Subgraph'; import { DependenciesOf } from '../../types'; import { injectHook, injectHookWithArguments } from './InjectHook'; +import { Obsidian } from '../..'; describe('injectHook', () => { type InjectedProps = DependenciesOf<[MainGraph, Subgraph]>; @@ -37,6 +38,13 @@ describe('injectHook', () => { const { result } = renderHook(injectedHook, { initialProps: { ownProp: expectedResult.ownProp } }); expect(result.current).toStrictEqual(expectedResult); }); + + it('injects hook from a registered graph', () => { + Obsidian.registerGraph('mainGraph', () => MainGraph); + const injectedHook = injectHook(hook, 'mainGraph'); + const { result } = renderHook(injectedHook, { initialProps: { ownProp: expectedResult.ownProp } }); + expect(result.current).toStrictEqual(expectedResult); + }); }); describe('injectHookWithArguments', () => { @@ -45,5 +53,12 @@ describe('injectHook', () => { const { result } = renderHook(injectedHook, { initialProps: { ownProp: expectedResult.ownProp } }); expect(result.current).toStrictEqual(expectedResult); }); + + it('injects hook from a registered graph', () => { + Obsidian.registerGraph('mainGraph', () => MainGraph); + const injectedHook = injectHookWithArguments(hook, 'mainGraph'); + const { result } = renderHook(injectedHook, { initialProps: { ownProp: expectedResult.ownProp } }); + expect(result.current).toStrictEqual(expectedResult); + }); }); }); diff --git a/packages/react-obsidian/src/injectors/hooks/InjectHook.ts b/packages/react-obsidian/src/injectors/hooks/InjectHook.ts index faaf8008..4028e23f 100644 --- a/packages/react-obsidian/src/injectors/hooks/InjectHook.ts +++ b/packages/react-obsidian/src/injectors/hooks/InjectHook.ts @@ -12,14 +12,14 @@ const hookInjector = new HookInjector(); export function injectHookWithArguments( hook: (args: Injected & Own) => Result, - Graph: Constructable, + keyOrGraph: string | Constructable, ): (props: Own & Partial) => Result { - return hookInjector.inject(hook, Graph) as (props: Own & Partial) => Result; + return hookInjector.inject(hook, keyOrGraph) as (props: Own & Partial) => Result; } export function injectHook( hook: (args: Injected) => Result, - Graph: Constructable, + keyOrGraph: string | Constructable, ): (props?: Partial) => Result { - return hookInjector.inject(hook, Graph); + return hookInjector.inject(hook, keyOrGraph); } From 99f16b743ebbc7c74d38e0ad7d33c5fe94c72433 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Tue, 29 Oct 2024 19:38:26 +0200 Subject: [PATCH 3/9] inject component with registered graphs --- .../src/injectors/components/ComponentInjector.tsx | 10 +++++----- .../src/injectors/components/InjectComponent.test.tsx | 10 ++++++++-- .../src/injectors/components/InjectComponent.ts | 11 ++++++----- .../src/injectors/components/useInjectionToken.ts | 7 +++++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/react-obsidian/src/injectors/components/ComponentInjector.tsx b/packages/react-obsidian/src/injectors/components/ComponentInjector.tsx index 0476843e..1d5d8a40 100644 --- a/packages/react-obsidian/src/injectors/components/ComponentInjector.tsx +++ b/packages/react-obsidian/src/injectors/components/ComponentInjector.tsx @@ -11,24 +11,24 @@ import { useInjectionToken } from './useInjectionToken'; export default class ComponentInjector { inject

( Target: React.FunctionComponent

, - Graph: Constructable, + keyOrGraph: string | Constructable, ): React.FunctionComponent> { - const Wrapped = this.wrapComponent(Target, Graph); + const Wrapped = this.wrapComponent(Target, keyOrGraph); hoistNonReactStatics(Wrapped, Target); return Wrapped; } private wrapComponent

( InjectionCandidate: React.FunctionComponent

, - Graph: Constructable, + keyOrGraph: string | Constructable, ): React.FunctionComponent> { const isMemoized = isMemoizedComponent(InjectionCandidate); const Target = isMemoized ? InjectionCandidate.type : InjectionCandidate; const compare = isMemoized ? InjectionCandidate.compare : undefined; return genericMemo((passedProps: P) => { - const injectionToken = useInjectionToken(Graph); - const graph = useGraph

(Graph, Target, passedProps, injectionToken); + const injectionToken = useInjectionToken(keyOrGraph); + const graph = useGraph

(keyOrGraph, Target, passedProps, injectionToken); const proxiedProps = new PropsInjector(graph).inject(passedProps); return ( diff --git a/packages/react-obsidian/src/injectors/components/InjectComponent.test.tsx b/packages/react-obsidian/src/injectors/components/InjectComponent.test.tsx index 4bc7c80d..1b95e0f1 100644 --- a/packages/react-obsidian/src/injectors/components/InjectComponent.test.tsx +++ b/packages/react-obsidian/src/injectors/components/InjectComponent.test.tsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react'; import React from 'react'; -import type { Constructable, ObjectGraph } from 'src'; +import { Obsidian, type Constructable, type ObjectGraph } from '../..'; import MainGraph, { Dependencies } from '../../../test/fixtures/MainGraph'; import { injectComponent } from './InjectComponent'; @@ -35,7 +35,6 @@ describe('injectComponent', () => { expect(container.textContent).toBe('error: own prop not provided - Fear kills progress'); }); - // it throws an error if the Graph is undefined it('Throws an error if the Graph is undefined', () => { const Graph = undefined as unknown as Constructable; expect(() => injectComponent(component, Graph)).toThrowError( @@ -45,4 +44,11 @@ describe('injectComponent', () => { + ` Check the implementation of component.`, ); }); + + it('Injects component by registered graph key', () => { + Obsidian.registerGraph('MainGraph', () => MainGraph); + const InjectedComponent = injectComponent(component, 'MainGraph'); + const { container } = render(); + expect(container.textContent).toBe('error: own prop not provided - Fear kills progress'); + }); }); diff --git a/packages/react-obsidian/src/injectors/components/InjectComponent.ts b/packages/react-obsidian/src/injectors/components/InjectComponent.ts index 22b258b5..1fe35c57 100644 --- a/packages/react-obsidian/src/injectors/components/InjectComponent.ts +++ b/packages/react-obsidian/src/injectors/components/InjectComponent.ts @@ -1,5 +1,6 @@ import { ObjectGraph } from '../../graph/ObjectGraph'; import { Constructable } from '../../types'; +import { isString } from '../../utils/isString'; import ComponentInjector from './ComponentInjector'; interface Discriminator { @@ -13,18 +14,18 @@ export const injectComponent = , - Graph: Constructable, + keyOrGraph: string | Constructable, ) => { - assertGraph(Graph, Target); + assertGraph(keyOrGraph, Target); - return componentInjector.inject(Target, Graph) as React.FunctionComponent< + return componentInjector.inject(Target, keyOrGraph) as React.FunctionComponent< InjectedProps extends Discriminator ? OwnProps extends Discriminator ? Partial : OwnProps : OwnProps extends InjectedProps ? Partial : OwnProps & Partial >; }; -function assertGraph(Graph: Constructable>, Target: any) { - if (!Graph) { +function assertGraph(keyOrGraph: string | Constructable, Target: any) { + if (!isString(keyOrGraph) && !keyOrGraph) { throw new Error( `injectComponent was called with an undefined Graph.` + `This is probably not an issue with Obsidian.` diff --git a/packages/react-obsidian/src/injectors/components/useInjectionToken.ts b/packages/react-obsidian/src/injectors/components/useInjectionToken.ts index 71ff1a84..33f9dbc0 100644 --- a/packages/react-obsidian/src/injectors/components/useInjectionToken.ts +++ b/packages/react-obsidian/src/injectors/components/useInjectionToken.ts @@ -2,9 +2,12 @@ import { useContext, useState } from 'react'; import { GraphContext } from './graphContext'; import type { Constructable, ObjectGraph } from '../..'; import { uniqueId } from '../../utils/uniqueId'; +import { isString } from '../../utils/isString'; -export const useInjectionToken = (Graph: Constructable) => { +export const useInjectionToken = (keyOrGraph: string | Constructable) => { const ctx = useContext(GraphContext); - const [injectionToken] = useState(() => ctx?.injectionToken ?? uniqueId(Graph.name)); + const [injectionToken] = useState(() => { + return ctx?.injectionToken ?? uniqueId(isString(keyOrGraph)? keyOrGraph : keyOrGraph.name); + }); return injectionToken; }; From 4cfae10eb9574672232103aab5a1c998cd1144ee Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 31 Oct 2024 10:40:46 +0200 Subject: [PATCH 4/9] Update eslintrc and ignore expect toThrow --- packages/react-obsidian/.eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-obsidian/.eslintrc.json b/packages/react-obsidian/.eslintrc.json index 9570fb6b..10a579cd 100644 --- a/packages/react-obsidian/.eslintrc.json +++ b/packages/react-obsidian/.eslintrc.json @@ -39,7 +39,7 @@ "code": 115, "comments": 200, "ignoreRegExpLiterals": true, - "ignorePattern": "throw new Error\\(.*\\);" + "ignorePattern": "throw new Error\\(.*\\);|expect\\(\\(\\) => uut\\.resolve\\('main'\\)\\)\\.toThrow\\(.*\\);" } ], "@stylistic/no-extra-semi": "error", From d319a7717747b876ebf06e505f985cfeb8e689f7 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 31 Oct 2024 10:59:15 +0200 Subject: [PATCH 5/9] Throw is graph is already registered --- .../src/graph/registry/GraphRegistry.test.ts | 8 ++++++++ .../react-obsidian/src/graph/registry/GraphRegistry.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts b/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts index 36554cd2..502d37cb 100644 --- a/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts +++ b/packages/react-obsidian/src/graph/registry/GraphRegistry.test.ts @@ -1,3 +1,4 @@ +import { mock } from 'jest-mock-extended'; import SingletonGraph from '../../../test/fixtures/SingletonGraph'; import MainGraph from '../../../test/fixtures/MainGraph'; import { GraphRegistry } from './GraphRegistry'; @@ -82,4 +83,11 @@ describe('GraphRegistry', () => { uut.clear(graph); expect(() => uut.resolve('main')).toThrow(); }); + + it('throws when registering a graph generator with the same key', () => { + uut.registerGraphGenerator('main', () => mock()); + expect( + () => uut.registerGraphGenerator('main', () => mock()), + ).toThrow('Attempted to register a graph generator for key "main" that is already registered.'); + }); }); diff --git a/packages/react-obsidian/src/graph/registry/GraphRegistry.ts b/packages/react-obsidian/src/graph/registry/GraphRegistry.ts index eba03ac5..508f7604 100644 --- a/packages/react-obsidian/src/graph/registry/GraphRegistry.ts +++ b/packages/react-obsidian/src/graph/registry/GraphRegistry.ts @@ -22,6 +22,7 @@ export class GraphRegistry { } registerGraphGenerator(key: string, generator: () => Constructable) { + if (this.keyToGenerator.has(key)) throw new Error(`Attempted to register a graph generator for key "${key}" that is already registered.`); this.keyToGenerator.set(key, generator); } From df71ee95c139484c959ae2819996809ac3110fc7 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 31 Oct 2024 12:13:59 +0200 Subject: [PATCH 6/9] Document register graphs - only in the API section for now --- .../docs/reference/mediatorObservable.mdx | 2 +- .../docs/reference/observable.mdx | 2 +- .../documentation/docs/reference/obsidian.mdx | 39 +++++++++++++++++++ .../docs/reference/useObserver.mdx | 2 +- .../docs/reference/useObservers.mdx | 2 +- 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 packages/documentation/docs/reference/obsidian.mdx diff --git a/packages/documentation/docs/reference/mediatorObservable.mdx b/packages/documentation/docs/reference/mediatorObservable.mdx index 58e6a156..b351bafc 100644 --- a/packages/documentation/docs/reference/mediatorObservable.mdx +++ b/packages/documentation/docs/reference/mediatorObservable.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 title: 'MediatorObservable' tags: [MediatorObservable, Reactivity] --- diff --git a/packages/documentation/docs/reference/observable.mdx b/packages/documentation/docs/reference/observable.mdx index 4fc88497..a567e2db 100644 --- a/packages/documentation/docs/reference/observable.mdx +++ b/packages/documentation/docs/reference/observable.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 2 title: 'Observable' tags: [Observable, Reactivity] --- diff --git a/packages/documentation/docs/reference/obsidian.mdx b/packages/documentation/docs/reference/obsidian.mdx new file mode 100644 index 00000000..74ae12a8 --- /dev/null +++ b/packages/documentation/docs/reference/obsidian.mdx @@ -0,0 +1,39 @@ +--- +sidebar_position: 1 +title: 'Obsidian' +--- + +`Obsidian` **exposes a set of functions that allow you to interact with the Obsidian framework imperatively.** + +* [Reference](#reference) + * [obtain(keyOrGraph, props?)](#obtainkeyorgraph-props) + * [registerGraph(key, graphGenerator)](#registergraphkey-graphgenerator) + * [inject(target, keyOrGraph)](#injecttarget-keyorgraph) +* [Usage](#usage) + * [Conditional rendering of a component](#conditional-rendering-of-a-component) +___ + +## Reference +### obtain(keyOrGraph, props?): ServiceLocator +The `obtain` function is used to obtain a graph instance to be used as a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern). + +#### Arguments +* `keyOrGraph?` - The class reference of the graph or its corresponding key if it was registered with a key. +* `props?` - An object containing props to be passed to the graph's constructor if this is the first time the graph is being instantiated. + +#### Returns +* `ServiceLocator` - A service locator instance that can be used to resolve dependencies from the graph. + +### registerGraph(key, graphGenerator) +The `registerGraph` function is used to register a graph generator function with a key. This allows the graph to be instantiated using the key instead of the class reference. This is useful when you want to decouple the graph's instantiation from its usage. + +#### Arguments +* `key` - The key to register the graph with. +* `graphGenerator` - A function that returns the graph class reference. The generator function is called only when the graph is being instantiated. It's recommended to retrieve the class reference using inline require to delay side effects until the graph is actually used. + +### inject(target, keyOrGraph) +The `inject` function is used to inject dependencies annotated with the `@LateInject` decorator into a class instance. + +#### Arguments +* `target` - The class instance to inject the dependencies into. +* `keyOrGraph` - The class reference of the graph or its corresponding key if it was registered with a key. diff --git a/packages/documentation/docs/reference/useObserver.mdx b/packages/documentation/docs/reference/useObserver.mdx index 773b586c..c7783fff 100644 --- a/packages/documentation/docs/reference/useObserver.mdx +++ b/packages/documentation/docs/reference/useObserver.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 title: 'useObserver' tags: [useObserver, Reactivity] --- diff --git a/packages/documentation/docs/reference/useObservers.mdx b/packages/documentation/docs/reference/useObservers.mdx index 89d44285..4b2f6168 100644 --- a/packages/documentation/docs/reference/useObservers.mdx +++ b/packages/documentation/docs/reference/useObservers.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 title: 'useObservers' tags: [useObservers, Reactivity] --- From 9dfaa6a227e65614a0b6292beff746f129bbb970 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 31 Oct 2024 13:45:02 +0200 Subject: [PATCH 7/9] Inject classes by registered graph key --- packages/react-obsidian/.eslintrc.json | 2 +- packages/react-obsidian/src/Obsidian.ts | 4 +-- .../src/graph/registry/GraphRegistry.ts | 4 ++- .../src/injectors/class/LateInjector.ts | 13 +++++++--- .../test/acceptance/lateInject.test.ts | 26 +++++++++++++++++++ 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/react-obsidian/.eslintrc.json b/packages/react-obsidian/.eslintrc.json index 10a579cd..acf0515f 100644 --- a/packages/react-obsidian/.eslintrc.json +++ b/packages/react-obsidian/.eslintrc.json @@ -39,7 +39,7 @@ "code": 115, "comments": 200, "ignoreRegExpLiterals": true, - "ignorePattern": "throw new Error\\(.*\\);|expect\\(\\(\\) => uut\\.resolve\\('main'\\)\\)\\.toThrow\\(.*\\);" + "ignorePattern": "throw new Error\\(.*\\);|expect\\(.*\\)\\.toThrow\\(.*\\);" } ], "@stylistic/no-extra-semi": "error", diff --git a/packages/react-obsidian/src/Obsidian.ts b/packages/react-obsidian/src/Obsidian.ts index ea9d6e0c..7feeb078 100644 --- a/packages/react-obsidian/src/Obsidian.ts +++ b/packages/react-obsidian/src/Obsidian.ts @@ -17,8 +17,8 @@ export default class Obsidian { return serviceLocatorFactory.fromGraph(keyOrGraph, props); } - inject(target: T, graph?: ObjectGraph) { - return lateInjector.inject(target, graph); + inject(target: T, keyOrGraph?: string | ObjectGraph) { + return lateInjector.inject(target, keyOrGraph); } addGraphMiddleware(middleware: GraphMiddleware) { diff --git a/packages/react-obsidian/src/graph/registry/GraphRegistry.ts b/packages/react-obsidian/src/graph/registry/GraphRegistry.ts index 508f7604..4bf2ea10 100644 --- a/packages/react-obsidian/src/graph/registry/GraphRegistry.ts +++ b/packages/react-obsidian/src/graph/registry/GraphRegistry.ts @@ -26,7 +26,9 @@ export class GraphRegistry { this.keyToGenerator.set(key, generator); } - ensureRegistered(graph: Graph) { + ensureRegistered(keyOrGraph: string | Graph) { + if (isString(keyOrGraph)) return; + const graph = keyOrGraph; if (this.instanceToConstructor.get(graph)) return; this.set(graph.constructor as any, graph); } diff --git a/packages/react-obsidian/src/injectors/class/LateInjector.ts b/packages/react-obsidian/src/injectors/class/LateInjector.ts index 90e5bd0a..81e61406 100644 --- a/packages/react-obsidian/src/injectors/class/LateInjector.ts +++ b/packages/react-obsidian/src/injectors/class/LateInjector.ts @@ -1,3 +1,4 @@ +import { isString } from 'lodash'; import { ObjectGraph } from '../../graph/ObjectGraph'; import graphRegistry from '../../graph/registry/GraphRegistry'; import InjectionMetadata from './InjectionMetadata'; @@ -5,16 +6,22 @@ import InjectionMetadata from './InjectionMetadata'; export const GRAPH_INSTANCE_NAME_KEY = 'GRAPH_INSTANCE_NAME'; class LateInjector { - inject(target: T, sourceGraph?: ObjectGraph): T { - if (sourceGraph) graphRegistry.ensureRegistered(sourceGraph); + inject(target: T, keyOrGraph?: string | ObjectGraph): T { + if (keyOrGraph) graphRegistry.ensureRegistered(keyOrGraph); const injectionMetadata = new InjectionMetadata(); - const graph = sourceGraph ?? this.getGraphInstance(target); + const graph = this.getGraph(target, keyOrGraph); injectionMetadata.getLatePropertiesToInject(target.constructor).forEach((key) => { Reflect.set(target, key, graph.retrieve(key)); }); return target; } + private getGraph(target: T, keyOrGraph?: string | ObjectGraph) { + if (keyOrGraph instanceof ObjectGraph) return keyOrGraph; + if (isString(keyOrGraph)) return graphRegistry.resolve(keyOrGraph, 'classInjection'); + return this.getGraphInstance(target); + } + private getGraphInstance(target: T) { const graphInstanceName = Reflect.getMetadata(GRAPH_INSTANCE_NAME_KEY, target.constructor); return graphRegistry.getGraphInstance(graphInstanceName); diff --git a/packages/react-obsidian/test/acceptance/lateInject.test.ts b/packages/react-obsidian/test/acceptance/lateInject.test.ts index b858c8ca..6b98d200 100644 --- a/packages/react-obsidian/test/acceptance/lateInject.test.ts +++ b/packages/react-obsidian/test/acceptance/lateInject.test.ts @@ -37,6 +37,32 @@ describe('Late inject', () => { expect(new Injected().graphString).toBe('from mocked main from mocked subgraph'); }); + + it('injects using a registered graph key', () => { + Obsidian.registerGraph('main', () => MainGraph); + + class Injected { + @LateInject() graphString!: string; + + constructor() { + Obsidian.inject(this, 'main'); + } + } + + expect(new Injected().graphString).toBe('from main from subgraph'); + }); + + it('throws an error if the graph is not registered', () => { + class Injected { + @LateInject() graphString!: string; + + constructor() { + Obsidian.inject(this, 'main'); + } + } + + expect(() => new Injected()).toThrow('Attempted to resolve a graph by key "main" that is not registered. Did you forget to call Obsidian.registerGraph?'); + }); }); @Graph() From 5bb6ab0361519fca235aab4bb4e8e42ed3e90d9d Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 31 Oct 2024 14:00:32 +0200 Subject: [PATCH 8/9] @Injectable decorator now accepts string key in addition to class reference --- .../src/decorators/inject/Injectable.ts | 4 ++-- .../src/injectors/class/ClassInjector.ts | 8 ++++---- .../test/integration/classInjection.test.tsx | 13 ++++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react-obsidian/src/decorators/inject/Injectable.ts b/packages/react-obsidian/src/decorators/inject/Injectable.ts index ee4551f4..dd5a16e3 100644 --- a/packages/react-obsidian/src/decorators/inject/Injectable.ts +++ b/packages/react-obsidian/src/decorators/inject/Injectable.ts @@ -3,6 +3,6 @@ import { Graph } from '../../graph/Graph'; import graphRegistry from '../../graph/registry/GraphRegistry'; import ClassInjector from '../../injectors/class/ClassInjector'; -export function Injectable(Graph: Constructable): any { - return new ClassInjector(graphRegistry).inject(Graph); +export function Injectable(keyOrGraph: string | Constructable): any { + return new ClassInjector(graphRegistry).inject(keyOrGraph); } diff --git a/packages/react-obsidian/src/injectors/class/ClassInjector.ts b/packages/react-obsidian/src/injectors/class/ClassInjector.ts index 367859f5..4b4732c8 100644 --- a/packages/react-obsidian/src/injectors/class/ClassInjector.ts +++ b/packages/react-obsidian/src/injectors/class/ClassInjector.ts @@ -11,14 +11,14 @@ export default class ClassInjector { private injectionMetadata: InjectionMetadata = new InjectionMetadata(), ) {} - inject(Graph: Constructable) { + inject(keyOrGraph: string | Constructable) { return (Target: Constructable) => { - return new Proxy(Target, this.createProxyHandler(Graph, this.graphRegistry, this.injectionMetadata)); + return new Proxy(Target, this.createProxyHandler(keyOrGraph, this.graphRegistry, this.injectionMetadata)); }; } private createProxyHandler( - Graph: Constructable, + keyOrGraph: string | Constructable, graphRegistry: GraphRegistry, injectionMetadata: InjectionMetadata, ): ProxyHandler { @@ -26,7 +26,7 @@ export default class ClassInjector { construct(target: any, args: any[], newTarget: Function): any { const isReactClassComponent = target.prototype?.isReactComponent; const source = isReactClassComponent ? 'lifecycleOwner' : 'classInjection'; - const graph = graphRegistry.resolve(Graph, source, args.length > 0 ? args[0] : undefined); + const graph = graphRegistry.resolve(keyOrGraph, source, args.length > 0 ? args[0] : undefined); if (isReactClassComponent) { referenceCounter.retain(graph); } diff --git a/packages/react-obsidian/test/integration/classInjection.test.tsx b/packages/react-obsidian/test/integration/classInjection.test.tsx index 72cbdf3e..d07258bd 100644 --- a/packages/react-obsidian/test/integration/classInjection.test.tsx +++ b/packages/react-obsidian/test/integration/classInjection.test.tsx @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '../../src'; +import { Inject, Injectable, Obsidian } from '../../src'; import { GraphWithOnBind } from '../fixtures/GraphWithOnBind'; import injectedValues from '../fixtures/injectedValues'; import MainGraph from '../fixtures/MainGraph'; @@ -37,6 +37,17 @@ describe('Class injection', () => { // expect(uut.anotherString).toBe(injectedValues.anotherString); // }); + it('injects properties from a registered graph', () => { + Obsidian.registerGraph('main', () => MainGraph); + const uut = new ClassToTestRegisteredGraph(); + expect(uut.someString).toBe(injectedValues.fromStringProvider); + }); + + @Injectable('main') + class ClassToTestRegisteredGraph { + @Inject() public readonly someString!: string; + } + @Injectable(GraphWithOnBind) class ClassToTestOnBind { @Inject() public readonly targetName!: string; From a6f7b7f7612d65546ac65891b6e38dfcb6fb9676 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 31 Oct 2024 14:32:23 +0200 Subject: [PATCH 9/9] update tags --- .../documentation/docs/documentation/usage/ServiceLocator.mdx | 1 + packages/documentation/docs/reference/obsidian.mdx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/documentation/docs/documentation/usage/ServiceLocator.mdx b/packages/documentation/docs/documentation/usage/ServiceLocator.mdx index 11cd24dc..a43a0b7c 100644 --- a/packages/documentation/docs/documentation/usage/ServiceLocator.mdx +++ b/packages/documentation/docs/documentation/usage/ServiceLocator.mdx @@ -1,6 +1,7 @@ --- sidebar_position: 6 title: "Service locator" +tags: [Service Locator] --- ## Obtaining dependencies imperatively diff --git a/packages/documentation/docs/reference/obsidian.mdx b/packages/documentation/docs/reference/obsidian.mdx index 74ae12a8..a83be013 100644 --- a/packages/documentation/docs/reference/obsidian.mdx +++ b/packages/documentation/docs/reference/obsidian.mdx @@ -1,6 +1,7 @@ --- sidebar_position: 1 title: 'Obsidian' +tags: [Service Locator] --- `Obsidian` **exposes a set of functions that allow you to interact with the Obsidian framework imperatively.** @@ -9,8 +10,6 @@ title: 'Obsidian' * [obtain(keyOrGraph, props?)](#obtainkeyorgraph-props) * [registerGraph(key, graphGenerator)](#registergraphkey-graphgenerator) * [inject(target, keyOrGraph)](#injecttarget-keyorgraph) -* [Usage](#usage) - * [Conditional rendering of a component](#conditional-rendering-of-a-component) ___ ## Reference