Skip to content

Commit

Permalink
feat(sdk): generate an sdk with spec from npm
Browse files Browse the repository at this point in the history
  • Loading branch information
fpaul-1A committed Apr 22, 2024
1 parent bbae039 commit 9284ce1
Show file tree
Hide file tree
Showing 25 changed files with 325 additions and 60 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"build:lint": "yarn nx run-many --target=build --projects=eslint-plugin --parallel $(yarn get:cpus-number)",
"build:swagger-gen": "yarn nx run-many --target=build-swagger --parallel $(yarn get:cpus-number)",
"prepare:publish": "yarn prepare-publish \"$(yarn workspaces:list)\" --append dist",
"publish": "yarn run prepare:publish && yarn nx run-many --target=publish --parallel $(yarn get:cpus-number) --nx-bail",
"publish": "yarn run prepare:publish && yarn nx run-many --target=publish --exclude=tag:private --parallel $(yarn get:cpus-number) --nx-bail",
"publish:extensions": "yarn nx run-many --target=publish-extension --parallel $(yarn get:cpus-number)",
"publish:extensions:affected": "yarn nx affected --target=publish-extension --parallel $(yarn get:cpus-number)",
"lint": "yarn nx run-many --target=lint --parallel $(yarn get:cpus-number)",
Expand All @@ -41,7 +41,8 @@
"verdaccio:start-persistent": "docker run -d -it --rm --name verdaccio -p 4873:4873 -v \"$(yarn get:current-dir)/.verdaccio/conf\":/verdaccio/conf -v \"$(yarn get:current-dir)/.verdaccio/storage\":/verdaccio/storage:z verdaccio/verdaccio",
"verdaccio:clean": "rimraf -g \".verdaccio/storage/@{o3r,ama-sdk,ama-terasu}\"",
"verdaccio:login": "yarn cpy --cwd=./.verdaccio/conf .npmrc . --rename=.npmrc-logged && npx --yes npm-cli-login -u verdaccio -p verdaccio -e test@test.com -r http://127.0.0.1:4873 --config-path \".verdaccio/conf/.npmrc-logged\"",
"verdaccio:publish": "yarn verdaccio:clean && yarn set:version 999.0.0 --include \"!**/!(dist)/package.json\" --include !package.json && yarn verdaccio:login && yarn run publish --userconfig \".verdaccio/conf/.npmrc-logged\" --tag=latest --@o3r:registry=http://127.0.0.1:4873 --@ama-sdk:registry=http://127.0.0.1:4873 --@ama-terasu:registry=http://127.0.0.1:4873",
"verdaccio:prepare-publish": "yarn verdaccio:clean && yarn set:version 999.0.0 --include \"!**/!(dist)/package.json\" --include !package.json && yarn verdaccio:login && replace-in-files --regex=\"private(.*)true\" --replacement=private\\$1false '**/dist/package.json' && yarn run prepare:publish",
"verdaccio:publish": "yarn run verdaccio:prepare-publish && yarn nx run-many --target=publish --parallel $(yarn get:cpus-number) --nx-bail --userconfig \".verdaccio/conf/.npmrc-logged\" --tag=latest --@o3r:registry=http://127.0.0.1:4873 --@ama-sdk:registry=http://127.0.0.1:4873 --@ama-terasu:registry=http://127.0.0.1:4873",
"verdaccio:stop": "docker container stop $(docker ps -a -q --filter=\"name=verdaccio\")",
"verdaccio:all": "yarn verdaccio:stop && yarn verdaccio:start && yarn verdaccio:publish",
"watch:vscode-extension": "yarn nx run vscode-extension:compile:watch",
Expand Down Expand Up @@ -238,6 +239,7 @@
"postcss-scss": "~4.0.9",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"replace-in-files-cli": "^2.2.0",
"rimraf": "^5.0.1",
"sass": "~1.75.0",
"sass-loader": "^14.0.0",
Expand Down
30 changes: 30 additions & 0 deletions packages/@ama-sdk/core/cli/update-spec-from-npm.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node

