Skip to content

Commit 8d312c3

Browse files
committed
feat(ama-openapi): support patterns as manifest models
1 parent 2f62400 commit 8d312c3

15 files changed

Lines changed: 504 additions & 19 deletions

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,14 @@
101101
"otter.extract.styling.prefix": "o3r",
102102
"search.useIgnoreFiles": true,
103103
"search.exclude": {
104-
"**/*.code-search": true,
105104
"**/github-actions/*/packaged-action/**": true,
106105
"**/.yarn": true,
107106
"**/.pnp.*": true,
108107
"**/.cache/**": true,
109108
"**/dist/**": true,
109+
"**/dist-*/**": true,
110+
"**/build/**": true,
111+
"**/coverage/**": true,
110112
"**/.tsbuildinfo*": true
111113
},
112114
"typescript.enablePromptUseWorkspaceTsdk": true,

docs/openapi/MANIFEST_CONFIGURATION.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,36 @@ Equivalent to:
5050

5151
<details>
5252

53+
<summary>Globs in model paths</summary>
54+
55+
```json5
56+
{
57+
"models": {
58+
"@my/specification-package": {
59+
"patterns": "models/*.yaml#/components/schemas/*"
60+
}
61+
}
62+
}
63+
```
64+
65+
Equivalent to:
66+
67+
```json5
68+
{
69+
"models": {
70+
"@my/specification-package": {
71+
"patterns": [
72+
"models/*.yaml#/components/schemas/*"
73+
]
74+
}
75+
}
76+
}
77+
```
78+
79+
</details>
80+
81+
<details>
82+
5383
<summary>Default package bundled specification</summary>
5484

