@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
226 lines (203 loc) • 7.96 kB
text/typescript
/*
* Copyright © 2019 Atomist, Inc.
*
* 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 { Configuration } from "@atomist/automation-client";
import { isInLocalMode } from "@atomist/sdm-core";
import {
Aspect,
FP,
} from "@atomist/sdm-pack-fingerprint";
import * as _ from "lodash";
import { FingerprintUsage } from "../analysis/offline/persist/ProjectAnalysisResultStore";
import { ProjectAnalysisResult } from "../analysis/ProjectAnalysisResult";
import { TagContext } from "../routes/api";
import {
ScoreWeightings,
WeightedScore,
} from "../scorer/Score";
import {
scoreOrg,
scoreRepos,
} from "../scorer/scoring";
import {
AspectRegistrations,
AspectRegistrationState,
} from "../typings/types";
import { showTiming } from "../util/showTiming";
import {
AspectRegistry,
isTagger,
RepositoryScorer,
RepoToScore,
ScoredRepo,
Tag,
TagAndScoreOptions,
TaggedRepo,
Tagger,
TaggerDefinition,
WorkspaceScorer,
WorkspaceSpecificTagger,
WorkspaceToScore,
} from "./AspectRegistry";
import {
AspectReportDetails,
AspectReportDetailsRegistry,
AspectWithReportDetails,
} from "./AspectReportDetailsRegistry";
import { IdealStore } from "./IdealStore";
import {
chainUndesirableUsageCheckers,
ProblemStore,
problemStoreBackedUndesirableUsageCheckerFor,
UndesirableUsageChecker,
} from "./ProblemStore";
export class DefaultAspectRegistry implements AspectRegistry, AspectReportDetailsRegistry {
private readonly taggers: TaggerDefinition[] = [];
/**
* Add a tagger that will work on all repositories.
*/
public withTaggers(...taggers: TaggerDefinition[]): this {
this.taggers.push(...taggers);
return this;
}
public async scoreWorkspace(workspaceId: string, workspaceToScore: WorkspaceToScore): Promise<WeightedScore> {
return scoreOrg(this.opts.workspaceScorers || [], workspaceToScore, this.opts.scoreWeightings);
}
public async tagAndScoreRepos(workspaceId: string,
repos: ProjectAnalysisResult[],
tsOpts: TagAndScoreOptions): Promise<ScoredRepo[]> {
const tagged = await showTiming(
`Tag ${repos.length} repos with ${this.taggers.length} taggers`,
async () => this.tagRepos({
repoCount: repos.length,
// TODO fix this
averageFingerprintCount: -1,
workspaceId,
aspectRegistry: this,
}, repos));
const scored = await showTiming(
`Score ${repos.length} repos with ${this.scorers.length} scorers`,
async () => scoreRepos(
this.scorers,
tagged,
this.opts.scoreWeightings,
tsOpts));
return scored;
}
get availableTags(): Tag[] {
return _.uniqBy(this.taggers, tag => tag.name);
}
get aspects(): Aspect[] {
return this.opts.aspects;
}
public aspectOf(type: string): Aspect | undefined {
return type ? this.aspects.find(f => f.name === type) : undefined;
}
public async reportDetailsOf(typeOrAspect: string | Aspect, workspaceId: string): Promise<AspectReportDetails | undefined> {
const type = typeof typeOrAspect === "string" ? typeOrAspect : typeOrAspect.name;
const aspect = this.aspectOf(type) as AspectWithReportDetails;
if (!!aspect) {
return aspect.details;
}
if (!isInLocalMode() && !!_.get(this.opts.configuration, "graphql.client.factory")) {
const aspectRegistrations = await this.opts.configuration.graphql.client.factory.create(workspaceId, this.opts.configuration)
.query<AspectRegistrations.Query, AspectRegistrations.Variables>({
name: "AspectRegistrations",
variables: {
state: [AspectRegistrationState.Enabled],
},
});
const aspectRegistration = (_.get(aspectRegistrations, "AspectRegistration") || [])
.filter(a => a.name === type);
if (!!aspectRegistration && aspectRegistration.length > 0) {
return aspectRegistration[0];
}
}
return undefined;
}
public async undesirableUsageCheckerFor(workspaceId: string): Promise<UndesirableUsageChecker | undefined> {
// TODO going for check functions is inelegant
if (this.opts.undesirableUsageChecker) {
return chainUndesirableUsageCheckers(
(await problemStoreBackedUndesirableUsageCheckerFor(this.problemStore, workspaceId)).check,
this.opts.undesirableUsageChecker.check);
}
return undefined;
}
get idealStore(): IdealStore {
return this.opts.idealStore;
}
get problemStore(): ProblemStore {
return this.opts.problemStore;
}
get scorers(): RepositoryScorer[] {
return this.opts.scorers || [];
}
private async tagRepos(tagContext: TagContext,
repos: ProjectAnalysisResult[]): Promise<TaggedRepo[]> {
const simpleTaggers = this.taggers.filter(isTagger);
const workspaceSpecificTaggers = await Promise.all(this.taggers
.filter(td => !isTagger(td))
// TODO why is this cast needed?
.map(td => taggerFrom(td as WorkspaceSpecificTagger, tagContext.workspaceId, this)));
const taggersToUse = [...simpleTaggers, ...workspaceSpecificTaggers];
return Promise.all(repos.map(repo => this.tagRepo(tagContext, repo, taggersToUse)));
}
private async tagRepo(
tagContext: TagContext,
repo: ProjectAnalysisResult,
taggers: Tagger[]): Promise<TaggedRepo> {
return {
...repo,
tags: await tagsFor(repo, tagContext, taggers),
};
}
constructor(private readonly opts: {
idealStore: IdealStore,
problemStore: ProblemStore,
aspects: AspectWithReportDetails[],
undesirableUsageChecker: UndesirableUsageChecker,
scorers?: RepositoryScorer[],
workspaceScorers?: WorkspaceScorer[],
scoreWeightings?: ScoreWeightings,
configuration?: Configuration,
}) {
opts.aspects.forEach(f => {
if (!f) {
throw new Error("A null aspect was passed in");
}
});
}
}
export function defaultedToDisplayableFingerprintName(aspect?: Aspect): (fingerprintName: string) => string {
return (aspect && aspect.toDisplayableFingerprintName) || (name => name);
}
export function defaultedToDisplayableFingerprint(aspect?: Aspect): (fpi: FP) => string {
return (aspect && aspect.toDisplayableFingerprint) || (fp => fp && fp.data);
}
async function tagsFor(rts: RepoToScore, tagContext: TagContext, taggers: Tagger[]): Promise<Tag[]> {
const tags = await Promise.all(taggers
.map(tagger => tagger.test(rts)
.then(yes => ({ ...tagger, tag: yes ? tagger.name : undefined }))),
);
return _.uniqBy(tags.filter(t => !!t.tag),
tag => tag.name);
}
async function taggerFrom(wst: WorkspaceSpecificTagger, workspaceId: string, ar: AspectRegistry): Promise<Tagger> {
return {
...wst,
test: await wst.createTest(workspaceId, ar),
};
}