Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/generator-cli/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ If the `version` property param is set it is not necessary to configure the `que
| openapi-generator-cli generate --generator-key v3.0 v2.0 | yes | yes |
| openapi-generator-cli generate --generator-key foo | no | no |

## Use Docker instead of running java locally

```json
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"useDocker": true
}
}
```

## Custom Generators

Custom generators can be used by passing the `--custom-generator=/my/custom-generator.jar` argument.
Expand Down
8 changes: 8 additions & 0 deletions apps/generator-cli/src/app/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export class ConfigService {
public readonly cwd = process.env.PWD || process.env.INIT_CWD || process.cwd()
public readonly configFile = path.resolve(this.cwd, 'openapitools.json')

public get useDocker() {
return this.get('generator-cli.useDocker', false);
}

public get dockerImageName() {
return this.get('generator-cli.dockerImageName', 'openapitools/openapi-generator-cli');
}

private readonly defaultConfig = {
$schema: './node_modules/@openapitools/openapi-generator-cli/config.schema.json',
spaces: 2,
Expand Down
55 changes: 43 additions & 12 deletions apps/generator-cli/src/app/services/generator.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { flatten, isString, kebabCase, sortBy, upperFirst } from 'lodash';
import {Inject, Injectable} from '@nestjs/common';
import {flatten, isString, kebabCase, sortBy, upperFirst} from 'lodash';

import * as concurrently from 'concurrently';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as glob from 'glob';
import * as chalk from 'chalk';
import { VersionManagerService } from './version-manager.service';
import { ConfigService } from './config.service';
import { LOGGER } from '../constants';
import {VersionManagerService} from './version-manager.service';
import {ConfigService} from './config.service';
import {LOGGER} from '../constants';

interface GeneratorConfig {
glob: string
Expand Down Expand Up @@ -96,6 +97,7 @@ export class GeneratorService {
}

private buildCommand(cwd: string, params: Record<string, unknown>, customGenerator?: string, specFile?: string) {
const dockerVolumes = {};
const absoluteSpecPath = specFile ? path.resolve(cwd, specFile) : String(params.inputSpec)

const command = Object.entries({
Expand All @@ -114,7 +116,19 @@ export class GeneratorService {
case 'boolean':
return undefined
default:
return `"${v}"`

if (this.configService.useDocker) {
if (key === 'output') {
fs.ensureDirSync(v);
}

if (fs.existsSync(v)) {
dockerVolumes[`/local/${key}`] = path.resolve(cwd, v);
return `"/local/${key}"`;
}
}

return `"${v}"`;
}
})()

Expand All @@ -139,14 +153,31 @@ export class GeneratorService {
ext: ext.split('.').slice(-1).pop()
}

return this.cmd(customGenerator, Object.entries(placeholders)
.filter(([, replacement]) => !!replacement)
.reduce((cmd, [search, replacement]) => {
return cmd.split(`#{${search}}`).join(replacement)
}, command))
return this.cmd(
customGenerator,
Object.entries(placeholders)
.filter(([, replacement]) => !!replacement)
.reduce((cmd, [search, replacement]) => {
return cmd.split(`#{${search}}`).join(replacement)
}, command),
dockerVolumes,
)
}

