Copy over missing APIs
Co-authored-by: Camila Belo <camilaibs@gmail.com> Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
05a0506afa
commit
a70e693707
@@ -14,49 +14,67 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { JSX } from 'react';
|
||||
import { ConfigReader, Config } from '@backstage/config';
|
||||
import {
|
||||
AppTree,
|
||||
appTreeApiRef,
|
||||
BackstagePlugin,
|
||||
coreExtensionData,
|
||||
ExtensionDataRef,
|
||||
ExtensionOverrides,
|
||||
RouteRef,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Core } from '../extensions/Core';
|
||||
import { CoreRoutes } from '../extensions/CoreRoutes';
|
||||
import { CoreLayout } from '../extensions/CoreLayout';
|
||||
import { CoreNav } from '../extensions/CoreNav';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
ApiHolder,
|
||||
AppComponents,
|
||||
AppContext,
|
||||
BackstagePlugin as LegacyBackstagePlugin,
|
||||
attachComponentData,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
appThemeApiRef,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
IconComponent,
|
||||
BackstagePlugin as LegacyBackstagePlugin,
|
||||
featureFlagsApiRef,
|
||||
identityApiRef,
|
||||
AppTheme,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { getAvailableFeatures } from './discovery';
|
||||
appLanguageApiRef,
|
||||
translationApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
ApiFactoryRegistry,
|
||||
ApiProvider,
|
||||
ApiResolver,
|
||||
AppThemeSelector,
|
||||
} from '@backstage/core-app-api';
|
||||
AppNode,
|
||||
AppTheme,
|
||||
AppTree,
|
||||
BackstagePlugin,
|
||||
ConfigApi,
|
||||
ExtensionDataRef,
|
||||
ExtensionOverrides,
|
||||
IconComponent,
|
||||
RouteRef,
|
||||
appThemeApiRef,
|
||||
appTreeApiRef,
|
||||
configApiRef,
|
||||
coreExtensionData,
|
||||
featureFlagsApiRef,
|
||||
identityApiRef,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
import { JSX, default as React } from 'react';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
import { Core } from '../extensions/Core';
|
||||
import { CoreLayout } from '../extensions/CoreLayout';
|
||||
import { CoreNav } from '../extensions/CoreNav';
|
||||
import { CoreRoutes } from '../extensions/CoreRoutes';
|
||||
import { DarkTheme, LightTheme } from '../extensions/themes';
|
||||
import { AppRouteBinder } from '../routing';
|
||||
import { RoutingProvider } from '../routing/RoutingProvider';
|
||||
import { collectRouteIds } from '../routing/collectRouteIds';
|
||||
import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
import { createAppTree } from '../tree';
|
||||
import { getAvailableFeatures } from './discovery';
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
apis as defaultApis,
|
||||
components as defaultComponents,
|
||||
icons as defaultIcons,
|
||||
} from '../../../app-defaults/src/defaults';
|
||||
// TODO: Get rid of all of these
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppThemeSelector } from '../../../core-app-api/src/apis/implementations/AppThemeApi';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
|
||||
@@ -73,26 +91,6 @@ import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementati
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
apis as defaultApis,
|
||||
components as defaultComponents,
|
||||
icons as defaultIcons,
|
||||
} from '../../../app-defaults/src/defaults';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import { DarkTheme, LightTheme } from '../extensions/themes';
|
||||
import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode';
|
||||
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
import {
|
||||
appLanguageApiRef,
|
||||
translationApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { AppRouteBinder } from '../routing';
|
||||
import { RoutingProvider } from '../routing/RoutingProvider';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
import { collectRouteIds } from '../routing/collectRouteIds';
|
||||
import { createAppTree } from '../tree';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const builtinExtensions = [
|
||||
Core,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiAggregator } from './ApiAggregator';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
|
||||
describe('ApiAggregator', () => {
|
||||
const apiARef = createApiRef<number>({ id: 'a' });
|
||||
const apiBRef = createApiRef<number>({ id: 'b' });
|
||||
|
||||
it('should forward implementations', () => {
|
||||
const agg = new ApiAggregator(
|
||||
ApiRegistry.from([
|
||||
[apiARef, 5],
|
||||
[apiBRef, 10],
|
||||
]),
|
||||
);
|
||||
expect(agg.get(apiARef)).toBe(5);
|
||||
expect(agg.get(apiBRef)).toBe(10);
|
||||
});
|
||||
|
||||
it('should return the first implementation', () => {
|
||||
const agg = new ApiAggregator(
|
||||
ApiRegistry.from([
|
||||
[apiARef, 1],
|
||||
[apiARef, 2],
|
||||
]),
|
||||
);
|
||||
expect(agg.get(apiARef)).toBe(2);
|
||||
expect(agg.get(apiBRef)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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, ApiHolder } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* An ApiHolder that queries multiple other holders from for
|
||||
* an Api implementation, returning the first one encountered..
|
||||
*/
|
||||
export class ApiAggregator implements ApiHolder {
|
||||
private readonly holders: ApiHolder[];
|
||||
|
||||
constructor(...holders: ApiHolder[]) {
|
||||
this.holders = holders;
|
||||
}
|
||||
|
||||
get<T>(apiRef: ApiRef<T>): T | undefined {
|
||||
for (const holder of this.holders) {
|
||||
const api = holder.get(apiRef);
|
||||
if (api) {
|
||||
return api;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
|
||||
const aRef = createApiRef<number>({ id: 'a' });
|
||||
const aFactory1 = { api: aRef, deps: {}, factory: () => 1 };
|
||||
const aFactory2 = { api: aRef, deps: {}, factory: () => 2 };
|
||||
const bRef = createApiRef<string>({ id: 'b' });
|
||||
const bFactory = { api: bRef, deps: {}, factory: () => 'x' };
|
||||
const cRef = createApiRef<string>({ id: 'c' });
|
||||
const cFactory = { api: cRef, deps: {}, factory: () => 'y' };
|
||||
|
||||
describe('ApiFactoryRegistry', () => {
|
||||
it('should be empty when created', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.getAllApis()).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should register a factory', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('default', aFactory1)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.getAllApis()).toEqual(new Set([aRef]));
|
||||
});
|
||||
|
||||
it('should prioritize factories based on scope', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('default', aFactory1)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('default', aFactory2)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('app', aFactory2)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory2);
|
||||
expect(registry.register('default', aFactory1)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory2);
|
||||
expect(registry.register('static', aFactory1)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('static', aFactory2)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('app', aFactory2)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.getAllApis()).toEqual(new Set([aRef]));
|
||||
});
|
||||
|
||||
it('should register multiple factories without conflict', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('static', aFactory1)).toBe(true);
|
||||
expect(registry.register('default', bFactory)).toBe(true);
|
||||
expect(registry.register('app', cFactory)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.get(bRef)).toBe(bFactory);
|
||||
expect(registry.get(cRef)).toBe(cFactory);
|
||||
expect(registry.getAllApis()).toEqual(new Set([aRef, bRef, cRef]));
|
||||
});
|
||||
|
||||
it('should identify ApiRefs by id but still return the correct factory ref when listing all apis', () => {
|
||||
const ref1 = createApiRef<number>({ id: 'a' });
|
||||
const ref2 = createApiRef<number>({ id: 'a' });
|
||||
|
||||
const factory1 = { api: ref1, deps: {}, factory: () => 3 };
|
||||
const factory2 = { api: ref2, deps: {}, factory: () => 3 };
|
||||
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('default', factory1)).toBe(true);
|
||||
expect(registry.register('default', factory2)).toBe(false);
|
||||
expect(registry.get(ref1)).toEqual(factory1);
|
||||
expect(registry.get(ref2)).toEqual(factory1);
|
||||
expect(registry.getAllApis()).toEqual(new Set([ref1]));
|
||||
|
||||
expect(registry.register('app', factory2)).toBe(true);
|
||||
expect(registry.get(ref1)).toEqual(factory2);
|
||||
expect(registry.get(ref2)).toEqual(factory2);
|
||||
expect(Array.from(registry.getAllApis())[0]).toBe(ref2);
|
||||
expect(Array.from(registry.getAllApis())[0]).not.toBe(ref1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { ApiFactoryHolder } from './types';
|
||||
import {
|
||||
ApiRef,
|
||||
ApiFactory,
|
||||
AnyApiRef,
|
||||
AnyApiFactory,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Scope type when registering API factories.
|
||||
* @public
|
||||
*/
|
||||
export type ApiFactoryScope =
|
||||
| 'default' // Default factories registered by core and plugins
|
||||
| 'app' // Factories registered in the app, overriding default ones
|
||||
| 'static'; // APIs that can't be overridden, e.g. config
|
||||
|
||||
enum ScopePriority {
|
||||
default = 10,
|
||||
app = 50,
|
||||
static = 100,
|
||||
}
|
||||
|
||||
type FactoryTuple = {
|
||||
priority: number;
|
||||
factory: AnyApiFactory;
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiFactoryRegistry is an ApiFactoryHolder implementation that enables
|
||||
* registration of API Factories with different scope.
|
||||
*
|
||||
* Each scope has an assigned priority, where factories registered with
|
||||
* higher priority scopes override ones with lower priority.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ApiFactoryRegistry implements ApiFactoryHolder {
|
||||
private readonly factories = new Map<string, FactoryTuple>();
|
||||
|
||||
/**
|
||||
* Register a new API factory. Returns true if the factory was added
|
||||
* to the registry.
|
||||
*
|
||||
* A factory will not be added to the registry if there is already
|
||||
* an existing factory with the same or higher priority.
|
||||
*/
|
||||
register<Api, Impl extends Api, Deps extends { [name in string]: unknown }>(
|
||||
scope: ApiFactoryScope,
|
||||
factory: ApiFactory<Api, Impl, Deps>,
|
||||
) {
|
||||
const priority = ScopePriority[scope];
|
||||
const existing = this.factories.get(factory.api.id);
|
||||
if (existing && existing.priority >= priority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.factories.set(factory.api.id, { priority, factory });
|
||||
return true;
|
||||
}
|
||||
|
||||
get<T>(
|
||||
api: ApiRef<T>,
|
||||
): ApiFactory<T, T, { [x: string]: unknown }> | undefined {
|
||||
const tuple = this.factories.get(api.id);
|
||||
if (!tuple) {
|
||||
return undefined;
|
||||
}
|
||||
return tuple.factory as ApiFactory<T, T, { [x: string]: unknown }>;
|
||||
}
|
||||
|
||||
getAllApis(): Set<AnyApiRef> {
|
||||
const refs = new Set<AnyApiRef>();
|
||||
for (const { factory } of this.factories.values()) {
|
||||
refs.add(factory.api);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
useApi,
|
||||
createApiRef,
|
||||
withApis,
|
||||
ApiHolder,
|
||||
ApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ApiProvider } from './ApiProvider';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
import { render } from '@testing-library/react';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import { useVersionedContext } from '@backstage/version-bridge';
|
||||
|
||||
describe('ApiProvider', () => {
|
||||
type Api = () => string;
|
||||
const apiRef = createApiRef<Api>({ id: 'x' });
|
||||
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 provide nested access to apis', () => {
|
||||
const aRef = createApiRef<string>({ id: 'a' });
|
||||
const bRef = createApiRef<string>({ id: 'b' });
|
||||
|
||||
const MyComponent = () => {
|
||||
const a = useApi(aRef);
|
||||
const b = useApi(bRef);
|
||||
return (
|
||||
<div>
|
||||
a={a} b={b}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderedHook = render(
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.from([
|
||||
[aRef, 'x'],
|
||||
[bRef, 'y'],
|
||||
])}
|
||||
>
|
||||
<ApiProvider apis={ApiRegistry.from([[aRef, 'z']])}>
|
||||
<MyComponent />
|
||||
</ApiProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
renderedHook.getByText('a=z b=y');
|
||||
});
|
||||
|
||||
it('should ignore deps in prototype', () => {
|
||||
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
|
||||
const xRef = createApiRef<number>({ id: 'x' });
|
||||
|
||||
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(/^API context is not available/);
|
||||
}).error,
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
detail: new Error('API context is not available'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
detail: new Error('API context is not available'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
expect.stringMatching(
|
||||
/^The above error occurred in the <MyHookConsumer> component/,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(
|
||||
withLogCollector(['error'], () => {
|
||||
expect(() => {
|
||||
render(<MyHocConsumer />);
|
||||
}).toThrow(/^API context is not available/);
|
||||
}).error,
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
detail: new Error('API context is not available'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
detail: new Error('API context is not available'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
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.objectContaining({
|
||||
detail: new Error('No implementation available for apiRef{x}'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
detail: new Error('No implementation available for apiRef{x}'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
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.objectContaining({
|
||||
detail: new Error('No implementation available for apiRef{x}'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
detail: new Error('No implementation available for apiRef{x}'),
|
||||
type: 'unhandled exception',
|
||||
}),
|
||||
expect.stringMatching(
|
||||
/^The above error occurred in the <withApis\(Component\)> component/,
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('v1 consumer', () => {
|
||||
function useMockApiV1<T>(apiRef: ApiRef<T>): T {
|
||||
const impl = useVersionedContext<{ 1: ApiHolder }>('api-context')
|
||||
?.atVersion(1)
|
||||
?.get(apiRef);
|
||||
if (!impl) {
|
||||
throw new Error('no impl');
|
||||
}
|
||||
return impl;
|
||||
}
|
||||
|
||||
type Api = () => string;
|
||||
const apiRef = createApiRef<Api>({ id: 'x' });
|
||||
const registry = ApiRegistry.from([[apiRef, () => 'hello']]);
|
||||
|
||||
const MyHookConsumerV1 = () => {
|
||||
const api = useMockApiV1(apiRef);
|
||||
return <p>hook message: {api()}</p>;
|
||||
};
|
||||
|
||||
it('should provide apis', () => {
|
||||
const renderedHook = render(
|
||||
<ApiProvider apis={registry}>
|
||||
<MyHookConsumerV1 />
|
||||
</ApiProvider>,
|
||||
);
|
||||
renderedHook.getByText('hook message: hello');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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, { useContext, ReactNode, PropsWithChildren } from 'react';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { ApiAggregator } from './ApiAggregator';
|
||||
import {
|
||||
createVersionedValueMap,
|
||||
createVersionedContext,
|
||||
} from '@backstage/version-bridge';
|
||||
|
||||
/**
|
||||
* Prop types for the ApiProvider component.
|
||||
* @public
|
||||
*/
|
||||
export type ApiProviderProps = {
|
||||
apis: ApiHolder;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ApiContext = createVersionedContext<{ 1: ApiHolder }>('api-context');
|
||||
|
||||
/**
|
||||
* Provides an {@link @backstage/core-plugin-api#ApiHolder} for consumption in
|
||||
* the React tree.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ApiProvider = (props: PropsWithChildren<ApiProviderProps>) => {
|
||||
const { apis, children } = props;
|
||||
const parentHolder = useContext(ApiContext)?.atVersion(1);
|
||||
const holder = parentHolder ? new ApiAggregator(apis, parentHolder) : apis;
|
||||
|
||||
return (
|
||||
<ApiContext.Provider
|
||||
value={createVersionedValueMap({ 1: holder })}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
|
||||
describe('ApiRegistry', () => {
|
||||
const x1Ref = createApiRef<number>({ id: 'x1' });
|
||||
const x1DuplicateRef = createApiRef<number>({ id: 'x1' });
|
||||
const x2Ref = createApiRef<string>({ id: 'x2' });
|
||||
|
||||
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(x1DuplicateRef)).toBe(3);
|
||||
expect(registry.get(x2Ref)).toBe('y');
|
||||
});
|
||||
|
||||
it('should be built', () => {
|
||||
const registry = ApiRegistry.builder().build();
|
||||
expect(registry.get(x1Ref)).toBe(undefined);
|
||||
expect(registry.get(x1DuplicateRef)).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(x1DuplicateRef)).toBe(3);
|
||||
expect(registry.get(x2Ref)).toBe('y');
|
||||
});
|
||||
|
||||
it('should be created with API', () => {
|
||||
const reg1 = ApiRegistry.with(x1Ref, 3);
|
||||
const reg2 = reg1.with(x2Ref, 'y');
|
||||
const reg3 = reg2.with(x2Ref, 'z');
|
||||
const reg4 = reg3.with(x1Ref, 2);
|
||||
const reg5 = reg3.with(x1DuplicateRef, 4);
|
||||
|
||||
expect(reg1.get(x1Ref)).toBe(3);
|
||||
expect(reg1.get(x2Ref)).toBe(undefined);
|
||||
expect(reg2.get(x1Ref)).toBe(3);
|
||||
expect(reg2.get(x2Ref)).toBe('y');
|
||||
expect(reg3.get(x1Ref)).toBe(3);
|
||||
expect(reg3.get(x2Ref)).toBe('z');
|
||||
expect(reg4.get(x1Ref)).toBe(2);
|
||||
expect(reg4.get(x2Ref)).toBe('z');
|
||||
expect(reg5.get(x1Ref)).toBe(4);
|
||||
expect(reg5.get(x2Ref)).toBe('z');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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, ApiHolder } from '@backstage/core-plugin-api';
|
||||
|
||||
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
|
||||
|
||||
/** @internal */
|
||||
class ApiRegistryBuilder {
|
||||
private apis: [string, unknown][] = [];
|
||||
|
||||
add<T, I extends T>(api: ApiRef<T>, impl: I): I {
|
||||
this.apis.push([api.id, impl]);
|
||||
return impl;
|
||||
}
|
||||
|
||||
build(): ApiRegistry {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
return new ApiRegistry(new Map(this.apis));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry for utility APIs.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class ApiRegistry implements ApiHolder {
|
||||
static builder() {
|
||||
return new ApiRegistryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ApiRegistry with a list of API implementations.
|
||||
*
|
||||
* @param apis - A list of pairs mapping an ApiRef to its respective implementation
|
||||
*/
|
||||
static from(apis: ApiImpl[]) {
|
||||
return new ApiRegistry(new Map(apis.map(([api, impl]) => [api.id, impl])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ApiRegistry with a single API implementation.
|
||||
*
|
||||
* @param api - ApiRef for the API to add
|
||||
* @param impl - Implementation of the API to add
|
||||
*/
|
||||
static with<T>(api: ApiRef<T>, impl: T): ApiRegistry {
|
||||
return new ApiRegistry(new Map([[api.id, impl]]));
|
||||
}
|
||||
|
||||
constructor(private readonly apis: Map<string, unknown>) {}
|
||||
|
||||
/**
|
||||
* Returns a new ApiRegistry with the provided API added to the existing ones.
|
||||
*
|
||||
* @param api - ApiRef for the API to add
|
||||
* @param impl - Implementation of the API to add
|
||||
*/
|
||||
with<T>(api: ApiRef<T>, impl: T): ApiRegistry {
|
||||
return new ApiRegistry(new Map([...this.apis, [api.id, impl]]));
|
||||
}
|
||||
|
||||
get<T>(api: ApiRef<T>): T | undefined {
|
||||
return this.apis.get(api.id) as T | undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiResolver } from './ApiResolver';
|
||||
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
|
||||
const aRef = createApiRef<number>({ id: 'a' });
|
||||
const otherARef = createApiRef<number>({ id: 'a' });
|
||||
const bRef = createApiRef<string>({ id: 'b' });
|
||||
const otherBRef = createApiRef<string>({ id: 'b' });
|
||||
const cRef = createApiRef<{ x: string }>({ id: 'c' });
|
||||
const otherCRef = createApiRef<{ x: string }>({ id: 'c' });
|
||||
|
||||
function createRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: {},
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: bRef,
|
||||
deps: {},
|
||||
factory: () => 'b',
|
||||
});
|
||||
registry.register('default', {
|
||||
api: cRef,
|
||||
deps: { b: otherBRef },
|
||||
factory: ({ b }) => ({ x: 'x', b }),
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
function createSelfCyclicRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: { a: aRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
function createShortCyclicRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: { b: bRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: bRef,
|
||||
deps: { a: aRef },
|
||||
factory: () => 'x',
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
function createShortCyclicRegistryWithOther() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: { b: bRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: otherBRef,
|
||||
deps: { a: otherARef },
|
||||
factory: () => 'x',
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
function createLongCyclicRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: { b: otherBRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: bRef,
|
||||
deps: { c: cRef },
|
||||
factory: () => 'b',
|
||||
});
|
||||
registry.register('default', {
|
||||
api: cRef,
|
||||
deps: { a: aRef },
|
||||
factory: () => ({ x: 'x' }),
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
describe('ApiResolver', () => {
|
||||
it('should be created empty', () => {
|
||||
const resolver = new ApiResolver(new ApiFactoryRegistry());
|
||||
expect(resolver.get(aRef)).toBe(undefined);
|
||||
expect(resolver.get(bRef)).toBe(undefined);
|
||||
expect(resolver.get(otherBRef)).toBe(undefined);
|
||||
expect(resolver.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should instantiate APIs', () => {
|
||||
const resolver = new ApiResolver(createRegistry());
|
||||
expect(resolver.get(aRef)).toBe(1);
|
||||
expect(resolver.get(otherARef)).toBe(1);
|
||||
expect(resolver.get(bRef)).toBe('b');
|
||||
expect(resolver.get(otherBRef)).toBe('b');
|
||||
expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' });
|
||||
expect(resolver.get(cRef)).toBe(resolver.get(otherCRef));
|
||||
});
|
||||
|
||||
it('should detect self dependency cycles', () => {
|
||||
const resolver = new ApiResolver(createSelfCyclicRegistry());
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect short dependency cycles', () => {
|
||||
const resolver = new ApiResolver(createShortCyclicRegistry());
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => resolver.get(bRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect short dependency cycles with other refs', () => {
|
||||
const resolver = new ApiResolver(createShortCyclicRegistryWithOther());
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => resolver.get(bRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => resolver.get(otherARef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => resolver.get(otherBRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect long dependency cycles', () => {
|
||||
const resolver = new ApiResolver(createLongCyclicRegistry());
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
// Second call for same ref should still throw
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => resolver.get(bRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => resolver.get(otherBRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => resolver.get(cRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate a factory holder', () => {
|
||||
expect(() => {
|
||||
ApiResolver.validateFactories(createRegistry(), [
|
||||
aRef,
|
||||
bRef,
|
||||
otherBRef,
|
||||
cRef,
|
||||
]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should find self cycles with validation', () => {
|
||||
const self = createSelfCyclicRegistry();
|
||||
expect(() => ApiResolver.validateFactories(self, [aRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(self, [otherARef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should find dependency cycles with validation', () => {
|
||||
const short = createShortCyclicRegistry();
|
||||
expect(() => ApiResolver.validateFactories(short, [aRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(short, [otherARef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(short, [otherBRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
|
||||
const shortOther = createShortCyclicRegistryWithOther();
|
||||
expect(() => ApiResolver.validateFactories(shortOther, [aRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() =>
|
||||
ApiResolver.validateFactories(shortOther, [otherARef]),
|
||||
).toThrow('Circular dependency of api factory for apiRef{a}');
|
||||
expect(() => ApiResolver.validateFactories(shortOther, [bRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() =>
|
||||
ApiResolver.validateFactories(shortOther, [otherBRef]),
|
||||
).toThrow('Circular dependency of api factory for apiRef{b}');
|
||||
|
||||
const long = createLongCyclicRegistry();
|
||||
expect(() =>
|
||||
ApiResolver.validateFactories(long, long.getAllApis()),
|
||||
).toThrow('Circular dependency of api factory for apiRef{a}');
|
||||
expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(long, [otherBRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should only call factory func once', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
const factory = jest.fn().mockReturnValue(2);
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: {},
|
||||
factory,
|
||||
});
|
||||
|
||||
const resolver = new ApiResolver(registry);
|
||||
expect(factory).toHaveBeenCalledTimes(0);
|
||||
expect(resolver.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(resolver.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(resolver.get(otherARef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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,
|
||||
ApiHolder,
|
||||
AnyApiRef,
|
||||
TypesToApiRefs,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ApiFactoryHolder } from './types';
|
||||
|
||||
/**
|
||||
* Handles the actual on-demand instantiation and memoization of APIs out of
|
||||
* an {@link ApiFactoryHolder}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ApiResolver implements ApiHolder {
|
||||
/**
|
||||
* Validate factories by making sure that each of the apis can be created
|
||||
* without hitting any circular dependencies.
|
||||
*/
|
||||
static validateFactories(
|
||||
factories: ApiFactoryHolder,
|
||||
apis: Iterable<AnyApiRef>,
|
||||
) {
|
||||
for (const api of apis) {
|
||||
const heap = [api];
|
||||
const allDeps = new Set<AnyApiRef>();
|
||||
|
||||
while (heap.length) {
|
||||
const apiRef = heap.shift()!;
|
||||
const factory = factories.get(apiRef);
|
||||
if (!factory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const dep of Object.values(factory.deps)) {
|
||||
if (dep.id === api.id) {
|
||||
throw new Error(`Circular dependency of api factory for ${api}`);
|
||||
}
|
||||
if (!allDeps.has(dep)) {
|
||||
allDeps.add(dep);
|
||||
heap.push(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly apis = new Map<string, unknown>();
|
||||
|
||||
constructor(private readonly factories: ApiFactoryHolder) {}
|
||||
|
||||
get<T>(ref: ApiRef<T>): T | undefined {
|
||||
return this.load(ref);
|
||||
}
|
||||
|
||||
private load<T>(ref: ApiRef<T>, loading: AnyApiRef[] = []): T | undefined {
|
||||
const impl = this.apis.get(ref.id);
|
||||
if (impl) {
|
||||
return impl as T;
|
||||
}
|
||||
|
||||
const factory = this.factories.get(ref);
|
||||
if (!factory) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (loading.includes(factory.api)) {
|
||||
throw new Error(`Circular dependency of api factory for ${factory.api}`);
|
||||
}
|
||||
|
||||
const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]);
|
||||
const api = factory.factory(deps);
|
||||
this.apis.set(ref.id, 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;
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,7 @@
|
||||
|
||||
export { createApiRef } from './ApiRef';
|
||||
export type { ApiRefConfig } from './ApiRef';
|
||||
export { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
export { ApiProvider } from './ApiProvider';
|
||||
export { ApiResolver } from './ApiResolver';
|
||||
export * from './types';
|
||||
|
||||
@@ -72,3 +72,12 @@ export type AnyApiFactory = ApiFactory<
|
||||
unknown,
|
||||
{ [key in string]: unknown }
|
||||
>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ApiFactoryHolder = {
|
||||
get<T>(
|
||||
api: ApiRef<T>,
|
||||
): ApiFactory<T, T, { [key in string]: unknown }> | undefined;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user