feat(scaffolder): make the scaffolder repository a little more flexible so we can load from multiple sources

This commit is contained in:
blam
2020-04-26 01:57:34 +02:00
committed by blam
parent 142f57949d
commit a86f111b97
6 changed files with 295 additions and 29 deletions
@@ -0,0 +1,83 @@
/*
* 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 { DiskRepository } from './disk';
import * as path from 'path';
describe('Template Repository', () => {
it('should load a simple template from a simple directory', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-simple-template-dir',
);
const templateInfo = require(`${testTemplateDir}/mock-template/template-info.json`);
const repository = new DiskRepository(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(1);
expect(templates[0].id).toBe(templateInfo.id);
expect(templates[0].name).toBe(templateInfo.name);
expect(templates[0].description).toBe(templateInfo.description);
expect(templates[0].ownerId).toBe(templateInfo.ownerId);
});
it('should successfully load multiple templates from the same folder', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-multiple-templates-dir',
);
const repository = new DiskRepository(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(2);
});
it('should return empty array when there are no templates', async () => {
const testTemplateDir = path.resolve(
__dirname,
'/some-folder-that-deffo-does-not-exist',
);
const repository = new DiskRepository(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(0);
});
it('should be able to handle templates with invalid json and ignore them from the returned array', async () => {
const testTemplateDir = path.resolve(
__dirname,
'../../../test/mock-failing-template-dir',
);
const repository = new DiskRepository(testTemplateDir);
await repository.reindex();
const templates = await repository.list();
expect(templates).toHaveLength(1);
});
});
@@ -0,0 +1,80 @@
/*
* 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 glob from 'glob';
import { promises as fs } from 'fs';
import { logger } from 'lib/logger';
import { Template, RepositoryBase as Base } from '.';
export class DiskRepository implements Base {
private repository: Template[] = [];
constructor(private repoDir = `${__dirname}/templates`) {
this.reindex();
}
public async list(): Promise<Template[]> {
return this.repository;
}
public async reindex(): Promise<void> {
this.repository = await this.index();
}
public async prepare(template: string): Promise<string> {
return template;
}
private async index(): Promise<Template[]> {
return new Promise((resolve, reject) => {
glob(`${this.repoDir}/**/template-info.json`, async (err, matches) => {
if (err) {
reject(err);
}
const fileContents: Array<{
path: string;
contents: string;
}> = await Promise.all(
matches.map(async (path: string) => ({
path,
contents: await fs.readFile(path, 'utf-8'),
})),
);
const validFiles = fileContents.reduce(
(templates: Template[], currentFile) => {
try {
const parsed: Template = JSON.parse(currentFile.contents);
return [...templates, parsed];
} catch (ex) {
logger.error('Failure parsing JSON for template', {
path: currentFile.path,
});
}
return templates;
},
[],
);
resolve(validFiles as Template[]);
});
});
}
}
export const Repository = new DiskRepository();
@@ -0,0 +1,56 @@
/*
* 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 { RepositoryBase, Repository } from '.';
describe('Repository Interface Test', () => {
const mockRepo = new (class MockRepository implements RepositoryBase {
list = jest.fn();
prepare = jest.fn();
reindex = jest.fn();
public reset = () => {
this.list.mockReset();
this.prepare.mockReset();
this.reindex.mockReset();
};
})();
afterEach(() => mockRepo.reset());
it('should call list of the set repo when calling list', async () => {
Repository.setRepository(mockRepo);
await Repository.list();
expect(mockRepo.list).toHaveBeenCalled();
});
it('should reindex on the repo when calling reindex', async () => {
Repository.setRepository(mockRepo);
await Repository.reindex();
expect(mockRepo.reindex).toHaveBeenCalled();
});
it('should call prepare with the correct id when calling prepare', async () => {
Repository.setRepository(mockRepo);
await Repository.prepare('testid');
expect(mockRepo.prepare).toHaveBeenCalledWith('testid');
});
});
@@ -0,0 +1,48 @@
import { DiskRepository } from './disk';
/*
* 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 interface Template {
id: string;
name: string;
description: string;
ownerId: string;
}
export abstract class RepositoryBase {
abstract async list(): Promise<Template[]>;
abstract async reindex(): Promise<void>;
// returns a directory to run cookiecutter in
abstract async prepare(id: string): Promise<string>;
}
class Interface implements RepositoryBase {
repo?: RepositoryBase;
constructor() {
this.repo = new DiskRepository();
}
public setRepository(repo: RepositoryBase) {
this.repo = repo;
}
list = () => this.repo!.list();
prepare = (id: string) => this.repo!.prepare(id);
reindex = () => this.repo!.reindex();
}
export const Repository = new Interface();
+26 -21
View File
@@ -13,26 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// /*
// * 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 { plugiroutn } from './plugin';
import { router } from './plugin';
// describe('scaffolder', () => {
// it('should export plugin', () => {
// expect(plugin).toBeDefined();
// });
// });
import { Repository, Template } from 'lib/repo';
import supertest from 'supertest';
import express from 'express';
describe('scaffolder backend', () => {
const app = express().use(router);
const mockTemplate: Template = {
id: 'test-mock-template',
name: 'mock',
description: 'test for tests',
ownerId: 'lol',
};
it('should return a list of templates that are in the directory which the plugin is initialised in', async () => {
jest.spyOn(Repository, 'list').mockResolvedValue([mockTemplate]);
const { body } = await supertest(app)
.get('/v1/templates')
.send();
expect(body.length).toBe(1);
expect(body[0]).toStrictEqual(mockTemplate);
});
});
+2 -8
View File
@@ -33,14 +33,8 @@ import express from 'express';
export const router = express.Router();
router.get('/v1/templates', async (_, res) => {
res
.status(200)
.send([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]);
const templates = await Repository.list();
res.status(200).json(templates);
});
router.get('/v1/template/:templateId', async (_, res) => {