private cmd = (customGenerator: string | undefined, appendix: string) => {
private cmd = (customGenerator: string | undefined, appendix: string, dockerVolumes = {}) => {

if (this.configService.useDocker) {
const volumes = Object.entries(dockerVolumes).map(([k, v]) => `-v "${v}:${k}"`).join(' ');

return [
`docker run --rm`,
volumes,
this.versionManager.getDockerImageName(),
'generate',
appendix
].join(' ');
}

const cliPath = this.versionManager.filePath();
const subCmd = customGenerator
? `-cp "${[cliPath, customGenerator].join(this.isWin() ? ';' : ':')}" org.openapitools.codegen.OpenAPIGenerator`
Expand Down
86 changes: 44 additions & 42 deletions apps/generator-cli/src/app/services/pass-through.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Test } from '@nestjs/testing'
import {Test} from '@nestjs/testing'
import * as chalk from 'chalk'
import { Command, createCommand } from 'commander'
import { COMMANDER_PROGRAM, LOGGER } from '../constants'
import { GeneratorService } from './generator.service'
import { PassThroughService } from './pass-through.service'
import { VersionManagerService } from './version-manager.service'
import {Command, createCommand} from 'commander'
import {COMMANDER_PROGRAM, LOGGER} from '../constants'
import {GeneratorService} from './generator.service'
import {PassThroughService} from './pass-through.service'
import {VersionManagerService} from './version-manager.service'
import {ConfigService} from "./config.service";

jest.mock('child_process')
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand All @@ -19,6 +20,7 @@ describe('PassThroughService', () => {
const generate = jest.fn().mockResolvedValue(true)
const getSelectedVersion = jest.fn().mockReturnValue('4.2.1')
const filePath = jest.fn().mockReturnValue(`/some/path/to/4.2.1.jar`)
const configServiceMock = {useDocker: false, get: jest.fn(), cwd: '/foo/bar'};

const getCommand = (name: string) => program.commands.find(c => c.name() === name);

Expand All @@ -29,17 +31,20 @@ describe('PassThroughService', () => {
const moduleRef = await Test.createTestingModule({
providers: [
PassThroughService,
{ provide: VersionManagerService, useValue: { filePath, getSelectedVersion } },
{ provide: GeneratorService, useValue: { generate, enabled: true } },
{ provide: COMMANDER_PROGRAM, useValue: program },
{ provide: LOGGER, useValue: { log } },
{provide: VersionManagerService, useValue: {filePath, getSelectedVersion, getDockerImageName: (v) => `openapitools/openapi-generator-cli:v${v || getSelectedVersion()}`}},
{provide: GeneratorService, useValue: {generate, enabled: true}},
{provide: ConfigService, useValue: configServiceMock},
{provide: COMMANDER_PROGRAM, useValue: program},
{provide: LOGGER, useValue: {log}},
],
}).compile()

fixture = moduleRef.get(PassThroughService)

childProcess.spawn.mockReset().mockReturnValue({ on: jest.fn() })

childProcess.spawn.mockReset().mockReturnValue({on: jest.fn()})
configServiceMock.get.mockClear()
configServiceMock.get.mockReset()
configServiceMock.useDocker = false;
})

describe('API', () => {
Expand Down Expand Up @@ -147,8 +152,28 @@ describe('PassThroughService', () => {
expect(cmd['_allowUnknownOption']).toBeTruthy()
})

describe('useDocker is true', () => {

beforeEach(() => {
configServiceMock.useDocker = true;
});

it('delegates to docker', async () => {
await program.parseAsync([name, ...argv], {from: 'user'})
expect(childProcess.spawn).toHaveBeenNthCalledWith(
1,
'docker run --rm -v "/foo/bar:/local" openapitools/openapi-generator-cli:v4.2.1',
[name, ...argv],
{
stdio: 'inherit',
shell: true
}
)
})
})

it('can delegate', async () => {
await program.parseAsync([name, ...argv], { from: 'user' })
await program.parseAsync([name, ...argv], {from: 'user'})
expect(childProcess.spawn).toHaveBeenNthCalledWith(
1,
'java -jar "/some/path/to/4.2.1.jar"',
Expand All @@ -162,7 +187,7 @@ describe('PassThroughService', () => {

it('can delegate with JAVA_OPTS', async () => {
process.env['JAVA_OPTS'] = 'java-opt-1=1'
await program.parseAsync([name, ...argv], { from: 'user' })
await program.parseAsync([name, ...argv], {from: 'user'})
expect(childProcess.spawn).toHaveBeenNthCalledWith(
1,
'java java-opt-1=1 -jar "/some/path/to/4.2.1.jar"',
Expand All @@ -175,7 +200,7 @@ describe('PassThroughService', () => {
})

it('can delegate with custom jar', async () => {
await program.parseAsync([name, ...argv, '--custom-generator=../some/custom.jar'], { from: 'user' })
await program.parseAsync([name, ...argv, '--custom-generator=../some/custom.jar'], {from: 'user'})
const cpDelimiter = process.platform === 'win32' ? ';' : ':'

expect(childProcess.spawn).toHaveBeenNthCalledWith(
Expand All @@ -191,8 +216,8 @@ describe('PassThroughService', () => {

if (name === 'generate') {
it('can delegate with custom jar to generate command', async () => {
await program.parseAsync([name, ...argv, '--generator-key=genKey', '--custom-generator=../some/custom.jar'], { from: 'user' })
await program.parseAsync([name, ...argv, '--generator-key=genKey', '--custom-generator=../some/custom.jar'], {from: 'user'})

expect(generate).toHaveBeenNthCalledWith(
1,
'../some/custom.jar',
Expand All @@ -201,29 +226,6 @@ describe('PassThroughService', () => {
})
}

// if (name === 'help') {
// it('prints the help info and does not delegate, if args length = 0', async () => {
// childProcess.spawn.mockReset()
// cmd.args = []
// const logSpy = jest.spyOn(console, 'log').mockImplementation(noop)
// await program.parseAsync([name], { from: 'user' })
// expect(childProcess.spawn).toBeCalledTimes(0)
// expect(program.helpInformation).toBeCalledTimes(1)
// // expect(logSpy).toHaveBeenCalledTimes(2)
// expect(logSpy).toHaveBeenNthCalledWith(1, 'some help text')
// expect(logSpy).toHaveBeenNthCalledWith(2, 'has custom generator')
// })
// }
//
// if (name === 'generate') {
// it('generates by using the generator config', async () => {
// childProcess.spawn.mockReset()
// await program.parseAsync([name], { from: 'user' })
// expect(childProcess.spawn).toBeCalledTimes(0)
// expect(generate).toHaveBeenNthCalledWith(1)
// })
// }

})

describe('command behavior', () => {
Expand All @@ -239,13 +241,13 @@ describe('PassThroughService', () => {
${'help generate'} | ${commandHelp('generate')} | ${'a'}
${'help author'} | ${commandHelp('author')} | ${'b'}
${'help hidden'} | ${undefined} | ${'c'}
`('$cmd', ({ cmd, helpText, spawn }) => {
`('$cmd', ({cmd, helpText, spawn}) => {

let spy: jest.SpyInstance;

beforeEach(async () => {
spy = jest.spyOn(console, 'log').mockClear().mockImplementation();
await program.parseAsync(cmd.split(' '), { from: 'user' })
await program.parseAsync(cmd.split(' '), {from: 'user'})
})

describe('help text', () => {
Expand Down
31 changes: 20 additions & 11 deletions apps/generator-cli/src/app/services/pass-through.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Inject, Injectable } from '@nestjs/common'
import {Inject, Injectable} from '@nestjs/common'
import * as chalk from 'chalk'
import { exec, spawn } from 'child_process'
import { Command } from 'commander'
import { isString, startsWith, trim } from 'lodash'
import { COMMANDER_PROGRAM, LOGGER } from '../constants'
import { GeneratorService } from './generator.service'
import { VersionManagerService } from './version-manager.service'
import {exec, spawn} from 'child_process'
import {Command} from 'commander'
import {isString, startsWith, trim} from 'lodash'
import {COMMANDER_PROGRAM, LOGGER} from '../constants'
import {GeneratorService} from './generator.service'
import {VersionManagerService} from './version-manager.service'
import {ConfigService} from "./config.service";

@Injectable()
export class PassThroughService {
Expand All @@ -14,19 +15,20 @@ export class PassThroughService {
@Inject(LOGGER) private readonly logger: LOGGER,
@Inject(COMMANDER_PROGRAM) private readonly program: Command,
private readonly versionManager: VersionManagerService,
private readonly configService: ConfigService,
private readonly generatorService: GeneratorService
) {
}

public async init() {

this.program
.allowUnknownOption()
.option("--custom-generator <generator>", "Custom generator jar")
.allowUnknownOption()
.option("--custom-generator <generator>", "Custom generator jar")

const commands = (await this.getCommands()).reduce((acc, [name, desc]) => {
return acc.set(name, this.program
.command(name, { hidden: !desc })
.command(name, {hidden: !desc})
.description(desc)
.allowUnknownOption()
.action((_, c) => this.passThrough(c)))
Expand Down Expand Up @@ -93,7 +95,7 @@ export class PassThroughService {
.filter(line => startsWith(line, ' '))
.map<string>(trim)
.map(line => line.match(/^([a-z-]+)\s+(.+)/i).slice(1))
.reduce((acc, [cmd, desc]) => ({ ...acc, [cmd]: desc }), {});
.reduce((acc, [cmd, desc]) => ({...acc, [cmd]: desc}), {});

const allCommands = completion.split('\n')
.map<string>(trim)
Expand All @@ -114,6 +116,13 @@ export class PassThroughService {
});

private cmd() {
if (this.configService.useDocker) {
return [
`docker run --rm -v "${this.configService.cwd}:/local"`,
this.versionManager.getDockerImageName(),
].join(' ');
}

const customGenerator = this.program.opts()?.customGenerator;
const cliPath = this.versionManager.filePath();

Expand Down
Loading