Skip to content
Open
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
42 changes: 27 additions & 15 deletions apps/generator-cli/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ To make that happen, a version management was added to the package.
The first time you run the command `openapi-generator-cli` the last stable version
of [OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator) is downloaded by default.

That version is saved in the file *openapitools.json*. Therefore you should include this file in your version control,
That version is saved in the file *openapitools.json*. Therefore, you should include this file in your version control,
to ensure that the correct version is being used next time you call the command.

If you would like to use a different version of the [OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator),
Expand Down Expand Up @@ -66,7 +66,7 @@ After the installation has finished you can run `npx openapi-generator-cli` or a
"name": "my-cool-package",
"version": "0.0.0",
"scripts": {
"my-awesome-script-name": "openapi-generator-cli generate -i docs/openapi.yaml -g typescript-angular -o generated-sources/openapi --additional-properties=ngVersion=6.1.7,npmName=restClient,supportsES6=true,npmVersion=6.9.0,withInterfaces=true",
"my-awesome-script-name": "openapi-generator-cli generate -i docs/openapi.yaml -g typescript-angular -o generated-sources/openapi --additional-properties=ngVersion=6.1.7,npmName=restClient,supportsES6=true,npmVersion=6.9.0,withInterfaces=true"
}
}
```
Expand Down Expand Up @@ -164,17 +164,18 @@ is automatically used to generate your code. 🎉

##### Available placeholders

| placeholder | description | example |
|--------------|---------------------------------------------------------------|-------------------------------------------------------|
| name | just file name | auth |
| Name | just file name, but starting with a capital letter | Auth |
| cwd | the current cwd | /Users/some-user/projects/some-project |
| base | file name and extension | auth.yaml |
| path | full path and filename | /Users/some-user/projects/some-project/docs/auth.yaml |
| dir | path without the filename | /Users/some-user/projects/some-project/docs |
| relDir | directory name of file relative to the glob provided | docs |
| relPath | file name and extension of file relative to the glob provided | docs/auth.yaml |
| ext | just file extension | yaml |
| placeholder | description | example |
|-------------|---------------------------------------------------------------|-------------------------------------------------------|
| name | just file name | auth |
| Name | just file name, but starting with a capital letter | Auth |
| cwd | the current cwd | /Users/some-user/projects/some-project |
| base | file name and extension | auth.yaml |
| path | full path and filename | /Users/some-user/projects/some-project/docs/auth.yaml |
| dir | path without the filename | /Users/some-user/projects/some-project/docs |
| relDir | directory name of file relative to the glob provided | docs |
| relPath | file name and extension of file relative to the glob provided | docs/auth.yaml |
| ext | just file extension | yaml |
| env.<name> | environment variable (use ${env.name} syntax) | |

### Using custom / private maven registry

Expand All @@ -196,6 +197,17 @@ If you're using a private maven registry you can configure the `downloadUrl` and

If the `version` property param is set it is not necessary to configure the `queryUrl`.

`queryUrl` and `downloadUrl` can use the following placeholders:

| placeholder | description |
|-------------|----------------------------------------------------|
| groupId | maven groupId where '.' has been replace with / |
| artifactId | maven artifactId where '.' has been replace with / |
| versionName | maven version (only for downloadUrl) |
| group.id | maven groupId |
| artifact.id | maven artifactId |
| env.<name> | environment variable name |

### Use locally built JAR
In order to use a locally built jar of the generator CLI, you can copy the jar from your local build (i.e. if you were to `build` the [OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator) repository it would be in `~/openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar`) into `./node_modules/@openapitools/openapi-generator-cli/versions/` and change the `version` in the `openapitools.json` file to the base name of the jar file.
E.g.:
Expand All @@ -210,7 +222,7 @@ and then:
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "my-local-snapshot",
"version": "my-local-snapshot"
}
}
```
Expand All @@ -232,7 +244,7 @@ Change your `openapitools.json` to:
```

Example is with a snapshot of `7.17.0`, please change the `version` and `downloadUrl` accordingly.
You can find the published snapshots in the build log of the [Publish to Maven Central Github workflow](https://github.com/OpenAPITools/openapi-generator/actions/workflows/maven-release.yml) in OpenAPI Generator repo, e.g.
You can find the published snapshots in the build log of the [Publish to Maven Central GitHub workflow](https://github.com/OpenAPITools/openapi-generator/actions/workflows/maven-release.yml) in OpenAPI Generator repo, e.g.

```
[INFO] Uploading to central: https://central.sonatype.com/repository/maven-snapshots/org/openapitools/openapi-generator-cli/7.17.0-SNAPSHOT/openapi-generator-cli-7.17.0-20251003.020930-8.jar
Expand Down
95 changes: 94 additions & 1 deletion apps/generator-cli/src/app/services/config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('ConfigService', () => {
let program: Command;

const log = jest.fn();
const error = jest.fn();

beforeEach(async () => {
program = createCommand();
Expand All @@ -20,7 +21,7 @@ describe('ConfigService', () => {
const moduleRef = await Test.createTestingModule({
providers: [
ConfigService,
{ provide: LOGGER, useValue: { log } },
{ provide: LOGGER, useValue: { log, error } },
{ provide: COMMANDER_PROGRAM, useValue: program },
],
}).compile();
Expand Down Expand Up @@ -92,6 +93,43 @@ describe('ConfigService', () => {
});
});

describe('the config has values having placeholders', () => {
let originalEnv: NodeJS.ProcessEnv;

beforeEach(() => {
originalEnv = { ...process.env };

fs.readJSONSync.mockReturnValue({
$schema: 'foo.json',
spaces: 4,
'generator-cli': {
version: '1.2.3',
repository: {
queryUrl: 'https://${env.__unit_test_username}:${env.__unit_test_password}@server/api',
downloadUrl: 'https://${env.__unit_test_non_matching}@server/api'
}
},
});
process.env['__unit_test_username'] = 'myusername';
process.env['__unit_test_password'] = 'mypassword';
});

afterEach(() => {
process.env = { ...originalEnv };
});

it('verify placeholder replaced with env vars', () => {
const value = fixture.get('generator-cli.repository.queryUrl');
expect(value).toEqual('https://myusername:mypassword@server/api');
});

it('verify placeholders not matching env vars are not replaced', () => {
const value = fixture.get('generator-cli.repository.downloadUrl');
expect(value).toEqual('https://${env.__unit_test_non_matching}@server/api');
expect(error).toHaveBeenCalledWith('Environment variable for placeholder \'__unit_test_non_matching\' not found.');
});
});

describe('has()', () => {
beforeEach(() => {
fs.readJSONSync.mockReturnValue({
Expand Down Expand Up @@ -184,5 +222,60 @@ describe('ConfigService', () => {
});
});
});

describe('replacePlaceholders', () => {
let originalEnv: NodeJS.ProcessEnv;

beforeEach(() => {
jest.clearAllMocks();
originalEnv = { ...process.env };
});

afterEach(() => {
process.env = { ...originalEnv };
});

it('replaces a simple placeholder with an environment variable', () => {
process.env.TEST_VAR = 'value1';
const input = { key: 'Hello ${TEST_VAR}' };
const result = fixture['replacePlaceholders'](input);
expect(result.key).toBe('Hello value1');
});

it('leaves placeholder unchanged and logs error if env var is missing', () => {
delete process.env.MISSING_VAR;
const input = { key: 'Hello ${MISSING_VAR}' };
const result = fixture['replacePlaceholders'](input);
expect(result.key).toBe('Hello ${MISSING_VAR}');
expect(error).toHaveBeenCalledWith(expect.stringContaining('MISSING_VAR'));
});

it('replaces placeholders in nested objects and arrays', () => {
process.env.NESTED_VAR = 'nested';
const input = {
arr: ['${NESTED_VAR}', { inner: '${NESTED_VAR}' }],
obj: { deep: '${NESTED_VAR}' },
};
const result = fixture['replacePlaceholders'](input);
expect(result.arr[0]).toBe('nested');
expect((result.arr[1] as { inner: string }).inner).toBe('nested');
expect((result.obj as { deep: string }).deep).toBe('nested');
});

it('handles env. prefix in placeholders', () => {
process.env.PREFIX_VAR = 'prefix';
const input = { key: 'Value: ${env.PREFIX_VAR}' };
const result = fixture['replacePlaceholders'](input);
expect(result.key).toBe('Value: prefix');
});

it('replaces multiple placeholders in a single string', () => {
process.env.FIRST = 'one';
process.env.SECOND = 'two';
const input = { key: 'Values: ${FIRST}, ${SECOND}' };
const result = fixture['replacePlaceholders'](input);
expect(result.key).toBe('Values: one, two');
});
});
});
});
71 changes: 59 additions & 12 deletions apps/generator-cli/src/app/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,41 @@ import { Command } from 'commander';
@Injectable()
export class ConfigService {

public readonly cwd = process.env.PWD || process.env.INIT_CWD || process.cwd()
public readonly cwd = process.env.PWD || process.env.INIT_CWD || process.cwd();
public readonly configFile = this.configFileOrDefault();

private configFileOrDefault() {
this.program.parseOptions(process.argv);
const conf = this.program.opts().openapitools;

if(!conf) {
if (!conf) {
return path.resolve(this.cwd, 'openapitools.json');
}

return path.isAbsolute(conf) ? conf : path.resolve(this.cwd, conf);
}

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

public get dockerImageName() {
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',
$schema:
'./node_modules/@openapitools/openapi-generator-cli/config.schema.json',
spaces: 2,
'generator-cli': {
version: undefined,
},
}
};

constructor(
@Inject(LOGGER) private readonly logger: LOGGER,
@Inject(COMMANDER_PROGRAM) private readonly program: Command,
) {
}
) {}

get<T = unknown>(path: string, defaultValue?: T): T {
const getPath = (
Expand Down Expand Up @@ -110,9 +110,9 @@ export class ConfigService {

private read() {
const deepMerge = (
target: object,
target: Record<string, unknown>,
source: object,
): object => {
): Record<string, unknown> => {
if (!source || typeof source !== 'object') return target;

const result = { ...target };
Expand All @@ -124,7 +124,7 @@ export class ConfigService {
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
const value = (result[key] || {});
const value = (result[key] || {}) as Record<string, unknown>;
result[key] = deepMerge(value, source[key]);
} else {
result[key] = source[key];
Expand All @@ -137,10 +137,57 @@ export class ConfigService {

fs.ensureFileSync(this.configFile);

return deepMerge(
const config = deepMerge(
this.defaultConfig,
fs.readJSONSync(this.configFile, { throws: false, encoding: 'utf8' }),
);

return this.replacePlaceholders(config);
}

private replacePlaceholders(config: Record<string, unknown>): Record<string, unknown> {
const replacePlaceholderInString = (inputString: string): string => {
return inputString.replace(/\${(.*?)}/g, (fullMatch, placeholderKey) => {
const environmentVariableKey = placeholderKey.startsWith('env.')
? placeholderKey.substring(4)
: placeholderKey;

const environmentVariableValue = process.env[environmentVariableKey];

if (environmentVariableValue === undefined) {
this.logger.error(
`Environment variable for placeholder '${environmentVariableKey}' not found.`,
);
return fullMatch;
}

return environmentVariableValue;
});
};

const traverseConfigurationObject = (
configurationValue: unknown,
): unknown => {
if (typeof configurationValue === 'string') {
return replacePlaceholderInString(configurationValue);
}
if (Array.isArray(configurationValue)) {
return configurationValue.map(traverseConfigurationObject);
}
if (configurationValue && typeof configurationValue === 'object') {
return Object.fromEntries(
Object.entries(configurationValue as Record<string, unknown>).map(
([propertyKey, propertyValue]) => [
propertyKey,
traverseConfigurationObject(propertyValue),
],
),
);
}
return configurationValue;
};

return traverseConfigurationObject(config) as Record<string, unknown>;
}

private write(config) {
Expand Down