Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Options:
--inverse, -i output filtered elements only [boolean]
--flags, -f flags to filter by [array] [default: ["x-internal"]]
--flagValues, -v flag String values to match [array] [default: []]
--scopes filter based upon oauth security scheme scopes, instead of
'x-internal' flags [array]
--checkTags filter if flags given in --flags are in the tags array
[boolean]
--overrides, -o prefixes used to override named properties[arr] [default: []]
Expand All @@ -54,6 +56,12 @@ use `--` to separate flags or other array options from following options, i.e.:

or

`openapi-filter --flags x-private x-hidden -- source.yaml target.json`

for the target to be written as a JSON file

or

```javascript
let openapiFilter = require('openapi-filter');
let options = {}; // defaults are shown
Expand Down
22 changes: 21 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,34 @@ const recurse = require('reftools/lib/recurse.js').recurse;
const clone = require('reftools/lib/clone.js').clone;
const jptr = require('reftools/lib/jptr.js').jptr;

const httpOperations = [ 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace' ];

function securityIncludesScope(securitySchemes, scope) {

// Make sure we've been given an array of securitySchemes
if(!Array.isArray(securitySchemes))
return false;

return securitySchemes.some(scheme => {
return Object.keys(scheme).some(key => {
return Object.values(scheme[key]).some(value => value == scope);
});
});
}
function filter(obj,options) {

const defaults = {};
defaults.flags = ['x-internal'];
defaults.flagValues = [];
defaults.checkTags = false;
defaults.checkScopes = false;
defaults.inverse = false;
defaults.strip = false;
defaults.overrides = [];
options = Object.assign({},defaults,options);

const globalSecurity=obj.security || null;

let src = clone(obj);
let filtered = {};
let filteredpaths = [];
Expand All @@ -29,7 +46,10 @@ function filter(obj,options) {
}

for (let flag of options.flags) {
if ((options.checkTags == false && (obj[key] && ((options.flagValues.length == 0 && obj[key][flag]) || options.flagValues.includes(obj[key][flag])))) || (options.checkTags && (obj[key] && obj[key].tags && Array.isArray(obj[key].tags) && obj[key].tags.includes(flag)))) {
if ((options.checkTags == false && options.checkScopes == false && (obj[key] && ((options.flagValues.length == 0 && obj[key][flag]) || options.flagValues.includes(obj[key][flag])))) ||
(options.checkTags && (obj[key] && obj[key].tags && Array.isArray(obj[key].tags) && obj[key].tags.includes(flag))) ||
(options.checkScopes && (obj[key] && obj[key].security && securityIncludesScope(obj[key].security, flag))) ||
(options.checkScopes && (httpOperations.includes(key) && !obj[key].security && globalSecurity && securityIncludesScope(globalSecurity, flag)))) {
if (options.inverse) {
if (options.strip) {
delete obj[key][flag];
Expand Down
97 changes: 91 additions & 6 deletions openapi-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ const fs = require('fs');
const yaml = require('yaml');
const {strOptions} = require('yaml/types');
const openapiFilter = require('./index.js');
const recurse = require('reftools/lib/recurse.js').recurse;

let argv = require('yargs')
.usage('$0 <infile> [outfile]',false,(yargs) => {
yargs
.positional('infile',{ describe: 'the input file' })
.positional('outfile',{ describe: 'the output file' })
.positional('outfile',{ describe: 'the output file, can be .yaml or .json' })
})
.strict()
.boolean('info')
Expand All @@ -28,6 +29,8 @@ let argv = require('yargs')
.array('flagValues')
.alias('flagValues', 'v')
.describe('flagValues', 'flag string values to consider as a filter match (in addition to matching on boolean types)')
.array('scopes')
.describe('scopes', 'filter based upon oauth security scheme scopes, instead of \'x-internal\' flags')
.default('flagValues', [])
.boolean('checkTags')
.describe('checkTags', 'filter if flags given in --flags are in the tags array')
Expand Down Expand Up @@ -55,6 +58,12 @@ let argv = require('yargs')
.version()
.argv;

// --scopes on the cmdline turns on checkScopes options, and the listed scopes move to flags; to make the code simpler...
if(argv.scopes) {
argv.flags = argv.scopes;
argv.checkScopes = true;
}

// Helper function to display info message, depending on the verbose level
function info(msg) {
if (argv.verbose >= 1) {
Expand Down Expand Up @@ -84,13 +93,90 @@ info('Input file: ' + argv.infile)

let s = fs.readFileSync(argv.infile,'utf8');
let obj = yaml.parse(s, {maxAliasCount: argv.maxAliasCount});
let res = openapiFilter.filter(obj,argv);
if (argv.infile.indexOf('.json')>=0) {
s = JSON.stringify(res,null,2);
let oasSpec = openapiFilter.filter(obj,argv);

// All that filtering, may mean we have to prune the /components/schemas and tags....
let usedSchemas = [];
var additions=0;

// go looking for $refs,put the target schema into the list...
let checkRef = (obj, key, state) => {
if(obj.$ref && obj.$ref.startsWith('#/components/schemas/')) {

if(! usedSchemas.includes(obj.$ref.substring(21))) {
usedSchemas.push(obj.$ref.substring(21));
additions++; // keep track of additions made (in GLOBAL variable, naughty, naughty)
}
}
};

// check oasSpec.paths to get the set of components/schemas that're actually used...
Object.keys(oasSpec.paths).forEach((key, ndx, keys) => {
recurse(oasSpec.paths[key], {}, checkRef);
});


// check all the various components sections (parameters, responses, requestBodies, callbacks, etc)
// for more $refs to components/schemas
Object.keys(oasSpec.components).forEach((section, sNdx, sections) => {

// don't do component/schemas ... that has to be done carefully in just a sec
if(section != 'schemas') {

Object.keys(oasSpec.components[section]).forEach((key, kNdx, keys) => {
recurse(oasSpec.components[section][key], {}, checkRef);
});
}
});

// go looking for $refs in the set of usedSchemas...and augment the set of usedSchemas as appropriate
do { // keep going until we don't add any more schemas; cycles will break this.
additions = 0;
usedSchemas.forEach((key, ndx, keys) => {
recurse(oasSpec.components.schemas[key], {}, checkRef);
});
} while (additions > 0);


// prune the set of schemas in the original oasSpec, to be only the ones we're using...
Object.keys(oasSpec.components.schemas).forEach((key, ndx, keys) => {

if(! usedSchemas.includes(key)) {
delete oasSpec.components.schemas[key];
}
});

// Now check for tags...
let usedTagNames = []
let checkTags = (obj, key, state) => {
if(obj.tags) {
obj.tags.forEach((tag, tNdx, tags) => {
if(! usedTagNames.includes(tag)) {
usedTagNames.push(tag);
}
});
}
};
// check oasSpec.paths to get the set of tags that're actually used...
Object.keys(oasSpec.paths).forEach((key, ndx, keys) => {
recurse(oasSpec.paths[key], {}, checkTags);
});

// assemble a new set of tags, to be only the ones we're using...
let newTagDefns = [];
oasSpec.tags.forEach((tag, tNdx, tags) => {
if(usedTagNames.includes(tag.name) || tag.name.endsWith("Overview"))
newTagDefns.push(tag);
});
oasSpec.tags = newTagDefns;

// Now write out the result in either YAML or JSON
if (argv.infile.indexOf('.json')>=0 || (argv.outfile && argv.outfile.indexOf('.json')>=0)) {
s = JSON.stringify(oasSpec,null,2);
}
else {
strOptions.fold.lineWidth = argv.lineWidth;
s = yaml.stringify(res);
s = yaml.stringify(oasSpec);
}
if (argv.outfile) {
fs.writeFileSync(argv.outfile,s,'utf8');
Expand All @@ -102,4 +188,3 @@ else {
}

info('\n✅ Document was filtered successfully')

Loading