Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

openapi-diff

Package Overview
Dependencies
Maintainers
2
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

openapi-diff - npm Package Compare versions

Comparing version 0.4.0 to 0.5.0

dist/openapi-diff/severity-finder.js

10

CHANGELOG.md

@@ -0,1 +1,11 @@

<a name="0.5.0"></a>
# [0.5.0](https://bitbucket.org/atlassian/openapi-diff/compare/0.4.0...0.5.0) (2017-08-17)
### Features
* support to add/edit/delete Swagger 2's schemes property ([b0a4634](https://bitbucket.org/atlassian/openapi-diff/commits/b0a4634))
<a name="0.4.0"></a>

@@ -2,0 +12,0 @@ # [0.4.0](https://bitbucket.org/atlassian/openapi-diff/compare/0.3.0...v0.4.0) (2017-08-01)

48

dist/openapi-diff/result-reporter.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
const VError = require("verror");
const buildChangeSentence = (change) => {
let changeSentence;
switch (change.type) {
case 'add': {
changeSentence = `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was added with value \'${change.rhs}\'`;
break;
}
case 'delete': {
changeSentence = `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `with value \'${change.lhs}\' was removed`;
break;
}
case 'edit': {
changeSentence = `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was modified from \'${change.lhs}\' to \'${change.rhs}\'`;
break;
}
default: {
throw new VError(`ERROR: unable to handle ${change.type} as a change type`);
}
}
return changeSentence;
const buildChangeSentence = (targetChange) => {
const changeDescription = {
add: ((change) => {
return `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was added with value \'${change.newValue}\'`;
}),
'arrayContent.add': ((change) => {
return `${_.capitalize(change.severity)}: the value \'${change.newValue}\' was added to the`
+ ` array in the path [${change.printablePath.join('/')}]`;
}),
'arrayContent.delete': ((change) => {
return `${_.capitalize(change.severity)}: the value \'${change.oldValue}\' was removed from the`
+ ` array in the path [${change.printablePath.join('/')}]`;
}),
delete: ((change) => {
return `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `with value \'${change.oldValue}\' was removed`;
}),
edit: ((change) => {
return `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was modified from \'${change.oldValue}\' to \'${change.newValue}\'`;
})
};
return changeDescription[targetChange.type](targetChange);
};

@@ -29,0 +29,0 @@ const countSeverities = (changes, changeSeverity) => {

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const deepDiff = require("deep-diff");
const _ = require("lodash");
const utils_1 = require("./utils");
const processDiff = (parsedSpec, rawDiff) => {
const processedDiff = [];
if (rawDiff) {
for (const entry of rawDiff) {
const type = getChangeType(entry.kind);
const scope = getChangeScope(entry);
const taxonomy = findChangeTaxonomy(type, scope);
const processedEntry = {
index: getChangeNullableProperties(entry.index),
item: getChangeNullableProperties(entry.item),
kind: entry.kind,
lhs: entry.lhs,
path: entry.path,
printablePath: utils_1.default.findOriginalPath(parsedSpec, entry.path),
rhs: entry.rhs,
scope,
severity: findChangeSeverity(taxonomy),
taxonomy,
type
};
processedDiff.push(processedEntry);
}
const VError = require("verror");
const severity_finder_1 = require("./severity-finder");
const findPrintablePathForDiff = (options) => {
if (_.isUndefined(options.oldObject) && _.isUndefined(options.newObject)) {
throw new VError(`ERROR: impossible to find the path for ${options.propertyName} - ${options.type}`);
}
return processedDiff;
return options.oldObject ?
options.oldObject.originalPath :
options.newObject.originalPath;
};
const isEdit = (entry) => {
return entry.kind === 'E';
const findScopeForDiff = (propertyName) => {
return propertyName.includes('xProperties') ? 'unclassified' : propertyName;
};
const isInfoChange = (entry) => {
return isEdit(entry) && isInfoObject(entry) && !utils_1.default.isXProperty(entry.path[1]);
const createDiffEntry = (options) => {
const printablePath = findPrintablePathForDiff(options);
const scope = findScopeForDiff(options.propertyName);
const taxonomy = `${scope}.${options.type}`;
const severity = severity_finder_1.default.lookup(taxonomy);
return {
newValue: options.newObject ? options.newObject.value : undefined,
oldValue: options.oldObject ? options.oldObject.value : undefined,
printablePath,
scope,
severity,
taxonomy,
type: options.type
};
};
const isInfoObject = (entry) => {
return entry.path[0] === 'info';
const isDefined = (target) => {
return !_.isUndefined(target);
};
const isTopLevelProperty = (entry) => {
const topLevelPropertyNames = [
'basePath',
'host',
'openapi'
];
return _.includes(topLevelPropertyNames, entry.path[0]);
const isDefinedDeep = (objectWithValue) => {
return isDefined(objectWithValue) && isDefined(objectWithValue.value);
};
const findChangeTaxonomy = (type, scope) => {
return (scope === 'unclassified.change') ? scope : `${scope}.${type}`;
const isUndefinedDeep = (objectWithValue) => {
return _.isUndefined(objectWithValue) || _.isUndefined(objectWithValue.value);
};
const findChangeSeverity = (taxonomy) => {
const isBreakingChange = _.includes(BreakingChanges, taxonomy);
const isNonBreakingChange = _.includes(nonBreakingChanges, taxonomy);
if (isBreakingChange) {
return 'breaking';
const findAdditionDiffsInProperty = (oldObject, newObject, propertyName) => {
const isAddition = isUndefinedDeep(oldObject) && isDefinedDeep(newObject);
if (isAddition) {
return [createDiffEntry({ newObject, oldObject, propertyName, type: 'add' })];
}
else if (isNonBreakingChange) {
return 'non-breaking';
return [];
};
const findDeletionDiffsInProperty = (oldObject, newObject, propertyName) => {
const isDeletion = isDefinedDeep(oldObject) && isUndefinedDeep(newObject);
if (isDeletion) {
return [createDiffEntry({ newObject, oldObject, propertyName, type: 'delete' })];
}
else {
return 'unclassified';
return [];
};
const findEditionDiffsInProperty = (oldObject, newObject, propertyName) => {
const isEdition = isDefinedDeep(oldObject) && isDefinedDeep(newObject) && (oldObject.value !== newObject.value);
if (isEdition) {
return [createDiffEntry({ newObject, oldObject, propertyName, type: 'edit' })];
}
return [];
};
const getChangeNullableProperties = (changeProperty) => {
return changeProperty || null;
const findDiffsInProperty = (oldObject, newObject, propertyName) => {
const additionDiffs = findAdditionDiffsInProperty(oldObject, newObject, propertyName);
const deletionDiffs = findDeletionDiffsInProperty(oldObject, newObject, propertyName);
const editionDiffs = findEditionDiffsInProperty(oldObject, newObject, propertyName);
return _.concat([], additionDiffs, deletionDiffs, editionDiffs);
};
const getChangeScope = (change) => {
if (isInfoChange(change)) {
return 'info.object';
const isValueInArray = (object, array) => {
return _.some(array, { value: object.value });
};
const findAdditionDiffsInArray = (oldArrayContent, newArrayContent, arrayName) => {
const arrayContentAdditionDiffs = _(newArrayContent)
.filter((entry) => {
return !isValueInArray(entry, oldArrayContent);
})
.map((addedEntry) => {
return createDiffEntry({
newObject: addedEntry,
oldObject: undefined,
propertyName: arrayName,
type: 'arrayContent.add'
});
})
.flatten()
.value();
return arrayContentAdditionDiffs;
};
const findDeletionDiffsInArray = (oldArrayContent, newArrayContent, arrayName) => {
const arrayContentDeletionDiffs = _(oldArrayContent)
.filter((entry) => {
return !isValueInArray(entry, newArrayContent);
})
.map((deletedEntry) => {
return createDiffEntry({
newObject: undefined,
oldObject: deletedEntry,
propertyName: arrayName,
type: 'arrayContent.delete'
});
})
.flatten()
.value();
return arrayContentDeletionDiffs;
};
const findDiffsInArray = (oldArray, newArray, objectName) => {
const arrayAdditionDiffs = findAdditionDiffsInProperty(oldArray, newArray, objectName);
const arrayDeletionDiffs = findDeletionDiffsInProperty(oldArray, newArray, objectName);
let arrayContentAdditionDiffs = [];
if (!arrayAdditionDiffs.length) {
const oldArrayContent = oldArray.value;
const newArrayContent = newArray.value;
arrayContentAdditionDiffs = findAdditionDiffsInArray(oldArrayContent, newArrayContent, objectName);
}
else if (isTopLevelProperty(change)) {
return `${getTopLevelProperty(change)}.property`;
let arrayContentDeletionDiffs = [];
if (!arrayDeletionDiffs.length) {
const oldArrayContent = oldArray.value;
const newArrayContent = newArray.value;
arrayContentDeletionDiffs = findDeletionDiffsInArray(oldArrayContent, newArrayContent, objectName);
}
else {
return 'unclassified.change';
}
return _.concat([], arrayAdditionDiffs, arrayDeletionDiffs, arrayContentAdditionDiffs, arrayContentDeletionDiffs);
};
const getChangeType = (changeKind) => {
let resultingType;
switch (changeKind) {
case 'D': {
resultingType = 'delete';
break;
}
case 'E': {
resultingType = 'edit';
break;
}
case 'N': {
resultingType = 'add';
break;
}
default: {
resultingType = 'unknown';
break;
}
}
return resultingType;
const findDiffsInXProperties = (oldParsedXProperties, newParsedXProperties, xPropertyContainerName) => {
const xPropertyUniqueNames = _(_.keys(oldParsedXProperties))
.concat(_.keys(newParsedXProperties))
.uniq()
.value();
const xPropertyDiffs = _(xPropertyUniqueNames)
.map((xPropertyName) => {
return findDiffsInProperty(oldParsedXProperties[xPropertyName], newParsedXProperties[xPropertyName], `${xPropertyContainerName}.${xPropertyName}`);
})
.flatten()
.value();
return xPropertyDiffs;
};
const getTopLevelProperty = (entry) => {
return entry.path[0];
const findDiffsInSpecs = (oldParsedSpec, newParsedSpec) => {
const infoDiffs = _.concat([], findDiffsInProperty(oldParsedSpec.info.termsOfService, newParsedSpec.info.termsOfService, 'info.termsOfService'), findDiffsInProperty(oldParsedSpec.info.description, newParsedSpec.info.description, 'info.description'), findDiffsInProperty(oldParsedSpec.info.contact.name, newParsedSpec.info.contact.name, 'info.contact.name'), findDiffsInProperty(oldParsedSpec.info.contact.email, newParsedSpec.info.contact.email, 'info.contact.email'), findDiffsInProperty(oldParsedSpec.info.contact.url, newParsedSpec.info.contact.url, 'info.contact.url'), findDiffsInProperty(oldParsedSpec.info.license.name, newParsedSpec.info.license.name, 'info.license.name'), findDiffsInProperty(oldParsedSpec.info.license.url, newParsedSpec.info.license.url, 'info.license.url'), findDiffsInProperty(oldParsedSpec.info.title, newParsedSpec.info.title, 'info.title'), findDiffsInProperty(oldParsedSpec.info.version, newParsedSpec.info.version, 'info.version'), findDiffsInXProperties(oldParsedSpec.info.xProperties, newParsedSpec.info.xProperties, 'info.xProperties'));
const basePathDiffs = findDiffsInProperty(oldParsedSpec.basePath, newParsedSpec.basePath, 'basePath');
const hostDiffs = findDiffsInProperty(oldParsedSpec.host, newParsedSpec.host, 'host');
const openApiDiffs = findDiffsInProperty(oldParsedSpec.openapi, newParsedSpec.openapi, 'openapi');
const schemesDiffs = findDiffsInArray(oldParsedSpec.schemes, newParsedSpec.schemes, 'schemes');
const topLevelXPropertyDiffs = findDiffsInXProperties(oldParsedSpec.xProperties, newParsedSpec.xProperties, 'xProperties');
return _.concat([], infoDiffs, basePathDiffs, hostDiffs, openApiDiffs, schemesDiffs, topLevelXPropertyDiffs);
};
const BreakingChanges = [
'host.property.add',
'host.property.edit',
'host.property.delete',
'basePath.property.add',
'basePath.property.edit',
'basePath.property.delete'
];
const nonBreakingChanges = [
'info.object.edit',
'openapi.property.edit'
];
exports.default = {
diff: (oldParsedSpec, newParsedSpec) => {
const rawDiff = deepDiff.diff(oldParsedSpec, newParsedSpec);
const processedDiff = processDiff(oldParsedSpec, rawDiff);
return processedDiff;
return findDiffsInSpecs(oldParsedSpec, newParsedSpec);
}
};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
const utils_1 = require("./utils");
const parseInfoObject = (spec) => {
return spec.info;
const parseInfoContactObject = (infoContactObject) => {
return {
email: {
originalPath: ['info', 'contact', 'email'],
value: infoContactObject ? infoContactObject.email : undefined
},
name: {
originalPath: ['info', 'contact', 'name'],
value: infoContactObject ? infoContactObject.name : undefined
},
url: {
originalPath: ['info', 'contact', 'url'],
value: infoContactObject ? infoContactObject.url : undefined
}
};
};
const parseOpenApiProperty = (spec) => {
const parsedOpenApiProperty = {
originalPath: spec.swagger ? ['swagger'] : ['openapi'],
parsedValue: spec.swagger ? spec.swagger : spec.openapi
const parseInfoLicenseObject = (infoLicenseObject) => {
return {
name: {
originalPath: ['info', 'license', 'name'],
value: infoLicenseObject ? infoLicenseObject.name : undefined
},
url: {
originalPath: ['info', 'license', 'url'],
value: infoLicenseObject ? infoLicenseObject.url : undefined
}
};
return parsedOpenApiProperty;
};
const parseTopLevelProperties = (spec) => {
const parseInfoObject = (spec) => {
const parsedInfo = {
contact: parseInfoContactObject(spec.info.contact),
description: {
originalPath: ['info', 'description'],
value: spec.info.description
},
license: parseInfoLicenseObject(spec.info.license),
termsOfService: {
originalPath: ['info', 'termsOfService'],
value: spec.info.termsOfService
},
title: {
originalPath: ['info', 'title'],
value: spec.info.title
},
version: {
originalPath: ['info', 'version'],
value: spec.info.version
},
xProperties: {}
};
for (const entry of getXPropertiesInObject(spec.info)) {
_.set(parsedInfo.xProperties, entry.key, { originalPath: ['info', entry.key], value: entry.value });
}
return parsedInfo;
};
const isXProperty = (propertyPath) => {
return propertyPath.startsWith('x-');
};
const getXPropertiesInObject = (object) => {
const topLevelPropertiesArray = [];
_.forIn(spec, (value, key) => {
if (utils_1.default.isXProperty(key) || utils_1.default.isOptionalProperty(key)) {
_.forIn(object, (value, key) => {
if (isXProperty(key)) {
topLevelPropertiesArray.push({ key, value });

@@ -24,13 +71,73 @@ }

};
const parseTopLevelArrayProperties = (arrayName, inputArray) => {
const parsedSchemesArray = [];
if (inputArray.length) {
inputArray.forEach((value, index) => {
parsedSchemesArray.push({
originalPath: [arrayName, index.toString()],
value
});
});
}
return parsedSchemesArray;
};
const parseSwagger2Spec = (swagger2Spec) => {
const parsedSpec = {
basePath: {
originalPath: ['basePath'],
value: swagger2Spec.basePath
},
host: {
originalPath: ['host'],
value: swagger2Spec.host
},
info: parseInfoObject(swagger2Spec),
openapi: {
originalPath: ['swagger'],
value: swagger2Spec.swagger
},
schemes: {
originalPath: ['schemes'],
value: swagger2Spec.schemes ? parseTopLevelArrayProperties('schemes', swagger2Spec.schemes) : undefined
},
xProperties: {}
};
for (const entry of getXPropertiesInObject(swagger2Spec)) {
_.set(parsedSpec.xProperties, entry.key, { originalPath: [entry.key], value: entry.value });
}
return parsedSpec;
};
const parseOpenApi3Spec = (openApi3Spec) => {
const parsedSpec = {
basePath: {
originalPath: ['basePath'],
value: undefined
},
host: {
originalPath: ['host'],
value: undefined
},
info: parseInfoObject(openApi3Spec),
openapi: {
originalPath: ['openapi'],
value: openApi3Spec.openapi
},
schemes: {
originalPath: ['schemes'],
value: undefined
},
xProperties: {}
};
for (const entry of getXPropertiesInObject(openApi3Spec)) {
_.set(parsedSpec.xProperties, entry.key, { originalPath: [entry.key], value: entry.value });
}
return parsedSpec;
};
const isSwagger2 = (spec) => {
return !!spec.swagger;
};
exports.default = {
parse: (spec) => {
const parsedSpec = {
info: parseInfoObject(spec),
openapi: parseOpenApiProperty(spec)
};
for (const entry of parseTopLevelProperties(spec)) {
_.set(parsedSpec, entry.key, entry.value);
}
return parsedSpec;
return isSwagger2(spec) ? parseSwagger2Spec(spec) : parseOpenApi3Spec(spec);
}
};

@@ -5,8 +5,10 @@ import * as q from 'q';

import { OpenAPIObject } from 'openapi3-ts';
import { Spec } from 'swagger-schema-official';
import {
FileSystem,
HttpClient,
JsonLoaderFunction,
OpenAPI3Spec,
Swagger2Spec
JsonLoaderFunction
} from './types';

@@ -19,7 +21,7 @@

const parseAsJson = (location: string, content: string): q.Promise<Swagger2Spec | OpenAPI3Spec> => {
const parseAsJson = (location: string, content: string): q.Promise<Spec | OpenAPIObject> => {
try {
return q(JSON.parse(content));
} catch (error) {
return q.reject<OpenAPI3Spec>(new VError(error, `ERROR: unable to parse ${location} as a JSON file`));
return q.reject<OpenAPIObject>(new VError(error, `ERROR: unable to parse ${location} as a JSON file`));
}

@@ -26,0 +28,0 @@ };

import * as _ from 'lodash';
import * as VError from 'verror';
import {
DiffChange,
DiffChangeSeverity,
DiffEntry,
DiffEntrySeverity,
ResultObject
} from './types';
const buildChangeSentence = (change: DiffChange): string => {
let changeSentence: string;
switch (change.type) {
case 'add': {
changeSentence = `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was added with value \'${change.rhs}\'`;
break;
}
case 'delete': {
changeSentence = `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `with value \'${change.lhs}\' was removed`;
break;
}
case 'edit': {
changeSentence = `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was modified from \'${change.lhs}\' to \'${change.rhs}\'`;
break;
}
default: {
throw new VError(`ERROR: unable to handle ${change.type} as a change type`);
}
}
return changeSentence;
const buildChangeSentence = (targetChange: DiffEntry): string => {
const changeDescription: any = {
add: ((change: DiffEntry): string => {
return `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was added with value \'${change.newValue}\'`;
}),
'arrayContent.add': ((change: DiffEntry): string => {
return `${_.capitalize(change.severity)}: the value \'${change.newValue}\' was added to the`
+ ` array in the path [${change.printablePath.join('/')}]`;
}),
'arrayContent.delete': ((change: DiffEntry): string => {
return `${_.capitalize(change.severity)}: the value \'${change.oldValue}\' was removed from the`
+ ` array in the path [${change.printablePath.join('/')}]`;
}),
delete: ((change: DiffEntry): string => {
return `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `with value \'${change.oldValue}\' was removed`;
}),
edit: ((change: DiffEntry): string => {
return `${_.capitalize(change.severity)}: the path [${change.printablePath.join('/')}] `
+ `was modified from \'${change.oldValue}\' to \'${change.newValue}\'`;
})
};
return changeDescription[targetChange.type](targetChange);
};
const countSeverities = (changes: DiffChange[], changeSeverity: DiffChangeSeverity): number => {
const countSeverities = (changes: DiffEntry[], changeSeverity: DiffEntrySeverity): number => {
const changeCount = _.filter(changes, ['severity', changeSeverity]).length;

@@ -41,3 +42,3 @@ return changeCount;

export default {
build: (results: DiffChange[]): ResultObject => {
build: (results: DiffEntry[]): ResultObject => {
const numberOfBreakingChanges = countSeverities(results, 'breaking');

@@ -44,0 +45,0 @@

@@ -1,147 +0,254 @@

import * as deepDiff from 'deep-diff';
import * as _ from 'lodash';
import IDiff = deepDiff.IDiff;
import * as VError from 'verror';
import utils from './utils';
import severityFinder from './severity-finder';
import {
DiffChange,
DiffChangeSeverity,
DiffChangeTaxonomy,
DiffChangeType,
DiffEntry,
DiffEntrySeverity,
DiffEntryTaxonomy,
DiffEntryType,
ParsedProperty,
ParsedSpec
} from './types';
const processDiff = (parsedSpec: ParsedSpec, rawDiff: IDiff[] | undefined): DiffChange[] => {
interface CreateDiffEntryOptions<T> {
oldObject?: T;
newObject?: T;
propertyName: string;
type: DiffEntryType;
}
const processedDiff: DiffChange[] = [];
const findPrintablePathForDiff = <T>(options: CreateDiffEntryOptions<ParsedProperty<T>>): string[] => {
if (_.isUndefined(options.oldObject) && _.isUndefined(options.newObject)) {
throw new VError(`ERROR: impossible to find the path for ${options.propertyName} - ${options.type}`);
}
return options.oldObject ?
options.oldObject.originalPath :
(options.newObject as ParsedProperty<T>).originalPath;
};
if (rawDiff) {
for (const entry of rawDiff) {
const findScopeForDiff = (propertyName: string): string => {
return propertyName.includes('xProperties') ? 'unclassified' : propertyName;
};
const type = getChangeType(entry.kind);
const scope = getChangeScope(entry);
const taxonomy = findChangeTaxonomy(type, scope);
const createDiffEntry = <T>(options: CreateDiffEntryOptions<ParsedProperty<T>>): DiffEntry => {
const printablePath: string[] = findPrintablePathForDiff(options);
const scope: string = findScopeForDiff(options.propertyName);
const taxonomy: DiffEntryTaxonomy = `${scope}.${options.type}` as DiffEntryTaxonomy;
const severity: DiffEntrySeverity = severityFinder.lookup(taxonomy);
const processedEntry: DiffChange = {
index: getChangeNullableProperties(entry.index),
item: getChangeNullableProperties(entry.item),
kind: entry.kind,
lhs: entry.lhs,
path: entry.path,
printablePath: utils.findOriginalPath(parsedSpec, entry.path),
rhs: entry.rhs,
scope,
severity: findChangeSeverity(taxonomy),
taxonomy,
type
};
return {
newValue: options.newObject ? options.newObject.value : undefined,
oldValue: options.oldObject ? options.oldObject.value : undefined,
printablePath,
scope,
severity,
taxonomy,
type: options.type
};
};
processedDiff.push(processedEntry);
}
}
const isDefined = (target: any): boolean => {
return !_.isUndefined(target);
};
return processedDiff;
const isDefinedDeep = (objectWithValue: { value?: any }): boolean => {
return isDefined(objectWithValue) && isDefined(objectWithValue.value);
};
const isEdit = (entry: IDiff): boolean => {
return entry.kind === 'E';
const isUndefinedDeep = (objectWithValue: { value?: any }): boolean => {
return _.isUndefined(objectWithValue) || _.isUndefined(objectWithValue.value);
};
const isInfoChange = (entry: IDiff): boolean => {
return isEdit(entry) && isInfoObject(entry) && !utils.isXProperty(entry.path[1]);
const findAdditionDiffsInProperty = <T>(oldObject: ParsedProperty<T>,
newObject: ParsedProperty<T>,
propertyName: string): DiffEntry[] => {
const isAddition = isUndefinedDeep(oldObject) && isDefinedDeep(newObject);
if (isAddition) {
return [createDiffEntry({newObject, oldObject, propertyName, type: 'add'})];
}
return [];
};
const isInfoObject = (entry: IDiff): boolean => {
return entry.path[0] === 'info';
const findDeletionDiffsInProperty = <T>(oldObject: ParsedProperty<T>,
newObject: ParsedProperty<T>,
propertyName: string): DiffEntry[] => {
const isDeletion = isDefinedDeep(oldObject) && isUndefinedDeep(newObject);
if (isDeletion) {
return [createDiffEntry({newObject, oldObject, propertyName, type: 'delete'})];
}
return [];
};
const isTopLevelProperty = (entry: IDiff): boolean => {
const topLevelPropertyNames: string[] = [
'basePath',
'host',
'openapi'
];
return _.includes(topLevelPropertyNames, entry.path[0]);
const findEditionDiffsInProperty = (oldObject: ParsedProperty<string>,
newObject: ParsedProperty<string>,
propertyName: string): DiffEntry[] => {
const isEdition = isDefinedDeep(oldObject) && isDefinedDeep(newObject) && (oldObject.value !== newObject.value);
if (isEdition) {
return [createDiffEntry({newObject, oldObject, propertyName, type: 'edit'})];
}
return [];
};
const findChangeTaxonomy = (type: DiffChangeType, scope: string): DiffChangeTaxonomy => {
return (scope === 'unclassified.change') ? scope as DiffChangeTaxonomy : `${scope}.${type}` as DiffChangeTaxonomy;
const findDiffsInProperty = (oldObject: ParsedProperty<string>,
newObject: ParsedProperty<string>,
propertyName: string): DiffEntry[] => {
const additionDiffs: DiffEntry[] = findAdditionDiffsInProperty(oldObject, newObject, propertyName);
const deletionDiffs: DiffEntry[] = findDeletionDiffsInProperty(oldObject, newObject, propertyName);
const editionDiffs: DiffEntry[] = findEditionDiffsInProperty(oldObject, newObject, propertyName);
return _.concat<DiffEntry>([], additionDiffs, deletionDiffs, editionDiffs);
};
const findChangeSeverity = (taxonomy: DiffChangeTaxonomy): DiffChangeSeverity => {
const isBreakingChange = _.includes(BreakingChanges, taxonomy);
const isNonBreakingChange = _.includes(nonBreakingChanges, taxonomy);
const isValueInArray = (object: any, array?: any[]): boolean => {
return _.some(array, {value: object.value});
};
if (isBreakingChange) {
return 'breaking';
} else if (isNonBreakingChange) {
return 'non-breaking';
} else {
return 'unclassified';
}
const findAdditionDiffsInArray = <T>(oldArrayContent: Array<ParsedProperty<T>> | undefined,
newArrayContent: Array<ParsedProperty<T>> | undefined,
arrayName: string): DiffEntry[] => {
const arrayContentAdditionDiffs = _(newArrayContent)
.filter((entry) => {
return !isValueInArray(entry, oldArrayContent);
})
.map((addedEntry) => {
return createDiffEntry({
newObject: addedEntry,
oldObject: undefined,
propertyName: arrayName,
type: 'arrayContent.add'
});
})
.flatten<DiffEntry>()
.value();
return arrayContentAdditionDiffs;
};
const getChangeNullableProperties = (changeProperty: any): any => {
return changeProperty || null;
const findDeletionDiffsInArray = <T>(oldArrayContent: Array<ParsedProperty<T>> | undefined,
newArrayContent: Array<ParsedProperty<T>> | undefined,
arrayName: string): DiffEntry[] => {
const arrayContentDeletionDiffs = _(oldArrayContent)
.filter((entry) => {
return !isValueInArray(entry, newArrayContent);
})
.map((deletedEntry) => {
return createDiffEntry({
newObject: undefined,
oldObject: deletedEntry,
propertyName: arrayName,
type: 'arrayContent.delete'
});
})
.flatten<DiffEntry>()
.value();
return arrayContentDeletionDiffs;
};
const getChangeScope = (change: IDiff): string => {
if (isInfoChange(change)) {
return 'info.object';
} else if (isTopLevelProperty(change)) {
return `${getTopLevelProperty(change)}.property`;
} else {
return 'unclassified.change';
const findDiffsInArray = <T>(oldArray: ParsedProperty<Array<ParsedProperty<T>>>,
newArray: ParsedProperty<Array<ParsedProperty<T>>>,
objectName: string): DiffEntry[] => {
const arrayAdditionDiffs: DiffEntry[] = findAdditionDiffsInProperty(oldArray, newArray, objectName);
const arrayDeletionDiffs: DiffEntry[] = findDeletionDiffsInProperty(oldArray, newArray, objectName);
let arrayContentAdditionDiffs: DiffEntry[] = [];
if (!arrayAdditionDiffs.length) {
const oldArrayContent = oldArray.value;
const newArrayContent = newArray.value;
arrayContentAdditionDiffs = findAdditionDiffsInArray(oldArrayContent, newArrayContent, objectName);
}
};
const getChangeType = (changeKind: string): DiffChangeType => {
let resultingType: DiffChangeType;
switch (changeKind) {
case 'D': {
resultingType = 'delete';
break;
}
case 'E': {
resultingType = 'edit';
break;
}
case 'N': {
resultingType = 'add';
break;
}
default: {
resultingType = 'unknown';
break;
}
let arrayContentDeletionDiffs: DiffEntry[] = [];
if (!arrayDeletionDiffs.length) {
const oldArrayContent = oldArray.value;
const newArrayContent = newArray.value;
arrayContentDeletionDiffs = findDeletionDiffsInArray(oldArrayContent, newArrayContent, objectName);
}
return resultingType;
return _.concat<DiffEntry>([],
arrayAdditionDiffs,
arrayDeletionDiffs,
arrayContentAdditionDiffs,
arrayContentDeletionDiffs);
};
const getTopLevelProperty = (entry: IDiff): string => {
return entry.path[0];
const findDiffsInXProperties = (oldParsedXProperties: { [name: string]: ParsedProperty<any> },
newParsedXProperties: { [name: string]: ParsedProperty<any> },
xPropertyContainerName: string): DiffEntry[] => {
const xPropertyUniqueNames = _(_.keys(oldParsedXProperties))
.concat(_.keys(newParsedXProperties))
.uniq()
.value();
const xPropertyDiffs = _(xPropertyUniqueNames)
.map((xPropertyName) => {
return findDiffsInProperty(
oldParsedXProperties[xPropertyName],
newParsedXProperties[xPropertyName],
`${xPropertyContainerName}.${xPropertyName}`
);
})
.flatten<DiffEntry>()
.value();
return xPropertyDiffs;
};
const BreakingChanges: DiffChangeTaxonomy[] = [
'host.property.add',
'host.property.edit',
'host.property.delete',
'basePath.property.add',
'basePath.property.edit',
'basePath.property.delete'
];
const findDiffsInSpecs = (oldParsedSpec: ParsedSpec, newParsedSpec: ParsedSpec): DiffEntry[] => {
const nonBreakingChanges: DiffChangeTaxonomy[] = [
'info.object.edit',
'openapi.property.edit'
];
const infoDiffs = _.concat([],
findDiffsInProperty(oldParsedSpec.info.termsOfService,
newParsedSpec.info.termsOfService, 'info.termsOfService'),
findDiffsInProperty(oldParsedSpec.info.description,
newParsedSpec.info.description, 'info.description'),
findDiffsInProperty(oldParsedSpec.info.contact.name,
newParsedSpec.info.contact.name, 'info.contact.name'),
findDiffsInProperty(oldParsedSpec.info.contact.email,
newParsedSpec.info.contact.email, 'info.contact.email'),
findDiffsInProperty(oldParsedSpec.info.contact.url,
newParsedSpec.info.contact.url, 'info.contact.url'),
findDiffsInProperty(oldParsedSpec.info.license.name,
newParsedSpec.info.license.name, 'info.license.name'),
findDiffsInProperty(oldParsedSpec.info.license.url,
newParsedSpec.info.license.url, 'info.license.url'),
findDiffsInProperty(oldParsedSpec.info.title,
newParsedSpec.info.title, 'info.title'),
findDiffsInProperty(oldParsedSpec.info.version,
newParsedSpec.info.version, 'info.version'),
findDiffsInXProperties(oldParsedSpec.info.xProperties,
newParsedSpec.info.xProperties, 'info.xProperties')
);
const basePathDiffs = findDiffsInProperty(oldParsedSpec.basePath, newParsedSpec.basePath, 'basePath');
const hostDiffs = findDiffsInProperty(oldParsedSpec.host, newParsedSpec.host, 'host');
const openApiDiffs = findDiffsInProperty(oldParsedSpec.openapi, newParsedSpec.openapi, 'openapi');
const schemesDiffs = findDiffsInArray(oldParsedSpec.schemes, newParsedSpec.schemes, 'schemes');
const topLevelXPropertyDiffs = findDiffsInXProperties(oldParsedSpec.xProperties,
newParsedSpec.xProperties, 'xProperties');
return _.concat([], infoDiffs, basePathDiffs, hostDiffs, openApiDiffs, schemesDiffs, topLevelXPropertyDiffs);
};
export default {
diff: (oldParsedSpec: ParsedSpec,
newParsedSpec: ParsedSpec): DiffChange[] => {
const rawDiff: IDiff[] = deepDiff.diff(oldParsedSpec, newParsedSpec);
const processedDiff: DiffChange[] = processDiff(oldParsedSpec, rawDiff);
return processedDiff;
diff: (oldParsedSpec: ParsedSpec, newParsedSpec: ParsedSpec): DiffEntry[] => {
return findDiffsInSpecs(oldParsedSpec, newParsedSpec);
}
};
import * as _ from 'lodash';
import utils from './utils';
import {
ContactObject as OpenApi3InfoContactObject,
LicenseObject as OpenApi3InfoLicenseObject,
OpenAPIObject as OpenApi3
} from 'openapi3-ts';
import {
Contact as Swagger2InfoContactObject,
License as Swagger2InfoLicenseObject,
Spec as Swagger2
} from 'swagger-schema-official';
import {
GenericProperty,
OpenAPI3Spec,
ParsedContactObject,
ParsedInfoObject,
ParsedOpenApiProperty,
ParsedSpec,
Swagger2Spec
ParsedLicenseObject,
ParsedProperty,
ParsedSpec
} from './types';
const parseInfoObject = (spec: Swagger2Spec | OpenAPI3Spec): ParsedInfoObject => {
return spec.info;
const parseInfoContactObject = (infoContactObject?: Swagger2InfoContactObject |
OpenApi3InfoContactObject): ParsedContactObject => {
return {
email: {
originalPath: ['info', 'contact', 'email'],
value: infoContactObject ? infoContactObject.email : undefined
},
name: {
originalPath: ['info', 'contact', 'name'],
value: infoContactObject ? infoContactObject.name : undefined
},
url: {
originalPath: ['info', 'contact', 'url'],
value: infoContactObject ? infoContactObject.url : undefined
}
};
};
const parseOpenApiProperty = (spec: Swagger2Spec | OpenAPI3Spec): ParsedOpenApiProperty => {
const parsedOpenApiProperty: ParsedOpenApiProperty = {
originalPath: spec.swagger ? ['swagger'] : ['openapi'],
parsedValue: spec.swagger ? spec.swagger : spec.openapi
const parseInfoLicenseObject = (infoLicenseObject?: Swagger2InfoLicenseObject |
OpenApi3InfoLicenseObject): ParsedLicenseObject => {
return {
name: {
originalPath: ['info', 'license', 'name'],
value: infoLicenseObject ? infoLicenseObject.name : undefined
},
url: {
originalPath: ['info', 'license', 'url'],
value: infoLicenseObject ? infoLicenseObject.url : undefined
}
};
return parsedOpenApiProperty;
};
const parseTopLevelProperties = (spec: Swagger2Spec | OpenAPI3Spec): GenericProperty[] => {
const parseInfoObject = (spec: Swagger2 | OpenApi3): ParsedInfoObject => {
const parsedInfo: ParsedInfoObject = {
contact: parseInfoContactObject(spec.info.contact),
description: {
originalPath: ['info', 'description'],
value: spec.info.description
},
license: parseInfoLicenseObject(spec.info.license),
termsOfService: {
originalPath: ['info', 'termsOfService'],
value: spec.info.termsOfService
},
title: {
originalPath: ['info', 'title'],
value: spec.info.title
},
version: {
originalPath: ['info', 'version'],
value: spec.info.version
},
xProperties: {}
};
for (const entry of getXPropertiesInObject(spec.info)) {
_.set(parsedInfo.xProperties, entry.key, {originalPath: ['info', entry.key], value: entry.value});
}
return parsedInfo;
};
const isXProperty = (propertyPath: string): boolean => {
return propertyPath.startsWith('x-');
};
const getXPropertiesInObject = (object: any): GenericProperty[] => {
const topLevelPropertiesArray: GenericProperty[] = [];
_.forIn(spec, (value, key) => {
if (utils.isXProperty(key) || utils.isOptionalProperty(key)) {
_.forIn(object, (value, key) => {
if (isXProperty(key)) {
topLevelPropertiesArray.push({key, value});
}
});
return topLevelPropertiesArray;
};
export default {
parse: (spec: Swagger2Spec | OpenAPI3Spec): ParsedSpec => {
const parsedSpec: ParsedSpec = {
info: parseInfoObject(spec),
openapi: parseOpenApiProperty(spec)
};
const parseTopLevelArrayProperties = (arrayName: string,
inputArray: string[]): Array<ParsedProperty<string>> => {
const parsedSchemesArray: Array<ParsedProperty<string>> = [];
for (const entry of parseTopLevelProperties(spec)) {
_.set(parsedSpec, entry.key, entry.value);
}
if (inputArray.length) {
inputArray.forEach((value, index) => {
parsedSchemesArray.push({
originalPath: [arrayName, index.toString()],
value
});
});
}
return parsedSpec;
return parsedSchemesArray;
};
const parseSwagger2Spec = (swagger2Spec: Swagger2): ParsedSpec => {
const parsedSpec: ParsedSpec = {
basePath: {
originalPath: ['basePath'],
value: swagger2Spec.basePath
},
host: {
originalPath: ['host'],
value: swagger2Spec.host
},
info: parseInfoObject(swagger2Spec),
openapi: {
originalPath: ['swagger'],
value: swagger2Spec.swagger
},
schemes: {
originalPath: ['schemes'],
value: swagger2Spec.schemes ? parseTopLevelArrayProperties('schemes', swagger2Spec.schemes) : undefined
},
xProperties: {}
};
for (const entry of getXPropertiesInObject(swagger2Spec)) {
_.set(parsedSpec.xProperties, entry.key, {originalPath: [entry.key], value: entry.value});
}
return parsedSpec;
};
const parseOpenApi3Spec = (openApi3Spec: OpenApi3): ParsedSpec => {
const parsedSpec: ParsedSpec = {
basePath: {
originalPath: ['basePath'],
value: undefined
},
host: {
originalPath: ['host'],
value: undefined
},
info: parseInfoObject(openApi3Spec),
openapi: {
originalPath: ['openapi'],
value: openApi3Spec.openapi
},
schemes: {
originalPath: ['schemes'],
value: undefined
},
xProperties: {}
};
for (const entry of getXPropertiesInObject(openApi3Spec)) {
_.set(parsedSpec.xProperties, entry.key, {originalPath: [entry.key], value: entry.value});
}
return parsedSpec;
};
const isSwagger2 = (spec: Swagger2 | OpenApi3): boolean => {
return !!(spec as Swagger2).swagger;
};
export default {
parse: (spec: Swagger2 | OpenApi3): ParsedSpec => {
return isSwagger2(spec) ? parseSwagger2Spec(spec as Swagger2) : parseOpenApi3Spec(spec as OpenApi3);
}
};
// Diff types
import IDiff = deepDiff.IDiff;
import * as q from 'q';
export interface DiffChange extends IDiff {
severity: DiffChangeSeverity;
export interface DiffEntry {
oldValue?: any;
newValue?: any;
printablePath: string[];
scope: string;
taxonomy: DiffChangeTaxonomy;
type: DiffChangeType;
severity: DiffEntrySeverity;
taxonomy: DiffEntryTaxonomy;
type: DiffEntryType;
}
export type DiffChangeTaxonomy =
'basePath.property.add' |
'basePath.property.delete' |
'basePath.property.edit' |
'host.property.add' |
'host.property.delete' |
'host.property.edit' |
'info.object.edit' |
'openapi.property.edit' |
'unclassified.change';
export type DiffEntryTaxonomy =
'basePath.add' |
'basePath.delete' |
'basePath.edit' |
'host.add' |
'host.delete' |
'host.edit' |
'info.title.add' |
'info.title.delete' |
'info.title.edit' |
'info.description.add' |
'info.description.delete' |
'info.description.edit' |
'info.termsOfService.add' |
'info.termsOfService.delete' |
'info.termsOfService.edit' |
'info.version.add' |
'info.version.delete' |
'info.version.edit' |
'info.contact.name.add' |
'info.contact.name.delete' |
'info.contact.name.edit' |
'info.contact.email.add' |
'info.contact.email.delete' |
'info.contact.email.edit' |
'info.contact.url.add' |
'info.contact.url.delete' |
'info.contact.url.edit' |
'info.license.name.add' |
'info.license.name.delete' |
'info.license.name.edit' |
'info.license.url.add' |
'info.license.url.delete' |
'info.license.url.edit' |
'openapi.edit' |
'schemes.add' |
'schemes.arrayContent.add' |
'schemes.arrayContent.delete' |
'schemes.edit' |
'schemes.delete' |
'unclassified.add' |
'unclassified.delete' |
'unclassified.edit';
export type DiffChangeType =
export type DiffEntryType =
'add' |
'arrayContent.add' |
'arrayContent.delete' |
'delete' |
'edit' |
'delete' |
'unknown';
export type DiffChangeSeverity =
export type DiffEntrySeverity =
'breaking' |

@@ -36,62 +71,37 @@ 'non-breaking' |

// Open API types
// Parsed Spec types
export interface OpenAPISpecInfo {
title: string;
description?: string;
termsOfService?: string;
contact?: ParsedContactObject;
licence?: ParsedLicenseObject;
version: string;
[xProperty: string]: any;
export interface ParsedInfoObject {
title: ParsedProperty<string>;
description: ParsedProperty<string>;
termsOfService: ParsedProperty<string>;
contact: ParsedContactObject;
license: ParsedLicenseObject;
version: ParsedProperty<string>;
xProperties: { [name: string]: ParsedProperty<any> };
}
export interface Swagger2Spec {
basePath?: string;
host?: string;
info: OpenAPISpecInfo;
[xProperty: string]: any;
swagger: string;
}
export interface OpenAPI3Spec {
info: OpenAPISpecInfo;
[xProperty: string]: any;
openapi: string;
}
// Parsed Spec types
export interface ParsedContactObject {
name?: string;
url?: string;
email?: string;
name: ParsedProperty<string>;
url: ParsedProperty<string>;
email: ParsedProperty<string>;
}
export interface ParsedInfoObject {
title: string;
description?: string;
termsOfService?: string;
contact?: ParsedContactObject;
licence?: ParsedLicenseObject;
version: string;
[xProperty: string]: any;
}
export interface ParsedLicenseObject {
name: string;
url?: string;
name: ParsedProperty<string>;
url: ParsedProperty<string>;
}
export interface ParsedOpenApiProperty {
export interface ParsedProperty<T> {
originalPath: string[];
parsedValue: string;
value?: T;
}
export interface ParsedSpec {
basePath?: string;
host?: string;
basePath: ParsedProperty<string>;
host: ParsedProperty<string>;
info: ParsedInfoObject;
openapi: ParsedOpenApiProperty;
[xProperty: string]: any;
openapi: ParsedProperty<string>;
schemes: ParsedProperty<Array<ParsedProperty<string>>>;
xProperties: { [name: string]: ParsedProperty<any> };
}

@@ -107,3 +117,2 @@

// Various other types
export interface FileSystem {

@@ -110,0 +119,0 @@ readFile: JsonLoaderFunction;

{
"name": "openapi-diff",
"version": "0.4.0",
"version": "0.5.0",
"description": "A CLI tool to identify differences between Swagger/OpenAPI specs.",

@@ -37,9 +37,8 @@ "bin": {

"@types/commander": "^2.9.1",
"@types/deep-diff": "0.0.30",
"@types/express": "^4.0.36",
"@types/jasmine": "^2.5.52",
"@types/lodash": "^4.14.66",
"@types/proxyquire": "^1.3.27",
"@types/q": "^1.0.2",
"@types/request": "0.0.45",
"@types/swagger-schema-official": "^2.0.5",
"@types/verror": "^1.10.0",

@@ -56,2 +55,3 @@ "conventional-changelog-lint": "^1.1.9",

"gulp-typescript": "^3.1.7",
"jasmine": "^2.7.0",
"minimist": "^1.2.0",

@@ -64,4 +64,4 @@ "run-sequence": "^1.2.2",

"commander": "^2.10.0",
"deep-diff": "0.3.4",
"lodash": "^4.17.4",
"openapi3-ts": "^0.2.1",
"q": "^1.5.0",

@@ -68,0 +68,0 @@ "request": "^2.81.0",

@@ -16,3 +16,3 @@ # OpenAPI Diff

## Usage
Invoke the tool with two paths to Swagger/OpenAPI files in order to find differences between them, these paths can either be paths to the specs in the local filesystem or URLs to the specs.
Invoke the tool with two paths to Swagger/OpenAPI files in order to find differences between them, these paths can either be paths to the specs in the local filesystem or URLs to the specs (sorry, no YML support just yet).
The Open API specs should be in JSON format.

@@ -23,3 +23,3 @@ ```

The tool's output will display amount and type of changes (breaking, non-breaking, unclassified), and then list the changes with the relevant info.
The tool's output will display the amount and type of changes (breaking, non-breaking, unclassified), and then list the changes with the relevant info.

@@ -29,14 +29,2 @@ The command will exit with an exit code 1 if any breaking changes were found, so that you can fail builds in CI when this happens.

## Feature support
### Supported
- Specs in the local filesystem or as URLs
- Detects editions to the `swagger` / `openapi` object.
- Detects editions to the `info` object and `^x- properties` at the top level of the spec.
- Detects additions, editions and deletions of the `host` and `basePath` Swagger 2 properties.
### Beta support
- Additions and deletions to the `info` object and `^x- properties` at the top level of the spec.
### Not supported
- Any other additions, editions or deletions to the spec.
- Specs in YML format
See [SPEC_SUPPORT.md](SPEC_SUPPORT.md)

@@ -20,3 +20,4 @@ {

"swagger": "2.1",
"basePath": "/v2"
"basePath": "/v2",
"schemes": ["https", "ws"]
}

@@ -21,3 +21,4 @@ {

"host": "some host info",
"basePath": "/"
"basePath": "/",
"schemes": ["ws", "https"]
}

@@ -5,6 +5,11 @@ {

"title": "New test API",
"description": "Brand new spec description",
"license": {
"name": "spec license name"
},
"version": "New test version",
"x-info-property": "Some new content"
"x-info-property": "Some new content",
"x-brand-new-property": "Some brand new content"
},
"x-generic-property": "Some new content"
}

@@ -5,6 +5,11 @@ {

"title": "Test API",
"license": {
"name": "spec license name",
"url": "spec license url"
},
"version": "Test version",
"x-info-property": "Some content"
},
"x-generic-property": "Some content"
"x-generic-property": "Some content",
"x-deleted-property": "Some deleted content"
}

@@ -12,3 +12,3 @@ {

"license": {
"name": "Apache 2.1",
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"

@@ -42,3 +42,4 @@ }

"schemes": [
"http"
"https",
"ws"
],

@@ -45,0 +46,0 @@ "paths": {

@@ -12,4 +12,3 @@ {

"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
"name": "Apache 2.0"
}

@@ -43,2 +42,3 @@ },

],
"x-external-id": "some x value",
"paths": {

@@ -45,0 +45,0 @@ "/pet": {

@@ -23,3 +23,3 @@ import {exec} from 'child_process';

deferred.reject(new VError(error, `Failed to run ${command}. `
+ `Stdout: ${stdout.toString()}. Exit code: ${error.code}`));
+ `Stdout: ${stdout.toString()}. Exit code: ${error.code}`));
} else if (stderr) {

@@ -71,3 +71,3 @@ deferred.reject(stderr);

expect(error).toEqual(jasmine.stringMatching('ERROR: unable to read ' +
'test/e2e/fixtures/non-existing-old.json'));
'test/e2e/fixtures/non-existing-old.json'));

@@ -86,3 +86,3 @@ expect(error).toEqual(jasmine.stringMatching('Exit code: 2'));

expect(error).toEqual(jasmine.stringMatching('ERROR: unable to parse ' +
'test/e2e/fixtures/not-a-json.txt as a JSON file'));
'test/e2e/fixtures/not-a-json.txt as a JSON file'));

@@ -114,3 +114,3 @@ expect(error).toEqual(jasmine.stringMatching('Exit code: 2'));

expect(error).toEqual(jasmine.stringMatching('ERROR: unable to open ' +
'htt://localhost:3000/basic-old.json'));
'htt://localhost:3000/basic-old.json'));

@@ -123,4 +123,4 @@ expect(error).toEqual(jasmine.stringMatching('Exit code: 2'));

invokeCommand({
newSpecLocation: 'http://localhost:3000/non-existing-new.json',
oldSpecLocation: 'http://localhost:3000/non-existing-old.json'
newSpecLocation: 'http://localhost:3000/non-existing-new.json',
oldSpecLocation: 'http://localhost:3000/non-existing-old.json'
}).then(() => {

@@ -144,3 +144,3 @@ fail('test expected to error out but it didn\'t');

expect(error).toEqual(jasmine.stringMatching('ERROR: unable to parse ' +
'http://localhost:3000/not-a-json.txt as a JSON file'));
'http://localhost:3000/not-a-json.txt as a JSON file'));

@@ -172,3 +172,3 @@ expect(error).toEqual(jasmine.stringMatching('Exit code: 2'));

expect(result).toContain('Non-breaking: the path [info/title] was modified ' +
'from \'Test API\' to \'New Test API\'');
'from \'Test API\' to \'New Test API\'');
}).then(done, done.fail);

@@ -191,22 +191,24 @@ });

expect(error.message).toContain('Breaking: the path [basePath] was modified ' +
'from \'/\' to \'/v2\'');
'from \'/\' to \'/v2\'');
expect(error.message).toContain('Non-breaking: the path [info/termsOfService] was modified ' +
'from \'some terms\' to \'some new terms\'');
'from \'some terms\' to \'some new terms\'');
expect(error.message).toContain('Non-breaking: the path [info/contact/name] was modified ' +
'from \'Test name\' to \'New test name\'');
'from \'Test name\' to \'New test name\'');
expect(error.message).toContain('Non-breaking: the path [info/license/url] was modified ' +
'from \'http://license.example.com\' to \'http://new.license.example.com\'');
'from \'http://license.example.com\' to \'http://new.license.example.com\'');
expect(error.message).toContain('Non-breaking: the path [swagger] was modified ' +
'from \'2.0\' to \'2.1\'');
'from \'2.0\' to \'2.1\'');
expect(error.message).toContain('Unclassified: the path [info/x-info-property] was modified ' +
'from \'some content\' to \'some new content\'');
'from \'some content\' to \'some new content\'');
expect(error.message).toContain('Unclassified: the path [x-generic-property] was modified ' +
'from \'some content\' to \'some new content\'');
'from \'some content\' to \'some new content\'');
expect(error.message).not.toContain('the path [schemes');
expect(error.message).toEqual(jasmine.stringMatching('DANGER: Breaking changes found!'));

@@ -225,20 +227,32 @@

}).catch((error) => {
expect(error.message).toEqual(jasmine.stringMatching('2 breaking changes found.'));
expect(error.message).toEqual(jasmine.stringMatching('3 non-breaking changes found.'));
expect(error.message).toEqual(jasmine.stringMatching('0 unclassified changes found.'));
expect(error.message).toEqual(jasmine.stringMatching('3 breaking changes found.'));
expect(error.message).toEqual(jasmine.stringMatching('5 non-breaking changes found.'));
expect(error.message).toEqual(jasmine.stringMatching('1 unclassified changes found.'));
expect(error.message).toContain('Breaking: the path [host] was modified ' +
'from \'petstore.swagger.io\' to \'petstore.swagger.org\'');
'from \'petstore.swagger.io\' to \'petstore.swagger.org\'');
expect(error.message).toContain('Breaking: the path [basePath] was added with value \'/v2\'');
expect(error.message).toContain('Breaking: the value \'http\' was removed ' +
'from the array in the path [schemes/0]');
expect(error.message).toContain('Non-breaking: the path [swagger] was modified ' +
'from \'2.0\' to \'2.1\'');
'from \'2.0\' to \'2.1\'');
expect(error.message).toContain('Non-breaking: the path [info/version] was modified ' +
'from \'1.0.0\' to \'1.0.1\'');
'from \'1.0.0\' to \'1.0.1\'');
expect(error.message).toContain('Non-breaking: the path [info/license/name] was modified ' +
'from \'Apache 2.0\' to \'Apache 2.1\'');
expect(error.message).toContain('Non-breaking: the path [info/license/url] was added ' +
'with value \'http://www.apache.org/licenses/LICENSE-2.0.html\'');
expect(error.message).toContain('Non-breaking: the value \'https\' was added ' +
'to the array in the path [schemes/0]');
expect(error.message).toContain('Non-breaking: the value \'ws\' was added ' +
'to the array in the path [schemes/1]');
expect(error.message).toContain('Unclassified: the path [x-external-id] ' +
'with value \'some x value\' was removed');
expect(error.message).toEqual(jasmine.stringMatching('DANGER: Breaking changes found!'));

@@ -256,21 +270,33 @@

expect(result).toEqual(jasmine.stringMatching('0 breaking changes found.'));
expect(result).toEqual(jasmine.stringMatching('3 non-breaking changes found.'));
expect(result).toEqual(jasmine.stringMatching('2 unclassified changes found.'));
expect(result).toEqual(jasmine.stringMatching('5 non-breaking changes found.'));
expect(result).toEqual(jasmine.stringMatching('4 unclassified changes found.'));
expect(result).toContain('Non-breaking: the path [openapi] was modified ' +
'from \'3.0.0\' to \'3.0.0-RC1\'');
'from \'3.0.0\' to \'3.0.0-RC1\'');
expect(result).toContain('Non-breaking: the path [info/version] was modified ' +
'from \'Test version\' to \'New test version\'');
'from \'Test version\' to \'New test version\'');
expect(result).toContain('Non-breaking: the path [info/title] was modified ' +
'from \'Test API\' to \'New test API\'');
'from \'Test API\' to \'New test API\'');
expect(result).toContain('Non-breaking: the path [info/description] was added ' +
'with value \'Brand new spec description\'');
expect(result).toContain('Non-breaking: the path [info/license/url] with value ' +
'\'spec license url\' was removed');
expect(result).toContain('Unclassified: the path [info/x-info-property] was modified ' +
'from \'Some content\' to \'Some new content\'');
'from \'Some content\' to \'Some new content\'');
expect(result).toContain('Unclassified: the path [x-generic-property] was modified ' +
'from \'Some content\' to \'Some new content\'');
'from \'Some content\' to \'Some new content\'');
expect(result).toContain('Unclassified: the path [info/x-brand-new-property] was added ' +
'with value \'Some brand new content\'');
expect(result).toContain('Unclassified: the path [x-deleted-property] with value ' +
'\'Some deleted content\' was removed');
}).then(done, done.fail);
});
});
import jsonLoader from '../../../lib/openapi-diff/json-loader';
import {FileSystem, HttpClient} from '../../../lib/openapi-diff/types';
import fileSystemMockGenerator from '../support/file-system-mock-generator';

@@ -6,5 +7,9 @@ import httpClientMockGenerator from '../support/http-client-mock-generator';

describe('jsonLoader', () => {
let naiveFileSystem: FileSystem;
let naiveHttpClient: HttpClient;
const naiveFileSystem = fileSystemMockGenerator.createWithReturnValue('{}');
const naiveHttpClient = httpClientMockGenerator.createWithReturnValue('{}');
beforeEach(() => {
naiveFileSystem = fileSystemMockGenerator.createWithReturnValue('{}');
naiveHttpClient = httpClientMockGenerator.createWithReturnValue('{}');
});

@@ -11,0 +16,0 @@ describe('when the input location is a file', () => {

import specDiffer from '../../../lib/openapi-diff/spec-differ';
import {parsedSpecBuilder} from '../support/parsed-spec-builder';
import {
DiffChange,
ParsedSpec
} from '../../../lib/openapi-diff/types';
let result: DiffChange[];
describe('specDiffer', () => {
const buildParsedSpecWithoutBasePathProperty = (): ParsedSpec => {
const spec = {
info: {
title: 'spec title',
version: 'version'
},
openapi: {
originalPath: ['swagger'],
parsedValue: '2.0'
}
};
return spec;
};
describe('when there is an edition in the basePath property', () => {
beforeEach(() => {
const oldParsedSpec = buildParsedSpecWithoutBasePathProperty();
oldParsedSpec.basePath = 'basePath info';
const newParsedSpec = buildParsedSpecWithoutBasePathProperty();
newParsedSpec.basePath = 'NEW basePath info';
result = specDiffer.diff(oldParsedSpec, newParsedSpec);
});
it('should classify the change as a breaking edition in the basePath property', () => {
it('should classify the edition in the basePath property as breaking', () => {
expect(result.length).toEqual(1);
expect(result[0].severity).toEqual('breaking');
});
const oldParsedSpec = parsedSpecBuilder
.withBasePath('basePath info')
.build();
const newParsedSpec = parsedSpecBuilder
.withBasePath('NEW basePath info')
.build();
it('should locate the scope of the change in the basePath property', () => {
expect(result[0].scope).toEqual('basePath.property');
});
const result = specDiffer.diff(oldParsedSpec, newParsedSpec);
it('should populate the taxonomy and type of a change in the basePath property as an edition in it', () => {
expect(result[0].taxonomy).toEqual('basePath.property.edit');
expect(result[0].type).toEqual('edit');
expect(result.length).toEqual(1);
expect(result[0]).toEqual({
newValue: 'NEW basePath info',
oldValue: 'basePath info',
printablePath: ['basePath'],
scope: 'basePath',
severity: 'breaking',
taxonomy: 'basePath.edit',
type: 'edit'
});
});
it('should populate the paths of a single change in the basePath property correctly', () => {
expect(result[0].path[0]).toEqual('basePath');
expect(result[0].printablePath[0]).toEqual('basePath');
});
it('should copy the rest of the individual diff attributes across', () => {
expect(result[0].lhs).toEqual('basePath info');
expect(result[0].rhs).toEqual('NEW basePath info');
expect(result[0].index).toBeNull();
expect(result[0].item).toBeNull();
});
});

@@ -65,34 +34,24 @@

beforeEach(() => {
const oldParsedSpec = buildParsedSpecWithoutBasePathProperty();
const newParsedSpec = buildParsedSpecWithoutBasePathProperty();
newParsedSpec.basePath = 'NEW basePath info';
result = specDiffer.diff(oldParsedSpec, newParsedSpec);
});
it('should classify the change as a breaking addition of the basePath property', () => {
it('should classify the addition of the basePath property as breaking', () => {
expect(result.length).toEqual(1);
expect(result[0].severity).toEqual('breaking');
});
const oldParsedSpec = parsedSpecBuilder
.withNoBasePath()
.build();
const newParsedSpec = parsedSpecBuilder
.withBasePath('NEW basePath info')
.build();
it('should locate the scope of the change in the basePath property', () => {
expect(result[0].scope).toEqual('basePath.property');
});
const result = specDiffer.diff(oldParsedSpec, newParsedSpec);
it('should populate the taxonomy and type of a new basePath property as an addition', () => {
expect(result[0].taxonomy).toEqual('basePath.property.add');
expect(result[0].type).toEqual('add');
expect(result.length).toEqual(1);
expect(result[0]).toEqual({
newValue: 'NEW basePath info',
oldValue: undefined,
printablePath: ['basePath'],
scope: 'basePath',
severity: 'breaking',
taxonomy: 'basePath.add',
type: 'add'
});
});
it('should populate the paths of an added basePath property correctly', () => {
expect(result[0].path[0]).toEqual('basePath');
expect(result[0].printablePath[0]).toEqual('basePath');
});
it('should copy the rest of the individual diff attributes across', () => {
expect(result[0].lhs).toBeUndefined();
expect(result[0].rhs).toEqual('NEW basePath info');
expect(result[0].index).toBeNull();
expect(result[0].item).toBeNull();
});
});

@@ -102,35 +61,25 @@

beforeEach(() => {
const oldParsedSpec = buildParsedSpecWithoutBasePathProperty();
oldParsedSpec.basePath = 'OLD basePath info';
const newParsedSpec = buildParsedSpecWithoutBasePathProperty();
result = specDiffer.diff(oldParsedSpec, newParsedSpec);
});
it('should classify the change as a breaking deletion of the basePath property', () => {
it('should classify the addition of the basePath property as breaking', () => {
expect(result.length).toEqual(1);
expect(result[0].severity).toEqual('breaking');
});
const oldParsedSpec = parsedSpecBuilder
.withBasePath('OLD basePath info')
.build();
const newParsedSpec = parsedSpecBuilder
.withNoBasePath()
.build();
it('should locate the scope of the change in the basePath property', () => {
expect(result[0].scope).toEqual('basePath.property');
});
const result = specDiffer.diff(oldParsedSpec, newParsedSpec);
it('should populate the taxonomy and type of a new basePath property as a deletion', () => {
expect(result[0].taxonomy).toEqual('basePath.property.delete');
expect(result[0].type).toEqual('delete');
expect(result.length).toEqual(1);
expect(result[0]).toEqual({
newValue: undefined,
oldValue: 'OLD basePath info',
printablePath: ['basePath'],
scope: 'basePath',
severity: 'breaking',
taxonomy: 'basePath.delete',
type: 'delete'
});
});
it('should populate the paths of an added basePath property correctly', () => {
expect(result[0].path[0]).toEqual('basePath');
expect(result[0].printablePath[0]).toEqual('basePath');
});
it('should copy the rest of the individual diff attributes across', () => {
expect(result[0].lhs).toEqual('OLD basePath info');
expect(result[0].rhs).toBeUndefined();
expect(result[0].index).toBeNull();
expect(result[0].item).toBeNull();
});
});
});
import specDiffer from '../../../lib/openapi-diff/spec-differ';
import {parsedSpecBuilder} from '../support/parsed-spec-builder';
import {
DiffChange,
ParsedSpec
} from '../../../lib/openapi-diff/types';
let results: DiffChange[];
describe('specDiffer', () => {
const buildParsedSpecWithoutHostProperty = (): ParsedSpec => {
const spec = {
info: {
title: 'spec title',
version: 'version'
},
openapi: {
originalPath: ['swagger'],
parsedValue: '2.0'
}
};
return spec;
};
describe('when there is an edition in the host property', () => {
beforeEach(() => {
const oldParsedSpec = buildParsedSpecWithoutHostProperty();
oldParsedSpec.host = 'host info';
const newParsedSpec = buildParsedSpecWithoutHostProperty();
newParsedSpec.host = 'NEW host info';
results = specDiffer.diff(oldParsedSpec, newParsedSpec);
});
it('should classify the change as a breaking edition in the host property', () => {
it('should classify the edition in the host property as breaking', () => {
expect(results.length).toEqual(1);
expect(results[0].severity).toEqual('breaking');
});
const oldParsedSpec = parsedSpecBuilder
.withHost('host info')
.build();
const newParsedSpec = parsedSpecBuilder
.withHost('NEW host info')
.build();
it('should locate the scope of the change in the host property', () => {
expect(results[0].scope).toEqual('host.property');
});
const result = specDiffer.diff(oldParsedSpec, newParsedSpec);
it('should populate the taxonomy and type of a change in the host property as an edition in it', () => {
expect(results[0].taxonomy).toEqual('host.property.edit');
expect(results[0].type).toEqual('edit');
expect(result.length).toEqual(1);
expect(result[0]).toEqual({
newValue: 'NEW host info',
oldValue: 'host info',
printablePath: ['host'],
scope: 'host',
severity: 'breaking',
taxonomy: 'host.edit',
type: 'edit'
});
});
it('should populate the paths of a single change in the host property correctly', () => {
expect(results[0].path[0]).toEqual('host');
expect(results[0].printablePath[0]).toEqual('host');
});
it('should copy the rest of the individual diff attributes across', () => {
expect(results[0].lhs).toEqual('host info');
expect(results[0].rhs).toEqual('NEW host info');
expect(results[0].index).toBeNull();
expect(results[0].item).toBeNull();
});
});

@@ -65,34 +34,24 @@

beforeEach(() => {
const oldParsedSpec = buildParsedSpecWithoutHostProperty();
const newParsedSpec = buildParsedSpecWithoutHostProperty();
newParsedSpec.host = 'NEW host info';
results = specDiffer.diff(oldParsedSpec, newParsedSpec);
});
it('should classify the change as a breaking addition of the host property', () => {
it('should classify the addition of the host property as breaking', () => {
expect(results.length).toEqual(1);
expect(results[0].severity).toEqual('breaking');
});
const oldParsedSpec = parsedSpecBuilder
.withNoHost()
.build();
const newParsedSpec = parsedSpecBuilder
.withHost('NEW host info')
.build();
it('should locate the scope of the change in the host property', () => {
expect(results[0].scope).toEqual('host.property');
});
const result = specDiffer.diff(oldParsedSpec, newParsedSpec);
it('should populate the taxonomy and type of a new host property as an addition', () => {
expect(results[0].taxonomy).toEqual('host.property.add');
expect(results[0].type).toEqual('add');
expect(result.length).toEqual(1);
expect(result[0]).toEqual({
newValue: 'NEW host info',
oldValue: undefined,
printablePath: ['host'],
scope: 'host',
severity: 'breaking',
taxonomy: 'host.add',
type: 'add'
});
});
it('should populate the paths of an added host property correctly', () => {
expect(results[0].path[0]).toEqual('host');
expect(results[0].printablePath[0]).toEqual('host');
});
it('should copy the rest of the individual diff attributes across', () => {
expect(results[0].lhs).toBeUndefined();
expect(results[0].rhs).toEqual('NEW host info');
expect(results[0].index).toBeNull();
expect(results[0].item).toBeNull();
});
});

@@ -102,35 +61,25 @@

beforeEach(() => {
const oldParsedSpec = buildParsedSpecWithoutHostProperty();
oldParsedSpec.host = 'OLD host info';
const newParsedSpec = buildParsedSpecWithoutHostProperty();
results = specDiffer.diff(oldParsedSpec, newParsedSpec);
});
it('should classify the change as a breaking deletion of the host property', () => {
it('should classify the addition of the host property as breaking', () => {
expect(results.length).toEqual(1);
expect(results[0].severity).toEqual('breaking');
});
const oldParsedSpec = parsedSpecBuilder
.withHost('OLD host info')
.build();
const newParsedSpec = parsedSpecBuilder
.withNoHost()
.build();
it('should locate the scope of the change in the host property', () => {
expect(results[0].scope).toEqual('host.property');
});
const result = specDiffer.diff(oldParsedSpec, newParsedSpec);
it('should populate the taxonomy and type of a new host property as a deletion', () => {
expect(results[0].taxonomy).toEqual('host.property.delete');
expect(results[0].type).toEqual('delete');
expect(result.length).toEqual(1);
expect(result[0]).toEqual({
newValue: undefined,
oldValue: 'OLD host info',
printablePath: ['host'],
scope: 'host',
severity: 'breaking',
taxonomy: 'host.delete',
type: 'delete'
});
});
it('should populate the paths of an added host property correctly', () => {
expect(results[0].path[0]).toEqual('host');
expect(results[0].printablePath[0]).toEqual('host');
});
it('should copy the rest of the individual diff attributes across', () => {
expect(results[0].lhs).toEqual('OLD host info');
expect(results[0].rhs).toBeUndefined();
expect(results[0].index).toBeNull();
expect(results[0].item).toBeNull();
});
});
});
import specParser from '../../../lib/openapi-diff/spec-parser';
import {OpenAPI3Spec, ParsedSpec} from '../../../lib/openapi-diff/types';
import {openApi3SpecBuilder, openApi3SpecInfoBuilder} from '../support/openapi-3-spec-builder';
import {parsedSpecBuilder, parsedSpecInfoBuilder} from '../support/parsed-spec-builder';
import {swagger2SpecBuilder, swagger2SpecInfoBuilder} from '../support/swagger-2-spec-builder';
let resultingSpec: ParsedSpec;
describe('specParser, with regards to the info object,', () => {
const buildSimpleOpenApi3Spec = (): OpenAPI3Spec => {
const spec = {
info: {
title: 'spec title',
version: 'spec version'
},
openapi: '3.0.0'
};
return spec;
};
describe('when the input spec is in Swagger 2.0 format', () => {
const buildOpenApi3SpecWithCompleteInfoObject = (): OpenAPI3Spec => {
const spec = {
info: {
contact: {
email: 'contact email',
name: 'contact name',
url: 'contact url'
},
description: 'spec description',
licence: {
name: 'licence name',
url: 'licence url'
},
termsOfService: 'terms of service',
title: 'spec title',
version: 'version'
},
openapi: '3.0.0'
};
return spec;
};
describe('and the info object is minimal', () => {
describe('when the original spec has all the default fields populated', () => {
it('should generate a parsed spec copying across the info object properties and their values', () => {
beforeEach(() => {
const originalSpec = buildOpenApi3SpecWithCompleteInfoObject();
resultingSpec = specParser.parse(originalSpec);
const originalSpec = swagger2SpecBuilder
.withInfoObject(swagger2SpecInfoBuilder
.withTitle('spec title')
.withVersion('spec version'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withInfoObject(parsedSpecInfoBuilder
.withTitle('spec title')
.withVersion('spec version'))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
it('should generate a parsed spec with an info object', () => {
expect(resultingSpec.info).toBeDefined();
describe('and the info object is complete at the primitive level', () => {
it('should generate a parsed spec copying across the info object properties and their values', () => {
const originalSpec = swagger2SpecBuilder
.withInfoObject(swagger2SpecInfoBuilder
.withDescription('spec description')
.withTermsOfService('spec terms'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withInfoObject(parsedSpecInfoBuilder
.withDescription('spec description')
.withTermsOfService('spec terms'))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
it('should generate a parsed spec copying across all the default fields to the info object', () => {
expect(resultingSpec.info.title).toBe('spec title');
expect(resultingSpec.info.description).toBe('spec description');
expect(resultingSpec.info.termsOfService).toBe('terms of service');
describe('and the info object is complete at the object level', () => {
if (resultingSpec.info.contact) {
expect(resultingSpec.info.contact.name).toBe('contact name');
expect(resultingSpec.info.contact.url).toBe('contact url');
expect(resultingSpec.info.contact.email).toBe('contact email');
} else {
fail('info contact object was not defined when it should');
}
it('should generate a parsed spec copying across the info object properties and their values', () => {
if (resultingSpec.info.licence) {
expect(resultingSpec.info.licence.name).toBe('licence name');
expect(resultingSpec.info.licence.url).toBe('licence url');
expect(resultingSpec.info.version).toBe('version');
} else {
fail('info licence object was not defined when it should');
}
const originalSpec = swagger2SpecBuilder
.withInfoObject(swagger2SpecInfoBuilder
.withContact('contact email', 'contact name', 'contact url')
.withLicense('license name', 'license url'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withInfoObject(parsedSpecInfoBuilder
.withContact({
name: 'email',
originalPath: ['info', 'contact', 'email'],
value: 'contact email'
}, {
name: 'name',
originalPath: ['info', 'contact', 'name'],
value: 'contact name'
}, {
name: 'url',
originalPath: ['info', 'contact', 'url'],
value: 'contact url'
})
.withLicense({
name: 'name',
originalPath: ['info', 'license', 'name'],
value: 'license name'
}, {
name: 'url',
originalPath: ['info', 'license', 'url'],
value: 'license url'
}))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
describe('and the original spec has an x-property included in the info object', () => {
it('should generate a parsed spec copying across the x-property and its value', () => {
const originalSpec = swagger2SpecBuilder
.withInfoObject(swagger2SpecInfoBuilder
.withXProperty('external-id', 'some id'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withInfoObject(parsedSpecInfoBuilder
.withXProperty({
name: 'x-external-id',
originalPath: ['info', 'x-external-id'],
value: 'some id'
}))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
});
describe('when the original spec has only the required fields populated', () => {
describe('when the input spec is in OpenApi 3.0.0 format', () => {
beforeEach(() => {
const originalSpec = buildSimpleOpenApi3Spec();
resultingSpec = specParser.parse(originalSpec);
describe('and the info object is minimal', () => {
it('should generate a parsed spec copying across the info object properties and their values', () => {
const originalSpec = openApi3SpecBuilder
.withInfoObject(openApi3SpecInfoBuilder
.withTitle('spec title')
.withVersion('spec version'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withInfoObject(parsedSpecInfoBuilder
.withTitle('spec title')
.withVersion('spec version'))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
it('should generate a parsed spec with an info object', () => {
expect(resultingSpec.info).toBeDefined();
describe('and the info object is complete at the primitive level', () => {
it('should generate a parsed spec copying across the info object properties and their values', () => {
const originalSpec = openApi3SpecBuilder
.withInfoObject(openApi3SpecInfoBuilder
.withDescription('spec description')
.withTermsOfService('spec terms'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withInfoObject(parsedSpecInfoBuilder
.withDescription('spec description')
.withTermsOfService('spec terms'))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
it('should generate a parsed spec copying across only the required fields to the info object', () => {
expect(resultingSpec.info.title).toBe('spec title');
expect(resultingSpec.info.version).toBe('spec version');
describe('and the info object is complete at the object level', () => {
it('should generate a parsed spec copying across the info object properties and their values', () => {
const originalSpec = openApi3SpecBuilder
.withInfoObject(openApi3SpecInfoBuilder
.withContact('contact email', 'contact name', 'contact url')
.withLicense('license name', 'license url'))
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withInfoObject(parsedSpecInfoBuilder
.withContact({
name: 'email',
originalPath: ['info', 'contact', 'email'],
value: 'contact email'
}, {
name: 'name',
originalPath: ['info', 'contact', 'name'],
value: 'contact name'
}, {
name: 'url',
originalPath: ['info', 'contact', 'url'],
value: 'contact url'
})
.withLicense({
name: 'name',
originalPath: ['info', 'license', 'name'],
value: 'license name'
}, {
name: 'url',
originalPath: ['info', 'license', 'url'],
value: 'license url'
}))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});

@@ -94,16 +208,23 @@ });

beforeEach(() => {
const originalSpec = buildSimpleOpenApi3Spec();
originalSpec.info['x-external-id'] = 'some id';
resultingSpec = specParser.parse(originalSpec);
});
it('should generate a parsed spec copying across the x-property and its value', () => {
it('should generate a parsed spec with an info object', () => {
expect(resultingSpec.info).toBeDefined();
});
const originalSpec = openApi3SpecBuilder
.withInfoObject(openApi3SpecInfoBuilder
.withXProperty('external-id', 'some id'))
.build();
it('should generate a parsed spec copying across the x-property and its value', () => {
expect(resultingSpec.info['x-external-id']).toBe('some id');
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withInfoObject(parsedSpecInfoBuilder
.withXProperty({
name: 'x-external-id',
originalPath: ['info', 'x-external-id'],
value: 'some id'
}))
.build();
expect(actualResult.info).toEqual(expectedResult.info);
});
});
});
import specParser from '../../../lib/openapi-diff/spec-parser';
import { openApi3SpecBuilder } from '../support/openapi-3-spec-builder';
import { parsedSpecBuilder } from '../support/parsed-spec-builder';
import { swagger2SpecBuilder } from '../support/swagger-2-spec-builder';
import {
OpenAPI3Spec,
ParsedSpec,
Swagger2Spec
} from '../../../lib/openapi-diff/types';
let resultingSpec: ParsedSpec;
describe('specParser, with regards to the swagger/openapi object,', () => {
const buildSimpleSwagger2Spec = (): Swagger2Spec => {
const spec = {
info: {
title: 'spec title',
version: 'spec version'
},
swagger: '2.0'
};
return spec;
};
const buildSimpleOpenApi3Spec = (): OpenAPI3Spec => {
const spec = {
info: {
title: 'spec title',
version: 'spec version'
},
openapi: '3.0.0'
};
return spec;
};
describe('when the input spec is in Swagger 2.0 format', () => {
beforeEach(() => {
const originalSpec = buildSimpleSwagger2Spec();
resultingSpec = specParser.parse(originalSpec);
});
it('should generate a parsed spec copying across the swagger property and its value', () => {
it('should generate a parsed spec with an openapi object', () => {
expect(resultingSpec.openapi).toBeDefined();
});
const originalSpec = swagger2SpecBuilder
.build();
it('should generate a parsed spec copying across the value of the swagger property', () => {
expect(resultingSpec.openapi.parsedValue).toEqual('2.0');
});
const actualResult = specParser.parse(originalSpec);
it('should generate a parsed spec preserving the original path of the swagger property', () => {
expect(resultingSpec.openapi.originalPath).toEqual(['swagger']);
const expectedResult = parsedSpecBuilder
.withSwagger2()
.build();
expect(actualResult.openapi).toEqual(expectedResult.openapi);
});

@@ -57,19 +26,15 @@ });

beforeEach(() => {
const originalSpec = buildSimpleOpenApi3Spec();
resultingSpec = specParser.parse(originalSpec);
});
it('should generate a parsed spec copying across the openapi property and its value', () => {
it('should generate a parsed spec with an openapi object', () => {
expect(resultingSpec.openapi).toBeDefined();
});
const originalSpec = openApi3SpecBuilder
.build();
it('should generate a parsed spec copying across the value of the openapi property', () => {
expect(resultingSpec.openapi.parsedValue).toEqual('3.0.0');
});
const actualResult = specParser.parse(originalSpec);
it('should generate a parsed spec preserving the original path of the openapi property', () => {
expect(resultingSpec.openapi.originalPath).toEqual(['openapi']);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.build();
expect(actualResult.openapi).toEqual(expectedResult.openapi);
});
});
});
import specParser from '../../../lib/openapi-diff/spec-parser';
import {OpenAPI3Spec, ParsedSpec, Swagger2Spec} from '../../../lib/openapi-diff/types';
import { openApi3SpecBuilder } from '../support/openapi-3-spec-builder';
import { parsedSpecBuilder } from '../support/parsed-spec-builder';
import { swagger2SpecBuilder } from '../support/swagger-2-spec-builder';

@@ -8,67 +10,169 @@ describe('specParser, with regards to the top level object,', () => {

it('should generate a parsed spec copying across the x-property and its value', () => {
describe('and it is in Swagger 2 format', () => {
const originalSpec: OpenAPI3Spec = {
info: {
title: 'spec title',
version: 'version'
},
openapi: '3.0.0',
'x-external-id': 'some id',
'x-internal-id': 'some other id'
};
const resultingSpec: ParsedSpec = specParser.parse(originalSpec);
expect(resultingSpec['x-external-id']).toBe('some id');
expect(resultingSpec['x-internal-id']).toBe('some other id');
it('should generate a parsed spec copying across the x-property and its value', () => {
const originalSpec = swagger2SpecBuilder
.withTopLevelXProperty({
key: 'x-external-id',
value: 'some external id'
})
.withTopLevelXProperty({
key: 'x-internal-id',
value: 'some internal id'
})
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withTopLevelXProperty({
name: 'x-external-id',
originalPath: ['x-external-id'],
value: 'some external id'
})
.withTopLevelXProperty({
name: 'x-internal-id',
originalPath: ['x-internal-id'],
value: 'some internal id'
})
.build();
expect(actualResult.xProperties).toEqual(expectedResult.xProperties);
});
});
describe('and it is in OpenApi 3 format', () => {
it('should generate a parsed spec copying across the x-property and its value', () => {
const originalSpec = openApi3SpecBuilder
.withTopLevelXProperty({
key: 'x-external-id',
value: 'some external id'
})
.withTopLevelXProperty({
key: 'x-internal-id',
value: 'some internal id'
})
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withTopLevelXProperty({
name: 'x-external-id',
originalPath: ['x-external-id'],
value: 'some external id'
})
.withTopLevelXProperty({
name: 'x-internal-id',
originalPath: ['x-internal-id'],
value: 'some internal id'
})
.build();
expect(actualResult.xProperties).toEqual(expectedResult.xProperties);
});
});
});
describe('when the original spec is in Swagger 2 format', () => {
describe('with regards to the host property', () => {
describe('with regards to the host property', () => {
describe('and it is in Swagger 2 format', () => {
const originalSpec: Swagger2Spec = {
host: 'some host url',
info: {
title: 'spec title',
version: 'version'
},
swagger: '2.0'
};
it('should generate a parsed spec copying across the host property and its value when present', () => {
it('should generate a parsed spec copying across the host property and its value when present', () => {
const resultingSpec: ParsedSpec = specParser.parse(originalSpec);
expect(resultingSpec.host).toBe('some host url');
const originalSpec = swagger2SpecBuilder
.withHost('some host url')
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withHost('some host url')
.build();
expect(actualResult.host).toEqual(expectedResult.host);
});
it('should generate a parsed spec without host property when not present', () => {
delete(originalSpec.host);
const resultingSpec: ParsedSpec = specParser.parse(originalSpec);
expect(resultingSpec.host).not.toBeDefined();
it('should generate a parsed spec with undefined value for host property when not present', () => {
const originalSpec = swagger2SpecBuilder
.withNoHost()
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withNoHost()
.build();
expect(actualResult.host).toEqual(expectedResult.host);
});
});
describe('with regards to the basePath property', () => {
describe('and it is in OpenApi 3 format', () => {
const originalSpec: Swagger2Spec = {
basePath: 'some basePath info',
info: {
title: 'spec title',
version: 'version'
},
swagger: '2.0'
};
it('should generate a parsed spec with undefined value for the host property ', () => {
it('should generate a parsed spec copying across the basePath property and its value', () => {
const resultingSpec: ParsedSpec = specParser.parse(originalSpec);
expect(resultingSpec.basePath).toBe('some basePath info');
const originalSpec = openApi3SpecBuilder.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withNoHost()
.build();
expect(actualResult.host).toEqual(expectedResult.host);
});
});
});
it('should generate a parsed spec without basepath property when not present', () => {
delete(originalSpec.basePath);
const resultingSpec: ParsedSpec = specParser.parse(originalSpec);
expect(resultingSpec.basePath).not.toBeDefined();
describe('with regards to the basePath property', () => {
describe('and it is in Swagger 2 format', () => {
it('should generate a parsed spec copying across the basePath property and value when present', () => {
const originalSpec = swagger2SpecBuilder
.withBasePath('some basePath info')
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withBasePath('some basePath info')
.build();
expect(actualResult.basePath).toEqual(expectedResult.basePath);
});
it('should generate a parsed spec with undefined value for basePath property when not present', () => {
const originalSpec = swagger2SpecBuilder
.withNoBasePath()
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withNoBasePath()
.build();
expect(actualResult.basePath).toEqual(expectedResult.basePath);
});
});
describe('and it is in OpenApi 3 format', () => {
it('should generate a parsed spec with undefined value for the basePath property ', () => {
const originalSpec = openApi3SpecBuilder
.build();
const actualResult = specParser.parse(originalSpec);
const expectedResult = parsedSpecBuilder
.withOpenApi3()
.withNoBasePath()
.build();
expect(actualResult.basePath).toEqual(expectedResult.basePath);
});
});
});
});

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc