From b0716b55d3c5488d91faf7d35036ff982f2e1780 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Nov 2023 19:25:54 +0100 Subject: [PATCH] frontend-plugin-api: remove wrong apis Signed-off-by: Vincenzo Scamporlino --- .../src/apis/system/ApiAggregator.test.ts | 46 --- .../src/apis/system/ApiAggregator.ts | 39 --- .../apis/system/ApiFactoryRegistry.test.ts | 91 ------ .../src/apis/system/ApiFactoryRegistry.ts | 95 ------- .../src/apis/system/ApiProvider.test.tsx | 234 ---------------- .../src/apis/system/ApiProvider.tsx | 53 ---- .../src/apis/system/ApiRegistry.test.ts | 75 ----- .../src/apis/system/ApiRegistry.ts | 80 ------ .../src/apis/system/ApiResolver.test.ts | 263 ------------------ .../src/apis/system/ApiResolver.ts | 116 -------- .../src/apis/system/index.ts | 5 - 11 files changed, 1097 deletions(-) delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts delete mode 100644 packages/frontend-plugin-api/src/apis/system/ApiResolver.ts diff --git a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts deleted file mode 100644 index c2754878f2..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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({ id: 'a' }); - const apiBRef = createApiRef({ 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); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts b/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts deleted file mode 100644 index a6b4471f8c..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiAggregator.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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(apiRef: ApiRef): T | undefined { - for (const holder of this.holders) { - const api = holder.get(apiRef); - if (api) { - return api; - } - } - return undefined; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts deleted file mode 100644 index e859e8a491..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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({ id: 'a' }); -const aFactory1 = { api: aRef, deps: {}, factory: () => 1 }; -const aFactory2 = { api: aRef, deps: {}, factory: () => 2 }; -const bRef = createApiRef({ id: 'b' }); -const bFactory = { api: bRef, deps: {}, factory: () => 'x' }; -const cRef = createApiRef({ 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({ id: 'a' }); - const ref2 = createApiRef({ 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); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts b/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts deleted file mode 100644 index 5f56793cae..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiFactoryRegistry.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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(); - - /** - * 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( - scope: ApiFactoryScope, - factory: ApiFactory, - ) { - 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( - api: ApiRef, - ): ApiFactory | undefined { - const tuple = this.factories.get(api.id); - if (!tuple) { - return undefined; - } - return tuple.factory as ApiFactory; - } - - getAllApis(): Set { - const refs = new Set(); - for (const { factory } of this.factories.values()) { - refs.add(factory.api); - } - return refs; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx b/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx deleted file mode 100644 index 6460e12900..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiProvider.test.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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({ id: 'x' }); - const registry = ApiRegistry.from([[apiRef, () => 'hello']]); - - const MyHookConsumer = () => { - const api = useApi(apiRef); - return

hook message: {api()}

; - }; - - const MyHocConsumer = withApis({ getMessage: apiRef })(({ getMessage }) => { - return

hoc message: {getMessage()}

; - }); - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - - const renderedHoc = render( - - - , - ); - renderedHoc.getByText('hoc message: hello'); - }); - - it('should provide nested access to apis', () => { - const aRef = createApiRef({ id: 'a' }); - const bRef = createApiRef({ id: 'b' }); - - const MyComponent = () => { - const a = useApi(aRef); - const b = useApi(bRef); - return ( -
- a={a} b={b} -
- ); - }; - - const renderedHook = render( - - - - - , - ); - renderedHook.getByText('a=z b=y'); - }); - - it('should ignore deps in prototype', () => { - // 100% coverage + happy typescript = hasOwnProperty + this atrocity - const xRef = createApiRef({ 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

hoc message: {getMessage()}

; - }); - - const renderedHoc = render( - - - , - ); - renderedHoc.getByText('hoc message: hello'); - }); - - it('should error if no provider is available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render(); - }).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 component/, - ), - ]); - - expect( - withLogCollector(['error'], () => { - expect(() => { - render(); - }).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 component/, - ), - ]); - }); - - it('should error if api is not available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).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 component/, - ), - ]); - - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).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 component/, - ), - ]); - }); -}); - -describe('v1 consumer', () => { - function useMockApiV1(apiRef: ApiRef): 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({ id: 'x' }); - const registry = ApiRegistry.from([[apiRef, () => 'hello']]); - - const MyHookConsumerV1 = () => { - const api = useMockApiV1(apiRef); - return

hook message: {api()}

; - }; - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx b/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx deleted file mode 100644 index 4894d25de0..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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) => { - const { apis, children } = props; - const parentHolder = useContext(ApiContext)?.atVersion(1); - const holder = parentHolder ? new ApiAggregator(apis, parentHolder) : apis; - - return ( - - ); -}; diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts deleted file mode 100644 index c0be9f8d02..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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({ id: 'x1' }); - const x1DuplicateRef = createApiRef({ id: 'x1' }); - const x2Ref = createApiRef({ 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'); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts b/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts deleted file mode 100644 index d4990a04c5..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiRegistry.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 = readonly [ApiRef, T]; - -/** @internal */ -class ApiRegistryBuilder { - private apis: [string, unknown][] = []; - - add(api: ApiRef, 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(api: ApiRef, impl: T): ApiRegistry { - return new ApiRegistry(new Map([[api.id, impl]])); - } - - constructor(private readonly apis: Map) {} - - /** - * 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(api: ApiRef, impl: T): ApiRegistry { - return new ApiRegistry(new Map([...this.apis, [api.id, impl]])); - } - - get(api: ApiRef): T | undefined { - return this.apis.get(api.id) as T | undefined; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts b/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts deleted file mode 100644 index 01491684f4..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiResolver.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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({ id: 'a' }); -const otherARef = createApiRef({ id: 'a' }); -const bRef = createApiRef({ id: 'b' }); -const otherBRef = createApiRef({ 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); - }); -}); diff --git a/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts b/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts deleted file mode 100644 index 0c050ebc30..0000000000 --- a/packages/frontend-plugin-api/src/apis/system/ApiResolver.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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, - ) { - for (const api of apis) { - const heap = [api]; - const allDeps = new Set(); - - 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(); - - constructor(private readonly factories: ApiFactoryHolder) {} - - get(ref: ApiRef): T | undefined { - return this.load(ref); - } - - private load(ref: ApiRef, 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( - dependent: ApiRef, - apis: TypesToApiRefs, - 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; - } -} diff --git a/packages/frontend-plugin-api/src/apis/system/index.ts b/packages/frontend-plugin-api/src/apis/system/index.ts index ec8984f00f..6aad24daa9 100644 --- a/packages/frontend-plugin-api/src/apis/system/index.ts +++ b/packages/frontend-plugin-api/src/apis/system/index.ts @@ -16,9 +16,4 @@ export { createApiRef } from './ApiRef'; export type { ApiRefConfig } from './ApiRef'; -export { ApiFactoryRegistry } from './ApiFactoryRegistry'; -export type { ApiFactoryScope } from './ApiFactoryRegistry'; -export { ApiProvider } from './ApiProvider'; -export type { ApiProviderProps } from './ApiProvider'; -export { ApiResolver } from './ApiResolver'; export * from './types';