/*
* Update the OpenAPI spec from an NPM package
*/

import { existsSync } from 'node:fs';
import { copyFile, readFile } from 'node:fs/promises';
import type { OpenApiToolsConfiguration, OpenApiToolsGenerator } from '../src/fwk/open-api-tools-configuration';

void (async () => {
const packageName = process.argv[2];
const packagePath = process.argv[3];

if (!packageName || !packagePath) {
console.error('Need to provide packageName and packagePath\nUsage: amasdk-update-spec-from-npm <package-name> <package-path>');
process.exit(-1);
}

const specSourcePath = require.resolve(`${packageName}/${packagePath}`);
const openApiConfigPath = './openapitools.json';
let specDestinationPath = './openapi.yml';
if (existsSync(openApiConfigPath)) {
const openApiConfig = JSON.parse(await readFile(openApiConfigPath, {encoding: 'utf8'})) as OpenApiToolsConfiguration;
const generators = Object.values(openApiConfig['generator-cli']?.generators ?? {});
specDestinationPath = generators.find((generatorConfig: OpenApiToolsGenerator) => generatorConfig.inputSpec)?.inputSpec || specDestinationPath;
}

await copyFile(specSourcePath, specDestinationPath);
})();
3 changes: 2 additions & 1 deletion packages/@ama-sdk/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"schematics": "./collection.json",
"bin": {
"amasdk-clear-index": "./dist/cli/clear-index.cjs",
"amasdk-files-pack": "./dist/cli/files-pack.cjs"
"amasdk-files-pack": "./dist/cli/files-pack.cjs",
"amasdk-update-spec-from-npm": "./dist/cli/update-spec-from-npm.cjs"
}
}
1 change: 1 addition & 0 deletions packages/@ama-sdk/core/src/fwk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from './errors';
export * from './ignore-enum.type';
export * from './logger';
export * from './mocks/index';
export * from './open-api-tools-configuration';
export * from './Reviver';
27 changes: 27 additions & 0 deletions packages/@ama-sdk/core/src/fwk/open-api-tools-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface OpenApiToolsGenerator {
/** Location of the OpenAPI spec, as URL or file */
inputSpec: string;
/** Output path for the generated SDK */
output: string;
/** Generator to use */
generatorName: string;
/** Path to configuration file. It can be JSON or YAML */
config?: string;
/** Sets specified global properties */
globalProperty?: string | Record<string, any>;
}

export interface OpenApiToolsGeneratorObject {
[generatorName: string]: OpenApiToolsGenerator;
}

export interface OpenApiToolsGeneratorCli {
version: string;
storageDir?: string;
generators: OpenApiToolsGeneratorObject;
}

