From 466354aaa7bf93c747527322d5ea974a22ebce9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 Jan 2021 19:41:56 +0100 Subject: [PATCH] integration: build out the integrations class hierarchy --- .changeset/flat-walls-burn.md | 5 ++ .../integration/src/ScmIntegrations.test.ts | 76 +++++++++++++++++++ packages/integration/src/ScmIntegrations.ts | 59 +++++++++++--- .../src/azure/AzureIntegration.test.ts | 5 +- .../integration/src/azure/AzureIntegration.ts | 21 +++-- .../bitbucket/BitbucketIntegration.test.ts | 5 +- .../src/bitbucket/BitbucketIntegration.ts | 23 ++++-- .../src/github/GitHubIntegration.test.ts | 13 +++- .../src/github/GitHubIntegration.ts | 21 +++-- .../src/gitlab/GitLabIntegration.test.ts | 5 +- .../src/gitlab/GitLabIntegration.ts | 21 +++-- packages/integration/src/helpers.ts | 20 +++++ packages/integration/src/types.ts | 49 ++++++++---- 13 files changed, 255 insertions(+), 68 deletions(-) create mode 100644 .changeset/flat-walls-burn.md create mode 100644 packages/integration/src/ScmIntegrations.test.ts diff --git a/.changeset/flat-walls-burn.md b/.changeset/flat-walls-burn.md new file mode 100644 index 0000000000..370b651a4b --- /dev/null +++ b/.changeset/flat-walls-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Build out the `ScmIntegrations` class, as well as the individual `*Integration` classes diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts new file mode 100644 index 0000000000..b43e69eba4 --- /dev/null +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 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 { AzureIntegrationConfig } from './azure'; +import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketIntegrationConfig } from './bitbucket'; +import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { GitHubIntegrationConfig } from './github'; +import { GitHubIntegration } from './github/GitHubIntegration'; +import { GitLabIntegrationConfig } from './gitlab'; +import { GitLabIntegration } from './gitlab/GitLabIntegration'; +import { basicIntegrations } from './helpers'; +import { ScmIntegrations } from './ScmIntegrations'; + +describe('ScmIntegrations', () => { + const azure = new AzureIntegration({ + host: 'azure.local', + } as AzureIntegrationConfig); + + const bitbucket = new BitbucketIntegration({ + host: 'bitbucket.local', + } as BitbucketIntegrationConfig); + + const github = new GitHubIntegration({ + host: 'github.local', + } as GitHubIntegrationConfig); + + const gitlab = new GitLabIntegration({ + host: 'gitlab.local', + } as GitLabIntegrationConfig); + + const i = new ScmIntegrations({ + azure: basicIntegrations([azure], i => i.config.host), + bitbucket: basicIntegrations([bitbucket], i => i.config.host), + github: basicIntegrations([github], i => i.config.host), + gitlab: basicIntegrations([gitlab], i => i.config.host), + }); + + it('can get the specifics', () => { + expect(i.azure.byUrl('https://azure.local')).toBe(azure); + expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket); + expect(i.github.byUrl('https://github.local')).toBe(github); + expect(i.gitlab.byUrl('https://gitlab.local')).toBe(gitlab); + }); + + it('can list', () => { + expect(i.list()).toEqual( + expect.arrayContaining([azure, bitbucket, github, gitlab]), + ); + }); + + it('can select by url and host', () => { + expect(i.byUrl('https://azure.local')).toBe(azure); + expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket); + expect(i.byUrl('https://github.local')).toBe(github); + expect(i.byUrl('https://gitlab.local')).toBe(gitlab); + + expect(i.byHost('azure.local')).toBe(azure); + expect(i.byHost('bitbucket.local')).toBe(bitbucket); + expect(i.byHost('github.local')).toBe(github); + expect(i.byHost('gitlab.local')).toBe(gitlab); + }); +}); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 515a32502d..102273a03b 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -21,27 +21,64 @@ import { GitHubIntegration } from './github/GitHubIntegration'; import { GitLabIntegration } from './gitlab/GitLabIntegration'; import { ScmIntegration, - ScmIntegrationPredicateTuple, ScmIntegrationRegistry, + ScmIntegrationsGroup, } from './types'; +type IntegrationsByType = { + azure: ScmIntegrationsGroup; + bitbucket: ScmIntegrationsGroup; + github: ScmIntegrationsGroup; + gitlab: ScmIntegrationsGroup; +}; + export class ScmIntegrations implements ScmIntegrationRegistry { + private readonly byType: IntegrationsByType; + static fromConfig(config: Config): ScmIntegrations { - return new ScmIntegrations([ - ...AzureIntegration.factory({ config }), - ...BitbucketIntegration.factory({ config }), - ...GitHubIntegration.factory({ config }), - ...GitLabIntegration.factory({ config }), - ]); + return new ScmIntegrations({ + azure: AzureIntegration.factory({ config }), + bitbucket: BitbucketIntegration.factory({ config }), + github: GitHubIntegration.factory({ config }), + gitlab: GitLabIntegration.factory({ config }), + }); } - constructor(private readonly integrations: ScmIntegrationPredicateTuple[]) {} + constructor(integrationsByType: IntegrationsByType) { + this.byType = integrationsByType; + } + + get azure(): ScmIntegrationsGroup { + return this.byType.azure; + } + + get bitbucket(): ScmIntegrationsGroup { + return this.byType.bitbucket; + } + + get github(): ScmIntegrationsGroup { + return this.byType.github; + } + + get gitlab(): ScmIntegrationsGroup { + return this.byType.gitlab; + } list(): ScmIntegration[] { - return this.integrations.map(i => i.integration); + return Object.values(this.byType).flatMap( + i => i.list() as ScmIntegration[], + ); } - byUrl(url: string): ScmIntegration | undefined { - return this.integrations.find(i => i.predicate(new URL(url)))?.integration; + byUrl(url: string | URL): ScmIntegration | undefined { + return Object.values(this.byType) + .map(i => i.byUrl(url)) + .find(Boolean); + } + + byHost(host: string): ScmIntegration | undefined { + return Object.values(this.byType) + .map(i => i.byHost(host)) + .find(Boolean); } } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 4a0c32badb..90c098b637 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -31,8 +31,9 @@ describe('AzureIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('dev.azure.com'); }); it('returns the basics', () => { diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index 84dc3800d2..446e6a9480 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -14,27 +14,32 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; export class AzureIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new AzureIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new AzureIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: AzureIntegrationConfig) {} + constructor(private readonly integrationConfig: AzureIntegrationConfig) {} get type(): string { return 'azure'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): AzureIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts index 87dbac2263..3f130a393c 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -34,8 +34,9 @@ describe('BitbucketIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('bitbucket.org'); }); it('returns the basics', () => { diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index b271e2f408..f3e69b946a 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -14,30 +14,37 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { BitbucketIntegrationConfig, readBitbucketIntegrationConfigs, } from './config'; export class BitbucketIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ + config, + }) => { const configs = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new BitbucketIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new BitbucketIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: BitbucketIntegrationConfig) {} + constructor(private readonly integrationConfig: BitbucketIntegrationConfig) {} get type(): string { return 'bitbucket'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): BitbucketIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts index 0c326d81cd..9056517f32 100644 --- a/packages/integration/src/github/GitHubIntegration.test.ts +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -33,13 +33,20 @@ describe('GitHubIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('github.com'); }); it('returns the basics', () => { - const integration = new GitHubIntegration({ host: 'h.com' } as any); + const integration = new GitHubIntegration({ + host: 'h.com', + apiBaseUrl: 'a', + rawBaseUrl: 'r', + token: 't', + }); expect(integration.type).toBe('github'); expect(integration.title).toBe('h.com'); + expect(integration.config.host).toBe('h.com'); }); }); diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index 92c5951873..c103597d74 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -14,30 +14,35 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitHubIntegrationConfig, readGitHubIntegrationConfigs, } from './config'; export class GitHubIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitHubIntegrationConfigs( config.getOptionalConfigArray('integrations.github') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new GitHubIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new GitHubIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: GitHubIntegrationConfig) {} + constructor(private readonly integrationConfig: GitHubIntegrationConfig) {} get type(): string { return 'github'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): GitHubIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 260afd23d8..8814e33302 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -31,8 +31,9 @@ describe('GitLabIntegration', () => { }, }), }); - expect(integrations.length).toBe(2); // including default - expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + expect(integrations.list().length).toBe(2); // including default + expect(integrations.list()[0].config.host).toBe('h.com'); + expect(integrations.list()[1].config.host).toBe('gitlab.com'); }); it('returns the basics', () => { diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 4d035cb24e..d939917366 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -14,30 +14,35 @@ * limitations under the License. */ -import { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { basicIntegrations } from '../helpers'; +import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitLabIntegrationConfig, readGitLabIntegrationConfigs, } from './config'; export class GitLabIntegration implements ScmIntegration { - static factory: ScmIntegrationFactory = ({ config }) => { + static factory: ScmIntegrationsFactory = ({ config }) => { const configs = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); - return configs.map(integration => ({ - predicate: (url: URL) => url.host === integration.host, - integration: new GitLabIntegration(integration), - })); + return basicIntegrations( + configs.map(c => new GitLabIntegration(c)), + i => i.config.host, + ); }; - constructor(private readonly config: GitLabIntegrationConfig) {} + constructor(private readonly integrationConfig: GitLabIntegrationConfig) {} get type(): string { return 'gitlab'; } get title(): string { - return this.config.host; + return this.integrationConfig.host; + } + + get config(): GitLabIntegrationConfig { + return this.integrationConfig; } } diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index 02393f99e1..cc1c59a238 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -14,9 +14,29 @@ * limitations under the License. */ +import { ScmIntegration, ScmIntegrationsGroup } from './types'; + /** Checks whether the given url is a valid host */ export function isValidHost(url: string): boolean { const check = new URL('http://example.com'); check.host = url; return check.host === url; } + +export function basicIntegrations( + integrations: T[], + getHost: (integration: T) => string, +): ScmIntegrationsGroup { + return { + list(): T[] { + return integrations; + }, + byUrl(url: string | URL): T | undefined { + const parsed = typeof url === 'string' ? new URL(url) : url; + return integrations.find(i => getHost(i) === parsed.hostname); + }, + byHost(host: string): T | undefined { + return integrations.find(i => getHost(i) === host); + }, + }; +} diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index ae9c360980..d8fb7a14e2 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -15,11 +15,15 @@ */ import { Config } from '@backstage/config'; +import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { GitHubIntegration } from './github/GitHubIntegration'; +import { GitLabIntegration } from './gitlab/GitLabIntegration'; /** * Encapsulates a single SCM integration. */ -export type ScmIntegration = { +export interface ScmIntegration { /** * The type of integration, e.g. "github". */ @@ -30,30 +34,43 @@ export type ScmIntegration = { * differentiate between different integrations. */ title: string; -}; +} /** - * Holds all registered SCM integrations. + * Encapsulates several integrations, that are all of the same type. */ -export type ScmIntegrationRegistry = { +export interface ScmIntegrationsGroup { /** - * Lists all registered integrations. + * Lists all registered integrations of this type. */ - list(): ScmIntegration[]; + list(): T[]; /** - * Fetches an integration by URL. + * Fetches an integration of this type by URL. * - * @param url A URL that matches a registered integration + * @param url A URL that matches a registered integration of this type */ - byUrl(url: string): ScmIntegration | undefined; -}; + byUrl(url: string | URL): T | undefined; -export type ScmIntegrationPredicateTuple = { - predicate: (url: URL) => boolean; - integration: ScmIntegration; -}; + /** + * Fetches an integration of this type by host name. + * + * @param url A host name that matches a registered integration of this type + */ + byHost(host: string): T | undefined; +} -export type ScmIntegrationFactory = (options: { +/** + * Holds all registered SCM integrations, of all types. + */ +export interface ScmIntegrationRegistry + extends ScmIntegrationsGroup { + azure: ScmIntegrationsGroup; + bitbucket: ScmIntegrationsGroup; + github: ScmIntegrationsGroup; + gitlab: ScmIntegrationsGroup; +} + +export type ScmIntegrationsFactory = (options: { config: Config; -}) => ScmIntegrationPredicateTuple[]; +}) => ScmIntegrationsGroup;