Skip to content

Commit

Permalink
fix: copy specs local file refs
Browse files Browse the repository at this point in the history
  • Loading branch information
fpaul-1A committed Aug 9, 2024
1 parent 483d28a commit 33707b9
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { cleanVirtualFileSystem, useVirtualFileSystem } from '@o3r/test-helpers';
import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';

describe('Copy Referenced Files', () => {
const virtualFileSystem = useVirtualFileSystem();
const copyReferencedFiles = require('./copy-referenced-files').copyReferencedFiles;

const migrationScriptMocksPath = join(__dirname, '../../../../testing/mocks');
const specFilePath = '../models/split-spec/split-spec.yaml';
const outputDirectory = './local-references';

const copyMockFile = async (virtualPath: string, realPath: string) => {
if (!virtualFileSystem.existsSync(dirname(virtualPath))) {
await virtualFileSystem.promises.mkdir(dirname(virtualPath), {recursive: true});
}
await virtualFileSystem.promises.writeFile(virtualPath, await readFile(join(migrationScriptMocksPath, realPath), {encoding: 'utf8'}));
};

beforeAll(async () => {
await virtualFileSystem.promises.mkdir(dirname(specFilePath), {recursive: true});
await copyMockFile(specFilePath, 'split-spec/split-spec.yaml');
await copyMockFile('../models/split-spec/spec-chunk1.yaml', 'split-spec/spec-chunk1.yaml');
await copyMockFile('../models/spec-chunk2.yaml', 'spec-chunk2.yaml');
await copyMockFile('../models/spec-chunk3/spec-chunk3.yaml', 'spec-chunk3/spec-chunk3.yaml');
await copyMockFile('../models/spec-chunk4/spec-chunk4.yaml', 'spec-chunk4/spec-chunk4.yaml');
});

afterAll(() => {
cleanVirtualFileSystem();
});

it('should copy the local files referenced in the spec', async () => {
const baseRelativePath = await copyReferencedFiles(specFilePath, outputDirectory);
expect(baseRelativePath).toMatch(/^local-references[\\/]split-spec$/);
expect(virtualFileSystem.existsSync(join(outputDirectory, 'split-spec/split-spec.yaml'))).toBe(true);
expect(virtualFileSystem.existsSync(join(outputDirectory, 'split-spec/spec-chunk1.yaml'))).toBe(true);
expect(virtualFileSystem.existsSync(join(outputDirectory, 'spec-chunk2.yaml'))).toBe(true);
expect(virtualFileSystem.existsSync(join(outputDirectory, 'spec-chunk3/spec-chunk3.yaml'))).toBe(true);
expect(virtualFileSystem.existsSync(join(outputDirectory, 'spec-chunk4/spec-chunk4.yaml'))).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { existsSync } from 'node:fs';
import { copyFile, mkdir, readFile, rm } from 'node:fs/promises';
import { dirname, join, normalize, posix, relative, resolve } from 'node:path';

const refMatcher = /\B['"]?[$]ref['"]?\s*:\s*([^#\n]+)/g;

/**
* Extract the list of local references from a single spec file content
* @param specContent
* @param basePath
*/
function extractRefPaths(specContent: string, basePath: string): string[] {
const refs = specContent.match(refMatcher);
return refs ?
refs
.map((capture) => capture.replace(refMatcher, '$1').replace(/['"]/g, ''))
.filter((refPath) => refPath.startsWith('.'))
.map((refPath) => join(basePath, refPath))
: [];
}

/**
* Recursively extract the list of local references starting from the input spec file
* @param specFilePath
* @param referenceFilePath
* @param visited
*/
async function extractRefPathRecursive(specFilePath: string, referenceFilePath: string, visited: Set<string>): Promise<string[]> {
const resolvedFilePath = resolve(specFilePath);
if (!visited.has(resolvedFilePath)) {
visited.add(resolvedFilePath);

const specContent = await readFile(specFilePath, {encoding: 'utf8'});
const refPaths = extractRefPaths(specContent, relative(dirname(referenceFilePath), dirname(specFilePath)));
const recursiveRefPaths = await Promise.all(
refPaths.map((refPath) => extractRefPathRecursive(join(dirname(referenceFilePath), refPath), referenceFilePath, visited))
);
return [
...refPaths,
...recursiveRefPaths.flat()
];
}
return [];
}

/**
* Replace all the local relative references using the new base relative path
* @param specContent
* @param newBaseRelativePath
*/
export function updateLocalRelativeRefs(specContent: string, newBaseRelativePath: string) {
const formatPath = (inputPath:string) => (inputPath.startsWith('.') ? inputPath : `./${inputPath}`).replace(/\\+/g, '/');
return specContent.replace(refMatcher, (match, ref: string) => {
const refPath = ref.replace(/['"]/g, '');
return refPath.startsWith('.') ?
match.replace(refPath, formatPath(normalize(posix.join(newBaseRelativePath, refPath))))
: match;
});
}

/**
* Copy the local files referenced in the input spec file to the output directory
* @param specFilePath
* @param outputDirectory
*/
export async function copyReferencedFiles(specFilePath: string, outputDirectory: string) {
const dedupe = (paths: string[]) =>
paths.filter((refPath, index) => {
const actualPath = join(dirname(specFilePath), refPath);
return paths.findIndex((otherRefPath) => join(dirname(specFilePath), otherRefPath) === actualPath) === index;
});
const refPaths = dedupe(await extractRefPathRecursive(specFilePath, specFilePath, new Set()));
if (refPaths.length) {
if (existsSync(outputDirectory)) {
await rm(outputDirectory, { recursive: true });
}

// Calculate the lowest level base path to keep the same directory structure
const maxDepth = Math.max(...refPaths.map((refPath) => refPath.split('..').length));
const basePath = join(specFilePath, '../'.repeat(maxDepth));
const baseRelativePath = relative(basePath, dirname(specFilePath));

// Copy the files
await Promise.all(refPaths.map(async (refPath) => {
const sourcePath = join(dirname(specFilePath), refPath);
const destPath = join(outputDirectory, baseRelativePath, refPath);
if (!existsSync(dirname(destPath))) {
await mkdir(dirname(destPath), { recursive: true });
}
await copyFile(sourcePath, destPath);
}));

return join(outputDirectory, baseRelativePath);
}
return '';
}
10 changes: 10 additions & 0 deletions packages/@ama-sdk/schematics/schematics/typescript/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { OpenApiCliOptions } from '../../code-generator/open-api-cli-generator/o
import { treeGlob } from '../../helpers/tree-glob';
import { NgGenerateTypescriptSDKCoreSchematicsSchema } from './schema';
import { OpenApiCliGenerator } from '../../code-generator/open-api-cli-generator/open-api-cli.generator';
import { copyReferencedFiles, updateLocalRelativeRefs } from './helpers/copy-referenced-files';
import { generateOperationFinderFromSingleFile } from './helpers/path-extractor';

const JAVA_OPTIONS = ['specPath', 'specConfigPath', 'globalProperty', 'outputPath'];
Expand Down Expand Up @@ -153,10 +154,19 @@ function ngGenerateTypescriptSDKFn(options: NgGenerateTypescriptSDKCoreSchematic
let specContent!: string;
if (URL.canParse(generatorOptions.specPath) && (new URL(generatorOptions.specPath)).protocol.startsWith('http')) {
specContent = await (await fetch(generatorOptions.specPath)).text();
specContent = updateLocalRelativeRefs(specContent, path.dirname(generatorOptions.specPath));
} else {
const specPath = path.isAbsolute(generatorOptions.specPath) || !options.directory ?
generatorOptions.specPath : path.join(options.directory, generatorOptions.specPath);
specContent = readFileSync(specPath, {encoding: 'utf-8'}).toString();

if (path.relative(process.cwd(), specPath).startsWith('..')) {
// TODO would be better to create files on tree instead of FS
const newRelativePath = await copyReferencedFiles(specPath, './spec-local-references');
if (newRelativePath) {
specContent = updateLocalRelativeRefs(specContent, newRelativePath);
}
}
}

try {
Expand Down
9 changes: 9 additions & 0 deletions packages/@ama-sdk/schematics/testing/mocks/spec-chunk2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: Pet
type: object
properties:
id:
type: integer
format: int64
example: 10
category:
$ref: './split-spec/split-spec.yaml#/components/schemas/Category'
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: Pet
type: object
properties:
id:
type: integer
format: int64
example: 10
category:
$ref: '../spec-chunk4/spec-chunk4.yaml#/components/schemas/Category'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
components:
schemas:
Category:
type: object
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: "test"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: Pet
type: object
properties:
id:
type: integer
format: int64
example: 10
category:
$ref: './split-spec.yaml#/components/schemas/Category'
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
openapi: 3.0.2
info:
description: test
title: test
version: 0.0.0
paths:
/test:
get:
responses:
'200':
description: test
content:
application/json:
schema:
$ref: './spec-chunk1.yaml'
/test2:
get:
responses:
'200':
description: test
content:
application/json:
schema:
$ref: '../spec-chunk2.yaml'
/test3:
get:
responses:
'200':
description: test
content:
application/json:
schema:
$ref: '../spec-chunk3/spec-chunk3.yaml'
components:
schemas:
Category:
type: object
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: "test"

0 comments on commit 33707b9

Please sign in to comment.