From 64cdb6bbe1e1d1b4281873b298e719633c0dfe36 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Mar 2020 10:58:08 +0100 Subject: [PATCH] packages/core: added api provider --- packages/core/package.json | 3 +- packages/core/src/api/api.ts | 5 + .../core/src/api/apis/ApiProvider.test.tsx | 148 ++++++++++++++++++ packages/core/src/api/apis/ApiProvider.tsx | 88 +++++++++++ packages/core/src/api/apis/ApiRef.test.ts | 39 +++++ packages/core/src/api/apis/ApiRef.ts | 47 ++++++ .../core/src/api/apis/ApiRegistry.test.ts | 52 ++++++ packages/core/src/api/apis/ApiRegistry.ts | 50 ++++++ .../core/src/api/apis/ApiTestRegistry.test.ts | 121 ++++++++++++++ packages/core/src/api/apis/ApiTestRegistry.ts | 92 +++++++++++ packages/core/src/api/apis/index.ts | 20 +++ packages/core/src/api/apis/types.ts | 37 +++++ packages/core/src/api/index.ts | 1 + 13 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/api/apis/ApiProvider.test.tsx create mode 100644 packages/core/src/api/apis/ApiProvider.tsx create mode 100644 packages/core/src/api/apis/ApiRef.test.ts create mode 100644 packages/core/src/api/apis/ApiRef.ts create mode 100644 packages/core/src/api/apis/ApiRegistry.test.ts create mode 100644 packages/core/src/api/apis/ApiRegistry.ts create mode 100644 packages/core/src/api/apis/ApiTestRegistry.test.ts create mode 100644 packages/core/src/api/apis/ApiTestRegistry.ts create mode 100644 packages/core/src/api/apis/index.ts create mode 100644 packages/core/src/api/apis/types.ts diff --git a/packages/core/package.json b/packages/core/package.json index 2b1e507ae2..4f35d0fb76 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" } } diff --git a/packages/core/src/api/api.ts b/packages/core/src/api/api.ts index 96ac07c73e..abaa28cbbf 100644 --- a/packages/core/src/api/api.ts +++ b/packages/core/src/api/api.ts @@ -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(config: ApiRefConfig) { + return new ApiRef(config); +} + export function createWidgetView() { return new WidgetViewBuilder(); } diff --git a/packages/core/src/api/apis/ApiProvider.test.tsx b/packages/core/src/api/apis/ApiProvider.test.tsx new file mode 100644 index 0000000000..20634f74af --- /dev/null +++ b/packages/core/src/api/apis/ApiProvider.test.tsx @@ -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({ id: 'x', description: '' }); + 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 ignore deps in prototype', () => { + // 100% coverage + happy typescript = hasOwnProperty + this atrocity + const xRef = new ApiRef({ 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

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('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 component/, + ), + ]); + + expect( + withLogCollector(['error'], () => { + expect(() => { + render(); + }).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 component/, + ), + ]); + }); + + it('should error if api is not available', () => { + expect( + withLogCollector(['error'], () => { + expect(() => { + render( + + + , + ); + }).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 component/, + ), + ]); + + expect( + withLogCollector(['error'], () => { + expect(() => { + render( + + + , + ); + }).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 component/, + ), + ]); + }); +}); diff --git a/packages/core/src/api/apis/ApiProvider.tsx b/packages/core/src/api/apis/ApiProvider.tsx new file mode 100644 index 0000000000..db87cc1610 --- /dev/null +++ b/packages/core/src/api/apis/ApiProvider.tsx @@ -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(undefined); + +const ApiProvider: FC = ({ apis, children }) => { + return ; +}; + +ApiProvider.propTypes = { + apis: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired, + children: PropTypes.node, +}; + +export function useApi(apiRef: ApiRef): 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(apis: TypesToApiRefs) { + return function withApisWrapper

( + WrappedComponent: React.ComponentType

, + ) { + const Hoc: FC> = 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 ; + }; + const displayName = + WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + Hoc.displayName = `withApis(${displayName})`; + + return Hoc; + }; +} + +export default ApiProvider; diff --git a/packages/core/src/api/apis/ApiRef.test.ts b/packages/core/src/api/apis/ApiRef.test.ts new file mode 100644 index 0000000000..902ab1bfbe --- /dev/null +++ b/packages/core/src/api/apis/ApiRef.test.ts @@ -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}'`, + ); + } + }); +}); diff --git a/packages/core/src/api/apis/ApiRef.ts b/packages/core/src/api/apis/ApiRef.ts new file mode 100644 index 0000000000..c28bb100b9 --- /dev/null +++ b/packages/core/src/api/apis/ApiRef.ts @@ -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 { + 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}}`; + } +} diff --git a/packages/core/src/api/apis/ApiRegistry.test.ts b/packages/core/src/api/apis/ApiRegistry.test.ts new file mode 100644 index 0000000000..6eb78892b2 --- /dev/null +++ b/packages/core/src/api/apis/ApiRegistry.test.ts @@ -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({ id: 'x', description: '' }); + const x2Ref = new ApiRef({ 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'); + }); +}); diff --git a/packages/core/src/api/apis/ApiRegistry.ts b/packages/core/src/api/apis/ApiRegistry.ts new file mode 100644 index 0000000000..b7af906b6c --- /dev/null +++ b/packages/core/src/api/apis/ApiRegistry.ts @@ -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 = readonly [ApiRef, T]; + +class ApiRegistryBuilder { + private apis: ApiImpl[] = []; + + add(api: ApiRef, 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, unknown>) {} + + get(api: ApiRef): T | undefined { + return this.apis.get(api) as T | undefined; + } +} diff --git a/packages/core/src/api/apis/ApiTestRegistry.test.ts b/packages/core/src/api/apis/ApiTestRegistry.test.ts new file mode 100644 index 0000000000..434de430f7 --- /dev/null +++ b/packages/core/src/api/apis/ApiTestRegistry.test.ts @@ -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({ id: 'a', description: '' }); + const bRef = new ApiRef({ id: 'b', description: '' }); + const cRef = new ApiRef({ 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); + }); +}); diff --git a/packages/core/src/api/apis/ApiTestRegistry.ts b/packages/core/src/api/apis/ApiTestRegistry.ts new file mode 100644 index 0000000000..c359b5878a --- /dev/null +++ b/packages/core/src/api/apis/ApiTestRegistry.ts @@ -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(); + private factories = new Map>(); + private savedFactories = new Map>(); + + get(ref: ApiRef): T | undefined { + return this.load(ref); + } + + register(ref: ApiRef, factoryFunc: () => T): ApiTestRegistry; + register(factory: ApiFactory): ApiTestRegistry; + register(factory: ApiRef | ApiFactory, 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(ref: ApiRef, 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(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/core/src/api/apis/index.ts b/packages/core/src/api/apis/index.ts new file mode 100644 index 0000000000..a49a622858 --- /dev/null +++ b/packages/core/src/api/apis/index.ts @@ -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'; diff --git a/packages/core/src/api/apis/types.ts b/packages/core/src/api/apis/types.ts new file mode 100644 index 0000000000..129afdaaa5 --- /dev/null +++ b/packages/core/src/api/apis/types.ts @@ -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; + +export type ApiRefType = T extends ApiRef ? U : never; + +export type TypesToApiRefs = { [key in keyof T]: ApiRef }; + +export type ApiRefsToTypes }> = { + [key in keyof T]: ApiRefType; +}; + +export type ApiHolder = { + get(api: ApiRef): T | undefined; +}; + +export type ApiFactory = { + implements: ApiRef; + deps: TypesToApiRefs; + factory(deps: D): I extends A ? I : never; +}; diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 33ea57c96a..dff75514ae 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -15,4 +15,5 @@ */ export * from './api'; +export * from './apis'; export { useApp } from './app/AppContext';