integration: build out the integrations class hierarchy

This commit is contained in:
Fredrik Adelöw
2021-01-04 19:41:56 +01:00
parent ded5005799
commit 466354aaa7
13 changed files with 255 additions and 68 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': minor
---
Build out the `ScmIntegrations` class, as well as the individual `*Integration` classes
@@ -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);
});
});
+48 -11
View File
@@ -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<AzureIntegration>;
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
github: ScmIntegrationsGroup<GitHubIntegration>;
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
};
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<AzureIntegration> {
return this.byType.azure;
}
get bitbucket(): ScmIntegrationsGroup<BitbucketIntegration> {
return this.byType.bitbucket;
}
get github(): ScmIntegrationsGroup<GitHubIntegration> {
return this.byType.github;
}
get gitlab(): ScmIntegrationsGroup<GitLabIntegration> {
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);
}
}
@@ -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', () => {
@@ -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<AzureIntegration> = ({ 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;
}
}
@@ -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', () => {
@@ -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<BitbucketIntegration> = ({
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;
}
}
@@ -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');
});
});
@@ -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<GitHubIntegration> = ({ 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;
}
}
@@ -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', () => {
@@ -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<GitLabIntegration> = ({ 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;
}
}
+20
View File
@@ -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<T extends ScmIntegration>(
integrations: T[],
getHost: (integration: T) => string,
): ScmIntegrationsGroup<T> {
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);
},
};
}
+33 -16
View File
@@ -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<T extends ScmIntegration> {
/**
* 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<ScmIntegration> {
azure: ScmIntegrationsGroup<AzureIntegration>;
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
github: ScmIntegrationsGroup<GitHubIntegration>;
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
}
export type ScmIntegrationsFactory<T extends ScmIntegration> = (options: {
config: Config;
}) => ScmIntegrationPredicateTuple[];
}) => ScmIntegrationsGroup<T>;