integration: build out the integrations class hierarchy
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user