api-quality-spectral-ruleset 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Validates that URL path segments use clear, unambiguous names
3
+ * @param {string} given - The path key (e.g. "/users/{id}/items")
4
+ * @param {object} options - Function options
5
+ * @param {string} options.ambiguous-words - Comma-separated list of ambiguous words
6
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
7
+ */
8
+ module.exports = (given, options, context) => {
9
+ const results = [];
10
+
11
+ if (typeof given !== 'string') {
12
+ return results;
13
+ }
14
+
15
+ // Default ambiguous words (from the original regex)
16
+ const defaultAmbiguousWords = [
17
+ 'elements', 'instances', 'resources', 'values', 'terms', 'objects', 'items'
18
+ ];
19
+
20
+ // Parse custom ambiguous words from options if provided
21
+ let ambiguousWords = defaultAmbiguousWords;
22
+ if (options && options['ambiguous-words']) {
23
+ ambiguousWords = options['ambiguous-words']
24
+ .split(',')
25
+ .map(w => w.trim().toLowerCase())
26
+ .filter(Boolean);
27
+ }
28
+
29
+ // Split path into segments and filter out parameters
30
+ const segments = given
31
+ .split('/')
32
+ .filter(segment => {
33
+ // Keep only static segments (not empty, not {param})
34
+ return segment.length > 0 && !segment.startsWith('{');
35
+ });
36
+
37
+ // Check each segment for ambiguous words
38
+ for (const segment of segments) {
39
+ const lowerSegment = segment.toLowerCase();
40
+ for (const word of ambiguousWords) {
41
+ if (lowerSegment.includes(word)) {
42
+ results.push({
43
+ message: `${context.rule.message || 'OAR032'}: Path segment '${segment}' is ambiguous. Avoid using words like '${word}' in resource names.`,
44
+ path: context.path
45
+ });
46
+ break; // Report only once per segment
47
+ }
48
+ }
49
+ }
50
+
51
+ return results;
52
+ };
@@ -39,6 +39,18 @@ module.exports = (given, options, context) => {
39
39
  return checkSchema(schema.items);
40
40
  }
41
41
 
42
+ if (schema.allOf && Array.isArray(schema.allOf)) {
43
+ return schema.allOf.some(subSchema => checkSchema(subSchema));
44
+ }
45
+
46
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
47
+ return schema.oneOf.some(subSchema => checkSchema(subSchema));
48
+ }
49
+
50
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
51
+ return schema.anyOf.some(subSchema => checkSchema(subSchema));
52
+ }
53
+
42
54
  return false;
43
55
  };
44
56
 
@@ -1,11 +1,13 @@
1
1
  /**
2
- * @param {object} given
3
- * @param {object} options
4
- * @param {string} options.property
5
- * @param {string} options.equalTo
6
- * @param {string} options.result
7
- * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
8
- *
2
+ * Enhanced semantic text comparison function
3
+ * Detects both exact matches and semantic similarity
4
+ * @param {object} given
5
+ * @param {object} options
6
+ * @param {string} options.property
7
+ * @param {string} options.equalTo
8
+ * @param {string} options.result
9
+ * @param {number} options.threshold - Similarity threshold (0.0-1.0), default 0.6
10
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
9
11
  */