5585
```json5

packages/@ama-openapi/core/schemas/manifest.schema.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,31 @@
6666
"path"
6767
],
6868
"additionalProperties": false
69+
},
70+
{
71+
"type": "object",
72+
"description": "Detailed model patterns inclusion",
73+
"properties": {
74+
"patterns": {
75+
"oneOf": [
76+
{
77+
"type": "array",
78+
"description": "Glob patterns to match models to include. The patterns are relative to the artifact root (e.g., 'models/**/*.yaml')",
79+
"items": {
80+
"type": "string"
81+
}
82+
},
83+
{
84+
"type": "string",
85+
"description": "Glob pattern to match models to include. The pattern is relative to the artifact root (e.g., 'models/**/*.yaml')"
86+
}
87+
]
88+
}
89+
},
90+
"required": [
91+
"patterns"
92+
],
93+
"additionalProperties": false
6994
}
7095
]
7196
}

packages/@ama-openapi/core/src/core/manifest/extract-dependency-models.mts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
promises as fs,
3+
readFileSync,
34
} from 'node:fs';
45
import {
56
createRequire,
@@ -11,6 +12,9 @@ import {
1112
join,
1213
resolve,
1314
} from 'node:path';
15+
import {
16+
globbySync,
17+
} from 'globby';
1418
import type {
1519
PackageJson,
1620
} from 'type-fest';
@@ -27,10 +31,15 @@ import {
2731
isJsonFile,
2832
parseFile,
2933
} from '../file-system/parse-file.mjs';
30-
import type {
31-
Manifest,
32-
Model,
33-
Transform,
34+
import {
35+
deserialize,
36+
} from '../serialization.mjs';
37+
import {
38+
isPatternsModel,
39+
type Manifest,
40+
type Model,
41+
type PatternsModel,
42+
type Transform,
3443
} from './manifest.mjs';
3544

3645
const INNER_PATH_SEPARATOR = '#[\\/]';
@@ -75,7 +84,7 @@ export const sanitizePackagePath = (artifactName: string) => {
7584
* Split model path into file path and inner path
7685
* @param modelPath
7786
*/
78-
const splitModelPath = (modelPath: string) => {
87+
export const splitModelPath = (modelPath: string) => {
7988
const [filePath, innerPath] = modelPath.split(new RegExp(INNER_PATH_SEPARATOR));
8089
return { filePath, innerPath };
8190
};
@@ -118,6 +127,19 @@ const getArtifactInfo = async (require: NodeJS.Require, artifactName: string) =>
118127
return { artifactBasePath, version };
119128
};
120129

130+
/**
131+
* Get information for artifactory manifest file
132+
* @param require
133+
* @param artifactName
134+
*/
135+
const getArtifactInfoSync = (require: NodeJS.Require, artifactName: string) => {
136+
const artifactPackageJson = require.resolve(`${artifactName}/package.json`);
137+
const artifactBasePath = dirname(artifactPackageJson);
138+
const packageJsonContent = readFileSync(artifactPackageJson, { encoding: 'utf8' });
139+
const version: string = (JSON.parse(packageJsonContent) as PackageJson).version || 'latest';
140+
return { artifactBasePath, version };
141+
};
142+
121143
/**
122144
* Extract dependency model given simple model definition (as string or boolean)
123145
* @param cwd
@@ -206,6 +228,63 @@ export const extractDependencyModelsObject = async (
206228
};
207229
};
208230

231+
/**
232+
* Extract dependency models from patterns model definition
233+
* @param artifactName
234+
* @param models
235+
* @param context
236+
* @param outputDirectory
237+
*/
238+
const extractDependencyPatternsModelsObject = (
239+
artifactName: string,
240+
models: PatternsModel,
241+
context: Context,
242+
outputDirectory = OUTPUT_DIRECTORY) => {
243+
const { cwd, logger } = context;
244+
const paths = Array.isArray(models.patterns) ? models.patterns : [models.patterns];
245+
const require = createRequire(resolve(cwd, 'package.json'));
246+
const { artifactBasePath } = getArtifactInfoSync(require, artifactName);
247+
248+
return paths.flatMap((modelPath) => {
249+
const { filePath, innerPath } = splitModelPath(modelPath);
250+
return globbySync(filePath, { cwd: artifactBasePath })
251+
.flatMap((matchedFilePath) => {
252+
if (!innerPath.includes('*')) {
253+
return [extractDependencyModelsSimple(cwd, artifactName, matchedFilePath + (innerPath ? `#/${innerPath}` : ''), logger, outputDirectory)];
254+
}
255+
256+
const content = readFileSync(matchedFilePath, { encoding: 'utf8' });
257+
const deserialized = deserialize(content, { isInputJson: isJsonFile(matchedFilePath) });
258+
const innerPathParts = innerPath.split('/');
259+
return innerPathParts
260+
.reduce((nodes, part) => {
261+
if (part.includes('*')) {
262+
const regex = new RegExp(`^${part.replaceAll('*', '.*')}$`);
263+
return nodes
264+
.flatMap(({ node, path }) => {
265+
const keys = Object.keys(node).filter((k) => regex.test(k));
266+
return keys
267+
.map((k) => ({
268+
node: node[k],
269+
path: `${path}/${k}`
270+
}))
271+
.filter(({ node: n }) => !!n);
272+
});
273+
} else {
274+
return nodes
275+
.map(({ node, path }) => ({
276+
node: node?.[part],
277+
path: `${path}/${part}`
278+
}))
279+
.filter(({ node }) => !!node);
280+
}
281+
}, [{ node: deserialized, path: '' }])
282+
.map(({ path }) => path)
283+
.map((path) => extractDependencyModelsSimple(cwd, artifactName, matchedFilePath + (path ? `#${path}` : ''), logger, outputDirectory));
284+
});
285+
});
286+
};
287+
209288
/**
210289
* Extract dependency models from the manifest
211290
* @param manifest
@@ -217,8 +296,14 @@ export const extractDependencyModels = (manifest: Manifest, context: Context) =>
217296
logger?.debug?.('Extracting information from the manifest configuration: ', manifest);
218297
return Object.entries(manifest.models).flatMap(([dependencyName, modelDefinition]) =>
219298
(Array.isArray(modelDefinition) ? modelDefinition : [modelDefinition])
220-
.map((model) => (typeof model === 'string' || typeof model === 'boolean')
221-
? extractDependencyModelsSimple(cwd, dependencyName, model, logger)
222-
: extractDependencyModelsObject(dependencyName, model, retrieveTransform(cwd, model.transform), context))
299+
.flatMap((model) => {
300+
if (isPatternsModel(model)) {
301+
return extractDependencyPatternsModelsObject(dependencyName, model, context);
302+
}
303+
const singleModel = (typeof model === 'string' || typeof model === 'boolean')
304+
? extractDependencyModelsSimple(cwd, dependencyName, model, logger)
305+
: extractDependencyModelsObject(dependencyName, model, retrieveTransform(cwd, model.transform), context);
306+
return [singleModel];
307+
})
223308
);
224309
};

0 commit comments

Comments
 (0)