export interface OpenApiToolsConfiguration {
// eslint-disable-next-line @typescript-eslint/naming-convention
'generator-cli': OpenApiToolsGeneratorCli;
}
7 changes: 6 additions & 1 deletion packages/@ama-sdk/core/tsconfig.cli.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
{
"extends": "../../../tsconfig.build",
"compilerOptions": {
"lib": [],
"incremental": true,
"composite": true,
"outDir": "./dist",
"module": "CommonJS",
"rootDir": ".",
"tsBuildInfoFile": "build/.tsbuildinfo.cli"
},
"references": [
{
"path": "./tsconfig.build.json"
}
],
"include": [
"cli/**/*.cts"
],
Expand Down
7 changes: 6 additions & 1 deletion packages/@ama-sdk/create/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,10 @@ npm create @ama-sdk typescript <project-name> -- --package-manager=yarn [...opti
- `--o3r-metrics`: Enable or disable the collection of anonymous data for Otter
- `--exact-o3r-version` : use a pinned version for [otter packages](https://github.com/AmadeusITGroup/otter/blob/main/docs/README.md).

- `--spec-package-name`: The npm package name where the spec file can be fetched
- `--spec-package-registry`: The npm registry where the spec file can be fetched
- `--spec-package-path`: The path inside the package where to find the spec file
- `--spec-package-version`: The version to target for the npm package where the spec file can be fetched

> [!NOTE]
> If the `--spec-path` is specified, the SDK will be generated based on this specification at the creation time.
> If `--spec-path` or `--spec-package-name` is specified, the SDK will be generated based on this specification at the creation time.
21 changes: 21 additions & 0 deletions packages/@ama-sdk/create/src/index.it.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ describe('Create new sdk command', () => {
).not.toThrow();
expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
expect(existsSync(path.join(sdkPackagePath, 'src', 'models', 'base', 'pet', 'pet.reviver.ts'))).toBeTruthy();
expect(() => packageManagerRun({script: 'spec:upgrade'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
});

test('should generate a full SDK when the specification is provided as npm dependency', () => {
expect(() =>
packageManagerCreate({
script: '@ama-sdk',
args: [
'typescript',
sdkPackageName,
'--package-manager', packageManager,
'--spec-package-name', '@ama-sdk/showcase-sdk',
'--spec-package-path', 'openapi.yml',
'--spec-package-version', o3rEnvironment.testEnvironment.o3rVersion,
'--spec-package-registry', o3rEnvironment.testEnvironment.packageManagerConfig.registry
]
}, execAppOptions)
).not.toThrow();
expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
expect(existsSync(path.join(sdkPackagePath, 'src', 'models', 'base', 'pet', 'pet.reviver.ts'))).toBeTruthy();
expect(() => packageManagerRun({script: 'spec:upgrade'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
});

test('should generate an SDK with no package scope', () => {
Expand Down
32 changes: 26 additions & 6 deletions packages/@ama-sdk/create/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ const getYarnVersion = () => {
}
};

const schematicArgs = [
if (argv['spec-path'] && argv['spec-package-name']) {
console.error('--spec-path cannot be set with --spec-package-name');
process.exit(-4);
}

const commonSchematicArgs = [
argv.debug !== undefined ? `--debug=${argv.debug as string}` : '--debug=false', // schematics enable debug mode per default when using schematics with relative path
...(name ? ['--name', name] : []),
'--package', pck,
Expand All @@ -74,28 +79,43 @@ const resolveTargetDirectory = resolve(process.cwd(), targetDirectory);

const run = () => {
const isSpecRelativePath = !!argv['spec-path'] && !parse(argv['spec-path']).root;
const shellSchematicArgs = [
...commonSchematicArgs,
...(argv['spec-package-name'] ? ['--spec-package-name', argv['spec-package-name']] : []),
...(argv['spec-package-registry'] ? ['--spec-package-registry', argv['spec-package-registry']] : []),
...(argv['spec-package-path'] ? ['--spec-package-path', argv['spec-package-path']] : []),
...(argv['spec-package-version'] ? ['--spec-package-version', argv['spec-package-version']] : [])
];
const coreSchematicArgs = [
...commonSchematicArgs,
'--spec-path', argv['spec-package-name'] ? './openapi.yml' : isSpecRelativePath ? relative(resolveTargetDirectory, resolve(process.cwd(), argv['spec-path'])) : argv['spec-path']
];

const runner = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
const steps: { args: string[]; cwd?: string; runner?: string }[] = [
{ args: [binPath, `${schematicsPackage}:typescript-shell`, ...schematicArgs, '--directory', targetDirectory] },
{ args: [binPath, `${schematicsPackage}:typescript-shell`, ...shellSchematicArgs, '--directory', targetDirectory] },
...(
packageManager === 'yarn'
? [{ runner, args: ['set', 'version', getYarnVersion()], cwd: resolveTargetDirectory }]
: []
),
...(argv['spec-path'] ? [{
...(argv['spec-package-name'] ? [{
runner,
args: ['exec', 'amasdk-update-spec-from-npm', argv['spec-package-name'], argv['spec-package-path']],
cwd: resolveTargetDirectory
}] : []),
...((argv['spec-path'] || argv['spec-package-name']) ? [{
args: [
binPath,
`${schematicsPackage}:typescript-core`,
...schematicArgs,
'--spec-path', isSpecRelativePath ? relative(resolveTargetDirectory, resolve(process.cwd(), argv['spec-path'])) : argv['spec-path']
...coreSchematicArgs
],
cwd: resolveTargetDirectory
}] : [])
];

const errors = steps
.map((step) => spawnSync(step.runner || process.execPath, step.args, { stdio: 'inherit', cwd: step.cwd || process.cwd(), shell: true }))
.map((step) => spawnSync(step.runner || `"${process.execPath}"`, step.args, { stdio: 'inherit', cwd: step.cwd || process.cwd(), shell: true }))
.filter(({error, status}) => (error || status !== 0));

if (errors.length > 0) {
Expand Down
3 changes: 2 additions & 1 deletion packages/@ama-sdk/schematics/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { OpenApiToolsConfiguration } from '@ama-sdk/core';
import { isJsonObject } from '@angular-devkit/core';
import { chain, externalSchematic, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import * as path from 'node:path';
Expand Down Expand Up @@ -79,7 +80,7 @@ const createOpenApiToolsConfig: Rule = (tree) => {
const openApiDefaultStorageDir = '.openapi-generator';
if (tree.exists(openApiConfigPath)) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const openapitoolsConfig = tree.readJson(openApiConfigPath) as { 'generator-cli'?: { storageDir?: string; version?: string } } || {};
const openapitoolsConfig = (tree.readJson(openApiConfigPath) || {}) as any as OpenApiToolsConfiguration;
openapitoolsConfig['generator-cli'] = {storageDir: openApiDefaultStorageDir, ...openapitoolsConfig['generator-cli'], version: openApiGeneratorVersion};
tree.overwrite(openApiConfigPath, JSON.stringify(openapitoolsConfig));
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { OpenApiToolsConfiguration } from '@ama-sdk/core';
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import * as path from 'node:path';
Expand Down Expand Up @@ -32,7 +33,7 @@ describe('Typescript Core Generator', () => {
const tree = await runner.runSchematic('typescript-core', {
specPath: path.join(__dirname, '..', '..', '..', 'testing', 'MOCK_swagger.yaml')
}, baseTree);
const content: any = tree.readJson('/openapitools.json');
const content = tree.readJson('/openapitools.json') as any as OpenApiToolsConfiguration;

expect(content['generator-cli'].generators['test-sdk-sdk'].inputSpec.endsWith('openapi.yaml')).toBe(true);
expect(tree.exists('/openapi.yaml')).toBe(true);
Expand All @@ -43,7 +44,7 @@ describe('Typescript Core Generator', () => {
const tree = await runner.runSchematic('typescript-core', {
specPath: path.join(__dirname, '..', '..', '..', 'testing', 'MOCK_swagger.json')
}, baseTree);
const content: any = tree.readJson('/openapitools.json');
const content = tree.readJson('/openapitools.json') as any as OpenApiToolsConfiguration;

expect(content['generator-cli'].generators['test-sdk-sdk'].inputSpec.endsWith('openapi.json')).toBe(true);
expect(tree.exists('/openapi.json')).toBe(true);
Expand Down
33 changes: 3 additions & 30 deletions packages/@ama-sdk/schematics/schematics/typescript/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { OpenApiToolsConfiguration, OpenApiToolsGenerator, Operation, PathObject } from '@ama-sdk/core';
import {
apply,
chain,
Expand All @@ -11,7 +12,6 @@ import {
Tree,
url
} from '@angular-devkit/schematics';
import type { Operation, PathObject } from '@ama-sdk/core';
import { existsSync, readFileSync } from 'node:fs';
import * as path from 'node:path';
import { URL } from 'node:url';
Expand All @@ -27,33 +27,6 @@ const JAVA_OPTIONS = ['specPath', 'specConfigPath', 'globalProperty', 'outputPat
const OPEN_API_TOOLS_OPTIONS = ['generatorName', 'output', 'inputSpec', 'config', 'globalProperty'];
const LOCAL_SPEC_FILENAME = 'openapi';

interface OpenApiToolsGenerator {
/** Location of the OpenAPI spec, as URL or file */
inputSpec: string;
/** Output path for the generated SDK */
output: string;
/** Generator to use */
generatorName: string;
/** Path to configuration file. It can be JSON or YAML */
config?: string;
/** Sets specified global properties */
globalProperty?: string | Record<string, any>;
}

interface OpenApiToolsGeneratorObject {
[generatorName: string]: OpenApiToolsGenerator;
}

interface OpenApiToolsGeneratorCli {
version: string;
generators: OpenApiToolsGeneratorObject;
}

interface OpenApiToolsConfiguration {
// eslint-disable-next-line @typescript-eslint/naming-convention
'generator-cli': OpenApiToolsGeneratorCli;
}

const getRegexpTemplate = (regexp: RegExp) => `new RegExp('${regexp.toString().replace(/\/(.*)\//, '$1').replace(/\\\//g, '/')}')`;

const getPathObjectTemplate = (pathObj: PathObject) => {
Expand Down Expand Up @@ -84,7 +57,7 @@ const getGeneratorOptions = (tree: Tree, context: SchematicContext, options: NgG
// read openapitools.json
if (tree.exists(openApiToolsPath)) {
try {
openApiToolsJson = (tree.readJson(openApiToolsPath) as any as OpenApiToolsConfiguration);
openApiToolsJson = tree.readJson(openApiToolsPath) as any as OpenApiToolsConfiguration;
} catch (e: any) {
context.logger.warn(`File ${openApiToolsPath} could not be parsed. Error message:\n${e}`);
}
Expand Down Expand Up @@ -257,7 +230,7 @@ function ngGenerateTypescriptSDKFn(options: NgGenerateTypescriptSDKCoreSchematic
const adaptDefaultFile: Rule = () => {
const openApiToolsPath = path.posix.join(targetPath, 'openapitools.json');
if (tree.exists(openApiToolsPath)) {
const openApiTools: any = tree.readJson(openApiToolsPath);
const openApiTools = tree.readJson(openApiToolsPath) as any as OpenApiToolsConfiguration;
Object.keys(openApiTools['generator-cli']?.generators)
.filter((key) => !openApiTools['generator-cli'].generators[key].inputSpec)
.forEach((key) => openApiTools['generator-cli'].generators[key].inputSpec = `./${defaultFileName}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { OpenApiToolsConfiguration } from '@ama-sdk/core';
import {Tree} from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import * as path from 'node:path';
import type { PackageJson } from 'type-fest';

const collectionPath = path.join(__dirname, '..', '..', '..', 'collection.json');

Expand Down Expand Up @@ -82,7 +84,7 @@ describe('Typescript Shell Generator', () => {
});

it('should generate correct package name', () => {
const {name} = JSON.parse(yarnTree.readContent('/package.json'));
const {name} = yarnTree.readJson('/package.json') as PackageJson;
expect(name).toEqual('@test-scope/test-sdk');
});

Expand All @@ -97,7 +99,7 @@ describe('Typescript Shell Generator', () => {
});

it('should generate correct openapitools.json', () => {
const openApiTools = JSON.parse(yarnTree.readContent('/openapitools.json'));
const openApiTools = yarnTree.readJson('/openapitools.json') as any as OpenApiToolsConfiguration;
// eslint-disable-next-line @typescript-eslint/naming-convention
expect(openApiTools['generator-cli'].generators).toEqual(expect.objectContaining({'test-scope-test-sdk': expect.anything()}));
});
Expand All @@ -109,7 +111,7 @@ describe('Typescript Shell Generator', () => {
skipInstall: true,
packageManager: 'npm'
}, Tree.empty());
const {name} = JSON.parse(tree.readContent('/package.json'));
const {name} = tree.readJson('/package.json') as PackageJson;
expect(name).toEqual('test-sdk');
});
});

0 comments on commit 9284ce1

Please sign in to comment.