10
12
  module.exports = (given, options, context) => {
11
13
  const errors = [];
@@ -16,18 +18,113 @@ module.exports = (given, options, context) => {
16
18
  const propA = given[options.property];
17
19
  const propB = given[options.equalTo];
18
20
 
19
- if (
20
- typeof propA === 'string' &&
21
- typeof propB === 'string' &&
22
- options.result === 'falsy' &&
23
- propA.trim().toUpperCase() === propB.trim().toUpperCase()
24
- ) {
21
+ if (typeof propA !== 'string' || typeof propB !== 'string') {
22
+ return errors;
23
+ }
24
+
25
+ if (options.result !== 'falsy') {
26
+ return errors;
27
+ }
28
+
29
+ const threshold = options.threshold || 0.6;
30
+
31
+ // Exact match (case-insensitive)
32
+ if (propA.trim().toUpperCase() === propB.trim().toUpperCase()) {
25
33
  errors.push({
26
34
  message: context.rule.message,
27
35
  path: [...context.path, options.property]
28
36
  });
37
+ return errors;
38
+ }
39
+
40
+ // Semantic similarity check
41
+ const similarity = calculateSimilarity(propA, propB);
42
+ if (similarity >= threshold) {
43
+ errors.push({
44
+ message: `${context.rule.message} (${Math.round(similarity * 100)}% similar)`,
45
+ path: [...context.path, options.property]
46
+ });
29
47
  }
30
48
 
31
49
  return errors;
32
50
  };
33
51
 
52
+ /**
53
+ * Calculate semantic similarity between two strings
54
+ * Uses token overlap and cosine similarity
55
+ */
56
+ function calculateSimilarity(str1, str2) {
57
+ const tokens1 = tokenize(str1);
58
+ const tokens2 = tokenize(str2);
59
+
60
+ if (tokens1.length === 0 || tokens2.length === 0) {
61
+ return 0;
62
+ }
63
+
64
+ // Calculate Jaccard similarity (token overlap)
65
+ const intersection = new Set([...tokens1].filter(x => tokens2.includes(x)));
66
+ const union = new Set([...tokens1, ...tokens2]);
67
+ const jaccardSimilarity = intersection.size / union.size;
68
+
69
+ // Calculate word order similarity
70
+ const order1 = tokens1.join(' ');
71
+ const order2 = tokens2.join(' ');
72
+ const levenshteinDist = levenshteinDistance(order1, order2);
73
+ const maxLen = Math.max(order1.length, order2.length);
74
+ const orderSimilarity = maxLen === 0 ? 0 : 1 - levenshteinDist / maxLen;
75
+
76
+ // Weighted average
77
+ return jaccardSimilarity * 0.6 + orderSimilarity * 0.4;
78
+ }
79
+
80
+ /**
81
+ * Tokenize string into meaningful words with stemming
82
+ */
83
+ function tokenize(str) {
84
+ return str
85
+ .toLowerCase()
86
+ .replace(/[^\w\s]/g, '')
87
+ .split(/\s+/)
88
+ .filter(token => token.length > 2) // Ignore very short words
89
+ .map(token => stem(token)) // Apply stemming
90
+ .sort();
91
+ }
92
+
93
+ /**
94
+ * Simple word stemming (removes common suffixes)
95
+ */
96
+ function stem(word) {
97
+ // Remove common verb/noun suffixes
98
+ return word
99
+ .replace(/s$/, '') // plural
100
+ .replace(/ed$/, '') // past tense
101
+ .replace(/ing$/, '') // continuous
102
+ .replace(/er$/, '') // agent
103
+ .replace(/est$/, ''); // superlative
104
+ }
105
+
106
+ /**
107
+ * Calculate Levenshtein distance between two strings
108
+ */
109
+ function levenshteinDistance(str1, str2) {
110
+ const len1 = str1.length;
111
+ const len2 = str2.length;
112
+ const matrix = Array(len2 + 1).fill(null).map(() => Array(len1 + 1).fill(0));
113
+
114
+ for (let i = 0; i <= len1; i++) matrix[0][i] = i;
115
+ for (let j = 0; j <= len2; j++) matrix[j][0] = j;
116
+
117
+ for (let j = 1; j <= len2; j++) {
118
+ for (let i = 1; i <= len1; i++) {
119
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
120
+ matrix[j][i] = Math.min(
121
+ matrix[j][i - 1] + 1,
122
+ matrix[j - 1][i] + 1,
123
+ matrix[j - 1][i - 1] + cost
124
+ );
125
+ }
126
+ }
127
+
128
+ return matrix[len2][len1];
129
+ }
130
+
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @param {object} given
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (given, options, context) => {
7
+ if (!given || typeof given !== 'object') {
8
+ return [];
9
+ }
10
+
11
+ const params = given.parameters;
12
+
13
+ if (!params || !Array.isArray(params)) {
14
+ return [{ message: context.rule.message }];
15
+ }
16
+
17
+ const hasFilter = params.some(
18
+ p => p && !p.$ref && p.name === '$filter' && p.in === 'query'
19
+ );
20
+
21
+ if (!hasFilter) {
22
+ return [{ message: context.rule.message }];
23
+ }
24
+
25
+ return [];
26
+ };
@@ -0,0 +1,124 @@
1
+ module.exports = (schema, options = {}, context) => {
2
+ const results = [];
3
+
4
+ if (!schema || typeof schema !== 'object') {
5
+ return results;
6
+ }
7
+
8
+ // Parse paging schema from options
9
+ let pagingSchema = {};
10
+ try {
11
+ const pagingSchemaStr = options['paging-schema'] || defaultPagingSchema;
12
+ pagingSchema = typeof pagingSchemaStr === 'string'
13
+ ? JSON.parse(pagingSchemaStr)
14
+ : pagingSchemaStr;
15
+ } catch (err) {
16
+ return [{
17
+ message: `${context.rule.name.split(':').pop()}: Invalid paging-schema configuration: ${err.message}`,
18
+ path: context.path,
19
+ }];
20
+ }
21
+
22
+ const pagingPropertyName = pagingSchema.pagingPropertyName || 'paging';
23
+ const requiredFields = pagingSchema.required || [];
24
+ const properties = pagingSchema.properties || {};
25
+
26
+ // Check if schema doesn't have properties at all - skip validation
27
+ if (!schema.properties) {
28
+ return results;
29
+ }
30
+
31
+ // Check if schema has the paging property
32
+ if (!schema.properties[pagingPropertyName]) {
33
+ // Only flag if schema looks like it should have paging (has data/items/results arrays)
34
+ const hasArrayProperty = Object.values(schema.properties || {}).some(
35
+ prop => prop && prop.type === 'array'
36
+ );
37
+
38
+ if (hasArrayProperty) {
39
+ results.push({
40
+ message: `${context.rule.name.split(':').pop()}: Response must include '${pagingPropertyName}' property for pagination`,
41
+ path: context.path,
42
+ });
43
+ }
44
+ return results;
45
+ }
46
+
47
+ const pagingObj = schema.properties[pagingPropertyName];
48
+
49
+ // Validate paging object structure
50
+ if (!pagingObj.properties) {
51
+ results.push({
52
+ message: `${context.rule.name.split(':').pop()}: '${pagingPropertyName}' must be an object with required properties`,
53
+ path: [...context.path, 'properties', pagingPropertyName],
54
+ });
55
+ return results;
56
+ }
57
+
58
+ // Check required fields
59
+ const missingFields = [];
60
+ for (const requiredField of requiredFields) {
61
+ if (!pagingObj.properties[requiredField]) {
62
+ missingFields.push(requiredField);
63
+ }
64
+ }
65
+
66
+ if (missingFields.length > 0) {
67
+ results.push({
68
+ message: `${context.rule.name.split(':').pop()}: Paging object must include required fields: ${missingFields.join(', ')}`,
69
+ path: [...context.path, 'properties', pagingPropertyName, 'properties'],
70
+ });
71
+ }
72
+
73
+ // Special validation for links object if it's required
74
+ if (requiredFields.includes('links') && pagingObj.properties.links) {
75
+ const linksObj = pagingObj.properties.links;
76
+ if (linksObj.properties) {
77
+ const requiredLinks = ['self', 'previous', 'next']; // From default schema
78
+ const linksRequired = linksObj.required || [];
79
+
80
+ for (const requiredLink of requiredLinks) {
81
+ if (!linksObj.properties[requiredLink]) {
82
+ results.push({
83
+ message: `${context.rule.name.split(':').pop()}: Links object must include '${requiredLink}' property`,
84
+ path: [...context.path, 'properties', pagingPropertyName, 'properties', 'links', 'properties'],
85
+ });
86
+ }
87
+ }
88
+
89
+ // Check if links has required array
90
+ if (!linksRequired.includes('self') || !linksRequired.includes('previous') || !linksRequired.includes('next')) {
91
+ results.push({
92
+ message: `${context.rule.name.split(':').pop()}: Links object must require 'self', 'previous', and 'next'`,
93
+ path: [...context.path, 'properties', pagingPropertyName, 'properties', 'links'],
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ return results;
100
+ };
101
+
102
+ // Default paging schema matching Java implementation
103
+ const defaultPagingSchema = JSON.stringify({
104
+ type: 'object',
105
+ properties: {
106
+ numPages: { type: 'integer' },
107
+ total: { type: 'integer' },
108
+ start: { type: 'integer' },
109
+ limit: { type: 'integer' },
110
+ links: {
111
+ type: 'object',
112
+ properties: {
113
+ next: { type: 'object', properties: { href: { type: 'string' } } },
114
+ previous: { type: 'object', properties: { href: { type: 'string' } } },
115
+ last: { type: 'object', properties: { href: { type: 'string' } } },
116
+ self: { type: 'object', properties: { href: { type: 'string' } } },
117
+ first: { type: 'object', properties: { href: { type: 'string' } } },
118
+ },
119
+ required: ['self', 'previous', 'next'],
120
+ },
121
+ },
122
+ required: ['start', 'limit', 'links'],
123
+ pagingPropertyName: 'paging',
124
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Validates that properties intended for password storage use format: password
3
+ * @param {object} given - The schema properties object
4
+ * @param {object} options - Function options
5
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
6
+ */
7
+ module.exports = (given, options, context) => {
8
+ const results = [];
9
+
10
+ if (!given || typeof given !== 'object') {
11
+ return results;
12
+ }
13
+
14
+ // Check each property in the schema
15
+ Object.entries(given).forEach(([propName, propSchema]) => {
16
+ if (!propSchema || typeof propSchema !== 'object') {
17
+ return;
18
+ }
19
+
20
+ // Check if this is a password-related field (name contains "password")
21
+ const isPasswordField = propName.toLowerCase().includes('password');
22
+
23
+ if (isPasswordField && propSchema.type === 'string') {
24
+ // Password fields must have format: password
25
+ if (propSchema.format !== 'password') {
26
+ results.push({
27
+ message: context.rule.message || 'OAR081: Password fields should use format: password',
28
+ path: [...context.path, propName, 'format']
29
+ });
30
+ }
31
+ }
32
+ });
33
+
34
+ return results;
35
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @param {string} targetVal
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (targetVal, options, context) => {
7
+ const maxDepth = options.maxDepth ?? 3
8
+ const ignore = options.ignoreSegments ?? []
9
+
10
+ const segments = targetVal
11
+ .split('/')
12
+ .filter(Boolean)
13
+ .filter(segment => !ignore.includes(segment))
14
+
15
+ if (segments.length > maxDepth) {
16
+ return [
17
+ { message: context.rule.message }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Validates that path parameters don't appear as query parameters
3
+ * This prevents ambiguity and design issues
4
+ *
5
+ * @param {object} given - The paths object
6
+ * @param {object} options - Function options
7
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
8
+ */
9
+ module.exports = (given, options, context) => {
10
+ const errors = [];
11
+
12
+ if (!given || typeof given !== 'object') {
13
+ return errors;
14
+ }
15
+
16
+ const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
17
+
18
+ for (const [pathKey, pathItem] of Object.entries(given)) {
19
+ if (!pathItem || typeof pathItem !== 'object') {
20
+ continue;
21
+ }
22
+
23
+ for (const [operationKey, operation] of Object.entries(pathItem)) {
24
+ if (!operation || typeof operation !== 'object' ||
25
+ !httpMethods.includes(operationKey)) {
26
+ continue;
27
+ }
28
+
29
+ const parameters = operation.parameters || [];
30
+
31
+ if (!Array.isArray(parameters)) {
32
+ continue;
33
+ }
34
+
35
+ const hasPathOrQueryParams = parameters.some(
36
+ param => param && (param.in === 'path' || param.in === 'query')
37
+ );
38
+
39
+ if (!hasPathOrQueryParams) {
40
+ continue;
41
+ }
42
+
43
+ const responses = operation.responses || {};
44
+ if (!responses['400']) {
45
+ errors.push({
46
+ message: `OAR069: Any param in PATH or QUERY, should have bad request (400) response.`,
47
+ path: [...context.path, pathKey, operationKey, 'responses']
48
+ });
49
+ }
50
+ }
51
+ }
52
+
53
+ return errors;
54
+ };
@@ -29,7 +29,7 @@ const DEFAULT_ALLOWED_PATTERNS = `
29
29
  ;delete:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
30
30
  `;
31
31
 
32
- const SUPPORTED_VERBS = ['get', 'post', 'put', 'patch', 'delete'];
32
+ const SUPPORTED_VERBS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
33
33
  const CONFIG_ERROR_PREFIX = 'OAR018:';
34
34
 
35
35
  const parseAllowedPatterns = (patterns) => {
@@ -41,7 +41,7 @@ module.exports = (responseNode, options = {}, context) => {
41
41
  const path = context.path?.find(p => typeof p === 'string' && p.startsWith('/'));
42
42
  const responseCode = context.path?.[context.path.length - 1];
43
43
 
44
- if (path && pathExclusions.includes(path)) {
44
+ if (path && pathExclusions.some(ex => path === ex || path.startsWith(ex + '/'))) {
45
45
  return [];
46
46
  }
47
47
 
@@ -85,7 +85,8 @@ module.exports = (responseNode, options = {}, context) => {
85
85
  const forbidden = headerNames.filter(h => !allowedHeaders.includes(h));
86
86
  if (forbidden.length > 0) {
87
87
  results.push({
88
- message: `${ruleCode}: Header not allowed`,
88
+ message: `${ruleCode}: Headers [${forbidden.join(', ')}] are not allowed`,
89
+ path: [...context.path, 'headers', forbidden[0]],
89
90
  });
90
91
  }
91
92
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @param {object} given
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+
7
+ const NO_BODY_CODES = new Set(['204']);
8
+
9
+ module.exports = (given, options, context) => {
10
+ const errors = [];
11
+ if (!given) return errors;
12
+
13
+ const root = context.document.parserResult.data;
14
+
15
+ if (root.swagger) {
16
+ const produces = given.produces ?? root.produces;
17
+
18
+ if (!produces || !Array.isArray(produces) || !produces.includes('application/json')) {
19
+ errors.push({
20
+ message: context.rule.message,
21
+ path: [...context.path]
22
+ });
23
+ }
24
+
25
+ return errors;
26
+ }
27
+
28
+ const responses = given.responses;
29
+ if (!responses) return errors;
30
+
31
+ for (const [statusCode, response] of Object.entries(responses)) {
32
+ if (NO_BODY_CODES.has(statusCode)) continue;
33
+
34
+ if (!response?.content?.['application/json']) {
35
+ errors.push({
36
+ message: context.rule.message,
37
+ path: [...context.path, 'responses', statusCode]
38
+ });
39
+ }
40
+ }
41
+
42
+ return errors;
43
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Validates that operations with security defined include a 401 Unauthorized response
3
+ * @param {object} given - The operation node
4
+ * @param {object} options - Function options
5
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
6
+ */
7
+ module.exports = (given, options, context) => {
8
+ const results = [];
9
+
10
+ if (!given || typeof given !== 'object') {
11
+ return results;
12
+ }
13
+
14
+ const responses = given.responses;
15
+ if (!responses || typeof responses !== 'object') {
16
+ return results;
17
+ }
18
+
19
+ // Check if this operation has security defined
20
+ const operationHasSecurity = given.security &&
21
+ Array.isArray(given.security) &&
22
+ given.security.length > 0;
23
+
24
+ // Check if there's global security in the document
25
+ let globalHasSecurity = false;
26
+ try {
27
+ const rootSecurity = context.document?.parserResult?.data?.security;
28
+ globalHasSecurity = rootSecurity &&
29
+ Array.isArray(rootSecurity) &&
30
+ rootSecurity.length > 0;
31
+ } catch (e) {
32
+ // Ignore errors accessing root security
33
+ }
34
+
35
+ // Only require 401 if security is defined (operation-level or global)
36
+ if (operationHasSecurity || globalHasSecurity) {
37
+ if (!responses['401']) {
38
+ results.push({
39
+ message: context.rule.message || 'OAR035: Response code 401 must be defined for operations with security schemes defined.',
40
+ path: [...context.path, 'responses']
41
+ });
42
+ }
43
+ }
44
+
45
+ return results;
46
+ };
@@ -22,7 +22,10 @@ module.exports = function apqStandardResponseCodes(given, options, context) {
22
22
 
23
23
  const exclusions = options?.['resources-exclusions'] || [];
24
24
  if (exclusions.some(ex => {
25
- const [exVerb, exPath] = ex.split(':');
25
+ const colonIdx = ex.indexOf(':');
26
+ if (colonIdx === -1) return false;
27
+ const exVerb = ex.slice(0, colonIdx);
28
+ const exPath = ex.slice(colonIdx + 1);
26
29
  return exVerb.toLowerCase() === verb.toLowerCase() && new RegExp(`^${exPath}$`).test(resourcePath);
27
30
  })) return results;
28
31
 
@@ -0,0 +1,32 @@
1
+ module.exports = (paths, options = {}, context) => {
2
+ const results = [];
3
+
4
+ if (!paths || typeof paths !== 'object') {
5
+ return results;
6
+ }
7
+
8
+ // Get configuration options
9
+ const statusEndpoint = (options['status-endpoint'] || '/status').trim();
10
+ const method = (options['method'] || 'get').toLowerCase().trim();
11
+
12
+ // Check if the endpoint exists
13
+ if (!paths[statusEndpoint]) {
14
+ results.push({
15
+ message: `${context.rule.name.split(':').pop()}: The status endpoint '${statusEndpoint}' must be declared`,
16
+ path: context.path,
17
+ });
18
+ return results;
19
+ }
20
+
21
+ const endpointNode = paths[statusEndpoint];
22
+
23
+ // Check if the configured method exists
24
+ if (!endpointNode[method]) {
25
+ results.push({
26
+ message: `${context.rule.name.split(':').pop()}: The status endpoint '${statusEndpoint}' must support the '${method.toUpperCase()}' method`,
27
+ path: [...context.path, statusEndpoint],
28
+ });
29
+ }
30
+
31
+ return results;
32
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [
@@ -1,23 +0,0 @@
1
- /**
2
- * @param {object} given
3
- * @param {object} options
4
- * @param {string} options.type
5
- * @param {string} options.match
6
- * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
7
- */
8
- module.exports = (given, options, context) => {
9
- const errors = [];
10
- if (!given) return errors;
11
-
12
- if (!given.type || given.type.toString() !== options.type.toString()) {
13
- errors.push({
14
- message: context.rule.message,
15
- });
16
- } else if (!given.default || given.default.toString() !== options.match.toString()) {
17
- errors.push({
18
- message: context.rule.message,
19
- });
20
- }
21
-
22
- return errors;
23
- }