@aep_dev/aep-openapi-linter 0.5.1

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.
package/aep/0158.yaml ADDED
@@ -0,0 +1,175 @@
1
+ aliases:
2
+ ListOperation:
3
+ description: A list operation is a get on path that does not end in a path parameter
4
+ targets:
5
+ - formats: ['oas2', 'oas3']
6
+ given:
7
+ # first condition excludes custom methods and second condition excludes paths ending in a path parameter
8
+ - $.paths[?(!@property.match(/:[^/]*$/) && !@property.match(/\}$/))].get
9
+
10
+ functionsDir: ../functions
11
+ functions:
12
+ - skipSingletonsSchema
13
+
14
+ rules:
15
+ aep-158-max-page-size-parameter:
16
+ description: Operations that return collections should define an integer max_page_size parameter.
17
+ severity: warn
18
+ formats: ['oas3']
19
+ given:
20
+ - '#ListOperation'
21
+ then:
22
+ function: skipSingletonsSchema
23
+ functionOptions:
24
+ schema:
25
+ type: object
26
+ required: ['parameters']
27
+ properties:
28
+ parameters:
29
+ type: array
30
+ contains:
31
+ type: object
32
+ required: ['name', 'schema']
33
+ properties:
34
+ name:
35
+ enum: ['max_page_size']
36
+ schema:
37
+ type: object
38
+ required: ['type']
39
+ properties:
40
+ type:
41
+ enum: ['integer']
42
+
43
+ aep-158-page-token-parameter:
44
+ description: Operations that return collections should define a string page_token parameter.
45
+ severity: warn
46
+ formats: ['oas3']
47
+ given:
48
+ - '#ListOperation'
49
+ then:
50
+ function: skipSingletonsSchema
51
+ functionOptions:
52
+ schema:
53
+ type: object
54
+ required: ['parameters']
55
+ properties:
56
+ parameters:
57
+ type: array
58
+ contains:
59
+ type: object
60
+ required: ['name', 'schema']
61
+ properties:
62
+ name:
63
+ enum: ['page_token']
64
+ schema:
65
+ type: object
66
+ required: ['type']
67
+ properties:
68
+ type:
69
+ enum: ['string']
70
+
71
+ aep-158-page-token-parameter-optional:
72
+ description: The page_token parameter must not be required.
73
+ severity: error
74
+ formats: ['oas3']
75
+ given:
76
+ - '#ListOperation.parameters[?(@.name == "page_token")]'
77
+ then:
78
+ function: schema
79
+ functionOptions:
80
+ schema:
81
+ type: object
82
+ properties:
83
+ required:
84
+ not:
85
+ enum: [true]
86
+
87
+ aep-158-response-array-property:
88
+ description: The response schema must include an array property.
89
+ severity: error
90
+ formats: ['oas3']
91
+ given:
92
+ - '#ListOperation.responses.200.content.application/json.schema'
93
+ then:
94
+ function: schema
95
+ functionOptions:
96
+ schema:
97
+ type: object
98
+ anyOf:
99
+ - required: ['properties']
100
+ properties:
101
+ properties:
102
+ # You can use this pattern to get an object contains constraint
103
+ type: object
104
+ not:
105
+ additionalProperties:
106
+ not:
107
+ type: object
108
+ required: ['type']
109
+ properties:
110
+ type:
111
+ enum: ['array']
112
+ - # Ignore for singleton resources
113
+ required: ['x-aep-resource']
114
+ properties:
115
+ 'x-aep-resource':
116
+ type: object
117
+ required: ['singleton']
118
+ properties:
119
+ 'singleton':
120
+ enum: [true]
121
+
122
+ aep-158-response-next-page-token-property:
123
+ description: The response schema must include a string next_page_token property.
124
+ severity: error
125
+ formats: ['oas3']
126
+ given:
127
+ - '#ListOperation.responses.200.content.application/json.schema'
128
+ then:
129
+ function: schema
130
+ functionOptions:
131
+ schema:
132
+ type: object
133
+ anyOf:
134
+ - required: ['properties']
135
+ properties:
136
+ properties:
137
+ type: object
138
+ required: ['next_page_token']
139
+ properties:
140
+ next_page_token:
141
+ type: object
142
+ required: ['type']
143
+ properties:
144
+ type:
145
+ enum: ['string']
146
+ - # Ignore for singleton resources
147
+ required: ['x-aep-resource']
148
+ properties:
149
+ 'x-aep-resource':
150
+ type: object
151
+ required: ['singleton']
152
+ properties:
153
+ 'singleton':
154
+ enum: [true]
155
+
156
+ aep-158-skip-parameter:
157
+ description: Operations that return collections may define an integer skip parameter.
158
+ message: The skip parameter must be an integer.
159
+ severity: warn
160
+ formats: ['oas3']
161
+ given:
162
+ - '#ListOperation.parameters[?(@.name == "skip")]'
163
+ then:
164
+ function: schema
165
+ functionOptions:
166
+ schema:
167
+ type: object
168
+ required: ['schema']
169
+ properties:
170
+ schema:
171
+ type: object
172
+ required: ['type']
173
+ properties:
174
+ type:
175
+ enum: ['integer']
package/aep/0193.yaml ADDED
@@ -0,0 +1,54 @@
1
+ rules:
2
+ aep-193-error-response-schema:
3
+ description: Error response body should contain all required fields according to AEP-193.
4
+ message: '{{error}}'
5
+ severity: warn
6
+ formats: ['oas2', 'oas3']
7
+ given: $.paths[*][*].responses[?(@property >= 400)].content[*].schema.properties
8
+ then:
9
+ function: schema
10
+ functionOptions:
11
+ schema:
12
+ type: object
13
+ required: ['type']
14
+ properties:
15
+ type:
16
+ type: object
17
+ required: ['type', 'format']
18
+ properties:
19
+ type:
20
+ enum: ['string']
21
+ format:
22
+ enum: ['uri-reference']
23
+ title:
24
+ type: object
25
+ required: ['type']
26
+ properties:
27
+ type:
28
+ enum: ['string']
29
+ status:
30
+ type: object
31
+ required: ['type']
32
+ properties:
33
+ type:
34
+ enum: ['integer']
35
+ minimum:
36
+ type: integer
37
+ minimum: 100
38
+ maximum:
39
+ type: integer
40
+ maximum: 599
41
+ detail:
42
+ type: object
43
+ required: ['type']
44
+ properties:
45
+ type:
46
+ enum: ['string']
47
+ instance:
48
+ type: object
49
+ required: ['type', 'format']
50
+ properties:
51
+ type:
52
+ enum: ['string']
53
+ format:
54
+ enum: ['uri-reference']
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Validates that fields with time-related suffixes use the correct OpenAPI types.
3
+ *
4
+ * Based on AEP-142 specification (https://aep.dev/142).
5
+ *
6
+ * AEP-documented suffixes and their expected types:
7
+ * - _time, _times → string with format: date-time (RFC 3339)
8
+ * - _date → string with format: date
9
+ * - _seconds → integer or number
10
+ * - _millis → integer or number
11
+ * - _micros → integer or number
12
+ * - _nanos → integer or number
13
+ *
14
+ * @param {object} field - The field object being validated
15
+ * @param {object} _opts - Options (unused)
16
+ * @param {object} context - Spectral context containing the path
17
+ * @returns {Array<object>} Array of error objects, or empty array if valid
18
+ */
19
+ module.exports = (field, _opts, context) => {
20
+ if (!field || typeof field !== 'object') {
21
+ return [];
22
+ }
23
+
24
+ // Extract the field name from the path - it should be the last element
25
+ // path looks like:
26
+ // ['paths', '/test', 'get', 'responses', '200', 'content',
27
+ // 'application/json', 'schema', 'properties', 'create_time']
28
+ const path = context.path || [];
29
+ const fieldName = path[path.length - 1];
30
+
31
+ if (typeof fieldName !== 'string' || !fieldName.includes('_')) {
32
+ return [];
33
+ }
34
+
35
+ // Extract the suffix (last word after underscore)
36
+ const parts = fieldName.split('_');
37
+ const suffix = parts[parts.length - 1];
38
+
39
+ // Define suffix categories and their expected types (AEP-142 documented only)
40
+ const timestampSuffixes = ['time', 'times'];
41
+ const dateSuffixes = ['date'];
42
+ const durationSecondSuffixes = ['seconds'];
43
+ const durationMilliSuffixes = ['millis'];
44
+ const durationMicroSuffixes = ['micros'];
45
+ const durationNanoSuffixes = ['nanos'];
46
+
47
+ const allSuffixes = [
48
+ ...timestampSuffixes,
49
+ ...dateSuffixes,
50
+ ...durationSecondSuffixes,
51
+ ...durationMilliSuffixes,
52
+ ...durationMicroSuffixes,
53
+ ...durationNanoSuffixes,
54
+ ];
55
+
56
+ // Check if this field has a time-related suffix
57
+ if (!allSuffixes.includes(suffix)) {
58
+ return [];
59
+ }
60
+
61
+ const errors = [];
62
+
63
+ // Validate timestamp fields (_time, _times)
64
+ if (timestampSuffixes.includes(suffix)) {
65
+ // For _times (plural), allow arrays of timestamps
66
+ if (suffix === 'times' && field.type === 'array') {
67
+ // Check that array items are strings with date-time format
68
+ if (!field.items || field.items.type !== 'string' || field.items.format !== 'date-time') {
69
+ errors.push({
70
+ message:
71
+ `Field "${fieldName}" should be an array with items of type "string" ` +
72
+ `and format "date-time" (RFC 3339 timestamp).`,
73
+ });
74
+ }
75
+ } else if (field.type !== 'string' || field.format !== 'date-time') {
76
+ errors.push({
77
+ message: `Field "${fieldName}" should have type "string" and format "date-time" (RFC 3339 timestamp).`,
78
+ });
79
+ }
80
+ }
81
+
82
+ // Validate date fields (_date)
83
+ else if (dateSuffixes.includes(suffix)) {
84
+ if (field.type !== 'string' || field.format !== 'date') {
85
+ errors.push({
86
+ message: `Field "${fieldName}" should have type "string" and format "date" (RFC 3339 date).`,
87
+ });
88
+ }
89
+ }
90
+
91
+ // Validate duration fields (seconds, millis, micros, nanos)
92
+ else if (
93
+ durationSecondSuffixes.includes(suffix) ||
94
+ durationMilliSuffixes.includes(suffix) ||
95
+ durationMicroSuffixes.includes(suffix) ||
96
+ durationNanoSuffixes.includes(suffix)
97
+ ) {
98
+ // Duration fields should be integers (or numbers for fractional values)
99
+ if (field.type !== 'integer' && field.type !== 'number') {
100
+ errors.push({
101
+ message: `Field "${fieldName}" should have type "integer" or "number" for duration values.`,
102
+ });
103
+ }
104
+ }
105
+
106
+ return errors;
107
+ };
@@ -0,0 +1,40 @@
1
+ // Check that services with long-running operations (202 responses) have the required operations endpoints
2
+
3
+ module.exports = (paths, _opts, context) => {
4
+ if (!paths || typeof paths !== 'object') {
5
+ return [];
6
+ }
7
+
8
+ const errors = [];
9
+ const pathKeys = Object.keys(paths);
10
+
11
+ // Check if any path has a 202 response
12
+ const hasLongRunningOperation = pathKeys.some((pathKey) => {
13
+ const pathItem = paths[pathKey];
14
+ if (!pathItem || typeof pathItem !== 'object') return false;
15
+
16
+ // Check all HTTP methods that can have 202 responses
17
+ const methods = ['post', 'put', 'patch', 'delete'];
18
+ return methods.some(
19
+ (method) => pathItem[method] && pathItem[method].responses && pathItem[method].responses['202']
20
+ );
21
+ });
22
+
23
+ // If no long-running operations, no need to check for operations endpoints
24
+ if (!hasLongRunningOperation) {
25
+ return [];
26
+ }
27
+
28
+ // Check for required operations endpoints
29
+ const hasOperationsList = paths['/v1/operations'] && paths['/v1/operations'].get;
30
+ const hasOperationsGet = paths['/v1/operations/{operation}'] && paths['/v1/operations/{operation}'].get;
31
+
32
+ if (!hasOperationsList || !hasOperationsGet) {
33
+ errors.push({
34
+ message: 'Services with long-running operations must define an operations endpoint with list and get operations',
35
+ path: context.path || ['paths'],
36
+ });
37
+ }
38
+
39
+ return errors;
40
+ };
@@ -0,0 +1,78 @@
1
+ // Check that the parameters of an operation -- including those specified on the path -- are
2
+ // are case-insensitive unique regardless of "in".
3
+
4
+ // Return the "canonical" casing for a string.
5
+ // Currently just lowercase but should be extended to convert kebab/camel/snake/Pascal.
6
+ function canonical(name) {
7
+ return typeof name === 'string' ? name.toLowerCase() : name;
8
+ }
9
+
10
+ // Accept an array and return a list of unique duplicate entries in canonical form.
11
+ // This function is intended to work on strings but is resilient to non-strings.
12
+ function dupIgnoreCase(arr) {
13
+ if (!Array.isArray(arr)) {
14
+ return [];
15
+ }
16
+
17
+ const isDup = (value, index, self) => self.indexOf(value) !== index;
18
+
19
+ return [...new Set(arr.map((v) => canonical(v)).filter(isDup))];
20
+ }
21
+
22
+ // targetVal should be a
23
+ // [path item object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#pathItemObject).
24
+ // The code assumes it is running on a resolved doc
25
+ module.exports = (pathItem, _opts, paths) => {
26
+ if (pathItem === null || typeof pathItem !== 'object') {
27
+ return [];
28
+ }
29
+ const path = paths.path || paths.target || [];
30
+
31
+ const errors = [];
32
+
33
+ const pathParams = pathItem.parameters ? pathItem.parameters.map((p) => p.name) : [];
34
+
35
+ // Check path params for dups
36
+ const pathDups = dupIgnoreCase(pathParams);
37
+
38
+ // Report all dups
39
+ pathDups.forEach((dup) => {
40
+ // get the index of all names that match dup
41
+ const dupKeys = [...pathParams.keys()].filter((k) => canonical(pathParams[k]) === dup);
42
+ // Report errors for all the others
43
+ dupKeys.slice(1).forEach((key) => {
44
+ errors.push({
45
+ message: `Duplicate parameter name (ignoring case): ${dup}.`,
46
+ path: [...path, 'parameters', key, 'name'],
47
+ });
48
+ });
49
+ });
50
+
51
+ ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].forEach((method) => {
52
+ // If this method exists and it has parameters, check them
53
+ if (pathItem[method] && Array.isArray(pathItem[method].parameters)) {
54
+ const allParams = [...pathParams, ...pathItem[method].parameters.map((p) => p.name)];
55
+
56
+ // Check method params for dups -- including path params
57
+ const dups = dupIgnoreCase(allParams);
58
+
59
+ // Report all dups
60
+ dups.forEach((dup) => {
61
+ // get the index of all names that match dup
62
+ const dupKeys = [...allParams.keys()].filter((k) => canonical(allParams[k]) === dup);
63
+ // Report errors for any others that are method parameters
64
+ dupKeys
65
+ .slice(1)
66
+ .filter((k) => k >= pathParams.length)
67
+ .forEach((key) => {
68
+ errors.push({
69
+ message: `Duplicate parameter name (ignoring case): ${dup}.`,
70
+ path: [...path, method, 'parameters', key - pathParams.length, 'name'],
71
+ });
72
+ });
73
+ });
74
+ }
75
+ });
76
+
77
+ return errors;
78
+ };
@@ -0,0 +1,87 @@
1
+ // Shared utility functions for singleton resource validation
2
+ // according to AEP-156 specifications
3
+
4
+ /**
5
+ * Normalize a path or pattern: ensure leading slash, no duplicate slashes
6
+ * @param {string} str
7
+ * @returns {string}
8
+ */
9
+ function normalizePath(str) {
10
+ if (!str) return '';
11
+ // Remove leading/trailing whitespace, ensure leading slash, remove duplicate slashes
12
+ let s = str.trim();
13
+ if (!s.startsWith('/')) s = `/${s}`;
14
+ s = s.replace(/\/+/g, '/');
15
+ return s;
16
+ }
17
+
18
+ /**
19
+ * Get all singleton resource patterns from the OAS document
20
+ * @param {Object} oasDoc - The OpenAPI document
21
+ * @returns {Array} Array of singleton patterns
22
+ */
23
+ function getSingletonPatterns(oasDoc) {
24
+ if (!oasDoc || !oasDoc.components || !oasDoc.components.schemas) {
25
+ return [];
26
+ }
27
+ const singletonPatterns = [];
28
+ const { schemas } = oasDoc.components;
29
+ Object.values(schemas).forEach((schema) => {
30
+ if (
31
+ schema &&
32
+ schema['x-aep-resource'] &&
33
+ schema['x-aep-resource'].singleton === true &&
34
+ Array.isArray(schema['x-aep-resource'].patterns)
35
+ ) {
36
+ singletonPatterns.push(...schema['x-aep-resource'].patterns);
37
+ }
38
+ });
39
+ return singletonPatterns;
40
+ }
41
+
42
+ /**
43
+ * Check if a path string matches any singleton pattern exactly
44
+ * @param {string} pathString - The path to check
45
+ * @param {Object} oasDoc - The OpenAPI document
46
+ * @returns {boolean} True if the path matches a singleton pattern
47
+ */
48
+ function pathMatchesSingletonPattern(pathString, oasDoc) {
49
+ if (!pathString) return false;
50
+ const normalizedPath = normalizePath(pathString);
51
+ const singletonPatterns = getSingletonPatterns(oasDoc);
52
+ return singletonPatterns.some((pattern) => {
53
+ const normalizedPattern = normalizePath(pattern);
54
+ const regexPattern = `^${normalizedPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\{[^/]+\}/g, '[^/]+')}$`;
55
+ const regex = new RegExp(regexPattern);
56
+ return regex.test(normalizedPath);
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Check if a path string matches a singleton list pattern
62
+ * @param {string} pathString - The path to check
63
+ * @param {Object} oasDoc - The OpenAPI document
64
+ * @returns {boolean} True if the path matches a singleton list pattern
65
+ */
66
+ function pathMatchesSingletonListPattern(pathString, oasDoc) {
67
+ if (!pathString) return false;
68
+ const normalizedPath = normalizePath(pathString);
69
+ const singletonPatterns = getSingletonPatterns(oasDoc);
70
+ return singletonPatterns.some((pattern) => {
71
+ // Convert singleton pattern to list pattern: {parent}/{parent-id}/{singleton} -> {parent}/-/configs
72
+ const listPattern = pattern.replace(/\/[^/]+$/, '/-/configs');
73
+ const normalizedListPattern = normalizePath(listPattern);
74
+ const regexPattern = `^${normalizedListPattern
75
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
76
+ .replace(/\{[^/]+\}/g, '[^/]+')}$`;
77
+ const regex = new RegExp(regexPattern);
78
+ return regex.test(normalizedPath);
79
+ });
80
+ }
81
+
82
+ module.exports = {
83
+ getSingletonPatterns,
84
+ pathMatchesSingletonPattern,
85
+ pathMatchesSingletonListPattern,
86
+ normalizePath,
87
+ };
@@ -0,0 +1,35 @@
1
+ // This function first checks if the target operation is for a singleton resource
2
+ // by looking for the `x-aep-resource` extension with `singleton: true`.
3
+ // If it is a singleton, it does not report any errors.
4
+ // Otherwise, it performs validation of the target using the `schema` specified in the function parameters.
5
+
6
+ // Spectral allows custom functions to invoke core functions.
7
+ // The documentation for this feature can be found at:
8
+ // https://docs.stoplight.io/docs/spectral/a781e290eb9f9-custom-functions#referencing-core-functions
9
+ // This function uses the `schema` function from Spectral to validate the operation object.
10
+ const { schema: schemaFunction } = require('@stoplight/spectral-functions');
11
+
12
+ // targetVal should be an operation object.
13
+ // The code assumes it is running on a resolved doc
14
+ module.exports = (op, opts, context) => {
15
+ if (op === null || typeof op !== 'object') {
16
+ return [];
17
+ }
18
+
19
+ // opts must contain a schema to validate the operation against
20
+ if (opts === null || typeof opts !== 'object' || !opts.schema) {
21
+ return [];
22
+ }
23
+
24
+ // Check if the operation is for a singleton resource
25
+ const responseSchema = op.responses?.['200']?.content?.['application/json']?.schema;
26
+ if (responseSchema?.['x-aep-resource']?.singleton) {
27
+ return []; // No errors for singleton resources
28
+ }
29
+
30
+ // The operation is not for a singleton resource, apply the schema to the operation
31
+ // and return any errors found
32
+ const schemaErrors = schemaFunction(op, opts, context);
33
+
34
+ return schemaErrors;
35
+ };
@@ -0,0 +1,50 @@
1
+ // Validates that standardized code field names follow AEP-143 conventions.
2
+ // Checks schema property names and suggests correct standardized field names.
3
+
4
+ // Map of incorrect field name variants to their correct standardized names
5
+ const fieldNameVariants = {
6
+ // Content/Media types
7
+ mime: 'content_type',
8
+ mimetype: 'content_type',
9
+ mime_type: 'content_type',
10
+ media_type: 'content_type',
11
+ mediatype: 'content_type',
12
+ // Countries/Regions
13
+ country: 'region_code',
14
+ country_code: 'region_code',
15
+ region: 'region_code',
16
+ // Currency
17
+ currency: 'currency_code',
18
+ // Language
19
+ lang: 'language_code',
20
+ language: 'language_code',
21
+ locale: 'language_code',
22
+ // Time zones
23
+ tz: 'time_zone',
24
+ timezone: 'time_zone',
25
+ };
26
+
27
+ // targetVal is a schema object containing properties
28
+ module.exports = (schema, _opts, paths) => {
29
+ if (!schema || typeof schema !== 'object') {
30
+ return [];
31
+ }
32
+
33
+ const errors = [];
34
+ const path = paths.path || paths.target || [];
35
+
36
+ // Check if this is a schema with properties
37
+ if (schema.properties && typeof schema.properties === 'object') {
38
+ Object.keys(schema.properties).forEach((propertyName) => {
39
+ const correctName = fieldNameVariants[propertyName];
40
+ if (correctName) {
41
+ errors.push({
42
+ message: `Use "${correctName}" instead of "${propertyName}" for standardized code fields.`,
43
+ path: [...path, 'properties', propertyName],
44
+ });
45
+ }
46
+ });
47
+ }
48
+
49
+ return errors;
50
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@aep_dev/aep-openapi-linter",
3
+ "version": "0.5.1",
4
+ "description": "Linter for OpenAPI definitions to check compliance to AEPs",
5
+ "main": "spectral.yaml",
6
+ "files": [
7
+ "aep",
8
+ "functions",
9
+ "spectral.yaml"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "lint": "prettier --check . && eslint --cache --quiet --ext '.js' functions test",
16
+ "lint-fix": "prettier --write . && eslint --cache --quiet --ext '.js' --fix functions test",
17
+ "test": "jest --coverage",
18
+ "release:patch": "./scripts/prepare-release.sh patch",
19
+ "release:minor": "./scripts/prepare-release.sh minor",
20
+ "release:major": "./scripts/prepare-release.sh major"
21
+ },
22
+ "author": "Mike Kistler",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "url": "https://github.com/aep-dev/aep-openapi-linter"
26
+ },
27
+ "devDependencies": {
28
+ "@jest/globals": "^29.7.0",
29
+ "@stoplight/spectral-core": "^1.20.0",
30
+ "@stoplight/spectral-parsers": "^1.0.5",
31
+ "@stoplight/spectral-ruleset-migrator": "^1.11.1",
32
+ "@stoplight/spectral-rulesets": "^1.21.3",
33
+ "ajv": "^8.6.2",
34
+ "eslint": "^8.57.0",
35
+ "eslint-config-airbnb-base": "^15.0.0",
36
+ "eslint-config-prettier": "^9.1.0",
37
+ "eslint-plugin-import": "^2.23.4",
38
+ "jest": "^29.7.0",
39
+ "prettier": "^3.2.5"
40
+ },
41
+ "jest": {
42
+ "collectCoverage": true,
43
+ "collectCoverageFrom": [
44
+ "functions/*.js"
45
+ ],
46
+ "coverageThreshold": {
47
+ "./functions/*.js": {
48
+ "statements": 80
49
+ }
50
+ },
51
+ "moduleNameMapper": {
52
+ "^nimma/legacy$": "<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js",
53
+ "^nimma/(.*)": "<rootDir>/node_modules/nimma/dist/cjs/$1",
54
+ "^@stoplight/spectral-ruleset-bundler/(.*)$": "<rootDir>/node_modules/@stoplight/spectral-ruleset-bundler/dist/$1"
55
+ }
56
+ },
57
+ "dependencies": {
58
+ "@stoplight/spectral-functions": "^1.10.1"
59
+ }
60
+ }