Skip to content
Draft
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
14 changes: 14 additions & 0 deletions docs/openapi/MANIFEST_CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,17 @@ Equivalent to:

> [!NOTE]
> A complete documentation relative to the masking feature is available in the [Transform section](./TRANSFORM.md).

## Generate Manifest Schema

To help to configure the manifest file, a JSON schema dedicated to the current repository can be generated by the [ama-openapi CLI](https://www.npmjs.com/package/@ama-openapi/cli) via the following command:

```shell
npm exec @ama-openapi/cli generate-schema -- [options]
```

> [!TIP]
> The available options are listed in the [@ama-openapi/cli documentation](../../packages/@ama-openapi/cli/README.md).

> [!NOTE]
> The repository generated by the [Ama OpenAPI initializer](https://www.npmjs.com/package/@ama-openapi/create) is configured to automatically regenerate the schema each time you run `npm install`.
7 changes: 3 additions & 4 deletions eslint.local.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ export default defineConfig([
'@o3r/json-dependency-versions-harmonize': [
'error',
{
ignoredPackages: [
'@o3r/build-helpers',
'@o3r/workspace-helpers'
ignoredDependencies: [
'npm',
'globby'
],
ignoredDependencies: ['npm'],
alignPeerDependencies: false,
alignEngines: true
}
Expand Down
9 changes: 4 additions & 5 deletions eslint.shared.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,11 @@ export default defineConfig([
'@o3r/json-dependency-versions-harmonize': [
'error',
{
ignoredPackages: [
'@o3r/build-helpers',
'@o3r/workspace-helpers'
],
alignPeerDependencies: false,
alignEngines: true
alignEngines: true,
ignoredDependencies: [
'globby'
]
}
]
}
Expand Down
21 changes: 18 additions & 3 deletions packages/@ama-openapi/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ The following global options can be used with any command:
| `--log-level`, `-l` | `"silent" \| "error" \| "warning" \| "info" \| "debug"` | `error` | Define the specific level of logs to display in the console |
| `--help`, `-h` | | | Show help information |

## Available Commands
## Available commands

### Command `install`
### `install` command

This command scans for [manifest files](https://github.com/AmadeusITGroup/otter/tree/main/docs/openapi/MANIFEST_CONFIGURATION.md) in the current working directory and installs all [OpenAPI specifications](https://www.openapis.org/) specified in those files. The installation process will download and process the OpenAPI specifications according to the configuration in your manifest.

Expand All @@ -62,7 +62,7 @@ ama-openapi install --debug
ama-openapi install --silent
```

### Command `watch`
### `watch` command

This command starts a file watcher that monitors manifest files for changes. When a manifest file is modified, it automatically triggers the installation process, ensuring your [OpenAPI specifications](https://www.openapis.org/) are always up to date during development.

Expand All @@ -80,6 +80,21 @@ ama-openapi watch
ama-openapi watch --debug
```

### `generate-schema` command

Generate the [JSON Schemas](https://json-schema.org/) that can be used for configuration auto completion.

```shell
npm exec @ama-openapi/cli generate-schema -- [options]
```

#### Options

| Option | Alias | Description | Default value |
| --- | --- | --- | --- |
| `--output` | `-o` | Output directory where to generate the schemas | `'./schemas'` |
| `--keywords` | `-k` | List of the keywords to include in the artifact to be considered in the schema generation | `['openapi']` |

## Expose shareable models

To expose models from the OpenAPI package, the following setups are required:
Expand Down
24 changes: 22 additions & 2 deletions packages/@ama-openapi/cli/src/main.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {
DEFAULT_MANIFEST_FILENAMES,
generateValidationSchemaFiles,
getLogger,
installDependencies,
type LogLevel,
Expand All @@ -25,15 +26,34 @@ void yargs(hideBin(process.argv))
description: 'Determine the level of logs to display',
default: 'error'
})

.command('generate-schema', 'Generate the schema that can be used for configuration auto completion', (yargsInstance) => {
return yargsInstance
.option('output', {
type: 'string',
alias: 'o',
description: 'Output directory where generating the schemas'
});
}, async (args) => {
const logger = args.logLevel === 'silent' ? undefined : getLogger(args.logLevel as LogLevel, console);
try {
await generateValidationSchemaFiles(process.cwd(), { logger, outputDirectory: args.output });
} catch (e) {
(logger?.error ?? console.error)(e);
process.exit(1);
}
})

.command('install', 'Install the OpenAPI specifications from manifest files', () => {}, async (args) => {
const logger = args.logLevel === 'silent' ? undefined : getLogger(args.logLevel as LogLevel, console);
try {
const logger = args.logLevel === 'silent' ? undefined : getLogger(args.logLevel as LogLevel, console);
await installDependencies(process.cwd(), { logger });
} catch (e) {
console.error(e);
(logger?.error ?? console.error)(e);
process.exit(1);
}
})

.command('watch', 'Watch and install the OpenAPI specifications from manifest files on changes', () => {}, async (args) => {
const { watch } = await import('chokidar');
const logger = args.logLevel === 'silent' ? undefined : getLogger(args.logLevel as LogLevel, console);
Expand Down
2 changes: 2 additions & 0 deletions packages/@ama-openapi/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"dependencies": {
"ajv": "~8.17.1",
"globby": "~16.0.0",
"js-yaml": "^4.1.1",
"semver": "^7.5.2",
"tslib": "^2.6.2"
Expand Down Expand Up @@ -93,6 +94,7 @@
"jest-resolve": "~30.2.0",
"jest-util": "~30.2.0",
"jsonc-eslint-parser": "~2.4.0",
"memfs": "~4.50.0",
"nx": "~21.6.0",
"ts-jest": "~29.4.0",
"type-fest": "^4.30.1",
Expand Down
9 changes: 9 additions & 0 deletions packages/@ama-openapi/core/src/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ export const DEFAULT_MANIFEST_FILENAMES = [
'package.json'
] as const;

/** Default directory where to generate the JSON Schemas */
export const DEFAULT_SCHEMA_OUTPUT_DIRECTORY = 'schemas';

/** NPM keywords to identify a specification package */
export const OPENAPI_NPM_KEYWORDS = ['openapi'] as const;

/** Name of the JSON schema validating the manifest file */
export const MANIFEST_SCHEMA_FILE = 'manifest.schema.json';

// Internal property keys :

/** Key to mark masked properties */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
* @param retrievedModel
* @param context
*/
export const writeModelFile = async (retrievedModel: RetrievedDependencyModel, context: Context) => {
export const writeModelFile = async (retrievedModel: Pick<RetrievedDependencyModel, 'outputFilePath' | 'content'>, context: Context) => {
const { outputFilePath, content } = retrievedModel;
const { logger } = context;
const directory = dirname(outputFilePath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export interface RetrievedDependencyModel {
* Sanitize the package path to be used in file system
* @param artifactName
*/
const sanitizePackagePath = (artifactName: string) => {
export const sanitizePackagePath = (artifactName: string) => {
return artifactName
.replace('/', '-')
.replace(/^@/, '');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
} from 'node:fs';
import {
extractDependencyModelsObject,
sanitizePackagePath,
} from './extract-dependency-models.mjs';
import type {
Transform,
Expand Down Expand Up @@ -145,3 +146,17 @@ describe('extract-dependency-models', () => {
});
});
});

describe('sanitizePackagePath', () => {
it('should replace / with -', () => {
expect(sanitizePackagePath('a/b')).toBe('a-b');
});

it('should replace ^/@ with empty string', () => {
expect(sanitizePackagePath('@a/b')).toBe('a-b');
});

it('should not throw an error if the input is empty', () => {
expect(sanitizePackagePath('')).toBe('');
});
});
4 changes: 2 additions & 2 deletions packages/@ama-openapi/core/src/core/serialization.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
* @param specification
* @param model
*/
export const deserialize = (specification: string, model: RetrievedDependencyModel): any => {
export const deserialize = (specification: string, model: Pick<RetrievedDependencyModel, 'isInputJson'>): any => {
if (model.isInputJson) {
return JSON.parse(specification);
}
Expand All @@ -23,7 +23,7 @@ export const deserialize = (specification: string, model: RetrievedDependencyMod
* @param specification
* @param model
*/
export const serialize = (specification: any, model: RetrievedDependencyModel): string => {
export const serialize = (specification: any, model: Pick<RetrievedDependencyModel, 'isOutputJson'>): string => {
if (model.isOutputJson) {
return JSON.stringify(specification, null, 2);
}
Expand Down
55 changes: 55 additions & 0 deletions packages/@ama-openapi/core/src/generate-schemas.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
existsSync,
promises as fs,
} from 'node:fs';
import {
dirname,
resolve,
} from 'node:path';
import * as process from 'node:process';
import {
DEFAULT_SCHEMA_OUTPUT_DIRECTORY,
MANIFEST_SCHEMA_FILE,
} from './constants.mjs';
import type {
Context,
} from './context.mjs';
import type {
Logger,
} from './logger.mjs';
import {
generateOpenApiManifestSchema,
type GenerateOpenApiManifestSchemaOptions,
} from './schema/generate-schema.mjs';

/** Options for Generate Validation Schemas */
export interface GenerateValidationSchemasOptions extends GenerateOpenApiManifestSchemaOptions {
/** Logger */
logger?: Logger;
/** Output directory */
outputDirectory?: string;
}

/**
* Generate the JSON Schema providing help on the OpenAPI Manifest
* @param cwd
* @param options
*/
export const generateValidationSchemaFiles = async (cwd = process.cwd(), options?: GenerateValidationSchemasOptions) => {
const outputDirectory = options?.outputDirectory ?? DEFAULT_SCHEMA_OUTPUT_DIRECTORY;
const opts = {
...options,
cwd
} as const satisfies Context;
const directory = resolve(cwd, outputDirectory);
const manifestSchemaObj = await generateOpenApiManifestSchema(opts);

if (!existsSync(directory)) {
await fs.mkdir(directory, { recursive: true });
}
await fs.writeFile(resolve(outputDirectory, MANIFEST_SCHEMA_FILE), JSON.stringify(manifestSchemaObj.manifest), { encoding: 'utf8' });
await Promise.all(
manifestSchemaObj.masks
.map(({ mask, fileName }) => fs.writeFile(resolve(outputDirectory, dirname(MANIFEST_SCHEMA_FILE), fileName), JSON.stringify(mask), { encoding: 'utf8' }))
);
};
3 changes: 2 additions & 1 deletion packages/@ama-openapi/core/src/public_api.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './logger.mjs';
export * from './constants.mjs';
export * from './main.mjs';
export * from './generate-schemas.mjs';
export * from './install-dependencies.mjs';
26 changes: 26 additions & 0 deletions packages/@ama-openapi/core/src/schema/generate-model-name.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
sanitizePackagePath,
} from '../core/manifest/extract-dependency-models.mjs';

/**
* Generate a reference name from a model definition
* @param artifactName
* @param modelPath
*/
export const generateModelNameRef = (artifactName: string, modelPath: string): string => {
const sanitizedArtifactName = sanitizePackagePath(artifactName);
const [filePath, innerPath] = modelPath.split('#/');
const modelName = (filePath.replace(/\.(json|ya?ml)$/, '') + (innerPath ?? ''))
.replace(/^\.+\//, '')
.replace(/-/g, '_')
.replace(/\//g, '-');
return `${sanitizedArtifactName}-${modelName}`;
};

/**
* Generate a mask schema file name from a model definition
* @param modelNameRef
*/
export const getMaskFileName = (modelNameRef: string) => {
return `mask-${modelNameRef}.json`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
sanitizePackagePath,
} from '../core/manifest/extract-dependency-models.mjs';
import {
generateModelNameRef,
getMaskFileName,
} from './generate-model-name.mjs';

jest.mock('../core/manifest/extract-dependency-models.mjs', () => ({
sanitizePackagePath: jest.fn()
}));

describe('generateModelNameRef', () => {
const sanitizePackagePathMock =
sanitizePackagePath as jest.MockedFunction<typeof sanitizePackagePath>;

beforeEach(() => {
jest.resetAllMocks();
});

it('should build model reference using sanitized artifact name and model file path without inner path', () => {
sanitizePackagePathMock.mockReturnValue('sanitized-pkg');

const artifactName = '@scope/my-pkg';
const modelPath = '/schemas/user.json';

const result = generateModelNameRef(artifactName, modelPath);

expect(result).toBe('sanitized-pkg--schemas-user');
});

it('should handle innerPath references, replacing separators as expected', () => {
sanitizePackagePathMock.mockReturnValue('pkg');

const artifactName = 'my-package';
const modelPath = 'schemas/user.json#/components/schemas/User';

const result = generateModelNameRef(artifactName, modelPath);

expect(result).toBe('pkg-schemas-usercomponents-schemas-User');
});

it('should remove leading relative segments and normalize dashes/paths', () => {
sanitizePackagePathMock.mockReturnValue('pkg_sanitized');

const artifactName = 'pkg-with-dash';
const modelPath = './nested-dir/my-model-name.yaml#/inner-path/value';

const result = generateModelNameRef(artifactName, modelPath);

expect(result).toBe('pkg_sanitized-nested_dir-my_model_nameinner_path-value');
});
});

describe('getMaskFileName', () => {
it('should prefix modelNameRef with mask- and suffix with .json', () => {
const modelNameRef = 'pkg-schemas-user';
const result = getMaskFileName(modelNameRef);

expect(result).toBe('mask-pkg-schemas-user.json');
});
});
Loading
Loading