This commit is contained in:
Nelson Daniel Troncoso 2018-11-27 16:13:53 -08:00
Родитель 7380c52153
Коммит 97991bb238
4 изменённых файлов: 257 добавлений и 9 удалений

2
.vscode/launch.json поставляемый
Просмотреть файл

@ -13,7 +13,7 @@
"dist/test",
"--timeout=2000000"
],
"cwd": "${workspaceRoot}/libraries/object-comparison",
"cwd": "${workspaceRoot}/libraries/deduplication",
"outFiles": [
"${workspaceFolder}/**/*.js"
]

Просмотреть файл

@ -2,4 +2,247 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// export * from "./some-file"
import { AnyObject, Data, DataHandle, DataSink, DataSource, Mapping, Node, Processor, ProxyObject, QuickDataSource, visit, } from '@microsoft.azure/datastore';
import { Dictionary, items, values } from '@microsoft.azure/linq';
import { areSimilar } from '@microsoft.azure/object-comparison';
import * as compareVersions from 'compare-versions';
type componentType = 'schemas' | 'responses' | 'parameters' | 'examples' | 'requestBodies' | 'headers' | 'securitySchemes' | 'links' | 'callbacks';
export class Deduplicator {
private hasRun = false;
// table:
// prevPointers -> newPointers
public mappings = new Dictionary<string>();
// table:
// oldRefs -> newRefs
private refs = new Dictionary<string>();
// sets containing the UIDs of already deduplicated components
private deduplicatedComponents = {
schemas: new Set<string>(),
responses: new Set<string>(),
parameters: new Set<string>(),
examples: new Set<string>(),
requestBodies: new Set<string>(),
headers: new Set<string>(),
securitySchemes: new Set<string>(),
links: new Set<string>(),
callbacks: new Set<string>()
};
// sets containing the UIDs of components already crawled
private crawledComponents = {
schemas: new Set<string>(),
responses: new Set<string>(),
parameters: new Set<string>(),
examples: new Set<string>(),
requestBodies: new Set<string>(),
headers: new Set<string>(),
securitySchemes: new Set<string>(),
links: new Set<string>(),
callbacks: new Set<string>()
};
// sets containing the UIDs of components already visited.
// This is used to prevent circular references.
private visitedComponents = {
schemas: new Set<string>(),
responses: new Set<string>(),
parameters: new Set<string>(),
examples: new Set<string>(),
requestBodies: new Set<string>(),
headers: new Set<string>(),
securitySchemes: new Set<string>(),
links: new Set<string>(),
callbacks: new Set<string>()
};
// initially the target is the same as the original object
private target: any;
constructor(originalFile: any) {
this.target = originalFile;
}
private init() {
this.setInitialRefsTable(this.target);
// deduplicate components
if (this.target.components) {
this.deduplicateComponents();
}
// TODO: deduplicate paths
}
private deduplicateComponents() {
for (const { key: type, children: componentsMember } of visit(this.target.components)) {
for (const { key: componentUid } of componentsMember) {
this.deduplicateComponent(componentUid, type);
}
}
}
private deduplicateComponent(componentUid: string, type: string) {
switch (type) {
case 'schemas':
case 'responses':
case 'parameters':
case 'examples':
case 'requestBodies':
case 'headers':
case 'securitySchemes':
case 'links':
case 'callbacks':
if (!this.deduplicatedComponents[type].has(componentUid)) {
if (!this.crawledComponents[type].has(componentUid)) {
this.crawlComponent(componentUid, type);
}
// deduplicate crawled component
const xMsMetadata = 'x-ms-metadata';
const component = this.target.components[type][componentUid];
// extract metadata to be merged
let apiVersions = component[xMsMetadata].apiVersions;
let filename = component[xMsMetadata].filename;
let originalLocations = component[xMsMetadata].originalLocations;
const name = component[xMsMetadata].name;
// extract component properties excluding metadata
const { 'x-ms-metadata': metadataCurrent, ...filteredReferencedComponent } = component;
// iterate over all the components of the same type of the component
for (const { key: anotherComponentUid, value: anotherComponent } of visit(this.target.components[type])) {
// ignore merge with itself
if (componentUid !== anotherComponentUid) {
// extract the another component's properties excluding metadata
const { xMsMetadata: metadataSchema, ...filteredSchema } = anotherComponent;
// TODO: Add more keys to ignore.
const keysToIgnore: Array<string> = ['description'];
// they should have the same name to be merged and they should be similar
if (areSimilar(filteredSchema, filteredReferencedComponent, ...keysToIgnore) && anotherComponent[xMsMetadata].name === component[xMsMetadata].name) {
// merge metadata
apiVersions = apiVersions.concat(anotherComponent[xMsMetadata].apiVersions);
filename = filename.concat(anotherComponent[xMsMetadata].filename);
originalLocations = originalLocations.concat(anotherComponent[xMsMetadata].originalLocations);
let uidComponentToDelete = anotherComponentUid;
// the discriminator to take contents is the api version
if (anotherComponent[xMsMetadata].apiVersions &&
!component[xMsMetadata].apiVersions &&
compareVersions(component[xMsMetadata].apiVersions, anotherComponent[xMsMetadata].apiVersions) === -1) {
// swap ids to prioritize the one with the latest version
uidComponentToDelete = componentUid;
componentUid = anotherComponentUid;
}
// finish up
delete this.target.components[type][uidComponentToDelete];
this.refs[`#/components/${type}/${uidComponentToDelete}`] = `#/components/${type}/${componentUid}`;
this.updateRefs(this.target);
this.updateMappings(`/components/${type}/${uidComponentToDelete}`, `/components/${type}/${componentUid}`);
this.deduplicatedComponents[type].add(uidComponentToDelete);
}
}
}
this.target.component[type][componentUid][xMsMetadata] = {
apiVersions: [...new Set([...apiVersions])],
filename: [...new Set([...filename])],
name,
originalLocations: [...new Set([...originalLocations])]
};
this.deduplicatedComponents[type].add(componentUid);
}
break;
default:
throw new Error(`Unknow component type: '${type}'`);
}
}
private crawlComponent(uid: string, type: componentType) {
if (this.visitedComponents[type]) {
if (!this.visitedComponents[type].has(uid)) {
if (this.target.components[type][uid]) {
this.visitedComponents[type].add(uid);
this.crawlObject(this.target.components[type][uid]);
} else {
throw new Error(`Trying to crawl undefined component with uid '${uid}' and type '${type}'!`);
}
}
this.crawledComponents[type].add(uid);
}
}
private crawlObject(obj: any) {
for (const { key, value } of visit(obj)) {
if (key === '$ref') {
const refParts = value.split('/');
const componentUid = refParts.pop();
const type = refParts.pop();
this.deduplicateComponent(componentUid, type);
} else if (Array.isArray(key) || typeof (value) === 'object') {
this.crawlObject(value);
}
}
}
private setInitialRefsTable(obj: any) {
for (const { value, key } of visit(obj)) {
if (key === '$ref') {
this.refs[value] = value;
} else if (typeof value === 'object') {
this.setInitialRefsTable(value);
}
}
}
private updateRefs(obj: any) {
for (const { value } of visit(obj)) {
if (typeof value === 'object') {
const ref = value.$ref;
if (ref) {
// see if this object has a $ref
const newRef = this.refs[ref];
if (newRef) {
value.$ref = newRef;
} else {
throw new Error(`$ref to original location '${ref}' is not found in the new refs collection`);
}
}
// now, recurse into this object
this.updateRefs(value);
}
}
}
private updateMappings(oldPointer: string, newPointer: string) {
this.mappings[oldPointer] = newPointer;
for (const [key, value] of Object.entries(this.mappings)) {
if (value === oldPointer) {
this.mappings[key] = newPointer;
}
}
}
public get output() {
if (!this.hasRun) {
this.init();
this.hasRun = true;
}
return this.target;
}
}

Просмотреть файл

@ -42,9 +42,15 @@
"devDependencies": {
"mocha-typescript": "1.1.17",
"@types/mocha": "5.2.5",
"@types/node": "10.11.3",
"@types/node": "^10.11.4",
"mocha": "5.2.0",
"typescript": "^3.1.3"
},
"dependencies": {}
"dependencies": {
"@microsoft.azure/datastore": "^2.1.31",
"@microsoft.azure/linq": "^2.1.14",
"@microsoft.azure/object-comparison": "^2.1.0",
"@microsoft.azure/openapi": "^2.1.16",
"compare-versions": "^3.4.0"
}
}

Просмотреть файл

@ -1,11 +1,10 @@
import { suite, test, slow, timeout, skip, only } from "mocha-typescript";
import * as assert from "assert";
import { Deduplicator } from "../main";
@suite class MyTests {
@suite class DeduplicatorTest {
/* @test async "Does test work"() {
assert.equal(await asyncio.exists(__filename), true);
}
*/
@test async ""() {
}
}