packages/core: added api provider
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
"@spotify/web-scripts": "^6.0.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2"
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef, { ApiRefConfig } from './apis/ApiRef';
|
||||
import AppBuilder from './app/AppBuilder';
|
||||
import WidgetViewBuilder from './widgetView/WidgetViewBuilder';
|
||||
import BackstagePlugin, { PluginConfig } from './plugin/Plugin';
|
||||
@@ -22,6 +23,10 @@ export function createApp() {
|
||||
return new AppBuilder();
|
||||
}
|
||||
|
||||
export function createApiRef<T>(config: ApiRefConfig) {
|
||||
return new ApiRef<T>(config);
|
||||
}
|
||||
|
||||
export function createWidgetView() {
|
||||
return new WidgetViewBuilder();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ApiProvider, { useApi, withApis } from './ApiProvider';
|
||||
import ApiRef from './ApiRef';
|
||||
import ApiRegistry from './ApiRegistry';
|
||||
import { render } from '@testing-library/react';
|
||||
import { withLogCollector } from '../../testUtils';
|
||||
|
||||
describe('ApiProvider', () => {
|
||||
type Api = () => string;
|
||||
const apiRef = new ApiRef<Api>({ id: 'x', description: '' });
|
||||
const registry = ApiRegistry.from([[apiRef, () => 'hello']]);
|
||||
|
||||
const MyHookConsumer = () => {
|
||||
const api = useApi(apiRef);
|
||||
return <p>hook message: {api()}</p>;
|
||||
};
|
||||
|
||||
const MyHocConsumer = withApis({ getMessage: apiRef })(({ getMessage }) => {
|
||||
return <p>hoc message: {getMessage()}</p>;
|
||||
});
|
||||
|
||||
it('should provide apis', () => {
|
||||
const renderedHook = render(
|
||||
<ApiProvider apis={registry}>
|
||||
<MyHookConsumer />
|
||||
</ApiProvider>,
|
||||
);
|
||||
renderedHook.getByText('hook message: hello');
|
||||
|
||||
const renderedHoc = render(
|
||||
<ApiProvider apis={registry}>
|
||||
<MyHocConsumer />
|
||||
</ApiProvider>,
|
||||
);
|
||||
renderedHoc.getByText('hoc message: hello');
|
||||
});
|
||||
|
||||
it('should ignore deps in prototype', () => {
|
||||
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
|
||||
const xRef = new ApiRef<number>({ id: 'x', description: '' });
|
||||
|
||||
const proto = { x: xRef };
|
||||
const props = { getMessage: { enumerable: true, value: apiRef } };
|
||||
const obj = Object.create(proto, props) as {
|
||||
getMessage: typeof apiRef;
|
||||
x: typeof xRef;
|
||||
};
|
||||
|
||||
const MyWeirdHocConsumer = withApis(obj)(({ getMessage }) => {
|
||||
return <p>hoc message: {getMessage()}</p>;
|
||||
});
|
||||
|
||||
const renderedHoc = render(
|
||||
<ApiProvider apis={registry}>
|
||||
<MyWeirdHocConsumer />
|
||||
</ApiProvider>,
|
||||
);
|
||||
renderedHoc.getByText('hoc message: hello');
|
||||
});
|
||||
|
||||
it('should error if no provider is available', () => {
|
||||
expect(
|
||||
withLogCollector(['error'], () => {
|
||||
expect(() => {
|
||||
render(<MyHookConsumer />);
|
||||
}).toThrow('No ApiProvider available in react context');
|
||||
}).error,
|
||||
).toEqual([
|
||||
expect.stringMatching(
|
||||
/^Error: Uncaught \[Error: No ApiProvider available in react context\]/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/^The above error occurred in the <MyHookConsumer> component/,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(
|
||||
withLogCollector(['error'], () => {
|
||||
expect(() => {
|
||||
render(<MyHocConsumer />);
|
||||
}).toThrow('No ApiProvider available in react context');
|
||||
}).error,
|
||||
).toEqual([
|
||||
expect.stringMatching(
|
||||
/^Error: Uncaught \[Error: No ApiProvider available in react context\]/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/^The above error occurred in the <withApis\(Component\)> component/,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error if api is not available', () => {
|
||||
expect(
|
||||
withLogCollector(['error'], () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<ApiProvider apis={ApiRegistry.from([])}>
|
||||
<MyHookConsumer />
|
||||
</ApiProvider>,
|
||||
);
|
||||
}).toThrow('No implementation available for apiRef{x}');
|
||||
}).error,
|
||||
).toEqual([
|
||||
expect.stringMatching(
|
||||
/^Error: Uncaught \[Error: No implementation available for apiRef{x}\]/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/^The above error occurred in the <MyHookConsumer> component/,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(
|
||||
withLogCollector(['error'], () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<ApiProvider apis={ApiRegistry.from([])}>
|
||||
<MyHocConsumer />
|
||||
</ApiProvider>,
|
||||
);
|
||||
}).toThrow('No implementation available for apiRef{x}');
|
||||
}).error,
|
||||
).toEqual([
|
||||
expect.stringMatching(
|
||||
/^Error: Uncaught \[Error: No implementation available for apiRef{x}\]/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/^The above error occurred in the <withApis\(Component\)> component/,
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, createContext, useContext, ReactNode } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiHolder, TypesToApiRefs } from './types';
|
||||
|
||||
type Props = {
|
||||
apis: ApiHolder;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Context = createContext<ApiHolder | undefined>(undefined);
|
||||
|
||||
const ApiProvider: FC<Props> = ({ apis, children }) => {
|
||||
return <Context.Provider value={apis} children={children} />;
|
||||
};
|
||||
|
||||
ApiProvider.propTypes = {
|
||||
apis: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export function useApi<T>(apiRef: ApiRef<T>): T {
|
||||
const apiHolder = useContext(Context);
|
||||
|
||||
if (!apiHolder) {
|
||||
throw new Error('No ApiProvider available in react context');
|
||||
}
|
||||
|
||||
const api = apiHolder.get(apiRef);
|
||||
if (!api) {
|
||||
throw new Error(`No implementation available for ${apiRef}`);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
export function withApis<T>(apis: TypesToApiRefs<T>) {
|
||||
return function withApisWrapper<P extends T>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
) {
|
||||
const Hoc: FC<Omit<P, keyof T>> = props => {
|
||||
const apiHolder = useContext(Context);
|
||||
|
||||
if (!apiHolder) {
|
||||
throw new Error('No ApiProvider available in react context');
|
||||
}
|
||||
|
||||
const impls = {} as T;
|
||||
|
||||
for (const key in apis) {
|
||||
if (apis.hasOwnProperty(key)) {
|
||||
const ref = apis[key];
|
||||
|
||||
const api = apiHolder.get(ref);
|
||||
if (!api) {
|
||||
throw new Error(`No implementation available for ${ref}`);
|
||||
}
|
||||
impls[key] = api;
|
||||
}
|
||||
}
|
||||
|
||||
return <WrappedComponent {...(props as P)} {...impls} />;
|
||||
};
|
||||
const displayName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
|
||||
Hoc.displayName = `withApis(${displayName})`;
|
||||
|
||||
return Hoc;
|
||||
};
|
||||
}
|
||||
|
||||
export default ApiProvider;
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
|
||||
describe('ApiRef', () => {
|
||||
it('should be created', () => {
|
||||
const ref = new ApiRef({ id: 'abc', description: '123' });
|
||||
expect(ref.id).toBe('abc');
|
||||
expect(ref.description).toBe('123');
|
||||
expect(String(ref)).toBe('apiRef{abc}');
|
||||
expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}');
|
||||
});
|
||||
|
||||
it('should require a ascii letters only in id', () => {
|
||||
for (const id of ['a', 'abc', 'ABC', 'aBC', 'aBc']) {
|
||||
expect(new ApiRef({ id, description: '123' }).id).toBe(id);
|
||||
}
|
||||
|
||||
for (const id of ['123', 'ab-c', 'ab_c', 'a2c', '', '_']) {
|
||||
expect(() => new ApiRef({ id, description: '123' }).id).toThrow(
|
||||
`API id must only contain ascii letters, got '${id}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type ApiRefConfig = {
|
||||
id: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default class ApiRef<T> {
|
||||
constructor(private readonly config: ApiRefConfig) {
|
||||
if (!config.id.match(/^[a-zA-Z]+$/)) {
|
||||
throw new Error(
|
||||
`API id must only contain ascii letters, got '${config.id}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config.description;
|
||||
}
|
||||
|
||||
// Utility for getting type of an api, using `typeof apiRef.T`
|
||||
get T(): T {
|
||||
throw new Error(`tried to read ApiRef.T of ${this}`);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `apiRef{${this.config.id}}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRegistry from './ApiRegistry';
|
||||
import ApiRef from './ApiRef';
|
||||
|
||||
describe('ApiRegistry', () => {
|
||||
const x1Ref = new ApiRef<number>({ id: 'x', description: '' });
|
||||
const x2Ref = new ApiRef<string>({ id: 'x', description: '' });
|
||||
|
||||
it('should be created', () => {
|
||||
const registry = ApiRegistry.from([]);
|
||||
expect(registry.get(x1Ref)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should be created with APIs', () => {
|
||||
const registry = ApiRegistry.from([
|
||||
[x1Ref, 3],
|
||||
[x2Ref, 'y'],
|
||||
]);
|
||||
expect(registry.get(x1Ref)).toBe(3);
|
||||
expect(registry.get(x2Ref)).toBe('y');
|
||||
});
|
||||
|
||||
it('should be built', () => {
|
||||
const registry = ApiRegistry.builder().build();
|
||||
expect(registry.get(x1Ref)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should be built with APIs', () => {
|
||||
const builder = ApiRegistry.builder();
|
||||
builder.add(x1Ref, 3);
|
||||
builder.add(x2Ref, 'y');
|
||||
|
||||
const registry = builder.build();
|
||||
expect(registry.get(x1Ref)).toBe(3);
|
||||
expect(registry.get(x2Ref)).toBe('y');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiHolder } from './types';
|
||||
|
||||
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
|
||||
|
||||
class ApiRegistryBuilder {
|
||||
private apis: ApiImpl[] = [];
|
||||
|
||||
add<T>(api: ApiRef<T>, impl: T): T {
|
||||
this.apis.push([api, impl]);
|
||||
return impl;
|
||||
}
|
||||
|
||||
build(): ApiRegistry {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return new ApiRegistry(new Map(this.apis));
|
||||
}
|
||||
}
|
||||
|
||||
export default class ApiRegistry implements ApiHolder {
|
||||
static builder() {
|
||||
return new ApiRegistryBuilder();
|
||||
}
|
||||
|
||||
static from(apis: ApiImpl[]) {
|
||||
return new ApiRegistry(new Map(apis));
|
||||
}
|
||||
|
||||
constructor(private readonly apis: Map<ApiRef<unknown>, unknown>) {}
|
||||
|
||||
get<T>(api: ApiRef<T>): T | undefined {
|
||||
return this.apis.get(api) as T | undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiTestRegistry from './ApiTestRegistry';
|
||||
import ApiRef from './ApiRef';
|
||||
|
||||
describe('ApiTestRegistry', () => {
|
||||
const aRef = new ApiRef<number>({ id: 'a', description: '' });
|
||||
const bRef = new ApiRef<string>({ id: 'b', description: '' });
|
||||
const cRef = new ApiRef<string>({ id: 'c', description: '' });
|
||||
|
||||
it('should be created', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
expect(registry.get(aRef)).toBe(undefined);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
expect(registry.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should register a factory', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register(aRef, () => 3);
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
expect(registry.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should remove factories when resetting', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register(aRef, () => 3);
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
registry.reset();
|
||||
expect(registry.get(aRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should keep saved factories when resetting', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register(aRef, () => 3);
|
||||
registry.save();
|
||||
registry.register(bRef, () => 'x');
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe('x');
|
||||
registry.reset();
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should register factories with dependencies', () => {
|
||||
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
|
||||
const cDeps = Object.create({ c: cRef }, { a: { enumerable: true, value: aRef } });
|
||||
cDeps.b = bRef;
|
||||
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
|
||||
registry.register({ implements: bRef, deps: { dep: aRef }, factory: ({ dep }) => `hello ${dep}` });
|
||||
registry.register({ implements: cRef, deps: cDeps, factory: ({ a, b }) => b.repeat(a) });
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe('hello 3');
|
||||
expect(registry.get(cRef)).toBe('hello 3hello 3hello 3');
|
||||
});
|
||||
|
||||
it('should not allow cyclic dependencies', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: { b: bRef }, factory: () => 1 });
|
||||
registry.register({ implements: bRef, deps: { c: cRef }, factory: () => 'b' });
|
||||
registry.register({ implements: cRef, deps: { a: aRef }, factory: () => 'c' });
|
||||
expect(() => registry.get(aRef)).toThrow('Circular dependency of api factory for apiRef{a}');
|
||||
expect(() => registry.get(bRef)).toThrow('Circular dependency of api factory for apiRef{b}');
|
||||
expect(() => registry.get(cRef)).toThrow('Circular dependency of api factory for apiRef{c}');
|
||||
});
|
||||
|
||||
it('should throw error if dependency is not available', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: { b: bRef }, factory: () => 1 });
|
||||
expect(() => registry.get(aRef)).toThrow(
|
||||
'No API factory available for dependency apiRef{b} of dependent apiRef{a}',
|
||||
);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
expect(registry.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should only call factory func once', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
const factory = jest.fn().mockReturnValue(2);
|
||||
registry.register(aRef, factory);
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(0);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call factory again after reset', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
const factory = jest.fn().mockReturnValue(2);
|
||||
registry.register(aRef, factory);
|
||||
registry.save();
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(0);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
registry.reset();
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
import { TypesToApiRefs, AnyApiRef, ApiHolder, ApiFactory } from './types';
|
||||
|
||||
export default class ApiTestRegistry implements ApiHolder {
|
||||
private readonly apis = new Map<AnyApiRef, unknown>();
|
||||
private factories = new Map<AnyApiRef, ApiFactory<unknown, unknown, unknown>>();
|
||||
private savedFactories = new Map<AnyApiRef, ApiFactory<unknown, unknown, unknown>>();
|
||||
|
||||
get<T>(ref: ApiRef<T>): T | undefined {
|
||||
return this.load(ref);
|
||||
}
|
||||
|
||||
register<T>(ref: ApiRef<T>, factoryFunc: () => T): ApiTestRegistry;
|
||||
register<A, I, D>(factory: ApiFactory<A, I, D>): ApiTestRegistry;
|
||||
register<A, I, D, T>(factory: ApiRef<T> | ApiFactory<A, I, D>, factoryFunc?: () => T): ApiTestRegistry {
|
||||
if (factory instanceof ApiRef) {
|
||||
this.factories.set(factory, {
|
||||
implements: factory,
|
||||
deps: {},
|
||||
factory: factoryFunc!,
|
||||
});
|
||||
} else {
|
||||
this.factories.set(factory.implements, factory);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.factories = this.savedFactories;
|
||||
this.apis.clear();
|
||||
}
|
||||
|
||||
save(): ApiTestRegistry {
|
||||
this.savedFactories = new Map(this.factories);
|
||||
return this;
|
||||
}
|
||||
|
||||
private load<T>(ref: ApiRef<T>, loading: AnyApiRef[] = []): T | undefined {
|
||||
const impl = this.apis.get(ref);
|
||||
if (impl) {
|
||||
return impl as T;
|
||||
}
|
||||
|
||||
const factory = this.factories.get(ref);
|
||||
if (!factory) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (loading.includes(factory.implements)) {
|
||||
throw new Error(`Circular dependency of api factory for ${factory.implements}`);
|
||||
}
|
||||
|
||||
const deps = this.loadDeps(ref, factory.deps, [...loading, factory.implements]);
|
||||
const api = factory.factory(deps);
|
||||
this.apis.set(ref, api);
|
||||
return api as T;
|
||||
}
|
||||
|
||||
private loadDeps<T>(dependent: ApiRef<unknown>, apis: TypesToApiRefs<T>, loading: AnyApiRef[]): T {
|
||||
const impls = {} as T;
|
||||
|
||||
for (const key in apis) {
|
||||
if (apis.hasOwnProperty(key)) {
|
||||
const ref = apis[key];
|
||||
|
||||
const api = this.load(ref, loading);
|
||||
if (!api) {
|
||||
throw new Error(`No API factory available for dependency ${ref} of dependent ${dependent}`);
|
||||
}
|
||||
impls[key] = api;
|
||||
}
|
||||
}
|
||||
|
||||
return impls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default as ApiProvider, useApi } from './ApiProvider';
|
||||
export { default as ApiRegistry } from './ApiRegistry';
|
||||
export { default as ApiTestRegistry } from './ApiTestRegistry';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
|
||||
export type AnyApiRef = ApiRef<any>;
|
||||
|
||||
export type ApiRefType<T> = T extends ApiRef<infer U> ? U : never;
|
||||
|
||||
export type TypesToApiRefs<T> = { [key in keyof T]: ApiRef<T[key]> };
|
||||
|
||||
export type ApiRefsToTypes<T extends { [key in any]: ApiRef<any> }> = {
|
||||
[key in keyof T]: ApiRefType<T[key]>;
|
||||
};
|
||||
|
||||
export type ApiHolder = {
|
||||
get<T>(api: ApiRef<T>): T | undefined;
|
||||
};
|
||||
|
||||
export type ApiFactory<A, I, D> = {
|
||||
implements: ApiRef<A>;
|
||||
deps: TypesToApiRefs<D>;
|
||||
factory(deps: D): I extends A ? I : never;
|
||||
};
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
export * from './api';
|
||||
export * from './apis';
|
||||
export { useApp } from './app/AppContext';
|
||||
|
||||
Reference in New Issue
Block a user