Skip to content

Commit a071d42

Browse files
committed
validate matching {} in templates. More linter rules
1 parent a501561 commit a071d42

File tree

3 files changed

+104
-6
lines changed

3 files changed

+104
-6
lines changed

linter/linter.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,24 @@ function loadRules(s) {
1212
let data = fs.readFileSync(s,'utf8');
1313
let obj = yaml.safeLoad(data,{json:true});
1414
rules = Object.assign({},rules,obj);
15+
for (let r in rules) {
16+
let rule = rules[r];
17+
if (!Array.isArray(rule.object)) rule.object = [ rule.object ];
18+
if (rule.truthy && !Array.isArray(rule.truthy)) rule.truthy = [ rule.truthy ];
19+
if (!rule.enabled) delete rules[r];
20+
}
1521
}
1622

1723
function lint(objectName,object,options) {
1824
for (let r in rules) {
1925
let rule = rules[r];
20-
if (rule.enabled && rule.object === objectName) {
26+
if ((rule.object[0] === '*') || (rule.object.indexOf(objectName)>=0)) {
2127
options.lintRule = rule;
2228
if (rule.truthy) {
23-
object.should.have.property(rule.truthy);
24-
object[rule.truthy].should.not.be.empty();
29+
for (let property of rule.truthy) {
30+
object.should.have.property(property);
31+
object[property].should.not.be.empty();
32+
}
2533
}
2634
if (rule.properties) {
2735
should(Object.keys(object).length).be.exactly(rule.properties);
@@ -37,12 +45,36 @@ function lint(objectName,object,options) {
3745
let found = false;
3846
for (let property of rule.xor) {
3947
if (typeof object[property] !== 'undefined') {
40-
if (found) should.fail(rule.description);
48+
if (found) should.fail(true,false,rule.description);
4149
found = true;
4250
}
4351
}
4452
found.should.be.exactly(true,rule.description);
4553
}
54+
if (rule.pattern) {
55+
let components = [];
56+
if (rule.pattern.split) {
57+
components = object[rule.pattern.property].split(rule.pattern.split);
58+
}
59+
else {
60+
components.push(object[rule.pattern.property]);
61+
}
62+
let re = new RegExp(rule.pattern.value);
63+
for (let component of components) {
64+
if (rule.pattern.omit) component = component.split(rule.pattern.omit).join('');
65+
if (component) {
66+
should(re.test(component)).be.exactly(true,rule.description);
67+
}
68+
}
69+
}
70+
if (rule.notContain) {
71+
for (let property of rule.notContain.properties) {
72+
if (object[property] && (typeof object[property] === 'string') &&
73+
(object[property].indexOf(rule.notContain.value)>=0)) {
74+
should.fail(true,false,rule.description);
75+
}
76+
}
77+
}
4678
}
4779
}
4880
delete options.lintRule;

linter/rules.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,61 @@
6969
"enabled": true,
7070
"description": "example should have either value or externalValue",
7171
"xor": ["value", "externalValue"]
72+
},
73+
"11": {
74+
"name": "reference-components-regex",
75+
"object": "reference",
76+
"enabled": true,
77+
"description": "reference components should all match spec. regex",
78+
"pattern": { "property": "$ref", "omit": "#", "split": "/", "value": "^[a-zA-Z0-9\\.\\-_]+$" }
79+
},
80+
"12": {
81+
"name":"no-script-tags-in-markdown",
82+
"object": "*",
83+
"enabled": true,
84+
"description": "markdown descriptions should not contain <script> tags",
85+
"notContain": { "properties": [ "description" ], "value": "<script" }
86+
},
87+
"13": {
88+
"name":"info-contact",
89+
"object": "info",
90+
"enabled": true,
91+
"description": "info object should contain contact object",
92+
"truthy": "contact"
93+
},
94+
"14": {
95+
"name":"contact-properties",
96+
"object": "contact",
97+
"enabled": true,
98+
"description": "contact object should have name, url and email",
99+
"truthy": [ "name", "url", "email" ]
100+
},
101+
"15": {
102+
"name":"license-url",
103+
"object": "license",
104+
"enabled": true,
105+
"description": "license object should include url",
106+
"truthy": "url"
107+
},
108+
"16": {
109+
"name":"server-not-example.com",
110+
"object": "server",
111+
"enabled": true,
112+
"description": "server url should not point at example.com",
113+
"notContain": { "properties": [ "url" ], "value": "example.com" }
114+
},
115+
"17": {
116+
"name":"license-apimatic-bug",
117+
"object": "license",
118+
"enabled": true,
119+
"description": "license url should not point at gruntjs",
120+
"notContain": { "properties": [ "url" ], "value": "gruntjs" }
121+
},
122+
"18": {
123+
"name":"no-eval-in-descriptions",
124+
"object": "*",
125+
"enabled": true,
126+
"description": "markdown descriptions should not contain 'eval('",
127+
"notContain": { "properties": [ "description","title" ], "value": "eval(" }
72128
}
73129
}

validate.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,8 @@ function checkExample(ex, contextServers, openapi, options) {
328328
function checkContent(content, contextServers, openapi, options) {
329329
contextAppend(options, 'content');
330330
for (let ct in content) {
331-
// TODO validate ct against https://tools.ietf.org/html/rfc6838#section-4.2
332-
should(ct.indexOf('/')).be.greaterThan(-1,'content-type must match RFC 6838');
331+
// validate ct against https://tools.ietf.org/html/rfc6838#section-4.2
332+
should(/[a-zA-Z0-9!#$%^&\*_\-\+{}\|'.`~]+\/[a-zA-Z0-9!#$%^&\*_\-\+{}\|'.`~]+/.test(ct)).be.exactly(true,'media-type should match RFC6838 format'); // this is a SHOULD not MUST
333333
contextAppend(options, jptr.jpescape(ct));
334334
var contentType = content[ct];
335335
should(contentType).be.an.Object();
@@ -828,6 +828,7 @@ function validateSync(openapi, options, callback) {
828828
openapi.info.license.url.should.not.be.empty();
829829
(function () { validateUrl(openapi.info.license.url, contextServers, 'license.url', options) }).should.not.throw();
830830
}
831+
if (options.lint) options.linter('license',openapi.info.license,options);
831832
options.context.pop();
832833
}
833834
if (typeof openapi.info.termsOfService !== 'undefined') {
@@ -846,8 +847,10 @@ function validateSync(openapi, options, callback) {
846847
openapi.info.contact.email.should.have.type('string');
847848
should(openapi.info.contact.email.indexOf('@')).be.greaterThan(-1,'Contact email must be a valid email address');
848849
}
850+
if (options.lint) options.linter('contact',openapi.info.contact,options);
849851
options.context.pop();
850852
}
853+
if (options.lint) options.linter('info',openapi.info,options);
851854
options.context.pop();
852855

853856
var contextServers = [];
@@ -991,6 +994,13 @@ function validateSync(openapi, options, callback) {
991994
should.fail(false,true,'Identical path templates detected');
992995
}
993996
paths[template] = {};
997+
let templateCheck = p.replace(/\{(.+?)\}/g, function (match, group1) {
998+
return '';
999+
});
1000+
if ((templateCheck.indexOf('{')>=0) || (templateCheck.indexOf('}')>=0)) {
1001+
should.fail(false,true,'Mismatched {} in path template');
1002+
}
1003+
9941004
checkPathItem(openapi.paths[p], p, openapi, options);
9951005
}
9961006
options.context.pop();

0 commit comments

Comments
 (0)