feat: added rails templater to scaffolder-backend

Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
Rogerio Angeliski
2021-06-11 11:01:53 -03:00
parent bbd2ab80a3
commit 7a3ad92b52
15 changed files with 1070 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Created a Rails templater to scaffold apps
+5 -1
View File
@@ -40,6 +40,9 @@
"@octokit/rest": "^18.5.3",
"@types/express": "^4.17.6",
"azure-devops-node-api": "^10.2.2",
"@types/git-url-parse": "^9.0.0",
"@types/dockerode": "^3.2.1",
"azure-devops-node-api": "^10.1.1",
"command-exists": "^1.2.9",
"compression": "^1.7.4",
"cors": "^2.8.5",
@@ -60,7 +63,8 @@
"octokit-plugin-create-pull-request": "^3.9.3",
"uuid": "^8.2.0",
"winston": "^3.2.1",
"yaml": "^1.10.0"
"yaml": "^1.10.0",
"dockerode": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.7.3",
@@ -10,4 +10,5 @@ spec:
- ./create-react-app/template.yaml
- ./springboot-grpc-template/template.yaml
- ./v1beta2-demo/template.yaml
- ./rails-demo/template.yaml
- ./pull-request/template.yaml
@@ -0,0 +1,174 @@
apiVersion: backstage.io/v1beta2
kind: Template
metadata:
name: rails-demo
title: Rails template
description: scaffolder Rails app
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: Fill in some steps
required:
- name
- owner
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
defaultKind: System
- title: Choose a location
required:
- repoUrl
- dryRun
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
dryRun:
title: Only perform a dry run, don't publish anything
type: boolean
default: false
railsArguments:
title: arguments to run the rails new command
type: object
properties:
minimal:
title: minimal
description: Preconfigure a minimal rails app
type: boolean
skipBundle:
title: skipBundle
description: Don't run bundle install
type: boolean
skipWebpackInstall:
title: skipWebpackInstall
description: Don't run Webpack install
type: boolean
api:
title: api
description: Preconfigure smaller stack for API only apps
type: boolean
template:
title: template
description: Path to some application template (can be a filesystem path or URL)
type: string
default: './rails-template-file.rb'
webpacker:
title: webpacker
description: 'Preconfigure Webpack with a particular framework (options: react,
vue, angular, elm, stimulus)'
type: string
enum:
- react
- vue
- angular
- elm
- stimulus
database:
title: database
description: 'Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)'
type: string
enum:
- mysql
- postgresql
- sqlite3
- oracle
- sqlserver
- jdbcmysql
- jdbcsqlite3
- jdbcpostgresql
- jdbc
railsVersion:
title: Rails version in Gemfile
description: 'Set up the application with Gemfile pointing to a specific version
(options: dev, edge, master)'
type: string
enum:
- dev
- edge
- master
steps:
- id: fetch-base
name: Fetch Base
action: fetch:rails
input:
url: ./template
values:
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
system: '{{ parameters.system }}'
railsArguments: '{{ json parameters.railsArguments }}'
- name: Write Catalog information
action: catalog:write
input:
component:
apiVersion: 'backstage.io/v1alpha1'
kind: Component
metadata:
name: '{{ parameters.name }}'
annotations:
github.com/project-slug: '{{ projectSlug parameters.repoUrl }}'
spec:
type: service
lifecycle: production
owner: '{{ parameters.owner }}'
- id: publish
if: '{{ not parameters.dryRun }}'
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: 'This is {{ parameters.name }}'
repoUrl: '{{ parameters.repoUrl }}'
- id: register
if: '{{ not parameters.dryRun }}'
name: Register
action: catalog:register
input:
repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
catalogInfoPath: '/catalog-info.yaml'
- name: Results
if: '{{ parameters.dryRun }}'
action: debug:log
input:
listWorkspace: true
output:
links:
- title: Repository
url: '{{ steps.publish.output.remoteUrl }}'
- title: Open in catalog
icon: 'catalog'
entityRef: '{{ steps.register.output.entityRef }}'
@@ -0,0 +1,14 @@
gem_group :development, :test do
gem "rspec"
gem "rspec-rails"
end
rakefile("example.rake") do
<<-TASK
namespace :example do
task :backstage do
puts "i like backstage!"
end
end
TASK
end
@@ -0,0 +1,9 @@
FROM ruby:3.0
RUN apt-get update -qq && \
apt-get install -y \
nodejs \
postgresql-client \
git
RUN gem install rails
@@ -24,7 +24,11 @@ import {
} from './catalog';
import { createDebugLogAction } from './debug';
import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch';
import {
createFetchCookiecutterAction,
createFetchPlainAction,
createFetchRailsAction,
} from './fetch';
import {
createFilesystemDeleteAction,
createFilesystemRenameAction,
@@ -62,6 +66,10 @@ export const createBuiltinActions = (options: {
integrations,
containerRunner,
}),
createFetchRailsAction({
reader,
integrations,
}),
createPublishGithubAction({
integrations,
config,
@@ -16,3 +16,4 @@
export { createFetchPlainAction } from './plain';
export { createFetchCookiecutterAction } from './cookiecutter';
export { createFetchRailsAction } from './rails';
@@ -0,0 +1,140 @@
/*
* 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.
*/
const mockRailsTemplater = { run: jest.fn() };
jest.mock('./helpers');
jest.mock('../../../stages/templater', () => {
return {
Rails: jest.fn().mockImplementation(() => {
return mockRailsTemplater;
}),
};
});
import { getVoidLogger, UrlReader } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import mock from 'mock-fs';
import os from 'os';
import { resolve as resolvePath } from 'path';
import { PassThrough } from 'stream';
import { createFetchRailsAction } from './rails';
import { fetchContents } from './helpers';
describe('fetch:rails', () => {
const integrations = ScmIntegrations.fromConfig(
new ConfigReader({
integrations: {
azure: [
{ host: 'dev.azure.com', token: 'tokenlols' },
{ host: 'myazurehostnotoken.com' },
],
},
}),
);
const mockTmpDir = os.tmpdir();
const mockContext = {
input: {
url: 'https://rubyonrails.org/generator',
targetPath: 'something',
values: {
help: 'me',
},
},
baseUrl: 'somebase',
workspacePath: mockTmpDir,
logger: getVoidLogger(),
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
};
const mockReader: UrlReader = {
read: jest.fn(),
readTree: jest.fn(),
search: jest.fn(),
};
const action = createFetchRailsAction({
integrations,
reader: mockReader,
});
beforeEach(() => {
mock({ [`${mockContext.workspacePath}/result`]: {} });
jest.restoreAllMocks();
});
afterEach(() => {
mock.restore();
});
it('should call fetchContents with the correct values', async () => {
await action.handler(mockContext);
expect(fetchContents).toHaveBeenCalledWith({
reader: mockReader,
integrations,
baseUrl: mockContext.baseUrl,
fetchUrl: mockContext.input.url,
outputPath: resolvePath(mockContext.workspacePath),
});
});
it('should execute the rails templater with the correct values', async () => {
await action.handler(mockContext);
expect(mockRailsTemplater.run).toHaveBeenCalledWith({
workspacePath: mockTmpDir,
logStream: mockContext.logStream,
values: mockContext.input.values,
});
});
it('should execute the rails templater with optional inputs if they are present and valid', async () => {
await action.handler({
...mockContext,
input: {
...mockContext.input,
imageName: 'foo/rails-custom-image',
},
});
expect(mockRailsTemplater.run).toHaveBeenCalledWith({
workspacePath: mockTmpDir,
logStream: mockContext.logStream,
values: {
...mockContext.input.values,
imageName: 'foo/rails-custom-image',
},
});
});
it('should throw if the target directory is outside of the workspace path', async () => {
await expect(
action.handler({
...mockContext,
input: {
...mockContext.input,
targetPath: '/foo',
},
}),
).rejects.toThrow(
/targetPath may not specify a path outside the working directory/,
);
});
});
@@ -0,0 +1,182 @@
/*
* 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 { DockerContainerRunner, UrlReader } from '@backstage/backend-common';
import { JsonObject } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { Rails, TemplaterValues } from '../../../stages/templater';
import { createTemplateAction } from '../../createTemplateAction';
import { fetchContents } from './helpers';
import Docker from 'dockerode';
export function createFetchRailsAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
}) {
const { reader, integrations } = options;
return createTemplateAction<{
url: string;
targetPath?: string;
values: JsonObject;
imageName?: string;
}>({
id: 'fetch:rails',
description:
'Downloads a template from the given URL into the workspace, and runs a rails generator on it.',
schema: {
input: {
type: 'object',
required: ['url'],
properties: {
url: {
title: 'Fetch URL',
description:
'Relative path or absolute URL pointing to the directory tree to fetch',
type: 'string',
},
targetPath: {
title: 'Target Path',
description:
'Target path within the working directory to download the contents to.',
type: 'string',
},
values: {
title: 'Template Values',
description: 'Values to pass on to rails for templating',
type: 'object',
properties: {
railsArguments: {
title: 'Arguments to pass to new command',
description:
'You can provide some arguments to create a custom app',
type: 'object',
properties: {
minimal: {
title: 'minimal',
description: 'Preconfigure a minimal rails app',
type: 'boolean',
},
skipBundle: {
title: 'skipBundle',
description: "Don't run bundle install",
type: 'boolean',
},
skipWebpackInstall: {
title: 'skipWebpackInstall',
description: "Don't run Webpack install",
type: 'boolean',
},
api: {
title: 'api',
description: 'Preconfigure smaller stack for API only apps',
type: 'boolean',
},
template: {
title: 'template',
description:
'Path to some application template (can be a filesystem path or URL)',
type: 'string',
},
webpacker: {
title: 'webpacker',
description:
'Preconfigure Webpack with a particular framework (options: react, vue, angular, elm, stimulus)',
type: 'string',
enum: ['react', 'vue', 'angular', 'elm', 'stimulus'],
},
database: {
title: 'database',
description:
'Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)',
type: 'string',
enum: [
'mysql',
'postgresql',
'sqlite3',
'oracle',
'sqlserver',
'jdbcmysql',
'jdbcsqlite3',
'jdbcpostgresql',
'jdbc',
],
},
railsVersion: {
title: 'Rails version in Gemfile',
description:
'Set up the application with Gemfile pointing to a specific version (options: fromImage, dev, edge, master)',
type: 'string',
enum: ['dev', 'edge', 'master', 'fromImage'],
},
},
},
},
},
imageName: {
title: 'Rails Docker image',
description:
'Specify a Docker image to run rails new. Used only when a local rails is not found.',
type: 'string',
},
},
},
},
async handler(ctx) {
ctx.logger.info('Fetching and then templating using rails');
const workDir = await ctx.createTemporaryDirectory();
const resultDir = resolvePath(workDir, 'result');
await fetchContents({
reader,
integrations,
baseUrl: ctx.baseUrl,
fetchUrl: ctx.input.url,
outputPath: workDir,
});
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
const templateRunner = new Rails({ containerRunner });
const values = {
...(ctx.input.values as TemplaterValues),
imageName: ctx.input.imageName,
};
// Will execute the template in ./template and put the result in ./result
await templateRunner.run({
workspacePath: workDir,
logStream: ctx.logStream,
values,
});
// Finally move the template result into the task workspace
const targetPath = ctx.input.targetPath ?? './';
const outputPath = resolvePath(ctx.workspacePath, targetPath);
if (!outputPath.startsWith(ctx.workspacePath)) {
throw new InputError(
`Fetch action targetPath may not specify a path outside the working directory`,
);
}
await fs.copy(resultDir, outputPath);
},
});
}
@@ -0,0 +1,21 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 * from './cookiecutter';
export * from './types';
export * from './helpers';
export * from './templaters';
export * from './cra';
export * from './rails';
@@ -0,0 +1,257 @@
/*
* 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.
*/
const runCommand = jest.fn();
const commandExists = jest.fn();
jest.mock('../helpers', () => ({ runCommand }));
jest.mock('command-exists', () => commandExists);
jest.mock('fs-extra');
import { ContainerRunner } from '@backstage/backend-common';
import fs from 'fs-extra';
import path from 'path';
import { PassThrough } from 'stream';
import { Rails } from './index';
describe('Rails Templater', () => {
const containerRunner: jest.Mocked<ContainerRunner> = {
runContainer: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('when running on docker', () => {
it('should run the correct bindings for the volumes', async () => {
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
imageName: 'foo/rails-custom-image',
};
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
jest
.spyOn(fs, 'realpath')
.mockImplementation(x => Promise.resolve(x.toString()));
const templater = new Rails({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith({
imageName: 'foo/rails-custom-image',
command: 'rails',
args: ['new', '/output/rails-project'],
envVars: { HOME: '/tmp' },
mountDirs: {
['tempdir']: '/input',
[path.join('tempdir', 'intermediate')]: '/output',
},
workingDir: '/input',
logStream: undefined,
});
});
it('should use the provided imageName', async () => {
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
imageName: 'foo/rails-custom-image',
};
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new Rails({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith(
expect.objectContaining({
imageName: 'foo/rails-custom-image',
}),
);
});
it('should pass through the streamer to the run docker helper', async () => {
const stream = new PassThrough();
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
imageName: 'foo/rails-custom-image',
};
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
const templater = new Rails({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream: stream,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith({
imageName: 'foo/rails-custom-image',
command: 'rails',
args: ['new', '/output/rails-project'],
envVars: { HOME: '/tmp' },
mountDirs: {
['tempdir']: '/input',
[path.join('tempdir', 'intermediate')]: '/output',
},
workingDir: '/input',
logStream: stream,
});
});
it('update the template path to correct location', async () => {
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
railsArguments: { template: './something.rb' },
imageName: 'foo/rails-custom-image',
};
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
jest
.spyOn(fs, 'realpath')
.mockImplementation(x => Promise.resolve(x.toString()));
const templater = new Rails({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
});
expect(containerRunner.runContainer).toHaveBeenCalledWith({
imageName: 'foo/rails-custom-image',
command: 'rails',
args: [
'new',
'/output/rails-project',
'--template',
'/input/something.rb',
],
envVars: { HOME: '/tmp' },
mountDirs: {
['tempdir']: '/input',
[path.join('tempdir', 'intermediate')]: '/output',
},
workingDir: '/input',
logStream: undefined,
});
});
});
describe('when rails is available', () => {
it('use the binary', async () => {
const stream = new PassThrough();
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
imageName: 'foo/rails-custom-image',
};
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
commandExists.mockImplementationOnce(() => () => true);
const templater = new Rails({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream: stream,
});
expect(runCommand).toHaveBeenCalledWith({
command: 'rails',
args: expect.arrayContaining([
'new',
path.join('tempdir', 'intermediate', 'rails-project'),
]),
logStream: stream,
});
});
it('update the template path to correct location', async () => {
const stream = new PassThrough();
const values = {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
railsArguments: { template: './something.rb' },
imageName: 'foo/rails-custom-image',
};
jest.spyOn(fs, 'readdir').mockResolvedValueOnce(['newthing'] as any);
commandExists.mockImplementationOnce(() => () => true);
const templater = new Rails({ containerRunner });
await templater.run({
workspacePath: 'tempdir',
values,
logStream: stream,
});
expect(runCommand).toHaveBeenCalledWith({
command: 'rails',
args: expect.arrayContaining([
'new',
path.join('tempdir', 'intermediate', 'rails-project'),
'--template',
path.join('tempdir', './something.rb'),
]),
logStream: stream,
});
});
});
describe('when nothing was generated', () => {
it('throws an error', async () => {
const stream = new PassThrough();
jest
.spyOn(fs, 'readdir')
.mockImplementationOnce(() => Promise.resolve([]));
const templater = new Rails({ containerRunner });
await expect(
templater.run({
workspacePath: 'tempdir',
values: {
owner: 'angeliski',
storePath: 'https://github.com/angeliski/rails-project',
name: 'rails-project',
imageName: 'foo/rails-custom-image',
},
logStream: stream,
}),
).rejects.toThrow(/No data generated by rails/);
});
});
});
@@ -0,0 +1,96 @@
/*
* 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 { ContainerRunner } from '@backstage/backend-common';
import fs from 'fs-extra';
import path from 'path';
import { runCommand } from '../helpers';
import commandExists from 'command-exists';
import { TemplaterBase, TemplaterRunOptions } from '../types';
import { railsArgumentResolver } from './railsArgumentResolver';
export class Rails implements TemplaterBase {
private readonly containerRunner: ContainerRunner;
constructor({ containerRunner }: { containerRunner: ContainerRunner }) {
this.containerRunner = containerRunner;
}
public async run({
workspacePath,
values,
logStream,
}: TemplaterRunOptions): Promise<void> {
const intermediateDir = path.join(workspacePath, 'intermediate');
await fs.ensureDir(intermediateDir);
const resultDir = path.join(workspacePath, 'result');
const { name, imageName, railsArguments } = values;
// Directories to bind on container
const mountDirs = {
[workspacePath]: '/input',
[intermediateDir]: '/output',
};
const baseCommand = 'rails';
const baseArguments = ['new'];
const commandExistsToRun = await commandExists(baseCommand);
if (commandExistsToRun) {
const arrayExtraArguments = railsArgumentResolver(
workspacePath,
railsArguments,
);
await runCommand({
command: baseCommand,
args: [
...baseArguments,
`${intermediateDir}/${name}`,
...arrayExtraArguments,
],
logStream,
});
} else {
const arrayExtraArguments = railsArgumentResolver(
'/input',
railsArguments,
);
await this.containerRunner.runContainer({
imageName: imageName,
command: baseCommand,
args: [...baseArguments, `/output/${name}`, ...arrayExtraArguments],
mountDirs,
workingDir: '/input',
// Set the home directory inside the container as something that applications can
// write to, otherwise they will just fail trying to write to /
envVars: { HOME: '/tmp' },
logStream,
});
}
// if command was successful, intermediateDir should contain
// exactly one directory.
const [generated] = await fs.readdir(intermediateDir);
if (generated === undefined) {
throw new Error(`No data generated by ${baseCommand}`);
}
await fs.move(path.join(intermediateDir, generated), resultDir);
}
}
@@ -0,0 +1,50 @@
/*
* 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 { railsArgumentResolver } from './railsArgumentResolver';
import { TemplaterValues } from '../types';
describe('railsArgumentResolver', () => {
describe('when provide the parameter', () => {
test.each([
[{}, []],
[{ minimal: true }, ['--minimal']],
[{ api: true }, ['--api']],
[{ skipBundle: true }, ['--skip-bundle']],
[{ skipWebpackInstall: true }, ['--skip-webpack-install']],
[{ webpacker: 'vue' }, ['--webpack', 'vue']],
[{ database: 'postgresql' }, ['--database', 'postgresql']],
[{ railsVersion: 'dev' }, ['--dev']],
[{ template: './rails.rb' }, ['--template', '/tmp/rails.rb']],
])(
'should include the argument to execution %p -> %p',
(passedArguments: object, expected: Array<string>) => {
// that step is to ensure the validation between the TemplaterValues and the resolver
const values: TemplaterValues = {
owner: 'r',
storePath: '',
railsArguments: passedArguments,
};
const { railsArguments } = values;
const argumentsToRun = railsArgumentResolver('/tmp', railsArguments);
expect(argumentsToRun).toEqual(expected);
},
);
});
});
@@ -0,0 +1,106 @@
/*
* 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.
*/
enum Webpacker {
react = 'react',
vue = 'vue',
angular = 'angular',
elm = 'elm',
stimulus = 'stimulus',
}
enum Database {
mysql = 'mysql',
postgresql = 'postgresql',
sqlite3 = 'sqlite3',
oracle = 'oracle',
sqlserver = 'sqlserver',
jdbcmysql = 'jdbcmysql',
jdbcsqlite3 = 'jdbcsqlite3',
jdbcpostgresql = 'jdbcpostgresql',
jdbc = 'jdbc',
}
enum RailsVersion {
dev = 'dev',
edge = 'edge',
master = 'master',
fromImage = 'fromImage',
}
export type RailsRunOptions = {
minimal?: boolean;
api?: boolean;
template?: string;
webpacker?: Webpacker;
database?: Database;
railsVersion?: RailsVersion;
skipBundle?: boolean;
skipWebpackInstall?: boolean;
};
export const railsArgumentResolver = (
projectRoot: string,
options: RailsRunOptions,
): string[] => {
const argumentsToRun: string[] = [];
if (options?.minimal) {
argumentsToRun.push('--minimal');
}
if (options?.api) {
argumentsToRun.push('--api');
}
if (options?.skipBundle) {
argumentsToRun.push('--skip-bundle');
}
if (options?.skipWebpackInstall) {
argumentsToRun.push('--skip-webpack-install');
}
if (
options?.webpacker &&
Object.values(Webpacker).includes(options?.webpacker as Webpacker)
) {
argumentsToRun.push('--webpack');
argumentsToRun.push(options.webpacker);
}
if (
options?.database &&
Object.values(Database).includes(options?.database as Database)
) {
argumentsToRun.push('--database');
argumentsToRun.push(options.database);
}
if (
options?.railsVersion !== RailsVersion.fromImage &&
Object.values(RailsVersion).includes(options?.railsVersion as RailsVersion)
) {
argumentsToRun.push(`--${options.railsVersion}`);
}
if (options?.template) {
argumentsToRun.push('--template');
argumentsToRun.push(options.template.replace('./', `${projectRoot}/`));
}
return argumentsToRun;
};